lardon3d/docs/architecture/project_database.md

549 lines
23 KiB
Markdown

# Base de données projet Lardon3D
> Version courante : **v10**. La migration transactionnelle v9→v10 ajoute
> la table `match_results` pour le Match Result Model. La migration v8→v9
> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable
> Candidate Pair. La migration v7→v8 ajoute la table `candidate_pairs` pour
> le sous-système Candidate Pair.
> Les migrations historiques restent ordonnées et les faults d'injection
> vérifient le rollback.
## Vision
La base de données projet stocke les métadonnées de reconstruction et les relations entre les entités. Elle est conçue pour être légère, persistante et permettre la reprise après interruption.
## Structure conceptuelle
### Entités principales
#### Project
- Identifiant unique
- Nom et description
- Date de création
- Configuration
- Chemins des répertoires
#### Scan Set
- Identifiant unique
- Nom de l'acquisition
- Date
- Provenance
- État de traitement
#### Image
- Identifiant unique
- Chemin du fichier
- Métadonnées EXIF
- État de traitement
- Appartenance aux scan sets
#### Feature Set
- Identifiant unique
- Type de descripteur
- Paramètres
- Chemin des données
#### Visual Signature
- Identifiant unique
- Type d'index
- Paramètres
- Chemin des données
#### Candidate Pair
- Identifiant unique (`candidate_pair_id`)
- Image source (`image_id_a`)
- Image cible (`image_id_b`)
- Ordre canonique : `image_id_a < image_id_b`
- Self-pairs interdits
- Unicité persistante
- Date de création (`created_at`)
#### Verified Pair
- Identifiant unique
- Candidate pair source
- Statut (validée, rejetée)
- Métriques
#### Track
- Identifiant unique
- Observations
- Point 3D associé
- Qualité
#### Observation
- Identifiant unique
- Image
- Position 2D
- Descripteur
- Track parent
#### Camera
- Identifiant unique
- Modèle
- Paramètres intrinsèques
- Distorsion
#### Pose
- Identifiant unique
- Camera
- Translation
- Rotation
- Qualité
#### Point3D
- Identifiant unique
- Position
- Couleur
- Qualité
- Observations
#### Reconstruction Layer
- Identifiant unique
- Type (sparse, dense, mesh, etc.)
- Provenance
- Transformations
- Qualité
- Chemin des données
#### Measurement
- Identifiant unique
- Type
- Valeur
- Incertitude
- Cible géométrique
#### Document Source
- Identifiant unique
- Type (plan, croquis, etc.)
- Chemin
- Métadonnées
#### Geometric Constraint
- Identifiant unique
- Type
- Paramètres
- Sources
- Poids
#### Artifact
- Identifiant unique
- Type
- État (temporaire, publié)
- Chemin
- Métadonnées
#### Checkpoint
- Identifiant unique
- État du pipeline
- Métadonnées
- Date
## Relations
- Project → Scan Set (1:N)
- Scan Set → Image logique (1:N)
- Image logique → Image Asset (N:1)
- Image → Feature Set (1:N)
- Image → Visual Signature (1:N)
- Candidate Pair → Image (2)
- Verified Pair → Candidate Pair (1)
- Track → Observation (N:M)
- Observation → Image (1)
- Observation → Point3D (N:1)
- Camera → Pose (1:N)
- Pose → Reconstruction Layer (N:M)
- Reconstruction Layer → Artifact (1:N)
- Measurement → Point3D (N:1)
- Document Source → Geometric Constraint (N:M)
- Geometric Constraint → Point3D (N:M)
- Artifact → Checkpoint (N:1)
## Invariants
- Chaque entité a un identifiant unique stable
- Les relations sont explicitement définies
- Les artefacts partiels ne sont jamais considérés comme valides
- La reprise commence à la dernière frontière connue
## Frontière avec les checkpoints de tâche
Le modèle durable v1 et son codec fichier sont implémentés indépendamment du
stockage. La base réutilise les mêmes règles de normalisation et de
validation ; elle ne stockera jamais les objets pthread, callbacks, pointeurs,
contrats ou réservations. Le fichier par tâche est une fondation, pas une
Project Database miniature.
La stratégie v1 retient un résumé logique interrogable dans SQLite et une
référence vers le fichier checkpoint. Le fichier checkpoint validé reste la
source complète pour `lardon3d_task_restore()` ; la DB seule ne reconstruit
jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
## Schéma v7 implémenté
- `metadata(key PRIMARY KEY, value)` contient `schema_version=7` et
`next_task_id`, prochain ID durable allouable.
- `project(singleton=1, stable_id UNIQUE, name, created_at, updated_at)` décrit
l'unique identité logique de la DB.
- `tasks(task_id PRIMARY KEY, name, task_kind, task_kind_version, saved_state, recovery_state, progress,
sequence_count, started_sec/nsec, finished_sec/nsec, updated_at)` contient le
résumé durable. Les IDs v1 sont compris entre 1 et `INT64_MAX`.
- `checkpoints(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE, path,
format_version, durability, updated_at)` représente `DURABLE` ou
`PUBLISHED_NOT_DURABLE`.
- `artifacts(artifact_id PRIMARY KEY, kind, path, state, size_bytes,
producer_task_id REFERENCES tasks, created_at, updated_at)` inventorie des
fichiers externes. Les états v1 sont `STAGED` et `READY`.
- `scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT, name, created_at, updated_at)`
représente les acquisitions logiques, y compris les ScanSets vides.
- `image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT, sha256 UNIQUE, path UNIQUE,
size_bytes, state, created_at)` décrit les contenus physiques `READY`.
- `images(image_id INTEGER PRIMARY KEY AUTOINCREMENT, scanset_id REFERENCES scansets,
asset_id REFERENCES image_assets, original_name, source_path,
producer_task_id REFERENCES tasks, imported_at)` décrit les images logiques.
`UNIQUE(scanset_id,asset_id)` interdit les doublons de contenu dans une même
acquisition sans fusionner deux acquisitions différentes.
- `image_import_tasks(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE,
source_path, scanset_id REFERENCES scansets)` conserve les paramètres
métier immuables de `import.images`.
- `feature_assets` conserve SHA-256, chemin content-addressed, taille,
durabilité et date de publication des Feature Files hors SQLite.
- `feature_sets` relie image, extracteur/version/fingerprint, hash source,
type/dimension descriptor, compte, producteur et métriques légères
`occupied_cells`, `total_cells`, `coverage_ratio` et densité/Mpx.
- `feature_extract_tasks` conserve les paramètres immuables ORB historiques.
- `sift_extract_tasks` conserve kind `sift`/`rootsift`, version 1, limites,
paramètres OpenCV binary64, grille et fingerprint exacts.
- `feature_support_sets` identifie une consolidation immutable, son image, ses
deux Feature Sets sources, son rayon et son fingerprint.
- `feature_support_groups` conserve position représentative, distance locale
et `support_count`; `feature_support_members` normalise chaque référence
`feature_set_id + feature_index`, sans descriptor SQLite.
- `visual_indexes` conserve l'identité `AUTOINCREMENT`, la configuration
Feature homogène, les paramètres LSH et leurs fingerprints.
- `visual_index_segments` conserve identité `AUTOINCREMENT`, génération,
SHA-256, chemin, taille, compteurs, durabilité et tâche productrice.
- `visual_index_memberships` a pour clé primaire
`(visual_index_id,feature_set_id)` et référence le segment immutable.
- `visual_index_update_tasks` conserve `visual_index_id` et le curseur durable.
Les indexes v6 sont
`visual_index_segments(visual_index_id,generation)` et
`visual_index_memberships(visual_index_segment_id,feature_set_id)`. Les FKs
ciblent `visual_indexes`, `feature_sets`, `visual_index_segments` et `tasks`.
Les postings ne sont jamais stockés dans SQLite.
`AUTOINCREMENT` couvre les identités publiées catalogue, Feature Store et
Visual Index. Il
empêche la réutilisation d'un ID issu d'une transaction validée même si sa ligne
maximale est supprimée plus tard. Le coût de `sqlite_sequence` est accepté pour
garantir qu'un futur Feature Store, match ou track ne voie jamais son identifiant
désigner un autre objet. Les IDs de transactions rollbackées ne sont pas
considérés publiés et peuvent être réutilisés.
Les indexes ajoutés en v4 sont `images(scanset_id,image_id)`, pagination réelle,
et `images(producer_task_id,image_id)`, recherche par tâche productrice. Le
SHA-256 et le chemin asset sont déjà indexés par leurs contraintes `UNIQUE`.
## Schéma v8 implémenté
La migration v7→v8 ajoute la table `candidate_pairs` :
```sql
CREATE TABLE candidate_pairs(
candidate_pair_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(candidate_pair_id>0),
image_id_a INTEGER NOT NULL REFERENCES images(image_id),
image_id_b INTEGER NOT NULL REFERENCES images(image_id),
created_at INTEGER NOT NULL CHECK(created_at>=0),
CHECK(image_id_a < image_id_b),
UNIQUE(image_id_a, image_id_b)
);
CREATE INDEX candidate_pairs_image_a_idx ON candidate_pairs(image_id_a);
CREATE INDEX candidate_pairs_image_b_idx ON candidate_pairs(image_id_b);
```
**Invariants** :
- `image_id_a < image_id_b` : ordre canonique garanti par CHECK SQL
- Self-pairs interdits (impliqué par `image_id_a < image_id_b`)
- `UNIQUE(image_id_a, image_id_b)` : unicité persistante
- `created_at` : timestamp Unix secondes, non-négatif
**API** :
- `lardon3d_project_db_create_candidate_pair()` — INSERT avec canonicalisation
- `lardon3d_project_db_load_candidate_pair()` — SELECT par ID
- `lardon3d_project_db_find_candidate_pair()` — SELECT par (image_a, image_b)
- `lardon3d_project_db_list_candidate_pairs()` — SELECT paginé ORDER BY id
**Notes** :
- Le score et la source ne sont pas persistés dans cette version
- La génération est déterministe pour mêmes entrées/configuration
- L'idempotence est garantie par find avant create
## Schéma v9 implémenté
La migration v8→v9 ajoute la table `candidate_pair_generate_tasks` :
```sql
CREATE TABLE candidate_pair_generate_tasks(
task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,
visual_index_id INTEGER NOT NULL CHECK(visual_index_id>0),
after_feature_set_id INTEGER NOT NULL CHECK(after_feature_set_id>=0),
top_k INTEGER NOT NULL CHECK(top_k>0 AND top_k<=256),
minimum_evidence_count INTEGER NOT NULL CHECK(minimum_evidence_count>=0
AND minimum_evidence_count<=1024),
scanset_filter INTEGER NOT NULL CHECK(scanset_filter>=0 AND scanset_filter<=2),
exclude_same_asset INTEGER NOT NULL CHECK(exclude_same_asset IN (0,1))
);
```
**Invariants** :
- `task_id` référence `tasks(task_id)` avec ON DELETE CASCADE
- `after_feature_set_id` : curseur de reprise, non-négatif
- `top_k` : borné entre 1 et 256 (LARDON3D_VISUAL_INDEX_TOP_K_MAX)
- `minimum_evidence_count` : borné entre 0 et 1024
- `scanset_filter` : 0=ANY, 1=CURRENT, 2=OTHER
**API** :
- `lardon3d_project_db_record_candidate_pair_generate_task()` — UPSERT checkpoint
- `lardon3d_project_db_load_candidate_pair_generate_task()` — SELECT par task_id
## Schéma v10 implémenté
La migration v9→v10 ajoute la table `match_results` pour le Match Result
Model :
```sql
CREATE TABLE match_results(
match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0),
candidate_pair_id INTEGER NOT NULL
REFERENCES candidate_pairs(candidate_pair_id),
feature_set_id_a INTEGER NOT NULL
REFERENCES feature_sets(feature_set_id),
feature_set_id_b INTEGER NOT NULL
REFERENCES feature_sets(feature_set_id),
matcher_kind TEXT NOT NULL
CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64),
matcher_version INTEGER NOT NULL CHECK(matcher_version>0),
parameter_fingerprint BLOB NOT NULL
CHECK(length(parameter_fingerprint)=32),
result_status INTEGER NOT NULL CHECK(result_status IN (0,1)),
match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192),
match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR
length(match_asset_sha256)=32),
match_asset_path TEXT CHECK(match_asset_path IS NULL OR
length(match_asset_path)>0),
match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR
match_asset_size_bytes>0),
created_at INTEGER NOT NULL CHECK(created_at>=0),
CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL
AND match_asset_path IS NULL AND match_asset_size_bytes IS NULL)
OR (result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL
AND match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL)),
UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b,
matcher_kind, matcher_version, parameter_fingerprint)
);
CREATE INDEX match_results_candidate_pair_idx
ON match_results(candidate_pair_id);
CREATE INDEX match_results_feature_set_a_idx
ON match_results(feature_set_id_a);
CREATE INDEX match_results_feature_set_b_idx
ON match_results(feature_set_id_b);
```
**Invariants** :
- `UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)` : identité déterministe 6 parties
- `candidate_pair_id` référence `candidate_pairs(candidate_pair_id)` avec l'action par défaut (NO ACTION)
- `feature_set_id_a` et `feature_set_id_b` référencent `feature_sets(feature_set_id)`
- `feature_set_id_a` appartient à `image_id_a` de la Candidate Pair, `feature_set_id_b` appartient à `image_id_b` (validé par l'API create)
- `NO_MATCH` impose `match_count=0` et aucun asset
- `MATCHED` impose `match_count>0` et SHA/path/taille complets
- les échecs d'exécution restent dans le Task Runtime et ne créent pas de ligne
- `matcher_kind` borné à 64 caractères
- `parameter_fingerprint` exactement 32 octets (SHA-256)
**API** :
- `lardon3d_project_db_create_match_result()` — INSERT avec validation des contraintes
- `lardon3d_project_db_load_match_result()` — SELECT par ID
- `lardon3d_project_db_find_match_result()` — SELECT par (candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)
- `lardon3d_project_db_list_match_results()` — SELECT paginé ORDER BY id
## Ouverture et migrations
Une DB vide reçoit directement le schéma v7 dans une transaction
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
`task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1,
v2, v3, v4, v5 et v6 sont migrées séquentiellement vers v7. Une version future est refusée et une DB contenant
des tables sans métadonnée de version est considérée corrompue. La fonction
interne de migration applique uniquement la chaîne séquentielle connue jusqu'à
v7 ; une valeur hors de 1..7 est refusée.
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
```sql
ALTER TABLE tasks ADD COLUMN task_kind TEXT;
ALTER TABLE tasks ADD COLUMN task_kind_version INTEGER
CHECK(task_kind_version IS NULL OR task_kind_version > 0);
UPDATE metadata SET value=2
WHERE key='schema_version' AND value=1;
```
Migration v2→v3 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
```sql
CREATE TABLE image_import_tasks(
task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,
source_path TEXT NOT NULL
);
INSERT INTO metadata(key,value)
VALUES('next_task_id',(
SELECT CASE
WHEN COALESCE(MAX(task_id),0)>=9223372036854775807 THEN 0
ELSE COALESCE(MAX(task_id),0)+1
END FROM tasks
));
UPDATE metadata SET value=3
WHERE key='schema_version' AND value=2;
```
Migration v3→v4 exacte, dans la même transaction :
```sql
CREATE TABLE scansets(
scanset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scanset_id>0),
name TEXT NOT NULL CHECK(length(name)>0 AND length(name)<256),
created_at INTEGER NOT NULL CHECK(created_at>=0),
updated_at INTEGER NOT NULL CHECK(updated_at>=created_at)
);
CREATE TABLE image_assets(
asset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(asset_id>0),
sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),
path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),
size_bytes INTEGER NOT NULL CHECK(size_bytes>=0),
state INTEGER NOT NULL CHECK(state=1),
created_at INTEGER NOT NULL CHECK(created_at>=0)
);
CREATE TABLE images(
image_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(image_id>0),
scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),
asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),
original_name TEXT NOT NULL CHECK(length(original_name)>0 AND length(original_name)<256),
source_path TEXT NOT NULL CHECK(length(source_path)>0 AND length(source_path)<4096),
producer_task_id INTEGER REFERENCES tasks(task_id),
imported_at INTEGER NOT NULL CHECK(imported_at>=0),
UNIQUE(scanset_id,asset_id)
);
CREATE INDEX images_scanset_idx ON images(scanset_id,image_id);
CREATE INDEX images_producer_idx ON images(producer_task_id,image_id);
ALTER TABLE image_import_tasks
ADD COLUMN scanset_id INTEGER REFERENCES scansets(scanset_id);
INSERT INTO scansets(name,created_at,updated_at)
SELECT 'Imports antérieurs à ScanSet v1',0,0
WHERE EXISTS(SELECT 1 FROM image_import_tasks);
UPDATE image_import_tasks
SET scanset_id=(SELECT scanset_id FROM scansets
WHERE name='Imports antérieurs à ScanSet v1'
ORDER BY scanset_id LIMIT 1)
WHERE scanset_id IS NULL;
INSERT INTO metadata(key,value)
VALUES('legacy_image_catalog_pending',
CASE WHEN EXISTS(SELECT 1 FROM image_import_tasks) THEN 1 ELSE 0 END);
UPDATE metadata SET value=4
WHERE key='schema_version' AND value=3;
```
Configuration v7 : `foreign_keys=ON`, `journal_mode=DELETE`,
`synchronous=FULL`, `busy_timeout=5000`. Le mode DELETE convient au propriétaire
unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation
forte. Le timeout borne l'attente d'un verrou externe à cinq secondes.
## Concurrence et ownership
Une connexion opaque est sérialisée par un mutex interne. Chaque opération
composée possède sa transaction entière ; aucune transaction publique ne peut
rester ouverte entre deux appels. Aucune I/O d'artefact n'a lieu sous le mutex :
le module confirme que le fichier publié et validé par l'appelant est régulier
avant la mise à jour `READY`. Fermer la DB pendant un
appel concurrent est interdit au propriétaire.
Les records et chaînes sont copiés dans des buffers fournis par l'appelant ;
aucun pointeur SQLite n'en sort. Tous les statements sont finalisés dans
l'appel. La liste de reprise utilise des pages fournies par l'appelant, limitées
à 256 entrées. Elle ne retourne que les tâches normalisées `PENDING` possédant
une référence checkpoint ; le fichier doit encore être chargé et validé.
## Branchement au projet
`Lardon3DAppState` est l'instance projet runtime actuelle et possède exactement
une `Lardon3DProjectDb *` pendant que `project_loaded` est vrai. La DB canonique
est `<project_root>/project.db`. Elle est ouverte à la création/ouverture du
projet et fermée une seule fois par `lardon3d_project_close()`. À l'arrêt de
l'application, la task queue est arrêtée avant la fermeture du projet et de sa
DB. Une fermeture concurrente à un appel projet/DB est interdite au propriétaire.
`project.ini` v2 contient `name`, `stable_id` hexadécimal sur 128 bits et
`version=2`. La même identité est enregistrée dans la table `project`. Toute
divergence est une erreur. Un INI v1 sans identité adopte l'identité d'une DB
existante ; sans DB, une identité est générée une seule fois puis l'INI est
migré atomiquement avant de devenir la référence des ouvertures suivantes. Une
DB existante sans ligne projet ne peut être initialisée que si l'INI possède
déjà son identité.
Les checkpoints sont référencés par chemins relatifs portables :
`.lardon3d/checkpoints/<task_id>.chk`. L'inventaire projet pagine les tâches DB,
résout ce chemin sous la racine, charge le checkpoint et vérifie la cohérence du
snapshot avec le résumé DB.
Après copie du record hors mutex SQLite, l'inventaire consulte la registry. Il
distingue `LEGACY_UNTYPED`, `UNKNOWN_TASK_KIND` et
`UNSUPPORTED_TASK_KIND_VERSION`. Aucun reconstructeur métier n'est appelé sous
le mutex DB. Un upsert ne peut pas changer le couple kind/version d'un task ID.
À l'ouverture, le projet lit des pages de 8 dans l'ordre croissant des task
IDs. Chaque record est copié hors mutex DB avant reconstruction et enqueue. Une
fenêtre pleine interrompt le scan sans modifier les tâches restantes. Le résumé
borné expose `inspected`, `resumed`, `skipped`, `failed`, le nombre de
checkpoints `PUBLISHED_NOT_DURABLE` repris et la saturation éventuelle.
Les erreurs d'ouverture/migration, de schéma ou d'identité restent fatales.
Les erreurs propres à une tâche — legacy, kind inconnu/futur, checkpoint
absent/invalide/futur, source indisponible ou reconstruction — sont non fatales.
Un `BUSY` après le timeout SQLite arrête le scan sans boucle et laisse le projet
ouvert.
## Statut
**IMPLEMENTED** — SQLite système, schéma v10 et migrations v1→v2→v3→v4→v5→v6→v7→v8→v9→v10, identité
projet, transactions tâche+checkpoint, pagination de reprise et artefacts
génériques.
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,
publication de checkpoints par le projet et inventaire de reprise validé.
**IMPLEMENTED** — kinds persistants, classification par registry et
reconstruction explicite testée hors scheduler.
**IMPLEMENTED** — allocation transactionnelle de task IDs, paramètres
immuables de `import.images` et reconstruction production explicite.
**IMPLEMENTED** — reprise automatique sélective, pagination de 8, ordre par ID,
fenêtre de queue non bloquante et résumé consultable.
**IMPLEMENTED** — ScanSets, images logiques, assets SHA-256 et pagination
bornée à 256. Les identités sont des `INTEGER PRIMARY KEY AUTOINCREMENT` SQLite
allouées sous transaction ; aucun `SELECT MAX()+1` n'est utilisé en
fonctionnement normal et une identité validée n'est jamais réutilisée.
La migration v3→v4 ne lit pas `manifest.tsv`. Elle crée le ScanSet legacy et
positionne `metadata.legacy_image_catalog_pending=1` dès qu'une ancienne tâche
d'import existe. Cet indicateur signifie « données legacy potentiellement non
cataloguées », pas « images migrées ».
**NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
indisponibles, migration de la TUI legacy et réconciliation des fichiers
orphelins et compaction Visual Index. Feature Store et Visual Index v1 sont implémentés.
Visual Index v1 borne un index à 256 segments de 16 memberships, soit 4096 Feature Sets;
la couverture de 50 000 Feature Sets nécessitera la compaction ou une évolution v2.
**PLANNED** — dépendances d'artefacts, graphe géométrique et
reconstruction incrémentale.