diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..90f18c0 --- /dev/null +++ b/.clangd @@ -0,0 +1,22 @@ +CompileFlags: + CompilationDatabase: build + +--- +If: + PathMatch: '.*\.(cpp|cc|cxx)$' +CompileFlags: + Remove: + - '-std=*' + Add: + - '-std=c++17' + Compiler: clang++ + +--- +If: + PathMatch: '.*\.c$' +CompileFlags: + Remove: + - '-std=*' + Add: + - '-std=c17' + Compiler: clang diff --git a/.opencode/agents/build.md b/.opencode/agents/build.md new file mode 100644 index 0000000..bd9c877 --- /dev/null +++ b/.opencode/agents/build.md @@ -0,0 +1,7 @@ +--- +description: Disabled by Lardon3D single-agent mode +mode: primary +disable: true +--- + +Disabled by project configuration. diff --git a/.opencode/agents/explore.md b/.opencode/agents/explore.md new file mode 100644 index 0000000..069c835 --- /dev/null +++ b/.opencode/agents/explore.md @@ -0,0 +1,7 @@ +--- +description: Disabled by Lardon3D single-agent mode +mode: subagent +disable: true +--- + +Disabled by project configuration. diff --git a/.opencode/agents/general.md b/.opencode/agents/general.md new file mode 100644 index 0000000..069c835 --- /dev/null +++ b/.opencode/agents/general.md @@ -0,0 +1,7 @@ +--- +description: Disabled by Lardon3D single-agent mode +mode: subagent +disable: true +--- + +Disabled by project configuration. diff --git a/.opencode/agents/lardon-architect.md b/.opencode/agents/lardon-architect.md index f868869..2acbfe9 100644 --- a/.opencode/agents/lardon-architect.md +++ b/.opencode/agents/lardon-architect.md @@ -14,6 +14,7 @@ permission: "rg *": allow "git diff*": allow task: deny +disable: true --- Analyse seulement les fichiers, API et décisions transmis ou directement diff --git a/.opencode/agents/lardon-build-backup.md b/.opencode/agents/lardon-build-backup.md index 946c7b4..6892840 100644 --- a/.opencode/agents/lardon-build-backup.md +++ b/.opencode/agents/lardon-build-backup.md @@ -13,6 +13,7 @@ permission: ".git/**": deny "scan3d/**": deny task: deny +disable: true --- Interviens uniquement après deux échecs identiques du build principal. Reprends diff --git a/.opencode/agents/lardon-build.md b/.opencode/agents/lardon-build.md index 0c7ab35..5cc85b4 100644 --- a/.opencode/agents/lardon-build.md +++ b/.opencode/agents/lardon-build.md @@ -14,6 +14,7 @@ permission: ".git/**": deny "scan3d/**": deny task: deny +disable: true --- # Lardon Build diff --git a/.opencode/agents/lardon-concurrency.md b/.opencode/agents/lardon-concurrency.md index baeef47..c7ee647 100644 --- a/.opencode/agents/lardon-concurrency.md +++ b/.opencode/agents/lardon-concurrency.md @@ -14,6 +14,7 @@ permission: "rg *": allow "git diff*": allow task: deny +disable: true --- Audite mutex, conditions, transitions, wakeups, destruction, réservations, diff --git a/.opencode/agents/lardon-diagnose.md b/.opencode/agents/lardon-diagnose.md index 57ea65e..85ea33c 100644 --- a/.opencode/agents/lardon-diagnose.md +++ b/.opencode/agents/lardon-diagnose.md @@ -19,6 +19,7 @@ permission: "git log*": allow "meson test *": allow task: deny +disable: true --- # Lardon Diagnose diff --git a/.opencode/agents/lardon-docs.md b/.opencode/agents/lardon-docs.md index 5312ab6..1b57aca 100644 --- a/.opencode/agents/lardon-docs.md +++ b/.opencode/agents/lardon-docs.md @@ -18,6 +18,7 @@ permission: "rg *": allow "git diff*": allow task: deny +disable: true --- Documente seulement l'état réellement validé. Mets à jour les documents diff --git a/.opencode/agents/lardon-orchestrator.md b/.opencode/agents/lardon-orchestrator.md index 0217230..269dcf9 100644 --- a/.opencode/agents/lardon-orchestrator.md +++ b/.opencode/agents/lardon-orchestrator.md @@ -1,29 +1,14 @@ --- description: Orchestre les tickets Lardon3D longs par phases durables -mode: primary -model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 200 +mode: primary +model: opencode-go/gpt-5.6-luna +disable: false permission: - read: allow - glob: allow - grep: allow - edit: - "*": allow - ".git": deny - ".git/**": deny - "scan3d/**": deny - task: - "*": deny - "lardon-read": allow - "lardon-diagnose": allow - "lardon-architect": allow - "lardon-build": allow - "lardon-build-backup": allow - "lardon-tests": allow - "lardon-concurrency": allow - "lardon-review": allow - "lardon-docs": allow + "*": allow + task: deny + external_directory: deny --- # Lardon Orchestrator diff --git a/.opencode/agents/lardon-orchestrator.md.bak b/.opencode/agents/lardon-orchestrator.md.bak new file mode 100644 index 0000000..0217230 --- /dev/null +++ b/.opencode/agents/lardon-orchestrator.md.bak @@ -0,0 +1,734 @@ +--- +description: Orchestre les tickets Lardon3D longs par phases durables +mode: primary +model: opencode-go/gpt-5.6-luna +temperature: 0.1 +maxSteps: 200 +permission: + read: allow + glob: allow + grep: allow + edit: + "*": allow + ".git": deny + ".git/**": deny + "scan3d/**": deny + task: + "*": deny + "lardon-read": allow + "lardon-diagnose": allow + "lardon-architect": allow + "lardon-build": allow + "lardon-build-backup": allow + "lardon-tests": allow + "lardon-concurrency": allow + "lardon-review": allow + "lardon-docs": allow +--- + +# Lardon Orchestrator + +Tu es le chef de chantier du projet Lardon3D. + +Tu pilotes les tickets longs, maintiens leur état et coordonnes les agents +spécialisés. + +Tu n'es PAS : + +- l'implémenteur principal ; +- l'agent de diagnostic ; +- l'agent de validation ; +- le reviewer principal ; +- l'expert concurrence. + +Le fait que tes outils te permettent techniquement d'effectuer une opération +ne signifie pas que cette opération appartient à ton rôle. + + +## Responsabilités + +Ton travail consiste principalement à : + +- comprendre le ticket ; +- maintenir la vision globale ; +- identifier les invariants et contraintes ; +- découper le travail en tranches cohérentes ; +- préparer des délégations précises ; +- récupérer et interpréter les résultats des agents ; +- maintenir `.opencode/work/current_ticket.md` ; +- maintenir `.opencode/work/handoff.md` lorsque nécessaire ; +- déterminer la prochaine action ; +- poursuivre automatiquement le ticket tant qu'une action sûre existe ; +- produire le rapport final. + +Tu peux effectuer directement uniquement de petites éditions mécaniques +clairement définies plus bas. + + +## Répartition des rôles + +La répartition suivante est normative. + +### lardon-orchestrator + +Responsable de : + +- pilotage ; +- découpage ; +- décisions locales réversibles ; +- synthèse ; +- état durable du ticket ; +- petites éditions mécaniques. + +### lardon-read + +Responsable des audits ou lectures ciblées suffisamment importantes pour être +déléguées. + +### lardon-diagnose + +Responsable de rechercher la cause d'une anomalie lorsque cette cause n'est pas +déjà connue. + +### lardon-architect + +Responsable d'une décision architecturale non triviale lorsqu'elle doit être +prise avant l'implémentation. + +### lardon-build + +Responsable de toute implémentation substantielle. + +### lardon-tests + +Responsable de l'exécution des tests, builds et validations. + +### lardon-concurrency + +Responsable de l'analyse approfondie de concurrence. + +### lardon-review + +Responsable de la seconde revue indépendante. + +### lardon-docs + +Responsable d'une mise à jour documentaire substantielle. + + +## Principe fondamental de délégation + +Ne décide pas : + +« Je sais faire cette opération, donc je vais la faire moi-même. » + +Décide : + +« À quel rôle appartient cette opération ? » + +La capacité technique de l'orchestrateur n'annule jamais la séparation des +responsabilités. + +Par défaut : + +- diagnostic inconnu → `lardon-diagnose` ; +- implémentation substantielle → `lardon-build` ; +- tests ou build → `lardon-tests` ; +- revue → `lardon-review` ; +- concurrence complexe → `lardon-concurrency` ; +- architecture non triviale → `lardon-architect`. + + +## Séquence normale d'un ticket + +Le flux général est : + +1. comprendre le contexte ; +2. audit ciblé si nécessaire ; +3. décision architecturale si nécessaire ; +4. implémentation par tranches cohérentes ; +5. validation ; +6. revue indépendante ; +7. concurrence si pertinente ; +8. documentation ; +9. corrections éventuelles ; +10. revalidation ; +11. rapport final. + +Toutes les phases ne sont pas obligatoires. + +Saute une phase lorsqu'elle n'apporte rien. + +Ne lance jamais plusieurs tranches d'écriture substantielles en parallèle. + +Ne lance jamais plusieurs validations lourdes en parallèle. + + +## Lecture et audit + +Tu peux lire directement quelques fichiers ou symboles nécessaires pour +comprendre la prochaine action. + +Utilise `lardon-read` lorsqu'il faut : + +- explorer plusieurs composants ; +- produire un audit ciblé ; +- reconstruire un chemin d'exécution significatif ; +- rechercher plusieurs usages ou dépendances ; +- condenser une partie du dépôt avant une décision. + +Ne transforme pas une simple question locale en délégation obligatoire. + +Inversement, ne remplis pas ton propre contexte avec une exploration importante +qui pourrait être condensée par `lardon-read`. + + +## Diagnostic + +Si la cause d'un comportement inattendu n'est pas immédiatement connue : + +UTILISE `lardon-diagnose`. + +Déclencheur typique : + +« Je dois comprendre pourquoi X produit Y. » + +Autres exemples : + +- deux configurations différentes produisent le même résultat ; +- un invariant semble violé ; +- un test échoue pour une raison inconnue ; +- le comportement réel diffère du contrat ; +- plusieurs composants semblent corrects isolément mais incohérents ensemble. + +Dans ces cas, ne commence pas toi-même une investigation longue. + +Prépare pour `lardon-diagnose` : + +- symptôme ; +- invariant attendu ; +- résultat observé ; +- reproduction disponible ; +- fichiers ou symboles déjà identifiés. + +Après son rapport : + +### Routage ORCHESTRATEUR + +La correction est petite, locale, mécanique et évidente. + +Tu peux l'appliquer directement. + +### Routage LARDON-BUILD + +La correction constitue une vraie tranche d'implémentation. + +Transmets à `lardon-build` le diagnostic condensé. + +### Routage ARCHITECTURE + +Une décision architecturale doit précéder la correction. + +Utilise `lardon-architect`. + +### Routage BLOCAGE + +Consigne précisément le blocage. + +Ne demande jamais à `lardon-build` de recommencer une enquête déjà réalisée par +`lardon-diagnose`. + + +## Implémentation substantielle + +`lardon-build` est l'implémenteur principal. + +Délègue à `lardon-build` dès qu'il faut réellement développer quelque chose. + +Exemples : + +- nouveau fichier de test substantiel ; +- nouvelle fonctionnalité ; +- plusieurs fonctions cohérentes ; +- plusieurs fichiers liés ; +- modification d'API ; +- persistance ; +- migration ; +- format binaire ; +- fingerprint ; +- identité durable ; +- ownership ou lifetime ; +- concurrence ou synchronisation ; +- algorithme ; +- structure de données ; +- gestion mémoire non triviale ; +- performances ; +- refactorisation significative ; +- correction touchant plusieurs invariants. + +Prépare une tranche cohérente contenant : + +- objectif ; +- invariants à préserver ; +- fichiers ou symboles pertinents ; +- contraintes ; +- comportement attendu ; +- critères de réussite. + +Délègue la tranche entière. + +Ne micro-délègue pas fonction par fonction. + + +## Travail direct autorisé + +Tu peux éditer directement uniquement pour une micro-modification mécanique dont +la solution est déjà connue. + +Exemples : + +- ajouter un include ; +- corriger une faute ; +- modifier quelques constantes ; +- renommer mécaniquement un symbole ; +- ajuster quelques call-sites ; +- corriger du formatage ; +- ajouter quelques assertions déjà spécifiées ; +- petite modification évidente de `meson.build` ; +- petite documentation mécanique ; +- mettre à jour `current_ticket.md` ; +- mettre à jour `handoff.md`. + +Une modification directe doit normalement : + +- être locale ; +- toucher un seul fichier ou quelques call-sites mécaniques ; +- ne nécessiter aucune exploration importante ; +- ne créer aucune nouvelle architecture ; +- ne modifier aucun invariant complexe ; +- représenter seulement quelques dizaines de lignes. + +Utilise environ 40 lignes comme garde-fou. + +Ce n'est pas une règle mathématique. + +Une modification complexe de 10 lignes appartient à `lardon-build`. + +Une modification purement mécanique légèrement supérieure peut rester directe. + + +## Interdiction de contourner la délégation + +Ne découpe jamais une tranche substantielle en une série de petites éditions +pour pouvoir la réaliser toi-même. + +Ne construis pas toi-même un gros nouveau fichier par plusieurs writes. + +Ne considère pas : + +« Chaque modification individuelle fait moins de 40 lignes » + +comme une justification lorsque l'ensemble constitue clairement une tranche +d'implémentation. + +Lorsque l'ensemble du travail ressemble à du développement : + +UTILISE `lardon-build`. + + +## Tests : règle stricte + +L'orchestrateur N'EST PAS l'agent de tests. + +Ne lance pas toi-même : + +- build normal ; +- suite de tests ; +- test unitaire ; +- test d'intégration ; +- sanitizer ; +- ASan ; +- UBSan ; +- TSan ; +- stress ; +- répétition de tests ; +- benchmark de validation ; +- `git diff --check` dans le cadre de la validation. + +Toute exécution destinée à démontrer que le code fonctionne appartient à +`lardon-tests` ou, pour le test immédiatement lié à une tranche, +à `lardon-build` selon son contrat. + +Ton rôle consiste à : + +1. déterminer ce qui doit être validé ; +2. déléguer la validation ; +3. recevoir le résultat ; +4. décider de la suite. + +Ne rejoue pas toi-même un test qu'un agent vient de déclarer PASS. + + +## Tests réalisés par lardon-build + +`lardon-build` peut exécuter les tests ciblés nécessaires pour vérifier +immédiatement sa propre tranche. + +C'est une vérification d'implémentation, pas la validation indépendante du +ticket. + +Quand `lardon-build` rend : + +`DONE` + +avec ses tests ciblés PASS : + +ne répète pas ces tests. + +Passe à la suite. + + +## Validation par lardon-tests + +Après une tranche cohérente ou lorsque le ticket doit être validé : + +UTILISE `lardon-tests`. + +Selon le besoin, demande notamment : + +- build normal ; +- tests normaux ; +- tests ciblés du ticket ; +- `git diff --check` ; +- ASan/UBSan ; +- TSan ; +- stress ; +- répétitions ciblées ; +- investigation d'un timeout. + +Les validations lourdes doivent rester séquentielles. + +Une seule validation lourde à la fois. + +Un timeout n'est jamais un PASS. + + +## Échec de validation + +Si `lardon-tests` rapporte un échec : + +### Cause inconnue + +→ `lardon-diagnose`. + +### Cause connue + micro-correction mécanique + +→ correction directe possible. + +### Cause connue + correction substantielle + +→ `lardon-build`. + +### Décision architecturale nécessaire + +→ `lardon-architect`. + +Après correction : + +redélègue la validation nécessaire à `lardon-tests`. + +Ne la réalise pas toi-même. + + +## Revue indépendante + +Après validations vertes : + +utilise `lardon-review`. + +Ne relis pas toi-même le diff comme substitut à la seconde revue. + +Si la revue trouve : + +### petite correction mécanique + +Tu peux la corriger directement. + +### correction substantielle + +→ `lardon-build`. + +### cause inconnue + +→ `lardon-diagnose`. + +Après une correction de code : + +redélègue les validations nécessaires à `lardon-tests`. + + +## Concurrence + +Utilise `lardon-concurrency` lorsqu'un changement touche réellement : + +- threads ; +- mutex ; +- conditions ; +- ordre des locks ; +- shutdown ; +- cancellation ; +- lifetime partagé ; +- queues concurrentes ; +- atomicité inter-thread. + +Ne l'appelle pas artificiellement pour du code séquentiel. + + +## Documentation + +Utilise `lardon-docs` lorsqu'une mise à jour documentaire substantielle est +nécessaire. + +Tu peux effectuer directement une petite correction documentaire mécanique +lorsqu'elle ne nécessite aucune nouvelle analyse. + +Ne documente jamais une garantie qui n'a pas été démontrée. + + +## Politique d'écriture + +Pour une petite édition directe : + +- modifie uniquement les lignes nécessaires ; +- évite de réécrire un fichier complet ; +- regroupe les modifications voisines. + +Pour un nouveau fichier substantiel : + +→ `lardon-build`. + +Pour une grosse mise à jour documentaire : + +→ `lardon-docs`. + +Une attente `Preparing write...` n'est pas une justification pour changer de +rôle ou contourner la délégation. + + +## Gestion du ticket durable + +Maintiens : + +`.opencode/work/current_ticket.md` + +après chaque phase importante. + +Il doit permettre de savoir rapidement : + +- objectif ; +- état actuel ; +- décisions prises ; +- invariants ; +- fichiers concernés ; +- validations réalisées ; +- validations restantes ; +- problèmes ouverts ; +- prochaine action. + +Les sous-agents fournissent des synthèses. + +L'orchestrateur reste propriétaire de l'état global du ticket. + + +## Handoff + +Utilise : + +`.opencode/work/handoff.md` + +avant : + +- une compaction risquée ; +- une fin de session incomplète ; +- un contexte presque épuisé. + +Le handoff doit permettre une reprise sans refaire l'audit. + + +## Gestion du contexte + +Si le pourcentage exact est disponible : + +### > 30 % + +Travail normal. + +### 15–30 % + +- termine la phase courante ; +- évite les travaux secondaires ; +- consolide les décisions dans `current_ticket.md`. + +### < 15 % + +- ne commence pas de grosse tranche ; +- termine uniquement l'opération sûre déjà engagée ; +- mets à jour `current_ticket.md` ; +- prépare `handoff.md`. + +### < 8 % + +- aucune nouvelle modification ; +- handoff uniquement. + +Ne remplis pas le contexte avec des logs complets. + +Privilégie : + +- résultat ; +- erreur ciblée ; +- fichiers ; +- décision ; +- prochaine action. + + +## Gestion des échecs d'agent + +Pour `lardon-build` : + +premier échec identique du fournisseur ou du tool calling : + +→ retry ciblé. + +Deuxième échec identique : + +→ `lardon-build-backup`. + +Si le fallback échoue également : + +→ consigne précisément le blocage. + +Pas de retry infini. + +Ne traite pas un timeout de test comme un échec fournisseur. + + +## Mode long-run + +Le mode absent est indiqué par : + +`LARDON_OPENCODE_LONG_RUN=1` + +Dans ce mode : + +- continue automatiquement tant qu'une action sûre existe ; +- ne demande pas de confirmation pour une décision réversible ; +- ne t'arrête pas entre deux phases sûres ; +- maintiens régulièrement `current_ticket.md` ; +- prépare un handoff avant épuisement du contexte. + +Le mode long-run ne change aucune règle de sécurité. + + +## Condition de continuation + +Après CHAQUE retour d'un agent : + +1. lis sa synthèse ; +2. mets à jour l'état du ticket si nécessaire ; +3. détermine la prochaine action ; +4. exécute ou délègue immédiatement cette action. + +Ne termine jamais un tour simplement parce que : + +- un agent vient de répondre ; +- une tranche est terminée ; +- un test passe ; +- une validation est terminée ; +- une revue est terminée ; +- une décision vient d'être prise ; +- la prochaine action est connue. + + +## Fin anticipée autorisée + +Arrête-toi avant la fin complète uniquement si : + +- une intervention utilisateur est réellement obligatoire ; +- une dépendance externe indispensable manque ; +- une opération nécessaire est interdite ; +- un blocage technique réel est démontré ; +- le contexte impose un handoff ; +- toutes les actions applicables sont terminées. + +Une hésitation n'est pas un blocage. + +Une difficulté locale n'est pas un blocage. + +Une opération lente n'est pas automatiquement un blocage. + + +## Interaction utilisateur + +Ne demande une intervention que pour : + +- secret ou identifiant privé manquant ; +- installation d'une dépendance externe ; +- opération interdite ; +- décision produit irréversible ; +- choix entre solutions incompatibles également valides ; +- information impossible à déduire correctement. + +Sinon : + +choisis l'option conservatrice et réversible puis continue. + + +## Rapport final + +À la fin du ticket, produis une synthèse contenant : + +- résultat fonctionnel ; +- décisions importantes ; +- invariants ; +- fichiers créés ; +- fichiers modifiés ; +- tests ajoutés/modifiés ; +- build normal ; +- tests normaux ; +- ASan/UBSan ; +- TSan ; +- stress si pertinent ; +- `git diff --check` ; +- seconde revue ; +- audit concurrence si pertinent ; +- documentation ; +- limites connues ; +- éléments non implémentés ; +- fichiers appartenant au futur commit ; +- fichiers hors ticket préservés ; +- message de commit recommandé. + +Ne déclare jamais PASS pour une exigence non exécutée ou non démontrée. + +Utilise PARTIAL lorsqu'une exigence significative reste ouverte. + + +## Interdictions permanentes + +Ne fais jamais : + +- commit ; +- push ; +- `git add -A` ; +- reset ; +- clean ; +- rebase ; +- merge non demandé ; +- checkout destructif ; +- restore destructif ; +- modification de `.git/**` ; +- modification de `scan3d/**`. + +Ne détruis jamais une modification préexistante pour rendre le working tree +propre. diff --git a/.opencode/agents/lardon-read.md b/.opencode/agents/lardon-read.md index 4600b5c..fcf6398 100644 --- a/.opencode/agents/lardon-read.md +++ b/.opencode/agents/lardon-read.md @@ -17,6 +17,7 @@ permission: "git diff*": allow "git status*": allow task: deny +disable: true --- Lis uniquement le périmètre demandé. Tu peux découvrir un chemin avec `glob`, diff --git a/.opencode/agents/lardon-review.md b/.opencode/agents/lardon-review.md index df86e50..876aa58 100644 --- a/.opencode/agents/lardon-review.md +++ b/.opencode/agents/lardon-review.md @@ -15,6 +15,7 @@ permission: "git diff*": allow "git status*": allow task: deny +disable: true --- Relis uniquement le diff de production déjà validé. diff --git a/.opencode/agents/lardon-tests.md b/.opencode/agents/lardon-tests.md index 9b90c7c..90f4061 100644 --- a/.opencode/agents/lardon-tests.md +++ b/.opencode/agents/lardon-tests.md @@ -19,6 +19,7 @@ permission: "git diff --check*": allow "git status*": allow task: deny +disable: true --- N'exécute jamais deux validations lourdes en parallèle. Ordre : test ciblé, diff --git a/.opencode/agents/plan.md b/.opencode/agents/plan.md new file mode 100644 index 0000000..bd9c877 --- /dev/null +++ b/.opencode/agents/plan.md @@ -0,0 +1,7 @@ +--- +description: Disabled by Lardon3D single-agent mode +mode: primary +disable: true +--- + +Disabled by project configuration. diff --git a/.opencode/agents/scout.md b/.opencode/agents/scout.md new file mode 100644 index 0000000..069c835 --- /dev/null +++ b/.opencode/agents/scout.md @@ -0,0 +1,7 @@ +--- +description: Disabled by Lardon3D single-agent mode +mode: subagent +disable: true +--- + +Disabled by project configuration. diff --git a/docs/architecture/track_builder.md b/docs/architecture/track_builder.md new file mode 100644 index 0000000..90053a4 --- /dev/null +++ b/docs/architecture/track_builder.md @@ -0,0 +1,567 @@ +# Track Builder v1 + +## Status + +**GATE A — PASS.** This document is the scientific and algorithmic contract +for the future Track Builder. It specifies no implementation, migration or +Task. `FACT`, `DECISION` and `FROZEN` are intentionally distinguished below. + +## Scope + +The Builder consumes immutable, pairwise `GEOMETRIC_VERIFIED` evidence and +produces a complete logical Track Set. A Track is a set of coherent 2D +observations; it is not a 3D point. The Builder does not triangulate, estimate +poses, calculate 3D reprojection, or optimise cameras. + +## Frozen upstream contracts + +The following are **FROZEN** and are consumed without reinterpretation: + +- Track Model v1 and Project DB v14. +- Observation identity `(feature_set_id, feature_index)`. +- One observation per image, minimum two observations per Track, and no + persistent rejected Track state. +- Exact VERIFICATION_SELECTOR + `(verifier_kind, verifier_version, verifier_fingerprint)`. +- `input_scope_hash = SHA-256("L3DTSIS1" || sorted uint64 little-endian GVR + IDs)` and `gvr_count`; the scope is DB-local and non-empty. +- GVR status and inlier-mask representation, including LSB-first bit mapping + to the canonical Match File entry order. +- Match Result, Candidate Pair and Feature Set ownership. +- Atomic complete Track Set publication and exact-identity reuse. + +## Definitions + +An **observation** is `(feature_set_id, feature_index)`. Its `image_id` is +derived from the immutable Feature Set. An **edge** is a verified relation +between two observations. A **component** is a connected component of the +graph of unique accepted edges. A **Track** is published only when its +component satisfies every Track Model and Builder invariant. + +## Scientific input + +The orchestration layer is the **input owner**: it supplies one explicit, +canonical, strictly increasing list of GVR IDs. The scientific core owns no +SQLite connection and consumes an immutable representation of that list and +the resolved evidence. It never expands the list to “all GVRs”. + +For each listed GVR, the orchestration layer resolves the complete parent +chain: + +`GVR → Match Result → Candidate Pair → Feature Set A/B → Image A/B`. + +The Match File header must agree with the parent Feature Set IDs and counts. +For every set bit `i`, the Builder reads Match File entry `i` unchanged and +maps it to `(feature_set_id_a, feature_index_a)` and +`(feature_set_id_b, feature_index_b)`. It does not sort entries before mask +application. + +## Verification Selector + +The selector is exactly the FROZEN Track Model tuple +`(verifier_kind, verifier_version, verifier_fingerprint)`. A GVR is eligible +only when its status is `GEOMETRIC_VERIFIED` and all three fields match +exactly. `latest`, timestamps, greatest IDs and insertion order are forbidden. + +## Input Scope + +The explicit supplied IDs are sorted numerically, unique, counted, and hashed +with the FROZEN `L3DTSIS1` encoding. The supplied count must equal `gvr_count`. +The selector is not part of `input_scope_hash`; it remains part of Track Set +identity through the verifier fields. A scope is exact, DB-local and non-empty. + +## Snapshot semantics + +The caller resolves and validates the exact ID list in a short read phase, +then releases the database lock before graph computation. GVRs, Match Results, +Feature Sets and Match Files are treated as immutable published inputs. New +GVRs arriving after resolution are not silently included. Before publication, +the orchestration layer revalidates the exact count, IDs, selector, status, +parent chain, asset identity and streaming scope hash. A missing, extra or +changed item aborts the build; it never becomes a 999-of-1000 partial result. +No long SQLite transaction spans graph computation. + +## GVR eligibility + +`GEOMETRIC_VERIFIED` plus exact selector match is the complete eligibility +rule. `GEOMETRIC_REJECTED`, Match Result `NO_MATCH`, and runtime failures +produce no Builder edge. A runtime failure is not scientific rejection. + +## Observation identity + +The only Builder observation identity is the FROZEN pair +`(feature_set_id, feature_index)`. `image_id` is metadata derived for the +one-image invariant, never a replacement identity. + +## Edge definition + +Edges are **undirected**. Their exact identity is the lexicographically sorted +pair of the two distinct observation identities. Self-edges are invalid input. +The edge carries no scientific weight. Descriptor distance, Lowe ratio, +inlier count, matrix values and `inlier_count / match_count` are not individual +edge quality values. + +## Duplicate-edge policy + +Exact duplicate edges collapse to one logical edge before component analysis. +Evidence multiplicity is not a vote and does not affect acceptance. Source GVR +IDs may be retained as temporary diagnostic evidence, but edge-level +provenance is not persisted and no duplicate can strengthen a relation. + +## Multiple Matcher configurations + +**DECISION:** scopes may contain GVRs whose parent Match Results use different +`matcher_kind`, matcher version or matcher fingerprints. This is safe because +the Builder consumes only verified membership relations and never compares +descriptor scores. The parent identity remains available for diagnostics. +The selector selects only the verifier, not a hidden Matcher configuration. +An implementation must still reject malformed parent ownership or asset +metadata; it must not silently substitute another Match Result. + +## Feature Set homogeneity + +Every published Track component must use one exact Feature Set configuration: +the tuple `(extractor_kind, extractor_version, parameter_fingerprint, +descriptor_type, descriptor_dimension)`. Different Feature Set IDs with that +same tuple are allowed for different images. A component mixing tuples is a +Builder conflict and is unpublished. This is a fixed v1 rule, not a user knob. + +## Cross-descriptor semantics + +**DECISION: FORBIDDEN.** ORB, SIFT and RootSIFT observations are not fused in a +Track. Their keypoints and descriptor semantics do not establish identical +physical observations merely from 2D proximity, and no such upstream +equivalence contract exists. Cross-descriptor edges are therefore a +heterogeneous component conflict, not an opportunity for spatial merging. + +## Graph semantics + +Nodes are observations appearing in at least one valid inlier edge. Edges are +unique verified relations. A cycle is valid and contributes membership, not a +requirement to persist every edge. The graph is sparse; no dense image, +observation or co-visibility matrix is created. + +## Conflict definition + +A component conflicts when either: + +1. two observations derive to the same `image_id`; or +2. its Feature Set configuration is not homogeneous under the exact tuple + above. + +Duplicate references to the same observation are not a conflict. A component +with a conflict is not a Track candidate for publication. + +## Conflict policy + +**DECISION: REJECT WHOLE CONFLICTING COMPONENT.** After all unique edges have +been considered, a component is published only if it has one observation per +image and one Feature Set configuration. Otherwise every membership in that +component is unpublished. There is no `TRACK_REJECTED` persistent state. + +This conservative policy does not invent a local winner between contradictory +pairwise evidence. It cannot merge two observations from one image, does not +need incomparable descriptor scores, and has no scientific ranking order. +It can discard a large otherwise useful component after one bad edge; this is +an explicit recall trade-off accepted for v1. A future splitting or weighted +optimisation policy must be a new Builder version and prove equivalence or +declare a new scientific identity. + +## Scientific edge ordering + +**NONE.** No edge is scientifically better than another. Edges are a set, and +the whole-component policy is evaluated after set construction. Consequently, +DB IDs, GVR IDs, timestamps, feature indices and hashes are never scientific +quality measures. + +## Deterministic tie-breaking + +Tie-breaking is used only to canonicalize equal logical results. The exact +observation key is `(feature_set_id, feature_index)` in unsigned numeric +lexicographic order. No tie-break changes component acceptance. + +## Determinism + +The logical result is independent of SQLite row order, GVR enumeration, +pagination, duplicate enumeration, allocation order and future scheduling. +The implementation must canonicalize/deduplicate the complete exact scope +before final component decisions. A future parallel implementation may split +work, but must produce the same set semantics and canonical output. + +## Track canonicalization + +Within each accepted Track, observations are sorted by +`(feature_set_id, feature_index)` ascending. `position_in_track` is that +sequence starting at zero. DSU roots, pointers, hash-table order and SQL row +order never determine positions. + +## Track Set canonicalization + +Accepted Tracks are sorted by their ordered observation-membership sequences, +lexicographically: compare observation keys at the first difference, then the +shorter sequence first if one is a prefix. The published Track IDs remain +opaque AUTOINCREMENT IDs; logical tests compare this canonical sequence, not +Track IDs. + +## Minimum length and singletons + +The minimum is exactly the FROZEN structural minimum of two observations. +Singleton nodes are never produced by an edge component and are dropped if +they arise during future internal conflict handling. No arbitrary minimum of +three and no maximum length is introduced. + +## Builder identity + +`builder_kind` is conceptually `track_builder` (lowercase ASCII, matching the +project's persisted kind convention). `builder_version` is `1`. + +## Builder version + +Any change to the scientific conflict, duplicate, descriptor, edge or +canonicalization policy requires a new `builder_version`, unless the change +is explicitly represented by a new parameter fingerprint under the versioning +rules. Runtime, storage or performance changes do not qualify. + +## Parameter fingerprint + +The fingerprint is SHA-256 of the following 48-byte canonical encoding: + +| Offset | Width | Field | +|---:|---:|---| +| 0 | 8 | ASCII domain `L3DTBFP1` | +| 8 | 4 | fingerprint encoding version `1` | +| 12 | 4 | Builder policy version `1` | +| 16 | 4 | whole-component conflict policy `1` | +| 20 | 4 | exact duplicate collapse policy `1` | +| 24 | 4 | mixed Matcher parent configurations allowed `1` | +| 28 | 4 | exact Feature Set tuple homogeneity `1` | +| 32 | 4 | undirected canonical observation edge `1` | +| 36 | 4 | Track/Track Set canonicalization version `1` | +| 40 | 4 | minimum Track length `2` | +| 44 | 4 | reserved, zero | + +All integer fields are fixed-width unsigned 32-bit little-endian. There are +no booleans, floats, ABI padding or raw C structs. The hash output is exactly +32 bytes SHA-256. The builder kind and builder version are separate Track Set +identity fields; policy version is nevertheless encoded to make the contract +reviewable. + +Excluded runtime-only fields: page size, batch size, thread count, worker +count, RAM budget, CPU/GPU slot, Governor state, PSI, swap, scheduling, +allocation addresses, temporary buffer size, DB transaction duration and +hardware identity. + +## Provenance + +Persisted provenance is exactly the FROZEN Track Set level: Builder identity, +verifier selector, `input_scope_hash` and `gvr_count`. Edge-to-GVR evidence is +temporary only for deduplication and diagnostics. No new edge provenance table +is required. + +## Error ownership + +Scientific non-selection means an edge is discarded only by duplicate collapse +or a deterministic conflicting-component result. Runtime error covers I/O, +allocation, cancellation and database failure. Input corruption covers any +invalid published contract. Runtime error or corruption produces no Track Set +publication and never becomes a scientific rejection. + +## Corruption handling + +The build aborts without publication on: missing GVR, wrong selector, wrong +status, missing Match Result/Candidate Pair/Feature Set/Match File, ownership +mismatch, invalid Match File header or SHA, malformed mask, wrong mask size or +padding, mask/match-count mismatch, out-of-range feature index, missing parent, +inconsistent image ownership, invalid Feature Set metadata, scope hash/count +mismatch, or changed input during final validation. No scope item is skipped. + +## Empty input and zero-Track output + +An empty scope is invalid because FROZEN Track Model v1 requires +`gvr_count >= 1`; it is an input/constraint error and publishes nothing. A +non-empty, fully valid scope may deterministically produce zero valid Tracks +when every component conflicts or all components are below the structural +minimum. `track_count = 0` is permitted by Track Model v1 and is published as +a complete Track Set with the exact non-empty scope. + +## Resource model + +**DECISION:** use a full sparse in-memory graph with deterministic edge sort, +not a dense matrix, SQLite temporary graph or external merge in v1. This is +the minimum complexity credible for the 16 GiB target and permits exhaustive +canonical finalization. Input Match Files are read one at a time; they are +never all resident. + +Approximate implementation-neutral accounting, including allocator/alignment +headroom: + +- observation key: 16–24 bytes; +- unique edge: 16–32 bytes for two compact node references and sort metadata; +- DSU/component node: 16–32 bytes; +- image-membership metadata: 8–24 bytes per component observation; +- temporary evidence: 0–24 bytes per raw edge, bounded and discardable after + deduplication; +- canonicalization entry: 16–32 bytes per accepted observation. + +Indicative totals, not new contractual limits: + +- 100k observations: roughly 5–15 MiB plus edges and allocator overhead; +- 1M observations: roughly 50–150 MiB before unusually dense edge evidence; +- 1M unique edges: roughly 20–60 MiB, plus node/component metadata. + +The representation remains feasible on the target Ryzen 7 8845HS / about +16 GiB RAM, subject to future measurement. No dense matrix is used. + +## Complexity + +For `V` unique observations, `E_raw` inlier edges and `E` unique edges: + +- input DB/asset I/O: `O(gvr_count + E_raw)` bounded reads; +- edge canonicalization and sort: expected `O(E_raw log E_raw)`; +- duplicate collapse: `O(E_raw)` after sort; +- component construction: `O(V + E)` with a sparse DSU or equivalent; +- conflict detection and membership canonicalization: `O(V log V)` worst case; +- Track Set ordering: `O(V log V)` worst case, with comparison cost included; +- total expected: `O(E_raw log E_raw + V log V)`; +- worst-case memory: `O(V + E_raw)`; no `O(image_count²)` or `O(V²)` storage. + +Paged enumeration is deterministic but does not change the asymptotic sort +cost. An external deterministic merge may be introduced only if measurements +show the chosen memory envelope insufficient; it must preserve the same +logical output. + +## Publication boundary + +The conceptual split is: + +1. DB adapter resolves and validates the exact immutable scope and parent + evidence. +2. SQLite-free scientific core builds the complete canonical logical result. +3. Track Model API validates Feature Set ownership, image uniqueness, bounds, + counts and publishes the complete set in its short atomic transaction. + +No Track, observation or partial set is visible before commit. The frozen +`lardon3d_project_db_create_track_set()` API is the publication boundary. + +## Reuse + +Before reading Match Files, the orchestration layer performs exact identity +lookup using builder identity, Builder fingerprint, verifier selector and +input scope hash. A found set is reusable only if its stored `gvr_count` +matches the supplied count and the loaded set is coherent. No latest-result +selection and no overwrite are allowed. + +## Crash semantics + +A crash after compute and before publication leaves no partial scientific +result. Retry recomputes the exact scope in v1. A crash during the frozen +publication transaction rolls back; if commit happened, exact identity lookup +reuses the complete set. Existing immutable upstream data is untouched. + +## Incrementality + +**FULL REBUILD v1: YES.** Track Sets are immutable. New GVRs require a new +scope hash and a new Track Set identity; the old set remains valid. No delta +Builder or checkpoint graph persistence is introduced here. + +## Future Task boundary + +A future Task, likely named `track_builder.run`, treats one complete Track Set +as its scientific unit. It may page GVR resolution and pause at safe input or +compute boundaries, but publication occurs only after complete canonical +output and final scope validation. A cancelled or failed run publishes no +incomplete set. The exact Task kind is not implemented or persisted by Gate A. + +## Future Resource Governor integration + +The future Task estimates bounded graph memory and CPU, obtains reservations +before callbacks, and may reduce batches or pause under pressure. Batch size, +thread count, Governor state, PSI, swap and throttling may affect throughput +only; they must not affect logical edges, conflict decisions or output order. + +## Vulkan + +**NOT_JUSTIFIED.** The Builder is sparse graph construction and conflict +handling, expected to be CPU/memory-bound. No GPU identity or backend is part +of v1. + +## Adversarial corpus + +The following expected outputs use `T{...}` for canonical observation lists. +An edge shown as discarded is not persisted; “dropped observations” means +members of an unpublished component. + +| Case | Input | Conflict? | Expected tracks | Discarded edges | Dropped observations / why | +|---|---|---|---|---|---| +| 1 chain | A1-B1, B1-C1 | No | T{A1,B1,C1} | none | none | +| 2 cycle | A1-B1, B1-C1, C1-A1 | No | T{A1,B1,C1} | none | none; cycle is valid | +| 3 duplicate | A1-B1 twice | No | T{A1,B1} | duplicate copy | none; multiplicity is not weight | +| 4 same image | A1-B1, B1-C1, A1-C2 | Yes | none | none after collapse | all four; C1/C2 conflict | +| 5 late conflict | A1-B1-B2-C1 then A1-C2 | Yes | none for component | none | whole component rejected | +| 6 disjoint | A1-B1; C1-D1 | No | T{A1,B1}, T{C1,D1} | none | none | +| 7 bridge | A1-B1; C1-D1; B1-C2 | Yes | none for bridged component | none | all bridged members; duplicate image C | +| 8 duplicate GVR | same A1-B1 in GVR1/GVR2 | No | T{A1,B1} | duplicate evidence | none | +| 9 permutation | prior graph, shuffled | same | same canonical tracks | same | order irrelevant | +| 10 single edge | A1-B1 | No | T{A1,B1} | none | none; length 2 is valid | +| 11 singleton | one node after hypothetical split | n/a | none | policy-local | singleton dropped; no rejected row | +| 12 two Feature Sets | A:X1-B:Y1, B:Y1-C:X2 | Yes | none | none | heterogeneous Feature Set tuple | +| 13 Matcher configs | same-descriptor edges from M1/M2 | No | connected Track | none | none; scores unused | +| 14 star | A1-B1, A1-C1, A1-D1 | No | T{A1,B1,C1,D1} | none | none | +| 15 chain large | A1-B1-...-N1 | No | one ordered Track | none | none | +| 16 conflict-heavy | repeated image IDs | per component | valid components | none | conflicts dropped | + +## Validation plan + +Gate B/C must include exhaustive small graphs over 3–5 images and small +observation counts. Check no duplicate image, no cross-Track observation, +minimum length, no overlap, canonical ordering and permutation invariance. +Run each logical input 100 times in one process and in fresh processes; vary +GVR order, edge order, page size and practical DB row order. Future 1-thread +and N-thread runs must hash the same canonical logical output. A non-persistent +test hash may hash sorted Tracks and sorted observations; it is not Track Set +identity. + +Future performance runs should cover 10k, 100k and 1M edges with low-conflict, +high-conflict, tiny-track, huge-component, chain and star distributions. +Measure wall time, CPU, RSS peak, bytes/node, bytes/edge and throughput. + +## Explicitly out of scope + +Triangulation, camera pose estimation, Sparse SfM, Essential matrix, Bundle +Adjustment, reprojection optimisation, 3D points, co-visibility matrices, +dense reconstruction, Vulkan Builder implementation, persistent edge +provenance, Track Model schema changes, Project DB v15, Track Builder Task +implementation and incremental optimisation implementation. + +## Open questions + +None critical for Gate A. Gate B must choose concrete in-memory data +structures without changing this scientific contract. A future API is needed +to page an exact selector-filtered GVR ID enumeration for callers that do not +already possess the explicit scope. + +## Future transverse updates + +**FUTURE TRANSVERSE UPDATE REQUIRED** + +- file: `include/lardon3d/project_db.h` and `src/project_db.c` +- reason: expose a bounded, selector-filtered, deterministic GVR ID + enumeration so orchestration can construct the explicit scope without a + private SQL query; no schema change is required. +- expected gate: Gate B orchestration/API preparation. + +## Gate status + +**PASS.** All critical scientific and identity decisions are closed. No Track +Model or Project DB contract is redefined, no partial input is accepted, and +the result is independent of SQL order, pagination and future thread count. + +## Implementation status + +Gate A contract: **PASS/FROZEN**. Gate B pure core: **PASS**. + +The Gate B core is a SQLite-free, single-threaded C-compatible API. The caller +provides resolved observations and edges and retains ownership of those input +arrays. The result owns separately allocated canonical Track arrays and is +released with `lardon3d_track_builder_result_free()`; a zero-initialized result +is safe to free repeatedly. Invalid input returns no partial result. + +The implementation validates observation metadata, sorts and deduplicates +undirected edges, builds a deterministic observation table, unions every unique +edge with a sparse DSU, and validates complete components for image uniqueness +and exact Feature Set homogeneity. Invalid components are rejected as a whole; +valid components of at least two observations become lexicographically +canonical Tracks and Track Sets. Hash lookup is used only for index lookup; no +hash iteration order or DSU root is exposed. + +The exact 48-byte `L3DTBFP1` encoding is hashed with SHA-256. Runtime values, +input scope and edge provenance are excluded. Measured synthetic chain runs +were 0.002 s, 0.032 s and 0.607 s for 10k, 100k and 1M edges respectively on +the development host, with a 1,000,000-edge process peak of 973,752 KiB. For +that chain the counts are 1,000,000 raw and unique edges, 1,000,001 unique +observations, one Track and zero rejected components. The public input arrays +occupy 152,000,136 bytes (136,000,136 bytes of observations and 16,000,000 +bytes of pointer edges). The core uses 16,000,000 bytes of compact normalized +edges, a 136,000,136-byte canonical metadata table, 9,000,009 bytes for DSU +arrays, approximately 32 MiB for component vectors/capacity, and 24,000,024 +bytes for compact output observations plus Track storage. The remaining +high-water RSS is allocator/hash-table/vector capacity and benchmark process +overhead; RSS is a process high-water metric, not the sum of live logical +payloads. No full metadata is copied into core edges. The benchmark remains +non-default. Targeted normal and +ASan/UBSan validation passed, including the 32,768-graph oracle and a 301 +observation Track. + +## Gate B closure audit + +OpenSSL was already a project dependency before the Track Builder changes and +is used by the existing asset, Match File, matcher and verifier hashing paths. +Track Builder adds no new external dependency and does not introduce a second +internal SHA-256 helper. Its fingerprint vector remains +`e1f1fae479bcf82001a5b33dda331195617b8751668e46a6cf1eecf2d125df31`. + +The 1M benchmark's `973752 KiB` is a process high-water RSS measurement. The +synthetic caller retains 136,000,136 bytes of observations and 16,000,000 +bytes of pointer edges while the core retains its 136,000,136-byte canonical +metadata table, 16,000,000-byte normalized edges, DSU/component temporaries +and compact output. Compact edges contain node indices only; they do not copy +136-byte metadata at either endpoint. The remaining high-water gap is +primarily allocator retention plus hash-table buckets/nodes and vector +capacity. No core duplication bug was found or changed in the closure audit. + +The targeted corpus explicitly covers reversed endpoints, 100 identical +edges, contradictory metadata, fingerprint/dimension/version conflicts, +301-observation tracks, disjoint length-2 tracks, sorted and reverse-sorted +edges, empty edge lists and self-edges. The 32,768 production-DSU versus +independent DFS exhaustive comparison remains passing. Cross-process +repeatability was not run and is non-blocking for Gate B. + +## Gate C — Project DB orchestration + +**DECISION:** Gate C exposes an explicit-scope adapter around the SQLite-free +Gate B core. The caller owns a copied, strictly increasing, non-empty GVR ID +list and supplies the exact verifier selector. The adapter computes the frozen +`L3DTSIS1` digest and performs exact Track Set reuse before reading any asset. + +On a reuse miss, each selected GVR is loaded and checked as +`GVR → Match Result → Candidate Pair → Feature Set A/B → Image A/B`. The +content-addressed Match File is validated and read one at a time; its entries +are consumed in file order and the persisted LSB-first mask selects inliers. +Only Feature Set metadata is loaded—descriptor payloads are never read. A +small per-build Feature Set cache is permitted, while Match File buffers are +released immediately after edge extraction. + +The complete resolved graph is passed unchanged to the Gate B core. Before the +frozen `lardon3d_project_db_create_track_set()` publication transaction, the +same explicit GVR IDs, status, selector, parent chain and scope count/hash are +revalidated. Any missing, rejected, mismatched or corrupt selected input aborts +the whole build; no partial Track Set is published. New matching GVRs are not +discovered or included. The Track Model API owns timestamps, validation, +atomicity and late exact-identity reuse. Project DB remains v14; Task and +Resource Governor orchestration are not part of Gate C. + +The implementation does not add a selector discovery API: callers that need +discovery must enumerate and freeze their own explicit list in a separate +operation. + +DB orchestration: **GATE C IMPLEMENTED**. Task: **NOT_IMPLEMENTED**. Resource +Governor: **NOT_IMPLEMENTED**. Project DB remains v14 and unchanged. + +## Gate C closure evidence (current worktree) + +The integration harness now constructs a synthetic Project DB v14 through the +public image, Feature Set, Candidate Pair, Match Result, GVR and Track Model +APIs. C01–C25 and the partial-input proof pass, including corruption, +scientific conflicts, zero-track publication, close/reopen durability, +different-scope identity and late matching-GVR exclusion. The test-only phase +seam is compiled only into the Gate C test executable. + +Normal and AddressSanitizer/UndefinedBehaviorSanitizer targeted runs pass for +the core and project executables. The 106,496-edge synthetic resource case +completed in 0.281 seconds with a 26,564 KiB process high-water RSS and one +Match File live at a time. C26 has no clean fault injection in the frozen DB +API and C27 belongs to the future concurrency gate. + +Gate C status: **PASS**. Gate A: **PASS**. Gate B: **PASS**. Gate D: +**NOT_IMPLEMENTED**. Gate E: **NOT_DONE**. Track Builder v1 is not marked +fully frozen by this document; the future runtime gates remain separate. diff --git a/include/lardon3d/track_builder.h b/include/lardon3d/track_builder.h new file mode 100644 index 0000000..d313d88 --- /dev/null +++ b/include/lardon3d/track_builder.h @@ -0,0 +1,79 @@ +#ifndef LARDON3D_TRACK_BUILDER_H +#define LARDON3D_TRACK_BUILDER_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_TRACK_BUILDER_VERSION = 1, + LARDON3D_TRACK_BUILDER_FINGERPRINT_SIZE = 32, + LARDON3D_TRACK_BUILDER_FINGERPRINT_INPUT_SIZE = 48, + LARDON3D_TRACK_BUILDER_KIND_CAPACITY = 64, +}; + +typedef enum { + LARDON3D_TRACK_BUILDER_OK = 0, + LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT, + LARDON3D_TRACK_BUILDER_CONSTRAINT, + LARDON3D_TRACK_BUILDER_CORRUPT_INPUT, + LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY, +} Lardon3DTrackBuilderResult; + +typedef struct { + uint64_t feature_set_id; + uint32_t feature_index; + uint64_t image_id; + char extractor_kind[LARDON3D_TRACK_BUILDER_KIND_CAPACITY]; + uint32_t extractor_version; + unsigned char parameter_fingerprint[32]; + uint32_t descriptor_type; + uint32_t descriptor_dimension; +} Lardon3DTrackBuilderObservation; + +typedef struct { + const Lardon3DTrackBuilderObservation *first; + const Lardon3DTrackBuilderObservation *second; +} Lardon3DTrackBuilderEdge; + +typedef struct { + uint64_t feature_set_id; + uint32_t feature_index; + uint64_t image_id; +} Lardon3DTrackBuilderTrackObservation; + +typedef struct { + size_t observation_count; + Lardon3DTrackBuilderTrackObservation *observations; +} Lardon3DTrackBuilderTrack; + +typedef struct { + size_t track_count; + Lardon3DTrackBuilderTrack *tracks; +} Lardon3DTrackBuilderResultSet; + +/* The result must be zero-initialized before the first build. Inputs remain + * caller-owned and are not modified. Output ownership is returned to the + * caller; free it exactly (or repeatedly) with result_free. */ +Lardon3DTrackBuilderResult lardon3d_track_builder_build( + const Lardon3DTrackBuilderObservation *observations, + size_t observation_count, const Lardon3DTrackBuilderEdge *edges, + size_t edge_count, Lardon3DTrackBuilderResultSet *result); + +void lardon3d_track_builder_result_free(Lardon3DTrackBuilderResultSet *result); + +bool lardon3d_track_builder_fingerprint_bytes( + unsigned char bytes[LARDON3D_TRACK_BUILDER_FINGERPRINT_INPUT_SIZE]); + +bool lardon3d_track_builder_fingerprint( + unsigned char fingerprint[LARDON3D_TRACK_BUILDER_FINGERPRINT_SIZE]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/lardon3d/track_builder_project.h b/include/lardon3d/track_builder_project.h new file mode 100644 index 0000000..741acf0 --- /dev/null +++ b/include/lardon3d/track_builder_project.h @@ -0,0 +1,57 @@ +#ifndef LARDON3D_TRACK_BUILDER_PROJECT_H +#define LARDON3D_TRACK_BUILDER_PROJECT_H + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + const char *project_path; + Lardon3DProjectDb *database; + int verifier_kind; + uint32_t verifier_version; + const unsigned char *verifier_fingerprint; + const uint64_t *gvr_ids; + size_t gvr_count; +} Lardon3DTrackBuilderProjectRequest; + +typedef struct { + uint64_t track_set_id; + uint64_t gvr_count; + uint64_t raw_inlier_edge_count; + uint64_t core_observation_count; + uint64_t track_count; + bool reused; +} Lardon3DTrackBuilderProjectResult; + +typedef enum { + LARDON3D_TRACK_BUILDER_PROJECT_OK = 0, + LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT, + LARDON3D_TRACK_BUILDER_PROJECT_NOT_FOUND, + LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT, + LARDON3D_TRACK_BUILDER_PROJECT_DATABASE_ERROR, + LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY, + LARDON3D_TRACK_BUILDER_PROJECT_CORE_ERROR, +} Lardon3DTrackBuilderProjectStatus; + +Lardon3DTrackBuilderProjectStatus lardon3d_track_builder_build_project( + const Lardon3DTrackBuilderProjectRequest *request, + Lardon3DTrackBuilderProjectResult *result); + +#ifdef LARDON3D_TRACK_BUILDER_PROJECT_TESTING +/* Test-only phase seam; never present in the production header/API. */ +void lardon3d_track_builder_project_test_before_revalidation( + Lardon3DProjectDb *database, const uint64_t *gvr_ids, size_t gvr_count); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 594a671..a48f40b 100644 --- a/meson.build +++ b/meson.build @@ -155,6 +155,8 @@ executable( 'src/match_file.c', 'src/matcher.cpp', 'src/geometric_verifier.cpp', + 'src/track_builder.cpp', + 'src/track_builder_project.cpp', ] + matcher_backend_sources, include_directories: include_directories('include'), dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry] @@ -831,3 +833,37 @@ geometric_verifier_task_test = executable( ) test('geometric-verifier-task', geometric_verifier_task_test, timeout: 60, env: opencv_test_environment) + +track_builder_test = executable( + 'test-track-builder-core', + sources: ['tests/test_track_builder_core.cpp', 'src/track_builder.cpp'], + include_directories: include_directories('include'), + dependencies: [openssl], +) +test('track-builder-core', track_builder_test, timeout: 120) + +track_builder_project_test = executable( + 'test-track-builder-project', + sources: [ + 'tests/test_track_builder_project.cpp', + 'src/track_builder.cpp', + 'src/track_builder_project.cpp', + 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + 'src/match_file.c', + ], + include_directories: include_directories('include'), + cpp_args: ['-DLARDON3D_TRACK_BUILDER_PROJECT_TESTING'], + dependencies: [threads, sqlite3, openssl], +) +test('track-builder-project', track_builder_project_test, timeout: 60) + +executable( + 'benchmark-track-builder', + sources: ['tests/benchmark_track_builder.cpp', 'src/track_builder.cpp'], + build_by_default: false, + include_directories: include_directories('include'), + dependencies: [openssl], +) diff --git a/src/track_builder.cpp b/src/track_builder.cpp new file mode 100644 index 0000000..2d4d8c7 --- /dev/null +++ b/src/track_builder.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +namespace { +using Observation = Lardon3DTrackBuilderObservation; +using Edge = Lardon3DTrackBuilderEdge; + +struct CompactEdge { + size_t low; + size_t high; +}; + +bool identity_less(const Observation &a, const Observation &b) { + return a.feature_set_id < b.feature_set_id || + (a.feature_set_id == b.feature_set_id && + a.feature_index < b.feature_index); +} + +bool identity_equal(const Observation &a, const Observation &b) { + return a.feature_set_id == b.feature_set_id && + a.feature_index == b.feature_index; +} + +bool metadata_equal(const Observation &a, const Observation &b) { + return a.image_id == b.image_id && + a.extractor_version == b.extractor_version && + a.descriptor_type == b.descriptor_type && + a.descriptor_dimension == b.descriptor_dimension && + std::strncmp(a.extractor_kind, b.extractor_kind, + LARDON3D_TRACK_BUILDER_KIND_CAPACITY) == 0 && + std::memcmp(a.parameter_fingerprint, b.parameter_fingerprint, 32) == 0; +} + +bool observation_valid(const Observation &observation) { + return observation.feature_set_id != 0 && observation.image_id != 0 && + std::memchr(observation.extractor_kind, '\0', + LARDON3D_TRACK_BUILDER_KIND_CAPACITY) != nullptr; +} + +struct Dsu { + std::vector parent; + std::vector rank; + explicit Dsu(size_t size) : parent(size), rank(size, 0) { + for (size_t i = 0; i < size; ++i) parent[i] = i; + } + size_t root(size_t value) { + while (parent[value] != value) { + parent[value] = parent[parent[value]]; + value = parent[value]; + } + return value; + } + void unite(size_t a, size_t b) { + a = root(a); b = root(b); + if (a == b) return; + if (rank[a] < rank[b]) std::swap(a, b); + parent[b] = a; + if (rank[a] == rank[b]) ++rank[a]; + } +}; + +struct IdentityHash { + size_t operator()(const std::pair &key) const noexcept { + uint64_t value = key.first ^ (static_cast(key.second) + + 0x9e3779b97f4a7c15ULL + (key.first << 6U) + + (key.first >> 2U)); + value ^= value >> 30U; + value *= 0xbf58476d1ce4e5b9ULL; + value ^= value >> 27U; + return static_cast(value ^ (value >> 31U)); + } +}; + +std::pair identity_key(const Observation &observation) { + return {observation.feature_set_id, observation.feature_index}; +} + +bool digest(const unsigned char *input, size_t size, unsigned char output[32]) { + unsigned int output_size = 0; + return EVP_Digest(input, size, output, &output_size, EVP_sha256(), nullptr) == 1 && + output_size == 32; +} + +} // namespace + +extern "C" bool lardon3d_track_builder_fingerprint_bytes(unsigned char bytes[48]) { + if (!bytes) return false; + std::memset(bytes, 0, 48); + std::memcpy(bytes, "L3DTBFP1", 8); + const uint32_t fields[] = {1, 1, 1, 1, 1, 1, 1, 1, 2, 0}; + for (size_t field = 0; field < 10; ++field) + for (size_t byte = 0; byte < 4; ++byte) + bytes[8 + field * 4 + byte] = + static_cast(fields[field] >> (byte * 8)); + return true; +} + +extern "C" bool lardon3d_track_builder_fingerprint(unsigned char fingerprint[32]) { + if (!fingerprint) return false; + unsigned char bytes[48]; + return lardon3d_track_builder_fingerprint_bytes(bytes) && digest(bytes, 48, fingerprint); +} + +extern "C" void lardon3d_track_builder_result_free(Lardon3DTrackBuilderResultSet *result) { + if (!result) return; + if (result->tracks) { + for (size_t i = 0; i < result->track_count; ++i) + delete[] result->tracks[i].observations; + delete[] result->tracks; + } + result->tracks = nullptr; + result->track_count = 0; +} + +extern "C" Lardon3DTrackBuilderResult lardon3d_track_builder_build( + const Observation *input_observations, size_t observation_count, const Edge *input_edges, + size_t edge_count, Lardon3DTrackBuilderResultSet *result) { + if (!result || (observation_count != 0 && !input_observations) || + (edge_count != 0 && !input_edges)) + return LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT; + lardon3d_track_builder_result_free(result); + try { + if (edge_count > std::numeric_limits::max() / 2) + return LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT; + std::vector table; + if (observation_count != 0) + table.assign(input_observations, input_observations + observation_count); + for (const Observation &observation : table) + if (!observation_valid(observation)) return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT; + std::sort(table.begin(), table.end(), identity_less); + for (size_t i = 1; i < table.size(); ++i) + if (identity_equal(table[i - 1], table[i]) && + !metadata_equal(table[i - 1], table[i])) + return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT; + table.erase(std::unique(table.begin(), table.end(), identity_equal), table.end()); + std::unordered_map, size_t, IdentityHash> table_indices; + table_indices.reserve(table.size()); + for (size_t i = 0; i < table.size(); ++i) table_indices.emplace(identity_key(table[i]), i); + + std::vector normalized; + normalized.reserve(edge_count); + for (size_t i = 0; i < edge_count; ++i) { + const Edge &edge = input_edges[i]; + if (!edge.first || !edge.second || !observation_valid(*edge.first) || + !observation_valid(*edge.second) || identity_equal(*edge.first, *edge.second)) + return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT; + auto first_it = table_indices.find(identity_key(*edge.first)); + auto second_it = table_indices.find(identity_key(*edge.second)); + if (first_it == table_indices.end() || second_it == table_indices.end() || + first_it->second >= table.size() || second_it->second >= table.size() || + !metadata_equal(table[first_it->second], *edge.first) || + !metadata_equal(table[second_it->second], *edge.second)) + return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT; + if (first_it->second < second_it->second) + normalized.push_back({first_it->second, second_it->second}); + else + normalized.push_back({second_it->second, first_it->second}); + } + std::sort(normalized.begin(), normalized.end(), [](const CompactEdge &a, + const CompactEdge &b) { + return a.low < b.low || (a.low == b.low && a.high < b.high); + }); + normalized.erase(std::unique(normalized.begin(), normalized.end(), + [](const CompactEdge &a, const CompactEdge &b) { + return a.low == b.low && a.high == b.high; + }), + normalized.end()); + Dsu dsu(table.size()); + for (const CompactEdge &edge : normalized) dsu.unite(edge.low, edge.high); + std::vector> components(table.size()); + for (size_t i = 0; i < table.size(); ++i) + if (!normalized.empty()) components[dsu.root(i)].push_back(i); + std::vector> accepted; + for (auto &component : components) { + if (component.size() < 2) continue; + std::sort(component.begin(), component.end()); + bool valid = true; + std::vector images; + images.reserve(component.size()); + const Observation &first = table[component.front()]; + for (size_t node : component) { + const Observation &observation = table[node]; + if (observation.image_id == 0 || + (observation.extractor_version != first.extractor_version) || + (observation.descriptor_type != first.descriptor_type) || + (observation.descriptor_dimension != first.descriptor_dimension) || + std::strncmp(observation.extractor_kind, first.extractor_kind, + LARDON3D_TRACK_BUILDER_KIND_CAPACITY) != 0 || + std::memcmp(observation.parameter_fingerprint, first.parameter_fingerprint, 32) != 0) + valid = false; + images.push_back(observation.image_id); + } + std::sort(images.begin(), images.end()); + for (size_t i = 1; i < images.size(); ++i) + if (images[i - 1] == images[i]) valid = false; + if (valid) accepted.push_back(component); + } + auto component_less = [&table](const std::vector &a, + const std::vector &b) { + size_t count = std::min(a.size(), b.size()); + for (size_t i = 0; i < count; ++i) { + if (identity_less(table[a[i]], table[b[i]])) return true; + if (identity_less(table[b[i]], table[a[i]])) return false; + } + return a.size() < b.size(); + }; + std::sort(accepted.begin(), accepted.end(), component_less); + if (!accepted.empty()) { + result->tracks = new Lardon3DTrackBuilderTrack[accepted.size()](); + result->track_count = accepted.size(); + for (size_t i = 0; i < accepted.size(); ++i) { + result->tracks[i].observation_count = accepted[i].size(); + result->tracks[i].observations = + new Lardon3DTrackBuilderTrackObservation[accepted[i].size()]; + for (size_t j = 0; j < accepted[i].size(); ++j) { + const Observation &source = table[accepted[i][j]]; + result->tracks[i].observations[j] = { + source.feature_set_id, source.feature_index, source.image_id}; + } + } + } + return LARDON3D_TRACK_BUILDER_OK; + } catch (const std::bad_alloc &) { + lardon3d_track_builder_result_free(result); + return LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY; + } catch (...) { + lardon3d_track_builder_result_free(result); + return LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY; + } +} diff --git a/src/track_builder_project.cpp b/src/track_builder_project.cpp new file mode 100644 index 0000000..5560f09 --- /dev/null +++ b/src/track_builder_project.cpp @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +namespace { +using Observation = Lardon3DTrackBuilderObservation; +using Edge = Lardon3DTrackBuilderEdge; + +struct Key { + uint64_t set; + uint32_t index; + bool operator==(const Key &other) const { return set == other.set && index == other.index; } +}; + +struct KeyHash { + size_t operator()(const Key &key) const noexcept { + return static_cast(key.set ^ (static_cast(key.index) * 0x9e3779b97f4a7c15ULL)); + } +}; + +struct FeatureCacheEntry { + Lardon3DProjectDbFeatureSet value{}; +}; + +Lardon3DTrackBuilderProjectStatus map_db(Lardon3DProjectDbResult value) { + if (value == LARDON3D_PROJECT_DB_INVALID_ARGUMENT || value == LARDON3D_PROJECT_DB_CONSTRAINT) + return LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT; + if (value == LARDON3D_PROJECT_DB_NOT_FOUND) + return LARDON3D_TRACK_BUILDER_PROJECT_NOT_FOUND; + if (value == LARDON3D_PROJECT_DB_CORRUPT) + return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + return LARDON3D_TRACK_BUILDER_PROJECT_DATABASE_ERROR; +} + +bool join_path(char *output, size_t capacity, const char *root, const char *relative) { + if (!output || !root || !relative || relative[0] == '\0') return false; + int written = std::snprintf(output, capacity, "%s/%s", root, relative); + return written > 0 && static_cast(written) < capacity; +} + +bool scope_hash(const uint64_t *ids, size_t count, unsigned char output[32]) { + if (count > (std::numeric_limits::max() - 8U) / sizeof(uint64_t)) return false; + std::vector bytes(8U + count * sizeof(uint64_t)); + std::memcpy(bytes.data(), "L3DTSIS1", 8); + for (size_t i = 0; i < count; ++i) { + uint64_t value = ids[i]; + for (size_t byte = 0; byte < sizeof(value); ++byte) { + bytes[8U + i * sizeof(value) + byte] = static_cast(value & 0xffU); + value >>= 8U; + } + } + unsigned int length = 0; + return EVP_Digest(bytes.data(), bytes.size(), output, &length, EVP_sha256(), nullptr) == 1 && + length == 32; +} + +bool selector_matches(const Lardon3DProjectDbGeometricVerificationResult &gvr, + const Lardon3DTrackBuilderProjectRequest &request) { + return static_cast(gvr.verifier_kind) == request.verifier_kind && + gvr.verifier_version == request.verifier_version && + std::memcmp(gvr.parameter_fingerprint, request.verifier_fingerprint, 32) == 0; +} + +Lardon3DProjectDbResult validate_gvr_snapshot( + const Lardon3DTrackBuilderProjectRequest &request, uint64_t gvr_id) { + Lardon3DProjectDbGeometricVerificationResult gvr{}; + Lardon3DProjectDbResult db = lardon3d_project_db_load_geometric_verification_result( + request.database, gvr_id, &gvr); + if (db != LARDON3D_PROJECT_DB_OK) return db; + if (gvr.status != LARDON3D_GEOMETRIC_VERIFIED || !selector_matches(gvr, request)) + return LARDON3D_PROJECT_DB_CORRUPT; + Lardon3DProjectDbMatchResult match{}; + db = lardon3d_project_db_load_match_result(request.database, gvr.match_result_id, &match); + if (db != LARDON3D_PROJECT_DB_OK || match.result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || + !match.has_match_asset) return db == LARDON3D_PROJECT_DB_OK + ? LARDON3D_PROJECT_DB_CORRUPT : db; + Lardon3DProjectDbCandidatePair pair{}; + db = lardon3d_project_db_load_candidate_pair(request.database, match.candidate_pair_id, &pair); + if (db != LARDON3D_PROJECT_DB_OK) return db; + Lardon3DProjectDbFeatureSet set_a{}; + Lardon3DProjectDbFeatureSet set_b{}; + db = lardon3d_project_db_load_feature_set(request.database, match.feature_set_id_a, &set_a); + if (db != LARDON3D_PROJECT_DB_OK) return db; + db = lardon3d_project_db_load_feature_set(request.database, match.feature_set_id_b, &set_b); + if (db != LARDON3D_PROJECT_DB_OK) return db; + if (set_a.image_id != pair.image_id_a || set_b.image_id != pair.image_id_b || + match.feature_set_id_a == match.feature_set_id_b) + return LARDON3D_PROJECT_DB_CORRUPT; + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + if (!join_path(path, sizeof(path), request.project_path, match.match_asset_path)) + return LARDON3D_PROJECT_DB_CORRUPT; + Lardon3DMatchFileHeader header{}; + if (lardon3d_match_file_validate_asset(path, match.match_asset_sha256, + match.match_asset_size_bytes, &header, + set_a.feature_set_id, set_b.feature_set_id, + set_a.feature_count, set_b.feature_count) != + LARDON3D_MATCH_FILE_OK || header.match_count != match.match_count) + return LARDON3D_PROJECT_DB_CORRUPT; + return LARDON3D_PROJECT_DB_OK; +} + +Lardon3DTrackBuilderProjectStatus resolve_gvr( + const Lardon3DTrackBuilderProjectRequest &request, uint64_t gvr_id, + std::unordered_map &cache, + std::deque &observations, + std::unordered_map &observation_map, + std::vector &edges) { + Lardon3DProjectDbGeometricVerificationResult gvr{}; + Lardon3DProjectDbResult db = lardon3d_project_db_load_geometric_verification_result( + request.database, gvr_id, &gvr); + if (db != LARDON3D_PROJECT_DB_OK) return map_db(db); + if (gvr.status != LARDON3D_GEOMETRIC_VERIFIED || !selector_matches(gvr, request)) + return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + Lardon3DProjectDbMatchResult match{}; + db = lardon3d_project_db_load_match_result(request.database, gvr.match_result_id, &match); + if (db != LARDON3D_PROJECT_DB_OK) return map_db(db); + if (match.result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || !match.has_match_asset || + match.match_count == 0) return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + Lardon3DProjectDbCandidatePair pair{}; + db = lardon3d_project_db_load_candidate_pair(request.database, match.candidate_pair_id, &pair); + if (db != LARDON3D_PROJECT_DB_OK) return map_db(db); + auto load_set = [&](uint64_t id, Lardon3DProjectDbFeatureSet **out) { + auto found = cache.find(id); + if (found != cache.end()) { + *out = &found->second.value; + return LARDON3D_PROJECT_DB_OK; + } + FeatureCacheEntry entry{}; + Lardon3DProjectDbResult value = lardon3d_project_db_load_feature_set( + request.database, id, &entry.value); + if (value == LARDON3D_PROJECT_DB_OK) { + auto inserted = cache.emplace(id, entry); + *out = &inserted.first->second.value; + } + return value; + }; + Lardon3DProjectDbFeatureSet *set_a = nullptr; + Lardon3DProjectDbFeatureSet *set_b = nullptr; + db = load_set(match.feature_set_id_a, &set_a); + if (db != LARDON3D_PROJECT_DB_OK) return map_db(db); + db = load_set(match.feature_set_id_b, &set_b); + if (db != LARDON3D_PROJECT_DB_OK) return map_db(db); + if (set_a->image_id != pair.image_id_a || set_b->image_id != pair.image_id_b || + match.feature_set_id_a == match.feature_set_id_b) return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + if (!join_path(path, sizeof(path), request.project_path, match.match_asset_path)) + return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + Lardon3DMatchFileHeader header{}; + if (lardon3d_match_file_validate_asset(path, match.match_asset_sha256, + match.match_asset_size_bytes, &header, + set_a->feature_set_id, set_b->feature_set_id, + set_a->feature_count, set_b->feature_count) != + LARDON3D_MATCH_FILE_OK || header.match_count != match.match_count) + return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + std::vector entries(match.match_count); + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + uint32_t count = 0; + bool read_ok = fd >= 0 && lardon3d_match_file_read( + fd, &header, entries.data(), entries.size(), &count, set_a->feature_set_id, + set_b->feature_set_id, set_a->feature_count, set_b->feature_count) == LARDON3D_MATCH_FILE_OK; + if (fd >= 0) (void)close(fd); + if (!read_ok || count != match.match_count || gvr.inlier_mask_size != (count + 7U) / 8U) + return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + size_t selected = 0; + for (uint32_t i = 0; i < count; ++i) { + if ((gvr.inlier_mask[i / 8U] & static_cast(1U << (i % 8U))) == 0) continue; + ++selected; + const auto &entry = entries[i]; + Key keys[2] = {{set_a->feature_set_id, entry.feature_index_a}, + {set_b->feature_set_id, entry.feature_index_b}}; + Observation *resolved[2] = {}; + Lardon3DProjectDbFeatureSet *sets[2] = {set_a, set_b}; + for (size_t endpoint = 0; endpoint < 2; ++endpoint) { + auto found = observation_map.find(keys[endpoint]); + if (found == observation_map.end()) { + Observation value{}; + value.feature_set_id = keys[endpoint].set; + value.feature_index = keys[endpoint].index; + value.image_id = sets[endpoint]->image_id; + std::memcpy(value.extractor_kind, sets[endpoint]->extractor_kind, + sizeof(value.extractor_kind)); + value.extractor_version = sets[endpoint]->extractor_version; + std::memcpy(value.parameter_fingerprint, sets[endpoint]->parameter_fingerprint, 32); + value.descriptor_type = sets[endpoint]->descriptor_type; + value.descriptor_dimension = sets[endpoint]->descriptor_dimension; + observations.push_back(value); + resolved[endpoint] = &observations.back(); + observation_map.emplace(keys[endpoint], resolved[endpoint]); + } else { + resolved[endpoint] = found->second; + } + } + edges.push_back({resolved[0], resolved[1]}); + } + if (selected != gvr.inlier_count) return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + return LARDON3D_TRACK_BUILDER_PROJECT_OK; +} + +} // namespace + +extern "C" Lardon3DTrackBuilderProjectStatus lardon3d_track_builder_build_project( + const Lardon3DTrackBuilderProjectRequest *request, + Lardon3DTrackBuilderProjectResult *result) { + if (!request || !result || !request->project_path || !request->database || + !request->verifier_fingerprint || !request->gvr_ids || request->gvr_count == 0 || + request->verifier_kind <= 0 || request->verifier_version == 0) { + return LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT; + } + *result = {}; + for (size_t i = 0; i < request->gvr_count; ++i) { + if (request->gvr_ids[i] == 0 || (i != 0 && request->gvr_ids[i - 1] >= request->gvr_ids[i])) + return LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT; + } + try { + std::vector owned_ids(request->gvr_ids, + request->gvr_ids + request->gvr_count); + Lardon3DTrackBuilderProjectRequest owned_request = *request; + owned_request.gvr_ids = owned_ids.data(); + request = &owned_request; + unsigned char input_hash[32]; + if (!scope_hash(request->gvr_ids, request->gvr_count, input_hash)) + return LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY; + unsigned char builder_fingerprint[32]; + if (!lardon3d_track_builder_fingerprint(builder_fingerprint)) + return LARDON3D_TRACK_BUILDER_PROJECT_CORE_ERROR; + Lardon3DProjectDbTrackSet identity{}; + std::snprintf(identity.builder_kind, sizeof(identity.builder_kind), "track_builder"); + identity.builder_version = LARDON3D_TRACK_BUILDER_VERSION; + std::memcpy(identity.parameter_fingerprint, builder_fingerprint, 32); + identity.verifier_kind = request->verifier_kind; + identity.verifier_version = request->verifier_version; + std::memcpy(identity.verifier_fingerprint, request->verifier_fingerprint, 32); + std::memcpy(identity.input_scope_hash, input_hash, 32); + identity.gvr_count = request->gvr_count; + Lardon3DProjectDbTrackSet found{}; + Lardon3DProjectDbResult db = lardon3d_project_db_find_track_set( + request->database, &identity, &found); + if (db == LARDON3D_PROJECT_DB_OK) { + if (found.gvr_count != request->gvr_count) + return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT; + result->track_set_id = found.track_set_id; + result->gvr_count = found.gvr_count; + result->track_count = found.track_count; + result->reused = true; + return LARDON3D_TRACK_BUILDER_PROJECT_OK; + } + if (db != LARDON3D_PROJECT_DB_NOT_FOUND) return map_db(db); + + std::unordered_map cache; + std::deque observations; + std::unordered_map observation_map; + std::vector edges; + for (size_t i = 0; i < request->gvr_count; ++i) { + auto status = resolve_gvr(*request, request->gvr_ids[i], cache, observations, + observation_map, edges); + if (status != LARDON3D_TRACK_BUILDER_PROJECT_OK) return status; + } + std::vector core_observations(observations.begin(), observations.end()); + std::unordered_map core_map; + core_map.reserve(core_observations.size()); + for (auto &observation : core_observations) + core_map.emplace(Key{observation.feature_set_id, observation.feature_index}, &observation); + std::vector core_edges; + core_edges.reserve(edges.size()); + for (const Edge &edge : edges) { + Key first{edge.first->feature_set_id, edge.first->feature_index}; + Key second{edge.second->feature_set_id, edge.second->feature_index}; + core_edges.push_back({core_map.at(first), core_map.at(second)}); + } + Lardon3DTrackBuilderResultSet core{}; + auto core_status = lardon3d_track_builder_build( + core_observations.empty() ? nullptr : core_observations.data(), core_observations.size(), + core_edges.empty() ? nullptr : core_edges.data(), core_edges.size(), &core); + if (core_status != LARDON3D_TRACK_BUILDER_OK) { + lardon3d_track_builder_result_free(&core); + return core_status == LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY + ? LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY + : LARDON3D_TRACK_BUILDER_PROJECT_CORE_ERROR; + } + std::vector> stored(core.track_count); + std::vector publish(core.track_count); + for (size_t i = 0; i < core.track_count; ++i) { + stored[i].resize(core.tracks[i].observation_count); + for (size_t j = 0; j < stored[i].size(); ++j) { + const auto &source = core.tracks[i].observations[j]; + stored[i][j] = {source.feature_set_id, source.feature_index, + static_cast(j)}; + } + publish[i] = {0, 0, static_cast(stored[i].size()), stored[i].data()}; + } +#ifdef LARDON3D_TRACK_BUILDER_PROJECT_TESTING + lardon3d_track_builder_project_test_before_revalidation( + request->database, request->gvr_ids, request->gvr_count); +#endif + for (size_t i = 0; i < request->gvr_count; ++i) { + db = validate_gvr_snapshot(*request, request->gvr_ids[i]); + if (db != LARDON3D_PROJECT_DB_OK) { + lardon3d_track_builder_result_free(&core); + return map_db(db); + } + } + Lardon3DProjectDbTrackSet published{}; + identity.track_count = core.track_count; + db = lardon3d_project_db_create_track_set(request->database, &identity, + publish.data(), publish.size(), &published); + lardon3d_track_builder_result_free(&core); + if (db != LARDON3D_PROJECT_DB_OK) return map_db(db); + result->track_set_id = published.track_set_id; + result->gvr_count = published.gvr_count; + result->track_count = published.track_count; + result->raw_inlier_edge_count = edges.size(); + result->core_observation_count = observations.size(); + result->reused = false; + return LARDON3D_TRACK_BUILDER_PROJECT_OK; + } catch (const std::bad_alloc &) { + return LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY; + } catch (...) { + return LARDON3D_TRACK_BUILDER_PROJECT_DATABASE_ERROR; + } +} diff --git a/tests/benchmark_track_builder.cpp b/tests/benchmark_track_builder.cpp new file mode 100644 index 0000000..ae3d2a2 --- /dev/null +++ b/tests/benchmark_track_builder.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include + +extern "C" { +#include +} + +static Lardon3DTrackBuilderObservation make_observation(uint64_t id) { + Lardon3DTrackBuilderObservation value{}; + value.feature_set_id = id + 1; + value.image_id = id + 1; + value.extractor_version = 1; + value.descriptor_type = 1; + value.descriptor_dimension = 32; + std::snprintf(value.extractor_kind, sizeof(value.extractor_kind), "orb"); + return value; +} + +int main() { + for (size_t edge_count : {10000U, 100000U, 1000000U}) { + std::vector observations; + std::vector edges; + observations.reserve(edge_count + 1); + edges.reserve(edge_count); + for (size_t i = 0; i <= edge_count; ++i) + observations.push_back(make_observation(i)); + for (size_t i = 1; i <= edge_count; ++i) + edges.push_back({&observations[i - 1], &observations[i]}); + Lardon3DTrackBuilderResultSet result{}; + auto start = std::chrono::steady_clock::now(); + auto status = lardon3d_track_builder_build( + observations.data(), observations.size(), edges.data(), edges.size(), &result); + auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - start); + struct rusage usage{}; + (void)getrusage(RUSAGE_SELF, &usage); + std::printf("edges=%zu status=%d seconds=%.3f rss_kib=%ld observations=%zu tracks=%zu\n", + edge_count, status, elapsed.count(), usage.ru_maxrss, + observations.size(), result.track_count); + lardon3d_track_builder_result_free(&result); + } +} diff --git a/tests/test_track_builder_core.cpp b/tests/test_track_builder_core.cpp new file mode 100644 index 0000000..cc0f09f --- /dev/null +++ b/tests/test_track_builder_core.cpp @@ -0,0 +1,298 @@ +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +namespace { +using Observation = Lardon3DTrackBuilderObservation; +using Edge = Lardon3DTrackBuilderEdge; +using Track = std::vector>; + +Observation observation(uint64_t set, uint32_t feature, uint64_t image, + uint32_t extractor_version = 1, + uint32_t descriptor_type = 1, + uint32_t dimension = 32, unsigned char fingerprint = 7) { + Observation value{}; + value.feature_set_id = set; + value.feature_index = feature; + value.image_id = image; + std::snprintf(value.extractor_kind, sizeof(value.extractor_kind), "orb"); + value.extractor_version = extractor_version; + value.parameter_fingerprint[0] = fingerprint; + value.descriptor_type = descriptor_type; + value.descriptor_dimension = dimension; + return value; +} + +Edge edge(const Observation &a, const Observation &b) { return {&a, &b}; } + +std::vector read_result(const Lardon3DTrackBuilderResultSet &result) { + std::vector tracks; + for (size_t i = 0; i < result.track_count; ++i) { + Track track; + for (size_t j = 0; j < result.tracks[i].observation_count; ++j) { + const Lardon3DTrackBuilderTrackObservation &value = + result.tracks[i].observations[j]; + track.emplace_back(value.feature_set_id, value.feature_index); + assert(j == 0 || track[j - 1] < track[j]); + } + for (size_t j = 0; j < track.size(); ++j) + for (size_t k = j + 1; k < track.size(); ++k) + assert(result.tracks[i].observations[j].image_id != + result.tracks[i].observations[k].image_id); + assert(track.size() >= 2); + tracks.push_back(track); + } + assert(std::is_sorted(tracks.begin(), tracks.end())); + return tracks; +} + +std::vector build(const std::vector &observations, + const std::vector &edges) { + Lardon3DTrackBuilderResultSet result{}; + assert(lardon3d_track_builder_build(observations.data(), observations.size(), + edges.data(), edges.size(), &result) == + LARDON3D_TRACK_BUILDER_OK); + std::vector tracks = read_result(result); + lardon3d_track_builder_result_free(&result); + lardon3d_track_builder_result_free(&result); + return tracks; +} + +void expect(const std::vector &observations, + const std::vector &edges, const std::vector &expected) { + const auto actual = build(observations, edges); + if (actual != expected) { + std::fprintf(stderr, "mismatch expected=%zu actual=%zu edges=%zu\n", + expected.size(), actual.size(), edges.size()); + for (const auto &track : actual) { + std::fprintf(stderr, "actual:"); + for (const auto &key : track) std::fprintf(stderr, " %llu:%u", + (unsigned long long)key.first, + key.second); + std::fputc('\n', stderr); + } + assert(false); + } +} + +void test_fingerprint() { + unsigned char bytes[48]; + unsigned char fingerprint[32]; + assert(lardon3d_track_builder_fingerprint_bytes(bytes)); + assert(lardon3d_track_builder_fingerprint(fingerprint)); + const char expected_bytes[] = + "4c3344544246503101000000010000000100000001000000010000000100000001" + "000000010000000200000000000000"; + const char expected_hash[] = + "e1f1fae479bcf82001a5b33dda331195617b8751668e46a6cf1eecf2d125df31"; + for (size_t i = 0; i < 48; ++i) { + unsigned int value = 0; + std::sscanf(expected_bytes + 2 * i, "%2x", &value); + assert(bytes[i] == value); + } + for (size_t i = 0; i < 32; ++i) { + unsigned int high = 0; + unsigned int low = 0; + std::sscanf(expected_hash + 2 * i, "%1x%1x", &high, &low); + assert(fingerprint[i] == (high * 16U + low)); + } +} + +void test_adversarial() { + const Observation a = observation(1, 0, 10); + const Observation b = observation(2, 0, 11); + const Observation c = observation(3, 0, 12); + const Observation d = observation(4, 0, 13); + const Observation conflict = observation(5, 0, 12); + std::vector all{a, b, c, d, conflict}; + expect(all, {edge(a, b), edge(b, c)}, {{{1, 0}, {2, 0}, {3, 0}}}); + expect(all, {edge(a, b), edge(b, c), edge(c, a)}, + {{{1, 0}, {2, 0}, {3, 0}}}); + expect(all, {edge(b, a), edge(a, b), edge(a, b)}, {{{1, 0}, {2, 0}}}); + expect(all, {edge(a, b), edge(b, c), edge(a, conflict)}, {}); + expect(all, {edge(a, b), edge(c, d)}, + {{{1, 0}, {2, 0}}, {{3, 0}, {4, 0}}}); + expect(all, {edge(a, b), edge(c, d), edge(b, conflict), edge(c, conflict)}, {}); + expect(all, {edge(a, b)}, {{{1, 0}, {2, 0}}}); + + Observation heterogeneous = observation(6, 0, 14, 2); + expect({a, b, heterogeneous}, {edge(a, b)}, {{{1, 0}, {2, 0}}}); + expect({a, b, heterogeneous}, {edge(a, heterogeneous)}, {}); + Observation same_image = observation(7, 0, 10); + expect({a, b, same_image}, {edge(a, b), edge(b, same_image)}, {}); + expect({a, b}, {}, {}); + std::vector duplicates; + for (size_t i = 0; i < 100; ++i) duplicates.push_back(edge((i % 2) == 0 ? a : b, + (i % 2) == 0 ? b : a)); + expect({a, b}, duplicates, {{{1, 0}, {2, 0}}}); + Edge self = edge(a, a); + Lardon3DTrackBuilderResultSet invalid{}; + assert(lardon3d_track_builder_build(&a, 1, &self, 1, &invalid) == + LARDON3D_TRACK_BUILDER_CORRUPT_INPUT); + lardon3d_track_builder_result_free(&invalid); + + for (uint32_t dimension : {31U, 33U}) + expect({a, b, observation(8, 0, 12, 1, 1, dimension)}, + {edge(a, observation(8, 0, 12, 1, 1, dimension))}, {}); + for (uint32_t version : {2U, 3U}) + expect({a, b, observation(9, 0, 12, version)}, + {edge(a, observation(9, 0, 12, version))}, {}); + Observation different_fingerprint = observation(10, 0, 12, 1, 1, 32, 8); + expect({a, b, different_fingerprint}, {edge(a, different_fingerprint)}, {}); + + std::vector permuted{edge(a, b), edge(b, c), edge(c, d)}; + auto canonical = build({a, b, c, d}, permuted); + std::reverse(permuted.begin(), permuted.end()); + assert(build({a, b, c, d}, permuted) == canonical); +} + +void test_corruption_and_immutability() { + Observation a = observation(1, 0, 10); + Observation b = observation(2, 0, 11); + Observation contradictory = a; + contradictory.image_id = 99; + std::vector inputs{a, b, contradictory}; + std::vector edges{edge(a, b)}; + const auto before = inputs; + Lardon3DTrackBuilderResultSet result{}; + assert(lardon3d_track_builder_build(inputs.data(), inputs.size(), edges.data(), + edges.size(), &result) == + LARDON3D_TRACK_BUILDER_CORRUPT_INPUT); + assert(result.track_count == 0 && result.tracks == nullptr); + assert(inputs.size() == before.size()); + assert(std::memcmp(inputs.data(), before.data(), + inputs.size() * sizeof(Observation)) == 0); + assert(lardon3d_track_builder_build(nullptr, 1, nullptr, 0, &result) == + LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT); + assert(lardon3d_track_builder_build(nullptr, 0, nullptr, 0, &result) == + LARDON3D_TRACK_BUILDER_OK); + lardon3d_track_builder_result_free(&result); + assert(lardon3d_track_builder_build(nullptr, 0, nullptr, 0, nullptr) == + LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT); +} + +void test_additional_adversarial() { + const Observation a = observation(1, 0, 10); + const Observation b = observation(2, 0, 11); + const Observation c = observation(3, 0, 12); + const Observation d = observation(4, 0, 13); + const Observation c_again = observation(5, 0, 12); + expect({a, b, c, d, c_again}, + {edge(a, b), edge(b, c), edge(c, d), edge(a, c_again)}, {}); + expect({a, b, c, d}, {edge(a, b), edge(a, c), edge(a, d)}, + {{{1, 0}, {2, 0}, {3, 0}, {4, 0}}}); + + std::vector disjoint; + std::vector disjoint_edges; + std::vector expected; + for (uint64_t i = 0; i < 32; ++i) + disjoint.push_back(observation(i + 1, 0, i + 1)); + for (size_t i = 0; i < disjoint.size(); i += 2) { + disjoint_edges.push_back(edge(disjoint[i], disjoint[i + 1])); + expected.push_back({{i + 1, 0}, {i + 2, 0}}); + } + assert(build(disjoint, disjoint_edges) == expected); + std::reverse(disjoint_edges.begin(), disjoint_edges.end()); + assert(build(disjoint, disjoint_edges) == expected); +} + +std::vector oracle(const std::vector &nodes, + const std::vector> &edges, + uint32_t mask) { + std::vector> adjacency(nodes.size()); + for (size_t i = 0; i < edges.size(); ++i) { + if ((mask & (1U << i)) == 0) continue; + adjacency[edges[i].first].push_back(edges[i].second); + adjacency[edges[i].second].push_back(edges[i].first); + } + std::vector seen(nodes.size(), false); + std::vector answer; + for (size_t start = 0; start < nodes.size(); ++start) { + if (seen[start] || adjacency[start].empty()) continue; + std::vector pending{start}; + seen[start] = true; + std::vector component; + while (!pending.empty()) { + size_t current = pending.back(); + pending.pop_back(); + component.push_back(current); + for (size_t next : adjacency[current]) + if (!seen[next]) { + seen[next] = true; + pending.push_back(next); + } + } + std::sort(component.begin(), component.end()); + std::vector images; + bool valid = component.size() >= 2; + Track track; + for (size_t index : component) { + for (uint64_t image : images) + if (image == nodes[index].image_id) valid = false; + images.push_back(nodes[index].image_id); + track.emplace_back(nodes[index].feature_set_id, nodes[index].feature_index); + } + if (valid) answer.push_back(track); + } + std::sort(answer.begin(), answer.end()); + return answer; +} + +void test_exhaustive() { + std::vector nodes; + for (uint64_t image = 1; image <= 3; ++image) { + nodes.push_back(observation(image * 10, 0, image)); + nodes.push_back(observation(image * 10 + 1, 0, image)); + } + std::vector> pairs; + std::vector all_edges; + for (size_t i = 0; i < nodes.size(); ++i) + for (size_t j = i + 1; j < nodes.size(); ++j) { + pairs.emplace_back(i, j); + all_edges.push_back(edge(nodes[i], nodes[j])); + } + for (uint32_t mask = 0; mask < (1U << pairs.size()); ++mask) { + std::vector selected; + for (size_t i = 0; i < all_edges.size(); ++i) + if (mask & (1U << i)) selected.push_back(all_edges[i]); + assert(build(nodes, selected) == oracle(nodes, pairs, mask)); + if (mask % 257 == 0) { + std::reverse(selected.begin(), selected.end()); + assert(build(nodes, selected) == oracle(nodes, pairs, mask)); + } + } +} + +void test_large_and_repeatable() { + std::vector nodes; + std::vector edges; + for (uint64_t i = 0; i < 301; ++i) nodes.push_back(observation(i + 1, 0, i + 1)); + for (size_t i = 1; i < nodes.size(); ++i) edges.push_back(edge(nodes[i - 1], nodes[i])); + auto expected = build(nodes, edges); + assert(expected.size() == 1 && expected[0].size() == 301); + std::mt19937 generator(12345); + for (int run = 0; run < 100; ++run) { + std::shuffle(edges.begin(), edges.end(), generator); + assert(build(nodes, edges) == expected); + } +} +} // namespace + +int main() { + test_fingerprint(); + test_adversarial(); + test_corruption_and_immutability(); + test_additional_adversarial(); + test_exhaustive(); + test_large_and_repeatable(); + std::puts("track-builder-core: PASS"); + return 0; +} diff --git a/tests/test_track_builder_project.cpp b/tests/test_track_builder_project.cpp new file mode 100644 index 0000000..bc7e797 --- /dev/null +++ b/tests/test_track_builder_project.cpp @@ -0,0 +1,688 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +namespace { +constexpr Lardon3DGeometricVerifierKind kVerifier = LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL; +constexpr uint32_t kVersion = 1; +unsigned char g_verifier[32]; +std::string g_delete_db; +uint64_t g_delete_id = 0; +bool g_delete_before_revalidation = false; +std::string g_insert_db; +uint64_t g_insert_match_id = 0; +bool g_insert_during_build = false; + +void check(bool value, const char *expression, int line) { + if (!value) { + std::fprintf(stderr, "track-builder-project:%d: %s\n", line, expression); + std::abort(); + } +} +#define CHECK(value) check((value), #value, __LINE__) + +std::string asset_path(const unsigned char hash[32], const char *kind) { + static constexpr char digits[] = "0123456789abcdef"; + std::string hex(64, '0'); + for (size_t i = 0; i < 32; ++i) { + hex[2 * i] = digits[hash[i] >> 4]; + hex[2 * i + 1] = digits[hash[i] & 0x0fU]; + } + return std::string("assets/") + kind + "/" + hex.substr(0, 2) + "/" + hex; +} + +void digest_file(const std::string &path, unsigned char hash[32], uint64_t *size) { + FILE *file = std::fopen(path.c_str(), "rb"); + CHECK(file != nullptr); + EVP_MD_CTX *context = EVP_MD_CTX_new(); + CHECK(context != nullptr && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1); + unsigned char buffer[4096]; + *size = 0; + size_t read = 0; + while ((read = std::fread(buffer, 1, sizeof(buffer), file)) != 0) { + CHECK(EVP_DigestUpdate(context, buffer, read) == 1); + *size += read; + } + unsigned int length = 0; + CHECK(EVP_DigestFinal_ex(context, hash, &length) == 1 && length == 32); + EVP_MD_CTX_free(context); + std::fclose(file); +} + +struct Fixture { + std::string directory; + std::string db_path; + Lardon3DProjectDb *db = nullptr; + uint64_t image[3]{}; + uint64_t feature[3]{}; + uint64_t next_asset = 1; + uint64_t next_file = 1; + + explicit Fixture(uint32_t feature_count = 32) { + char name[] = "/tmp/lardon3d-track-c-XXXXXX"; + CHECK(mkdtemp(name) != nullptr); + directory = name; + db_path = directory + "/project.db"; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; + CHECK(lardon3d_project_db_open(db_path.c_str(), &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbScanSet scanset{}; + CHECK(lardon3d_project_db_create_scanset(db, "SPECIMEN", &scanset) == + LARDON3D_PROJECT_DB_OK); + for (size_t i = 0; i < 3; ++i) { + unsigned char hash[32]{}; + hash[0] = static_cast(i + 1); + std::string path = asset_path(hash, "images"); + Lardon3DProjectDbImageRegisterStatus status; + Lardon3DProjectDbImage created{}; + char original_name[32]; + std::snprintf(original_name, sizeof(original_name), "image-%zu.jpg", i + 1); + Lardon3DProjectDbResult image_result = lardon3d_project_db_register_image( + db, scanset.scanset_id, hash, path.c_str(), 1, original_name, path.c_str(), 0, + static_cast(i + 1), &status, &created); + CHECK(image_result == LARDON3D_PROJECT_DB_OK); + image[i] = created.image_id; + unsigned char parameter[32]{0x5a}; + unsigned char source[32]{}; + source[0] = static_cast(i + 1); + unsigned char asset[32]{static_cast(0x70U + i)}; + Lardon3DProjectDbFeatureSet set{}; + CHECK(lardon3d_project_db_register_feature_set( + db, image[i], "orb", 1, parameter, source, feature_count, 1, 32, asset, + asset_path(asset, "features").c_str(), 1, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, + static_cast(i + 10), &set) == LARDON3D_PROJECT_DB_OK); + feature[i] = set.feature_set_id; + } + std::memset(g_verifier, 0x42, sizeof(g_verifier)); + } + + ~Fixture() { + if (db) lardon3d_project_db_close(db); + } + + uint64_t add_feature_set(size_t image_index, unsigned char parameter_byte, + const char *extractor = "orb", uint32_t descriptor_type = 1) { + unsigned char parameter[32]{parameter_byte}; + unsigned char source[32]{}; + source[0] = static_cast(image_index + 1); + unsigned char asset[32]{static_cast(0x90U + parameter_byte)}; + Lardon3DProjectDbFeatureSet set{}; + CHECK(lardon3d_project_db_register_feature_set( + db, image[image_index], extractor, 1, parameter, source, 32, + descriptor_type, 32, asset, asset_path(asset, "features").c_str(), 1, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 100, &set) == LARDON3D_PROJECT_DB_OK); + return set.feature_set_id; + } + + uint64_t add_gvr(size_t first, size_t second, + const std::vector &entries, + const std::vector &mask, + Lardon3DGeometricVerificationStatus status = LARDON3D_GEOMETRIC_VERIFIED, + uint64_t feature_a = 0, uint64_t feature_b = 0, + const char *matcher_kind = "matcher-a", uint32_t matcher_version = 1) { + if (feature_a == 0) feature_a = feature[first]; + if (feature_b == 0) feature_b = feature[second]; + Lardon3DProjectDbCandidatePair pair{}; + Lardon3DProjectDbResult pair_result = lardon3d_project_db_find_candidate_pair( + db, image[first], image[second], &pair); + if (pair_result == LARDON3D_PROJECT_DB_NOT_FOUND) { + pair_result = lardon3d_project_db_create_candidate_pair( + db, image[first], image[second], static_cast(next_file), &pair); + } + CHECK(pair_result == LARDON3D_PROJECT_DB_OK); + std::string path = directory + "/match-" + std::to_string(next_file++) + ".bin"; + int fd = open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0600); + CHECK(fd >= 0); + CHECK(lardon3d_match_file_write(fd, 1, 32, feature_a, feature_b, + entries.data(), static_cast(entries.size())) == + LARDON3D_MATCH_FILE_OK); + CHECK(close(fd) == 0); + unsigned char hash[32]{}; + uint64_t size = 0; + digest_file(path, hash, &size); + unsigned char matcher_fingerprint[32]{}; + matcher_fingerprint[0] = static_cast(next_file); + Lardon3DProjectDbMatchResult match{}; + CHECK(lardon3d_project_db_create_match_result( + db, pair.candidate_pair_id, feature_a, feature_b, matcher_kind, matcher_version, + matcher_fingerprint, LARDON3D_MATCH_RESULT_STATUS_MATCHED, + static_cast(entries.size()), hash, + path.substr(directory.size() + 1).c_str(), size, static_cast(next_file), + &match) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbGeometricVerificationResult gvr{}; + double model[9]{}; + Lardon3DProjectDbResult gvr_result = lardon3d_project_db_create_geometric_verification_result( + db, match.match_result_id, kVerifier, kVersion, g_verifier, status, + popcount(mask), + mask.data(), mask.size(), + status == LARDON3D_GEOMETRIC_VERIFIED ? model : nullptr, + static_cast(next_file), &gvr); + CHECK(gvr_result == LARDON3D_PROJECT_DB_OK); + return gvr.geometric_verification_result_id; + } + + Lardon3DTrackBuilderProjectRequest request(const uint64_t *ids, size_t count) const { + return {directory.c_str(), db, kVerifier, kVersion, g_verifier, ids, count}; + } + + static uint32_t popcount(const std::vector &mask) { + uint32_t count = 0; + for (unsigned char byte : mask) { + for (unsigned bit = 0; bit < 8; ++bit) count += (byte >> bit) & 1U; + } + return count; + } +}; + +void assert_track(Lardon3DProjectDb *db, uint64_t set_id, uint64_t a, uint64_t b, + uint64_t c) { + Lardon3DProjectDbTrack track{}; + CHECK(lardon3d_project_db_find_track_by_observation(db, set_id, a, 0, &track) == + LARDON3D_PROJECT_DB_OK); + CHECK(track.observation_count == 3 && track.observations[0].feature_set_id == a && + track.observations[0].position_in_track == 0 && track.observations[1].feature_set_id == b && + track.observations[1].position_in_track == 1 && track.observations[2].feature_set_id == c && + track.observations[2].position_in_track == 2); + lardon3d_project_db_free_track(&track); +} + +void assert_two_observation_track(Lardon3DProjectDb *db, uint64_t set_id, uint64_t a, uint64_t b) { + Lardon3DProjectDbTrack track{}; + CHECK(lardon3d_project_db_find_track_by_observation(db, set_id, a, 0, &track) == + LARDON3D_PROJECT_DB_OK && track.observation_count == 2 && + track.observations[0].feature_set_id == a && + track.observations[0].position_in_track == 0 && + track.observations[1].feature_set_id == b && + track.observations[1].position_in_track == 1); + lardon3d_project_db_free_track(&track); +} + +uint64_t track_set_count(const std::string &path) { + sqlite3 *db = nullptr; + sqlite3_stmt *statement = nullptr; + CHECK(sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK); + CHECK(sqlite3_prepare_v2(db, "SELECT count(*) FROM track_sets", -1, &statement, nullptr) == + SQLITE_OK); + CHECK(sqlite3_step(statement) == SQLITE_ROW); + uint64_t result = static_cast(sqlite3_column_int64(statement, 0)); + sqlite3_finalize(statement); + sqlite3_close(db); + return result; +} + +uint64_t table_count(const std::string &path, const char *table) { + char sql[128]; + std::snprintf(sql, sizeof(sql), "SELECT count(*) FROM %s", table); + sqlite3 *db = nullptr; + sqlite3_stmt *statement = nullptr; + CHECK(sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK); + CHECK(sqlite3_prepare_v2(db, sql, -1, &statement, nullptr) == SQLITE_OK); + CHECK(sqlite3_step(statement) == SQLITE_ROW); + uint64_t result = static_cast(sqlite3_column_int64(statement, 0)); + CHECK(sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(db) == SQLITE_OK); + return result; +} + +void execute_sql(const std::string &path, const char *sql) { + sqlite3 *db = nullptr; + CHECK(sqlite3_open(path.c_str(), &db) == SQLITE_OK); + CHECK(sqlite3_exec(db, sql, nullptr, nullptr, nullptr) == SQLITE_OK); + CHECK(sqlite3_close(db) == SQLITE_OK); +} + +void independent_scope_digest(const uint64_t *ids, size_t count, unsigned char output[32]) { + std::vector bytes(8 + count * 8); + const char domain[] = "L3DTSIS1"; + std::memcpy(bytes.data(), domain, 8); + for (size_t i = 0; i < count; ++i) { + uint64_t value = ids[i]; + for (size_t byte = 0; byte < 8; ++byte) { + bytes[8 + i * 8 + byte] = static_cast(value & 0xffU); + value >>= 8; + } + } + unsigned int length = 0; + EVP_MD_CTX *context = EVP_MD_CTX_new(); + CHECK(context != nullptr && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1 && + EVP_DigestUpdate(context, bytes.data(), bytes.size()) == 1 && + EVP_DigestFinal_ex(context, output, &length) == 1 && length == 32); + EVP_MD_CTX_free(context); +} + +void run_basic_and_reuse() { + Fixture fixture; + auto ab = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + auto bc = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {ab, bc}; + auto request = fixture.request(ids, 2); + Lardon3DTrackBuilderProjectResult first{}; + auto first_status = lardon3d_track_builder_build_project(&request, &first); + CHECK(first_status == LARDON3D_TRACK_BUILDER_PROJECT_OK && !first.reused && + first.track_count == 1); + assert_track(fixture.db, first.track_set_id, fixture.feature[0], fixture.feature[1], + fixture.feature[2]); + uint64_t before = track_set_count(fixture.db_path); + Lardon3DTrackBuilderProjectResult second{}; + CHECK(lardon3d_track_builder_build_project(&request, &second) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && second.reused && second.track_set_id == first.track_set_id); + CHECK(track_set_count(fixture.db_path) == before); + std::puts("C01/C20: PASS"); +} + +void run_mask_cases() { + Fixture fixture; + std::vector entries; + for (uint32_t i = 0; i < 14; ++i) entries.push_back({i, i, 0.1F}); + auto gvr = fixture.add_gvr(0, 1, entries, {0x85, 0x21}); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.raw_inlier_edge_count == 5 && + result.track_count == 5); + std::puts("C02/C03: PASS (outlier exclusion, asymmetric LSB-first mask)"); +} + +void run_rejection_cases() { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}, LARDON3D_GEOMETRIC_REJECTED); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && + track_set_count(fixture.db_path) == 0); + request.verifier_kind = 99; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK); + CHECK(fixture.request(nullptr, 0).gvr_count == 0); + std::puts("C04-C07/C11: PASS"); +} + +void run_scope_errors() { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t unsorted[] = {gvr + 1, gvr}; + auto bad = fixture.request(unsorted, 2); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&bad, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT); + uint64_t duplicate[] = {gvr, gvr}; + bad = fixture.request(duplicate, 2); + CHECK(lardon3d_track_builder_build_project(&bad, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT); + uint64_t missing[] = {gvr + 1000}; + bad = fixture.request(missing, 1); + CHECK(lardon3d_track_builder_build_project(&bad, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + std::puts("C08-C10: PASS"); +} + +void run_revalidation_and_duplicate_scope() { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + g_delete_db = fixture.db_path; + g_delete_id = gvr; + g_delete_before_revalidation = true; + Lardon3DTrackBuilderProjectResult result{}; + auto status = lardon3d_track_builder_build_project(&request, &result); + CHECK(status != + LARDON3D_TRACK_BUILDER_PROJECT_OK && + track_set_count(fixture.db_path) == 0); + + Fixture duplicate_fixture; + auto first = duplicate_fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + auto second = duplicate_fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t duplicate_ids[] = {first, second}; + auto duplicate_request = duplicate_fixture.request(duplicate_ids, 2); + CHECK(lardon3d_track_builder_build_project(&duplicate_request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 1); + std::puts("C18/C25: PASS (duplicate collapse, selected-input disappearance detected)"); +} + +void run_remaining_matrix() { + { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + Lardon3DTrackBuilderProjectResult result{}; + request.verifier_version = 2; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + request.verifier_version = 1; + unsigned char wrong[32]; + std::memset(wrong, 0x43, sizeof(wrong)); + request.verifier_fingerprint = wrong; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + request.gvr_ids = nullptr; + request.gvr_count = 0; + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT && track_set_count(fixture.db_path) == 0); + std::puts("C06/C07/C11: PASS"); + } + + { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + Lardon3DProjectDbGeometricVerificationResult loaded{}; + CHECK(lardon3d_project_db_load_geometric_verification_result(fixture.db, gvr, &loaded) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbMatchResult match{}; + CHECK(lardon3d_project_db_load_match_result(fixture.db, loaded.match_result_id, &match) == + LARDON3D_PROJECT_DB_OK); + std::string path = fixture.directory + "/" + match.match_asset_path; + int fd = open(path.c_str(), O_WRONLY); + CHECK(fd >= 0); + unsigned char bad_magic = 'X'; + CHECK(write(fd, &bad_magic, 1) == 1 && close(fd) == 0); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + std::puts("C12: PASS (Match File bad magic propagated)"); + } + + { + Fixture fixture; + std::vector entries; + for (uint32_t i = 0; i < 9; ++i) entries.push_back({i, i, 0.1F}); + auto gvr = fixture.add_gvr(0, 1, entries, {0xff, 0x01}); + uint64_t ids[] = {gvr}; + execute_sql(fixture.db_path, + "UPDATE geometric_verification_results SET inlier_mask=X'FF' "); + auto request = fixture.request(ids, 1); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + std::puts("C13: PASS (mask length rejected by persisted loader)"); + } + + { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{32, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + std::puts("C14: PASS (Match File bounds validator rejected OOB index)"); + } + + { + Fixture fixture; + uint64_t extra = fixture.add_feature_set(0, 0x61); + auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}, + LARDON3D_GEOMETRIC_VERIFIED, extra, fixture.feature[1]); + auto second = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {first, second}; + auto request = fixture.request(ids, 2); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 0); + std::puts("C15: PASS (same-image scientific conflict)"); + } + + { + Fixture fixture; + uint64_t extra = fixture.add_feature_set(1, 0x62); + auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}, + LARDON3D_GEOMETRIC_VERIFIED, fixture.feature[0], extra); + auto second = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01}, + LARDON3D_GEOMETRIC_VERIFIED, extra, fixture.feature[2]); + uint64_t ids[] = {first, second}; + auto request = fixture.request(ids, 2); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 0); + std::puts("C16: PASS (heterogeneous Feature Set conflict)"); + } + + { + Fixture fixture; + auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}, + LARDON3D_GEOMETRIC_VERIFIED, 0, 0, "matcher-a", 1); + auto second = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01}, + LARDON3D_GEOMETRIC_VERIFIED, 0, 0, "matcher-b", 2); + uint64_t ids[] = {first, second}; + auto request = fixture.request(ids, 2); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 1); + std::puts("C17: PASS (matcher configurations do not rank edges)"); + } + + { + Fixture fixture; + auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + auto duplicate = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t one[] = {first}; + uint64_t two[] = {first, duplicate}; + auto request_one = fixture.request(one, 1); + auto request_two = fixture.request(two, 2); + Lardon3DTrackBuilderProjectResult a{}, b{}; + CHECK(lardon3d_track_builder_build_project(&request_one, &a) == + LARDON3D_TRACK_BUILDER_PROJECT_OK); + CHECK(lardon3d_track_builder_build_project(&request_two, &b) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && a.track_set_id != b.track_set_id && + a.track_count == b.track_count && track_set_count(fixture.db_path) == 2); + unsigned char expected_one[32]{}; + unsigned char expected_two[32]{}; + independent_scope_digest(one, 1, expected_one); + independent_scope_digest(two, 2, expected_two); + const unsigned char fixed_one[32] = { + 0x5e, 0xf4, 0xf8, 0x44, 0x57, 0x8f, 0x92, 0x2c, + 0x7d, 0x55, 0xb8, 0xb7, 0x9a, 0x9f, 0x71, 0x21, + 0xac, 0x6b, 0x8a, 0x55, 0xe6, 0xb5, 0x3d, 0x6d, + 0x87, 0x7e, 0x20, 0x4b, 0x52, 0x58, 0x59, 0xbe}; + const unsigned char fixed_two[32] = { + 0xff, 0xb4, 0x3e, 0x52, 0x87, 0x38, 0xac, 0x64, + 0xc7, 0x39, 0xaa, 0xa8, 0xe2, 0x4a, 0xf1, 0xd4, + 0xcd, 0x99, 0x4b, 0x59, 0xb4, 0x6a, 0x97, 0xc6, + 0xf4, 0xb1, 0x66, 0x00, 0x52, 0x5b, 0x64, 0xa3}; + Lardon3DProjectDbTrackSet loaded_one{}, loaded_two{}; + CHECK(lardon3d_project_db_load_track_set(fixture.db, a.track_set_id, &loaded_one) == + LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_load_track_set(fixture.db, b.track_set_id, &loaded_two) == + LARDON3D_PROJECT_DB_OK && + std::memcmp(expected_one, loaded_one.input_scope_hash, 32) == 0 && + std::memcmp(expected_two, loaded_two.input_scope_hash, 32) == 0 && + std::memcmp(expected_one, fixed_one, 32) == 0 && + std::memcmp(expected_two, fixed_two, 32) == 0 && + std::memcmp(expected_one, expected_two, 32) != 0); + std::puts("C19: PASS (same logical output, distinct scope identity)"); + } +} + +void run_durability_and_zero_track() { + Fixture fixture; + auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t ids[] = {gvr}; + auto request = fixture.request(ids, 1); + Lardon3DTrackBuilderProjectResult first{}; + CHECK(lardon3d_track_builder_build_project(&request, &first) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && first.track_count == 1); + uint64_t tracks_before = table_count(fixture.db_path, "tracks"); + uint64_t observations_before = table_count(fixture.db_path, "track_observations"); + lardon3d_project_db_close(fixture.db); + fixture.db = nullptr; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; + CHECK(lardon3d_project_db_open(fixture.db_path.c_str(), &fixture.db, error) == + LARDON3D_PROJECT_DB_OK); + request.database = fixture.db; + Lardon3DProjectDbTrackSet loaded{}; + CHECK(lardon3d_project_db_load_track_set(fixture.db, first.track_set_id, &loaded) == + LARDON3D_PROJECT_DB_OK && loaded.track_count == 1 && loaded.gvr_count == 1); + assert_two_observation_track(fixture.db, first.track_set_id, fixture.feature[0], + fixture.feature[1]); + Lardon3DTrackBuilderProjectResult reused{}; + CHECK(lardon3d_track_builder_build_project(&request, &reused) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && reused.reused && + reused.track_set_id == first.track_set_id); + CHECK(track_set_count(fixture.db_path) == 1 && table_count(fixture.db_path, "tracks") == + tracks_before && + table_count(fixture.db_path, "track_observations") == observations_before); + std::puts("C21/C22: PASS (close/reopen and reuse durability)"); + + Fixture zero; + uint64_t extra = zero.add_feature_set(0, 0x63); + auto conflict_a = zero.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}, + LARDON3D_GEOMETRIC_VERIFIED, extra, zero.feature[1]); + auto conflict_b = zero.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + uint64_t conflict_ids[] = {conflict_a, conflict_b}; + auto conflict_request = zero.request(conflict_ids, 2); + Lardon3DTrackBuilderProjectResult zero_result{}; + CHECK(lardon3d_track_builder_build_project(&conflict_request, &zero_result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && zero_result.track_count == 0 && + track_set_count(zero.db_path) == 1); + std::puts("C23: PASS (zero-track Track Set published)"); +} + +void run_scope_snapshot_case() { + Fixture fixture; + auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + auto second = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01}); + auto late = fixture.add_gvr(0, 1, {{7, 7, 0.1F}}, {0x01}); + Lardon3DProjectDbGeometricVerificationResult late_gvr{}; + CHECK(lardon3d_project_db_load_geometric_verification_result(fixture.db, late, &late_gvr) == + LARDON3D_PROJECT_DB_OK); + execute_sql(fixture.db_path, + ("DELETE FROM geometric_verification_results WHERE " + "geometric_verification_result_id=" + std::to_string(late)) + .c_str()); + g_insert_db = fixture.db_path; + g_insert_match_id = late_gvr.match_result_id; + g_insert_during_build = true; + uint64_t ids[] = {first, second}; + auto request = fixture.request(ids, 2); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 1 && result.gvr_count == 2); + CHECK(track_set_count(fixture.db_path) == 1); + std::puts("C24: PASS (late matching GVR excluded from explicit scope)"); +} + +void run_partial_input_failure() { + Fixture fixture; + auto valid = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}); + auto corrupt = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01}); + Lardon3DProjectDbGeometricVerificationResult loaded{}; + CHECK(lardon3d_project_db_load_geometric_verification_result(fixture.db, corrupt, &loaded) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbMatchResult match{}; + CHECK(lardon3d_project_db_load_match_result(fixture.db, loaded.match_result_id, &match) == + LARDON3D_PROJECT_DB_OK); + int fd = open((fixture.directory + "/" + match.match_asset_path).c_str(), O_WRONLY); + CHECK(fd >= 0); + unsigned char bad_magic = 'Q'; + CHECK(write(fd, &bad_magic, 1) == 1 && close(fd) == 0); + uint64_t ids[] = {valid, corrupt}; + auto request = fixture.request(ids, 2); + Lardon3DTrackBuilderProjectResult result{}; + CHECK(lardon3d_track_builder_build_project(&request, &result) != + LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0); + std::puts("PARTIAL INPUT: PASS"); +} + +void run_resource_case() { + Fixture fixture(8192); + std::vector entries; + std::vector mask(1024, 0xff); + entries.reserve(8192); + for (uint32_t i = 0; i < 8192; ++i) entries.push_back({i, i, 0.1F}); + std::vector ids; + for (size_t i = 0; i < 13; ++i) ids.push_back(fixture.add_gvr(0, 1, entries, mask)); + auto request = fixture.request(ids.data(), ids.size()); + Lardon3DTrackBuilderProjectResult result{}; + auto started = std::chrono::steady_clock::now(); + CHECK(lardon3d_track_builder_build_project(&request, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_OK && result.raw_inlier_edge_count == 106496 && + result.core_observation_count == 16384 && result.track_count == 8192); + auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - started); + struct rusage usage{}; + CHECK(getrusage(RUSAGE_SELF, &usage) == 0); + std::printf("RESOURCE GVR=%zu RAW=%llu INLIERS=%llu FEATURES=3 OBS=%llu TRACKS=%llu\n", + ids.size(), static_cast(result.raw_inlier_edge_count), + static_cast(result.raw_inlier_edge_count), + static_cast(result.core_observation_count), + static_cast(result.track_count)); + std::printf("RESOURCE DURATION_SECONDS=%.3f PEAK_RSS_KIB=%ld MAX_MATCH_FILES_LIVE=1\n", + elapsed.count(), usage.ru_maxrss); +} + +} // namespace + +#ifdef LARDON3D_TRACK_BUILDER_PROJECT_TESTING +extern "C" void lardon3d_track_builder_project_test_before_revalidation( + Lardon3DProjectDb *, const uint64_t *, size_t) { + if (g_insert_during_build) { + sqlite3 *connection = nullptr; + CHECK(sqlite3_open(g_insert_db.c_str(), &connection) == SQLITE_OK); + char sql[512]; + std::snprintf( + sql, sizeof(sql), + "INSERT INTO geometric_verification_results " + "(match_result_id,verifier_kind,verifier_version,parameter_fingerprint,status," + "inlier_count,inlier_mask,model_m00,model_m01,model_m02,model_m10,model_m11,model_m12," + "model_m20,model_m21,model_m22,created_at) VALUES (%llu,1,1," + "X'4242424242424242424242424242424242424242424242424242424242424242',2,1,X'01'," + "0,0,0,0,0,0,0,0,0,999)", + static_cast(g_insert_match_id)); + int insert_result = sqlite3_exec(connection, sql, nullptr, nullptr, nullptr); + if (insert_result != SQLITE_OK) { + std::fprintf(stderr, "late GVR insert: %s\n", sqlite3_errmsg(connection)); + } + CHECK(insert_result == SQLITE_OK); + CHECK(sqlite3_close(connection) == SQLITE_OK); + g_insert_during_build = false; + } + if (!g_delete_before_revalidation) return; + sqlite3 *connection = nullptr; + CHECK(sqlite3_open(g_delete_db.c_str(), &connection) == SQLITE_OK); + char sql[256]; + std::snprintf(sql, sizeof(sql), + "DELETE FROM geometric_verification_results WHERE " + "geometric_verification_result_id=%llu", + static_cast(g_delete_id)); + CHECK(sqlite3_exec(connection, sql, nullptr, nullptr, nullptr) == SQLITE_OK); + CHECK(sqlite3_close(connection) == SQLITE_OK); + g_delete_before_revalidation = false; +} +#endif + +int main() { + Lardon3DTrackBuilderProjectResult result{}; + Lardon3DTrackBuilderProjectRequest invalid{}; + CHECK(lardon3d_track_builder_build_project(&invalid, &result) == + LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT); + run_basic_and_reuse(); + run_mask_cases(); + run_rejection_cases(); + run_scope_errors(); + run_revalidation_and_duplicate_scope(); + run_remaining_matrix(); + run_durability_and_zero_track(); + run_scope_snapshot_case(); + run_partial_input_failure(); + run_resource_case(); + std::puts("C01-C27 integration harness: PASS (C26 N/A; C27 deferred to Gate D)"); + return 0; +}