Compare commits

..

41 commits

Author SHA1 Message Date
fy59
6a52ef4957 feat: assemble autofocus study evidence 2026-09-03 14:46:15 +02:00
fy59
1a96bcce1f feat: bridge calibration workflow to autofocus study 2026-09-03 14:35:46 +02:00
fy59
524f08d1b3 feat: add calibration autofocus study evidence 2026-09-03 14:24:29 +02:00
fy59
34c6a82b35 feat: add calibration v2 autofocus foundation 2026-09-03 14:00:07 +02:00
fy59
038cc64032 feat: complete heterogeneous calibration workflow 2026-09-03 13:20:22 +02:00
fy59
a77d095a85 feat: publish heterogeneous calibration groups 2026-09-03 13:08:36 +02:00
fy59
8fee775586 feat: add calibration v2 optical state foundation 2026-09-03 12:51:15 +02:00
fy59
72375078df docs: freeze calibration science v2 2026-09-03 12:25:57 +02:00
fy59
cc903981ad docs: define calibration science v2 heterogeneous optics 2026-09-03 12:22:07 +02:00
fy59
53563ce1e7 feat: complete calibration workflow ready transition 2026-09-03 10:49:36 +02:00
fy59
df7b589174 feat: bind calibration workflow selected execution 2026-09-03 10:39:52 +02:00
fy59
6a670fd9e9 feat: materialize calibration workflow evidence 2026-09-03 10:17:00 +02:00
fy59
e1cf83a158 feat: add calibration workflow input boundary 2026-09-03 09:56:20 +02:00
fy59
311b303744 fix: repair calibration solver bundle evidence 2026-09-03 09:42:47 +02:00
fy59
c8349caa49 feat: retain calibration per-view evidence 2026-09-03 09:31:12 +02:00
fy59
33d9b58ce8 feat: bind calibration solver producer identity 2026-09-03 07:12:47 +02:00
fy59
c8447c66c4 feat: require measured calibration target border 2026-09-03 07:09:29 +02:00
fy59
b9e4309842 fix: align calibration tooling with science v1 2026-09-03 06:44:50 +02:00
fy59
28d714838b docs: advance execution contract to calibration workflow 2026-09-03 06:33:02 +02:00
fy59
e3c0dc1e2c chore: normalize user-facing language and repair v25 fixtures 2026-09-03 06:28:56 +02:00
fy59
b8c5dc8eb1 docs: add final execution contract 2026-09-03 05:59:25 +02:00
fy59
2e7d3c5656 docs: freeze product definition v1 2026-09-02 23:10:12 +02:00
fy59
4fc5a8ea48 docs: close source comment remediation 2026-09-02 23:01:25 +02:00
fy59
356cfd125c docs: close documentation finding remediation 2026-09-02 22:41:29 +02:00
fy59
a3a005b38e docs: reconcile current reconstruction architecture 2026-09-02 22:30:33 +02:00
fy59
7574362ba9 docs: reconcile development validation guidance 2026-09-02 22:10:32 +02:00
fy59
4278d124c2 docs: reconcile Task System with current execution policy 2026-09-02 22:02:00 +02:00
fy59
1d073fab46 docs: reconcile production Task Kind Registry 2026-09-02 21:54:43 +02:00
fy59
475f3f1eb0 docs: document current Feature batch execution path 2026-09-02 21:49:06 +02:00
fy59
7712fe19fe docs: reconcile Candidate Pair resource contract 2026-09-02 21:43:46 +02:00
fy59
c27be7776b docs: align architecture overview with current pipeline 2026-09-02 21:37:04 +02:00
fy59
d050b13ddc docs: update resource-aware pipeline for current execution model 2026-09-02 21:08:41 +02:00
fy59
eb454d5fae docs: reconcile resource boundary with current policy 2026-09-02 21:00:00 +02:00
fy59
4522602421 docs: reconcile persistence with schema v25 2026-09-02 15:13:28 +02:00
fy59
2d2bf31328 docs: reconcile Project Database through schema v25 2026-09-02 15:07:32 +02:00
fy59
06c78914ce docs: reconcile canonical roadmap with current state 2026-09-02 14:02:54 +02:00
fy59
306ace2fb3 docs: update agent contract for current project state 2026-09-02 13:25:38 +02:00
fy59
8434fcd773 docs: refresh README and adopt MIT license 2026-09-02 13:14:12 +02:00
fy59
d0329bc3db docs: standardize repository language on English 2026-09-02 12:53:23 +02:00
fy59
b23241eab2 docs: add documentation inventory audit 2026-09-02 12:24:27 +02:00
fy59
308cf739ac secure commit 2026-09-02 09:26:54 +02:00
132 changed files with 25270 additions and 7817 deletions

View file

@ -23,6 +23,36 @@
Resolve locally determinable engineering problems from the existing code and Resolve locally determinable engineering problems from the existing code and
canonical documentation. canonical documentation.
## Repository language policy
The canonical language of the Lardon3D repository is English.
This applies to:
- README.md;
- AGENTS.md;
- all current and future files under docs/**;
- prompt.md and all files under prompt/**;
- source comments under include/lardon3d/** and src/**;
- developer-facing technical documentation, diagnostics and engineering contracts where applicable.
The repository must not intentionally mix French and English technical prose.
Existing French documentation and source comments are to be converted to English during the planned documentation and source-comment remediation passes, without changing scientific meaning, FROZEN facts, historical evidence, numeric values, identities, lifecycle truth or contract authority.
Historical documents may be translated, but translation must not modernize or reinterpret the historical state they record.
The canonical user-interface language is also English. Existing non-English TUI labels must converge on English during an explicitly scoped UI-language remediation pass; documentation-only work must not silently modify executable UI behavior.
Canonical markers:
DOCUMENTATION_LANGUAGE=ENGLISH
SOURCE_COMMENT_LANGUAGE=ENGLISH
AGENT_CONTRACT_LANGUAGE=ENGLISH
USER_INTERFACE_LANGUAGE=ENGLISH
## 2. FROZEN integrity ## 2. FROZEN integrity
The following current project foundation is protected and may change only The following current project foundation is protected and may change only
@ -45,21 +75,24 @@ through an explicitly authorized, explicitly scoped human ticket:
- Durable Acquisition-Campaign Execution — PASS/FROZEN - Durable Acquisition-Campaign Execution — PASS/FROZEN
- Global Maintenance Audit — PASS/FROZEN - Global Maintenance Audit — PASS/FROZEN
- Real S21 Tracks scientific result — PASS/FROZEN - Real S21 Tracks scientific result — PASS/FROZEN
- Real A6000 pre-SfM scientific result — PASS/FROZEN
Detailed subcontracts remain defined by their canonical documents. This file Detailed subcontracts remain defined by their canonical documents. This file
must not duplicate every S3 substage, scientific threshold, migration detail, must not duplicate every S3 substage, scientific threshold, migration detail,
or persistence format. or persistence format.
Project DB v25 is the current additive operational schema in the active Feature Project DB v25 is an additive operational schema in the retained lineage. It preserves v24 RAW batch, the v23
batch tranche. It preserves v24 RAW batch, the v23 optical overlay and the v22 optical overlay and the v22 scientific/persistence foundation. Its additive purpose is typed durable
scientific/persistence foundation. Its authorized purpose is limited to typed persistence for `features.extract.batch/1` through `feature_extract_batch_tasks`; it adds no
durable persistence for `features.extract.batch/1` through scientific identity and must not reinterpret historical rows. The v25 Feature-batch path has completed
`feature_extract_batch_tasks`; it adds no scientific identity and must not its retained real A6000 proof. Historical references to older Project DB versions remain valid where
reinterpret historical rows. Until the v25/Feature-batch tranche has completed they describe the actual historical contract or migration path.
its real-data proof and final review, its lifecycle must remain truthful rather
than being marked `PASS/FROZEN` prematurely. Historical references to older Project DB v27 is the current additive operational schema. It preserves the v22 scientific/
Project DB versions remain valid where they describe the actual historical persistence foundation, v23 optical overlay, v24 RAW batch, v25 Feature batch and v26 exact
contract or migration path. Capture geometric-state/applicability foundation. Its v27 focus-domain rows are generic bounded
exact-token machinery with no historical backfill and no device-specific physical applicability
claim. Future schema changes beyond v27 require explicit human authorization.
The global maintenance implementation, fresh portable/Vulkan/sanitizer/ The global maintenance implementation, fresh portable/Vulkan/sanitizer/
concurrency validation and independent final review are acquired. Its lifecycle concurrency validation and independent final review are acquired. Its lifecycle
@ -78,6 +111,12 @@ their directly affected contracts, tests, documentation, and crossed dependency
boundaries. Unchanged PASS/FROZEN systems inherit this evidence and are reopened boundaries. Unchanged PASS/FROZEN systems inherit this evidence and are reopened
only by concrete evidence; do not repeat a global A-to-Z audit. only by concrete evidence; do not repeat a global A-to-Z audit.
The later real-data checkpoint is tag `real-a6000-pre-sfm-2026-09-02`. It establishes
`REAL_A6000_PRE_SFM=PASS/FROZEN` through Geometric Verification and Tracks while explicitly
leaving Sparse SfM and Dense/MVS unexecuted. This checkpoint adds later operational and real-data
evidence; it does not erase or replace the `global-maintenance-2026-09-01` review authority for
unchanged FROZEN systems.
When a ticket declares `NO_NEW_SUBSYSTEM`, do not introduce an unrelated: When a ticket declares `NO_NEW_SUBSYSTEM`, do not introduce an unrelated:
- Task Runtime; - Task Runtime;
@ -212,7 +251,7 @@ explicitly defines such an identity.
progress distinct from generic runtime percentage, and keep unknown progress distinct from generic runtime percentage, and keep unknown
provenance visibly UNKNOWN. Full layout starts at 100x30, compact is provenance visibly UNKNOWN. Full layout starts at 100x30, compact is
supported through 60x15 (72x20 is the reference compact boundary), and only supported through 60x15 (72x20 is the reference compact boundary), and only
the bounded "Terminal trop petit" fallback is allowed below that minimum. the bounded terminal-too-small fallback is allowed below that minimum.
- Preserve the contextual key contract. `F10 SSD` remains literally visible at - Preserve the contextual key contract. `F10 SSD` remains literally visible at
60 columns in idle, text-input and import-running modes; text input owns only 60 columns in idle, text-input and import-running modes; text input owns only
Enter/Escape/F10 and a running import owns only cancel (`X`) and F10 while Enter/Escape/F10 and a running import owns only cancel (`X`) and F10 while
@ -767,7 +806,9 @@ For Project DB:
overlay; overlay;
- v24 is the explicitly authorized additive operational RAW-batch migration and - v24 is the explicitly authorized additive operational RAW-batch migration and
must not reinterpret scientific history; must not reinterpret scientific history;
- future schema-version changes beyond the currently authorized v24 require - v25 is the additive Feature-batch migration for `features.extract.batch/1` and
must preserve v22/v23/v24 identities, rows and semantics;
- future schema-version changes beyond the current v27 head require
explicit human authorization; explicit human authorization;
- migrations must be additive unless a different migration is explicitly - migrations must be additive unless a different migration is explicitly
authorized; authorized;
@ -985,7 +1026,7 @@ Every completed ticket report must include:
STOP and request a human decision only when resolution requires: STOP and request a human decision only when resolution requires:
- changing a FROZEN scientific contract; - changing a FROZEN scientific contract;
- changing Project DB schema/version beyond already authorized v24 without - changing Project DB schema/version beyond the current v27 head without
prior authorization; prior authorization;
- introducing a genuinely new subsystem outside authorized scope; - introducing a genuinely new subsystem outside authorized scope;
- files outside the authorized scope; - files outside the authorized scope;

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 LabfyTools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

596
README.md
View file

@ -1,198 +1,334 @@
# Lardon3D # Lardon3D
Moteur de photogrammétrie générique, persistant, incrémental et sensible aux Lardon3D is a generic, persistent, incremental, resource-aware photogrammetry engine for Linux,
ressources, piloté par une TUI ncursesw. controlled through an ncursesw TUI.
## Vision ## Vision
Lardon3D est un moteur de photogrammétrie Linux qui privilégie : Lardon3D is designed around the following principles:
- **Stabilité** : aucune saturation du système hôte - **Scientific traceability**: results, identities, parameters and provenance are explicit.
- **Déterminisme** : résultats reproductibles et traçables - **Determinism**: equivalent inputs and contracts produce reproducible, auditable outputs.
- **Faible consommation mémoire** : traitement par lots adaptatifs - **Persistent progress**: long-running work is checkpointed and restartable.
- **Reprise après interruption** : résultats atomiques et persistants - **Bounded execution**: memory, CPU, GPU, I/O and temporary-storage use are explicitly bounded.
- **Protection de la machine** : budgets bornés et respectueux - **Maximum safe useful throughput**: after preserving the interactive host reserve, available
- **Traçabilité** : historique des opérations et métriques resources should be used whenever they provide useful throughput.
- **Enrichissement progressif** : reconstruction incrémentale - **Incremental reconstruction**: new observations can extend previous results without silently
rewriting validated history.
- **Atomic publication**: partially produced scientific outputs never masquerade as complete ones.
Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais un ensemble Lardon3D is not simply a "folder of photos -> 3D object" tool. Its target model is:
progressif d'observations et de contraintes donnant une reconstruction géométrique
persistante, enrichissable et versionnable.
## État actuel ```text
progressive observations and constraints
-> persistent geometric reconstruction
-> validated incremental enrichment
-> dense geometry / mesh / texture / export
```
### Briques validées ## Current repository state
- **Project** : cycle de vie persistant, identité stable et Project Database ouverte ### Current Project Database
- **Import** : premier task kind de production, exécuté par la file générique en lots bornés et reprenables
- **ScanSet / Image Catalog v1** : acquisitions, images logiques, provenance et assets SHA-256 persistants et paginés
- **Capture / Asset Provenance v1** : Captures par ScanSet, associations source/dérivé
et sélection explicite d'une image logique — PASS / FROZEN
- **Découverte et planification de campagne bornées** : racines explicites,
plan metadata-only et exécution par groupes via S3-E — PASS / FROZEN ; campagne
A6000 réelle validée sur 953 ARW + 953 JPEG MPF
- **Feature Store v1/v2** : ORB U8×32, SIFT/RootSIFT F32×128 et lecture typée bornée
- **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, protocole `.chk.next` → SQLite →
`.chk` sous verrou par tâche, et reprise sûre
- **Project Database v23** : overlay optique additif au-dessus de la fondation
v22 PASS / FROZEN ; profils de boîtier et d'objectif, configurations
body+lens+focale, affectations campagne/Capture et calibrations exactement
compatibles, sans inférence ni backfill — IMPLEMENTED / VALIDATED / REVIEWED
- **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG
- **[Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)** :
import borné d'une calibration optimisée et traçable avant le Sparse SfM à
calibration connue — PASS / FROZEN ; ni
auto-calibration interne ni EXIF comme source de calibration scientifique
- **Exécution durable de campagne d'acquisition** : tâche générique à requête
typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance
tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par
la registry, la Queue et le Resource Governor existants
- **Sparse SfM Gates C/D/E** : géométrie calibrée, noyau incrémental et Bundle
Adjustment final par composante, tous PASS / FROZEN
- **Sparse SfM Gate F** : orchestration durable, runtime gouverné et publication
atomique, PASS / FROZEN
- **MVS-M1** : frontière externe OpenMVS v2.4.0
`InterfaceCOLMAP`/`DensifyPointCloud`, export COLMAP déterministe (OpenCV
undistortion, observations transformées et tracks réels), texte exporté en
flux et tracks indexés sans rescanner quadratiquement les observations ; espace
de travail privé neuf par invocation sous le staging appelant, sans réemploi ;
identité dense liée à la reconstruction de base, au jeu source, au
`calibration_scope_identity` historique, au binding numérique MVS, au backend
et aux paramètres ; `L3DMDID2` v2 (220 octets) et binding `L3DMCAL1` v1 ; PLY
OpenMVS binaire little-endian validé (en-tête <= 1 MiB en octets bruts,
LF/CRLF acceptés, CR seul malformé rejeté, ligne <= 64 KiB), fusionné en
mode 0 — PASS / FROZEN
- **Geometric Verification Model** : identité, masque d'inliers et modèle 3×3
persistants pour les policies Verifier v1/v2 historiques et v3 courante
- **Geometric Verifier v3** : Fundamental USAC/MAGSAC, reprise et lots resource-aware
- **Internal Parallelism + Compute Resources v1** : parallélisme interne borné,
sorties canoniques et admission Governor — PASS / FROZEN
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
- **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
- **Hardware Profile** : détection des capacités matérielles
- **Resource Snapshot** : capture instantanée des ressources
- **Resource Governor** : arbitrage centralisé des budgets et réservations
- **TUI observatoire / centre de contrôle** : modèle de vue pur et borné,
observation coalescée, progression durable/ETA honnête, écrans Tasks,
Resources, Optique et SSD, avec ncurses exclusivement sur le thread principal
— CURRENT / VALIDATED OPERATIONAL
- **Contrôleur SSD externe optionnel** : frontière physique UDisks2/GDBus,
identité Drive+labels+UUIDs, drain sûr et capacités de contrôle exactes ; son
état physique est enregistré auprès du Governor, seul orchestrateur des
leases scratch de production — CURRENT / VALIDATED OPERATIONAL
### Intégration réelle validée The current Project DB schema is **v27**.
Sony A6000 et Samsung S21 FE sont des preuves de validation de la chaîne The current head is additive:
générique. Ils ne définissent ni l'identité produit, ni un profil caméra
hardcodé, ni une limite de CPU ou de dataset.
- **Intégration multi-campagne A6000 + S21 FE Engine Bay** : PASS — les plans ```text
réels A6000 (953 paires confirmées `CALLER_EXPLICIT`) et Samsung SM-G990B v22 Selected scientific execution foundation
(3544 JPEG singleton) ont été validés dans deux ScanSets d'un même projet v23 Generic optical-context overlay
temporaire, avec exécution durable, Governor/Queue et reprise sans Capture v24 raw.develop.batch/1 persistence
dupliqué. Les campagnes réelles actuellement évaluées sont v25 features.extract.batch/1 persistence
`CALIBRATION_UNAVAILABLE`, donc le Sparse SfM réel est v26 Capture geometric state and exact calibration applicability
`BLOCKED_BY_KNOWN_CALIBRATION_DATA` : ce n'est ni un échec logiciel, ni un v27 Generic bounded exact-token focus domains
rejet de qualité, ni une autorisation d'importer une pseudo-calibration. La ```
suite reste le pipeline scientifique aval, selon la
[roadmap canonique](docs/roadmap/roadmap.md).
### Plus tard / différé Earlier schema versions remain valid historical contracts where their own documentation says so.
No migration silently reinterprets historical scientific identities.
- publication durable dense/mesh et consommation Task explicite du scratch ### Current production task inventory
SSD optionnel ;
- vidéo/keyframes et **Capture Guidance / Live Coverage** : analyse et viewer de The production registry currently contains **16 Task kinds**.
couverture, suggestions de prises de vue puis assistance live, après
reconstruction mature ; All production Tasks pass through the existing Task -> Queue -> Resource Governor execution model.
- exports et publication live ; The Queue has one active callback at a time; Tasks may use bounded internal participants when their
- DAG général, pools multiples et parallélisme inter-tâches restent différés. contract and measured scaling justify it.
### Resource policy
The canonical operational objective is:
```text
MAXIMUM SAFE USEFUL THROUGHPUT
SERIALISM_REQUIRES_PROOF
```
Lardon3D first preserves the interactive host reserve required for the desktop, Firefox, audio and
light interactive use. Safe and useful resources beyond that reserve belong to the active workload.
On the current validation host, the normal observed outcome is approximately:
```text
16 logical CPUs total
4 logical CPUs reserved for interactive host use
12 logical CPUs available to the compute pool
~3 GiB MemAvailable preserved as the hard RAM reserve
Radeon 780M UMA available to validated and useful GPU backends
```
These are **reference-host observations, not portable product constants**. The Resource Governor
derives usable capacity from the current host, affinity, topology, memory and pressure state.
A long-running CPU1 or batch1 path is acceptable only when serialism, a measured scaling knee,
memory, I/O, GPU execution or another concrete constraint justifies it. Per-item atomicity does not
imply cross-item serialization.
## Validated foundations
The following major foundations are implemented and validated at their documented boundaries:
- **Project / persistent lifecycle**
- **Import and ScanSet / Image Catalog**
- **Capture / Asset Provenance v1 — PASS / FROZEN**
- **Bounded acquisition discovery and campaign execution — PASS / FROZEN**
- **Photo Quality Triage / Acquisition Selection — PASS / FROZEN**
- **Selected Scientific Execution — PASS / FROZEN**
- **Feature Store v1/v2**
- ORB U8x32
- SIFT / RootSIFT F32x128
- bounded typed readers
- **Visual Index v1**
- **Candidate Pair generation**
- **Matcher v1**
- ORB CPU / validated Vulkan hot path
- SIFT / RootSIFT CPU
- **Geometric Verification Model**
- **Geometric Verifier v3**
- **Track Model / Track Builder v1 — PASS / FROZEN**
- **Sparse SfM Gates C/D/E/F/G — PASS / FROZEN**
- **Phase H v1 incremental reconstruction — PASS / FROZEN**
- **MVS-M1 external OpenMVS boundary — PASS / FROZEN**
- **Task Runtime / checkpoints / recovery**
- **Task Queue**
- **Task Kind Registry**
- **Resource Governor / Compute Governor v2**
- **Bounded internal parallelism — PASS / FROZEN**
- **ORB Vulkan asynchronous execution — PASS / FROZEN**
- **TUI runtime observatory / control center — CURRENT / VALIDATED OPERATIONAL**
- **Optional external SSD controller — CURRENT / VALIDATED OPERATIONAL**
- **Calibration Bootstrap v1 — PASS / FROZEN**
- **Calibration Science v1 — PASS / FROZEN**
- **Calibration Tooling v1 — PASS / FROZEN**
- **Calibration Solver Preflight v1 — PASS**
- **Calibration Evidence Solver v1 — IMPLEMENTED / VALIDATED**
- **Calibration Tooling planarity alignment — PASS / FROZEN**
- **Project DB v24/v25/v26/v27 operational overlays — IMPLEMENTED / VALIDATED**
- raw.develop.batch/1 durable selected-execution path
- features.extract.batch/1 durable selected-execution path
## Real-data validation
Sony A6000 and Samsung S21 FE campaigns are validation evidence for the generic pipeline. They are
not product identities, hardcoded camera profiles, CPU limits or dataset-size limits.
### Real S21 Tracks
```text
REAL_S21_TRACKS=PASS/FROZEN
```
The retained real S21 proof validated the complete pre-SfM chain through Track Builder with the
compact Track memory model and deterministic restart semantics.
### Real A6000 pre-SfM
```text
REAL_A6000_PRE_SFM=PASS/FROZEN
```
The retained real A6000 proof uses the selected RAW-derived deterministic representation and
completed the pipeline through Geometric Verification and Tracks without replaying already acquired
upstream work.
Final retained counts:
```text
Selected images 689
Feature Sets 689
Candidate Pairs 38,420
Match Results 38,420
Applicable GVRs 37,805
Verified GVRs 10,952
Rejected GVRs 26,853
Track Sets 1
Tracks 130,714
Track observations 318,944
```
The restart proof reused the existing Track Set without duplicating GVR mappings, Track observations
or Tracks.
The real A6000 proof intentionally stopped before:
```text
Sparse SfM
Sparse reconstruction
Dense / MVS
multi-campaign fusion
```
### Calibration status of the historical real campaigns
The historical S21 and A6000 Engine Bay campaigns currently remain `CALIBRATION_UNAVAILABLE` for
the known-calibration Sparse SfM contract.
This is not a source failure, a quality rejection or permission to infer calibration from metadata.
No pseudo-calibration, silent interpolation or inferred calibration identity is allowed.
Therefore real Sparse SfM for those historical campaigns remains:
```text
BLOCKED_BY_KNOWN_CALIBRATION_DATA
```
Calibration Science v1 defines the protocol for future physically controlled calibration
acquisitions. The external Calibration Evidence Solver v1 implements the qualified OpenCV 5.x
evidence path. Calibration Tooling v1 validates bounded Science v1 evidence and produces the
L3DCALB1 artifact; Calibration Bootstrap v1 imports that artifact. None of these stages turns EXIF
into scientific calibration.
The current missing product boundary is the workflow coordinator that binds the immutable physical
session and solver bundle to one exact selected execution and drives Tooling/Bootstrap to `READY`.
## Architecture ## Architecture
```text ```text
TUI / Projet TUI / Project
|
Task Queue bornée (un worker, ordre/backpressure) v
bounded Task Queue
Resource Governor (admission et réservation) (one active callback)
|
Task callback admis (parallélisme interne borné si prouvé) v
Resource Governor
Résultats atomiques / persistants (admission and reservation)
|
Viewer (consommation passive de snapshots) v
admitted Task callback
(bounded internal participants when justified)
|
v
atomic / persistent scientific publication
|
v
passive snapshot consumers
(viewer remains future work)
``` ```
### Invariants fondamentaux Core invariants:
- Aucun callback de tâche sans réservation active validée - no Task callback starts without a valid active reservation;
- La Queue/runtime ne décide jamais des ressources - the Queue does not own resource policy;
- Le Resource Governor est l'unique propriétaire des budgets - the Resource Governor is the sole production resource authority;
- ncurses appartient exclusivement au thread principal - ncurses remains owned by the main thread;
- Les estimations de ressources sont immuables - Task estimates and installed sequence contracts remain immutable for their defined lifetime;
- Les buffers et files sont strictement bornés - buffers, queues, files, threads, participants and temporary work remain bounded;
- owner-only durable publication does not imply serial preparation;
- swap, zram and external scratch never become admitted RAM;
- UMA GPU memory is charged exactly once against host memory.
### TUI opérationnelle ## Current TUI
La TUI sépare le modèle de vue pur du rendu ncurses. Son observateur copie au The TUI is a validated operational observatory and control center.
plus 129 entrées Queue (64 pending, une active, 64 historiques) et coalesce les
captures hôte autour d'une seconde ; aucun scan DB ou `/proc` volumineux n'a
lieu par frame. La progression scientifique exacte provient seulement des
compteurs durables typés. Le taux EWMA et l'ETA restent « calcul » jusqu'à deux
intervalles positifs, excluent le préfixe repris et deviennent explicitement
indéterminés, stalled ou throttled lorsque l'évidence l'exige.
Les tailles supportées sont 100×30 et plus en vue complète, 72×20 en compacte It provides bounded observation of:
de référence, et jusqu'au minimum 60×15 ; en dessous, seul « Terminal trop
petit » est affiché. Les couleurs ont toujours un équivalent textuel/bold/dim.
`F1` à `F7` ouvrent aide, projets, import, viewer futur, tâches, ressources et
optique. Le segment littéral `F10 SSD` reste visible à 60 colonnes et déclenche
uniquement l'action autorisée par le contrôleur. Pendant une saisie, seules
Enter, Échap et F10 sont actives ; pendant un import, seules `X` et F10 le sont,
et quitter/retour accueil sont explicitement désactivés.
Ouvrir, fermer ou changer de projet détruit et joint d'abord l'unique Queue, - Project state;
callbacks terminaux inclus, puis ferme Project DB et recrée une Queue vide. Le - Tasks and durable progress;
workflow optique utilise les alias metadata exacts, accepte normalement les - Resource Governor state;
objectifs manuels sans EXIF, crée des profils/configurations immuables et exige - CPU / RAM / swap / GPU information;
une affectation/sélection de calibration explicite et exactement compatible. - optical profiles and explicit calibration selection;
- optional SSD state and safe control actions.
## Pipeline cible The ncurses renderer and input handling remain on the main thread. Runtime observation is bounded
and coalesced; the renderer does not scan Project DB or `/proc` extensively per frame.
The validated layout supports:
```text ```text
Acquisitions full layout >= 100x30
→ catalogue reference compact 72x20
→ features minimum supported 60x15
→ index visuel
→ paires candidates
→ matching
→ vérification géométrique
→ tracks / SfM
→ dense
→ mesh
→ consolidation
→ export
``` ```
Below the minimum, only the bounded terminal-too-small fallback is rendered.
The optical workflow supports electronic metadata aliases and manual lenses without EXIF. Missing,
ambiguous or incompatible calibration remains visible and is never silently guessed.
## Target pipeline
```text
acquisition
-> catalog / Capture / provenance
-> quality selection
-> selected scientific representation
-> features
-> visual index
-> candidate pairs
-> matching
-> geometric verification
-> tracks
-> Sparse SfM
-> incremental / multi-campaign reconstruction
-> dense / MVS
-> mesh
-> refinement
-> texturing
-> consolidation
-> export
```
## Planned product areas
The following areas remain future work and must not be confused with current implementation:
- durable dense / mesh publication;
- full Dense/MVS orchestration;
- mesh refinement and texturing;
- final export workflow;
- viewer;
- offline coverage analysis;
- suggested supplementary viewpoints;
- live camera localization;
- live coverage overlay;
- A6000 live acquisition integration;
- S21 live acquisition integration;
- capture guidance;
- video ingestion and deterministic keyframe extraction;
- explicit Task-owned scratch consumers;
- general DAG / dependency scheduling.
The final product contracts for these areas are being defined separately before implementation.
## Documentation ## Documentation
- [Documentation index](docs/README.md)
### Architecture ### Architecture
- [Vue d'ensemble](docs/architecture/overview.md)
- [Architecture overview](docs/architecture/overview.md)
- [Runtime](docs/architecture/runtime.md) - [Runtime](docs/architecture/runtime.md)
- [Système de tâches](docs/architecture/task_system.md) - [Task system](docs/architecture/task_system.md)
- [Registry des types de tâches](docs/architecture/task_kind_registry.md) - [Task Kind Registry](docs/architecture/task_kind_registry.md)
- [File de tâches](docs/architecture/task_queue.md) - [Task Queue](docs/architecture/task_queue.md)
- [Resource Governor](docs/architecture/resource_governor.md) - [Resource Governor](docs/architecture/resource_governor.md)
- [Parallélisme interne borné](docs/architecture/internal_parallelism.md) - [Bounded internal parallelism](docs/architecture/internal_parallelism.md)
- [Pipeline sensible aux ressources](docs/architecture/resource_aware_pipeline.md) - [Resource-aware pipeline](docs/architecture/resource_aware_pipeline.md)
- [Intégration Queue/runtime ↔ Governor](docs/architecture/scheduler_resource_integration.md) - [Queue / runtime / Governor integration](docs/architecture/scheduler_resource_integration.md)
- [Pipeline de reconstruction](docs/architecture/reconstruction_pipeline.md) - [Reconstruction pipeline](docs/architecture/reconstruction_pipeline.md)
- [Persistance](docs/architecture/persistence.md) - [Persistence](docs/architecture/persistence.md)
- [Base de données projet](docs/architecture/project_database.md) - [Project Database](docs/architecture/project_database.md)
- [Feature Store](docs/architecture/feature_store.md) - [Feature Store](docs/architecture/feature_store.md)
- [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md) - [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md)
- [Visual Index](docs/architecture/visual_index.md) - [Visual Index](docs/architecture/visual_index.md)
@ -202,36 +338,60 @@ Acquisitions
- [Geometric Verification](docs/architecture/geometric_verification.md) - [Geometric Verification](docs/architecture/geometric_verification.md)
- [Geometric Verifier](docs/architecture/geometric_verifier.md) - [Geometric Verifier](docs/architecture/geometric_verifier.md)
- [Track Model](docs/architecture/tracks.md) - [Track Model](docs/architecture/tracks.md)
- [Sparse SfM / Triangulation — Gate A](docs/architecture/sparse_sfm.md) - [Track Builder](docs/architecture/track_builder.md)
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md) - [Sparse SfM](docs/architecture/sparse_sfm.md)
- [Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)
- [Calibration Publication v2](docs/architecture/calibration_publication_v2.md)
- [Calibration Science v1](docs/architecture/calibration_science_v1.md)
- [Calibration Solver Preflight v1](docs/architecture/calibration_solver_preflight_v1.md)
- [Photo Quality Triage](docs/architecture/photo_quality_triage.md)
- [Vulkan ORB Matcher](docs/architecture/vulkan_matcher.md)
- [Viewer](docs/architecture/viewer.md) - [Viewer](docs/architecture/viewer.md)
- [Resource Boundary — No New Resource Subsystem](docs/architecture/resource_boundary.md) - [Resource Boundary](docs/architecture/resource_boundary.md)
- [Audit global de maintenance — état consolidé](docs/architecture/global_maintenance_audit.md)
- [Revue historique des fondations](docs/architecture/foundation_review.md) ### Historical audit records
- [Global Maintenance Audit](docs/architecture/global_maintenance_audit.md)
- [Foundation Review](docs/architecture/foundation_review.md)
Historical audit records preserve the state and evidence of their checkpoint. Older schema versions,
Task counts or resource measurements inside them must not be mechanically modernized.
### Concepts ### Concepts
- [Scan Sets](docs/concepts/scan_sets.md) - [Scan Sets](docs/concepts/scan_sets.md)
- [Index visuel](docs/concepts/visual_index.md) - [Visual Index](docs/concepts/visual_index.md)
- [Matching et tracks](docs/concepts/matching_and_tracks.md) - [Matching and Tracks](docs/concepts/matching_and_tracks.md)
- [Couches de reconstruction](docs/concepts/reconstruction_layers.md) - [Reconstruction Layers](docs/concepts/reconstruction_layers.md)
- [Contraintes géométriques](docs/concepts/geometric_constraints.md) - [Geometric Constraints](docs/concepts/geometric_constraints.md)
Some concept documents are explicitly historical or superseded. Their status header determines
whether they are current authority.
### Development
### Développement
- [Build](docs/development/build.md) - [Build](docs/development/build.md)
- [Tests](docs/development/testing.md) - [Testing](docs/development/testing.md)
- [Concurrence](docs/development/concurrency.md) - [Concurrency](docs/development/concurrency.md)
- [Profil de performance de la machine cible](docs/performance/target_hardware.md) - [Validation-host performance profile](docs/performance/target_hardware.md)
### Roadmap and audits
### Roadmap
- [Roadmap](docs/roadmap/roadmap.md) - [Roadmap](docs/roadmap/roadmap.md)
- [Documentation Inventory Audit](docs/audits/documentation_inventory.md)
## Build rapide ## Build
Meson and Ninja are the canonical build path.
```sh ```sh
CC=clang meson setup build --wipe CC=clang meson setup build
meson compile -C build -j8 meson compile -C build
``` ```
Build parallelism should use safe host capacity. Do not treat a historical `-j8` or the current
reference-host result of approximately `-j12` as a portable constant.
## Tests ## Tests
```sh ```sh
@ -239,57 +399,41 @@ meson test -C build --print-errorlogs
git diff --check git diff --check
``` ```
Pour les changements sensibles à la mémoire ou à la concurrence, ajouter ASan/UBSan et TSan. For memory-, lifetime- or concurrency-sensitive changes, use the applicable ASan/UBSan and TSan
validation described in [Testing](docs/development/testing.md) and preserve documented third-party
sanitizer qualifications.
## Statut ## Repository language
Lardon3D est en développement actif. La persistance des tâches, le catalogue, The canonical language for repository documentation, agent contracts and production source comments
le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator, is English.
Matcher v1, Geometric Verification Model et Geometric Verifier Fundamental v3
sont implémentés. Le runtime Feature + Matcher + Verifier emploie des tâches durables,
de petits lots, le Resource Governor interactif et un hot path Vulkan ORB exact avec
fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ; ces deux matchers
restent sur OpenCV L2. Track Model/Builder, les primitives géométriques Gate C,
le noyau Sparse SfM incrémental Gate D et le Bundle Adjustment final Gate E sont
implémentés et validés. L'orchestration Sparse SfM Gate F est PASS / FROZEN ;
l'intégration Governor Gate G est **PASS / FROZEN**. MVS-M1 est **PASS / FROZEN** :
une frontière OpenMVS v2.4.0 externe et bornée, sans publication dense durable
ni MVS complet. Les sources sont liées par SHA-256
complet, borné à 1 GiB par fichier régulier (sans budget agrégé de dataset) ; les
octets source restent un binding distinct de l'identité dense. Celle-ci lie la
reconstruction de base, le jeu d'images source, le `calibration_scope_identity`
historique, le binding numérique de calibration MVS `L3DMCAL1` v1, le backend et
les paramètres dans `L3DMDID2` v2 (220 octets). Chaque appel utilise un espace de
travail privé neuf sous le staging appelant, sans réemploi d'une scène,
profondeur, cache ou sortie antérieure. Le DAG, le viewer et les autres étapes
denses restent des tickets séparés planifiés.
La fondation Project DB v22, `raw.develop` et Calibration Bootstrap v1 reste
**PASS / FROZEN**. La tête courante v23 ajoute seulement le contexte optique
générique : neuf tables, migration transactionnelle sans backfill, objectifs
manuels sans EXIF, configurations multiples par campagne et sélection de
calibration exactement compatible. Les migrations de copies réelles S21/A6000
ont conservé leurs lignes scientifiques et laissé l'overlay vide. Les campagnes
réelles S21 et A6000 Engine Bay sont
`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données de
calibration connues ; le Sparse SfM réel reste
`BLOCKED_BY_KNOWN_CALIBRATION_DATA`, sans pseudo-calibration ni import inféré.
Le Resource Governor ne constitue pas un Resource System générique : il reste
l'unique propriétaire des budgets et le seul orchestrateur des leases scratch
de production. Le contrôleur SSD UDisks2 est une frontière physique séparée,
jamais un second scheduler ou Governor. Les seize Task kinds actuels ne
consomment encore aucun scratch : l'espace disponible est une capacité
observable, pas un usage fabriqué, et scratch/swap ne deviennent jamais de la
RAM. La TUI/F10 et cette intégration sont validées opérationnellement. L'audit
global est désormais `GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN` : après les builds
Clang frais portable/Vulkan, les suites normales 64/64 et 65/65, les sanitizers
applicables, TSan et les contrôles ABI, l'unique revue finale indépendante
GPT-5.6 SOL/ULTRA a conclu PASS sans finding bloquant. Elle a indépendamment
rejoué le build portable, la suite 64/64, une matrice focalisée 15/15 et les
76/76 probes strictes C17/C++17 couvrant 19 headers publics modifiés/nouveaux,
ainsi que l'ABI, les négatifs de seams production, le SHA du manifest GV et le
diff-check. Ce gel clôt la gate de maintenance ; il n'exécute pas à lui seul la
tranche scientifique suivante.
## Licence User-interface language is a separate product/localization concern.
Projet privé - Tous droits réservés. ## Status
Lardon3D is under active development.
The persistent pre-SfM pipeline is implemented through Tracks, the Sparse SfM C-G capability is
implemented and frozen at its documented boundaries, Phase H v1 is frozen, and MVS-M1 provides the
validated external OpenMVS boundary. Full real known-calibration Sparse SfM, dense publication,
mesh, texturing, viewer and live capture guidance remain future work.
The global maintenance checkpoint remains:
```text
global-maintenance-2026-09-01
```
The later real A6000 pre-SfM checkpoint is:
```text
real-a6000-pre-sfm-2026-09-02
```
Future reviews should preserve historical checkpoint meaning and review only the relevant delta
unless concrete evidence requires reopening an unchanged FROZEN boundary.
## License
Lardon3D is licensed under the MIT License.

258
docs/README.md Normal file
View file

@ -0,0 +1,258 @@
# Lardon3D Documentation Index
## Status
```text
DOCUMENTATION_INDEX=CURRENT
CURRENT_PROJECT_DB_SCHEMA=v27
CURRENT_PRODUCTION_TASK_KINDS=16
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
PRODUCT_DEFINITION_V1=PASS/FROZEN
PROMPT_TREE=CURRENT
CURRENT_NEXT=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION
```
This index separates current authority, historical evidence and future product-definition work.
Repository documentation is read in context: an older schema number, Task count or resource measurement
inside a historical checkpoint is not stale merely because the current repository has moved forward.
## Authority order
When current-state prose disagrees, use this order:
1. `AGENTS.md` for repository-wide engineering/agent obligations and protected boundaries.
2. Specialized architecture documents for the subsystem contract they own.
3. `docs/roadmap/roadmap.md` for current lifecycle / next work.
4. `README.md` and architecture overview for concise summaries.
5. Historical audits only for the checkpoint they explicitly record.
Executable source and schema implementation remain authoritative when a documentation excerpt claims to
quote an exact API, DDL or constant.
## Current repository state
```text
Project DB head v27
Production Task kinds 16
v22 selected scientific execution foundation
v23 generic optical-context overlay
v24 raw.develop.batch/1 persistence
v25 features.extract.batch/1 persistence
v26 Capture geometric state/applicability
v27 generic bounded exact-token focus domains
```
Canonical resource objective:
```text
MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF
```
Reference-host values are measurements, not portable constants.
## Current real-data checkpoints
### S21
```text
REAL_S21_TRACKS=PASS/FROZEN
```
The retained S21 proof reaches Tracks.
### A6000
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Retained A6000 counts:
```text
Feature Sets 689
Candidate Pairs 38,420
Match Results 38,420
Applicable GVRs 37,805
Verified GVRs 10,952
Rejected GVRs 26,853
Track Sets 1
Tracks 130,714
Track observations 318,944
```
The A6000 checkpoint stops before real Sparse SfM and Dense/MVS.
Sparse SfM capability exists through frozen Gates C-G. Real Sparse SfM on the historical S21/A6000
campaigns remains blocked by known-calibration data.
## Current architecture
Start here:
- [Architecture overview](architecture/overview.md)
- [Reconstruction pipeline](architecture/reconstruction_pipeline.md)
- [Runtime](architecture/runtime.md)
- [Persistence](architecture/persistence.md)
- [Project Database](architecture/project_database.md)
### Task / resource execution
- [Task System](architecture/task_system.md)
- [Task Queue](architecture/task_queue.md)
- [Task Kind Registry](architecture/task_kind_registry.md)
- [Resource Governor](architecture/resource_governor.md)
- [Resource Boundary](architecture/resource_boundary.md)
- [Resource-aware Pipeline](architecture/resource_aware_pipeline.md)
- [Internal Parallelism](architecture/internal_parallelism.md)
- [Scheduler / Resource Integration](architecture/scheduler_resource_integration.md)
Resource authority split:
```text
resource_governor.md runtime policy
AGENTS.md engineering / agent obligations
target_hardware.md reference-host evidence
other documents scoped summaries and links
```
## Scientific pipeline
- [Photo Quality Triage](architecture/photo_quality_triage.md)
- [Feature Store](architecture/feature_store.md)
- [Precision Feature Pipeline](architecture/precision_feature_pipeline.md)
- [Visual Index](architecture/visual_index.md)
- [Candidate Pair](architecture/candidate_pair.md)
- [Match Result](architecture/match_result.md)
- [Matcher](architecture/matcher.md)
- [Geometric Verification Model](architecture/geometric_verification.md)
- [Geometric Verifier](architecture/geometric_verifier.md)
- [Track Model](architecture/tracks.md)
- [Track Builder](architecture/track_builder.md)
- [Sparse SfM](architecture/sparse_sfm.md)
- [Vulkan ORB Matcher](architecture/vulkan_matcher.md)
Current production Geometric Verifier lineage:
```text
Fundamental v1 historical valid identity
Fundamental v2 historical valid identity
Fundamental v3 current production identity
```
Historical verifier results are never relabelled.
## Calibration
- [Calibration Science v1](architecture/calibration_science_v1.md)
- [Calibration Tooling v1](architecture/calibration_tooling.md)
- [Calibration Bootstrap v1](architecture/calibration_bootstrap.md)
- [Calibration Solver Preflight v1](architecture/calibration_solver_preflight_v1.md)
Historical S21/A6000 geometry evidence must not be retroactively given invented calibration.
## Development
- [Build](development/build.md)
- [Testing](development/testing.md)
- [Concurrency](development/concurrency.md)
- [Target Hardware](performance/target_hardware.md)
Build/test parallelism is host-aware. Fixed historical `-j8`, `-j12` or serialized-suite evidence does
not become a portable default.
TSan/OpenCV/TBB qualifications and Vulkan validation remain separate evidence boundaries.
## Roadmap
- [Roadmap](roadmap/roadmap.md)
The roadmap owns the current lifecycle / next-work cursor.
Historical progress cursors inside other records do not override it.
## Audits
- [Documentation Inventory Audit](audits/documentation_inventory.md)
- [Global Maintenance Audit](architecture/global_maintenance_audit.md)
- [Foundation Review](architecture/foundation_review.md)
### Historical audit rule
`global_maintenance_audit.md` and `foundation_review.md` are historical evidence.
Do not mechanically modernize their old:
- schema version;
- Task count;
- compiler/build count;
- resource measurement;
- checkpoint status.
The later A6000 checkpoint adds new operational evidence; it does not erase the maintenance checkpoint.
## Concepts
- [Scan Sets](concepts/scan_sets.md)
- [Visual Index concept](concepts/visual_index.md)
- [Matching and Tracks](concepts/matching_and_tracks.md)
- [Reconstruction Layers](concepts/reconstruction_layers.md)
- [Geometric Constraints](concepts/geometric_constraints.md)
Concept documents may be historical or explanatory. They do not outrank the current specialized
architecture contract.
## Product definition
Canonical target-product authority:
- [Product Definition v1](product/product_definition.md)
```text
PRODUCT_DEFINITION_V1=PASS/FROZEN
IMPLEMENTATION_AUTHORIZATION=NO
PROMPT_TREE=CURRENT
```
The product definition freezes desired end-product behavior without reopening existing FROZEN
scientific contracts. Future implementation is authorized only by the subsequent execution-contract
phase under explicit human authority.
## Repository language
Canonical target:
```text
DOCUMENTATION_LANGUAGE=ENGLISH
SOURCE_COMMENT_LANGUAGE=ENGLISH
AGENT_CONTRACT_LANGUAGE=ENGLISH
USER_INTERFACE_LANGUAGE=ENGLISH
```
The current-state documentation findings have been remediated in English.
Some untouched historical/current documents may still require a mechanical language-only normalization
pass. Such translation must preserve historical facts and must not silently modernize scientific or
lifecycle state.
Executable UI strings are changed only in an explicitly scoped UI-language implementation pass.
## Navigation rule for future agents
Before implementation:
```text
read AGENTS.md
-> read README.md
-> read this index
-> read the specialized architecture contract
-> read the current roadmap
-> inspect historical audits only when their evidence is relevant
```
Never use an older historical checkpoint as a substitute for current authority.

View file

@ -0,0 +1,225 @@
# Calibration AF Study Evidence v1
**Status: PASS / FROZEN.**
This boundary is an offline scientific-evidence helper for Calibration Science
v2. It does not perform camera calibration, Project DB mutation, autofocus
control, calibration selection, or physical-validity decisions.
## Purpose
Project DB v27 can persist a physically validated exact-token focus domain, but
it deliberately does not decide whether a set of focus observations is
physically compatible.
`Calibration AF Study Evidence v1` supplies the missing measurement artifact:
```text
independent calibration results at observed focus states
-> deterministic pairwise image-space projection deltas
-> L3DAFST1 artifact
-> SHA-256 retained evidence
-> later human/scientific applicability decision
-> v27 focus-domain creation only after that decision
```
The real A6000 + E PZ 16-50 autofocus applicability remains:
```text
A6000_E_PZ_16_50_AF_APPLICABILITY=BLOCKED_BY_PHYSICAL_VALIDATION
```
This tool cannot change that state by itself.
## Input contract
The caller supplies one exact study context:
- nonzero SHA-256 of the retained body/lens/focal/non-focus geometric context;
- decoded/oriented width and height;
- 2..64 already acquired calibration samples.
Each sample contains:
- `FIT` or `HOLDOUT` role;
- one bounded, nonempty opaque exact focus token;
- nonzero SHA-256 of that calibration's retained evidence;
- exact binary64 `fx, fy, cx, cy, k1, k2, p1, p2`.
Repeated samples at one focus token are allowed only when they identify distinct
calibration evidence. Repeating the exact same `(focus token, calibration
evidence SHA-256)` is rejected and cannot masquerade as repeatability evidence.
The API performs no metadata interpretation. A Sony MakerNote value, for
example, must first be converted by the future acquisition/evidence layer into
the exact retained token policy selected for that study.
## Probe model
Version 1 measures the same frozen pinhole + `k1/k2/p1/p2` forward projection
model used by calibration/Sparse SfM.
Nine normalized ideal rays are evaluated:
```text
centre
(0, 0)
edge probes
(-0.7, 0) (+0.7, 0) (0, -0.7) (0, +0.7)
corner probes
(-0.7, -0.7) (+0.7, -0.7)
(-0.7, +0.7) (+0.7, +0.7)
```
For every pair of calibration samples, the artifact stores:
- centre delta in pixels;
- maximum cardinal edge-probe delta;
- maximum corner-probe delta;
- maximum over all nine probes;
- whether both samples use the same focus token;
- whether the pair crosses FIT/HOLDOUT roles.
These are measurements, not acceptance thresholds.
## L3DAFST1
The binary artifact is little-endian and bounded to 128 KiB.
It contains:
```text
magic = L3DAFST1
artifact version
probe-model version
study-context SHA-256
width / height
sample / pair / role counts
canonical sample records
all canonical pair records and projection metrics
```
Sample order is canonicalized by:
```text
focus-token bytes
calibration-evidence SHA-256
sample role
```
Therefore caller input order does not change artifact bytes or SHA-256.
Every floating value is finite binary64. Negative zero is normalized to positive
zero before serialization.
The artifact SHA-256 is suitable as retained evidence for a later v27
`lardon3d_optical_focus_domain_v2_create(...)` call only after the physical
study has been reviewed and its scientific applicability decision has been made.
## Summary output
The API also returns bounded aggregate measurements:
- FIT/HOLDOUT/sample/pair counts;
- same-focus / cross-focus / FIT-HOLDOUT pair counts;
- maximum centre, edge-probe, corner-probe and global pairwise delta;
- maximum same-focus global delta;
- maximum cross-focus global delta;
- maximum FIT/HOLDOUT global delta.
The summary is for inspection and orchestration. It does not encode PASS/FAIL.
## Non-goals
This v1 boundary does not:
- solve ChArUco calibration;
- parse EXIF/MakerNotes;
- invent physical focus distances;
- derive autofocus envelopes;
- define an acceptance pixel threshold;
- interpolate or extrapolate focus;
- open Project DB;
- create a v27 focus domain;
- make an execution READY.
The future physical study supplies the evidence needed to decide whether the
A6000 + E PZ 16-50 supports one domain, discrete domains, or exact focus only.
## Materialized Workflow bridge v1
**Status: PASS / FROZEN.**
`CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1` is the additive conversion boundary
between the already-FROZEN Calibration Workflow materializer and AF-study
samples.
It accepts a `Lardon3DCalibrationWorkflowExternalEvidence` that has already
passed the existing immutable-file, solver-bundle and provenance checks. It
does not parse `solve.json` or any other solver file again.
The bridge:
```text
materialized Calibration Workflow evidence
+ caller-supplied exact focus token
+ caller-supplied FIT/HOLDOUT role
-> Lardon3DCalibrationAfStudySample
```
The sample publishes `repeated_parameters[0]` only after checking that all three
retained full solves remain exactly equal.
Its `calibration_evidence_sha256` is independent of the focus token and
FIT/HOLDOUT role and binds:
- target identity;
- optical-state identity;
- solver executable/configuration identity;
- initialization evidence;
- validation evidence;
- exact oriented dimensions;
- exact published binary64 intrinsics.
Therefore relabelling one calibration result cannot manufacture independent
calibration evidence.
The bridge performs no DB access, metadata interpretation, physical AF decision,
thresholding, interpolation or extrapolation.
## Materialized study assembly v1
**Status: PASS / FROZEN.**
`CALIBRATION_AF_STUDY_ASSEMBLY_V1` composes the frozen Workflow bridge and
`L3DAFST1` producer so a physical AF study does not need caller-written sample
arrays.
The caller supplies:
```text
study_context_sha256
2..64 entries {
materialized Calibration Workflow evidence
exact opaque focus token
FIT or HOLDOUT role
}
```
The assembly:
1. converts every entry through the frozen Workflow bridge;
2. requires one exact oriented width/height for the whole study;
3. rejects a repeated `calibration_evidence_sha256` even when the caller changes
focus token or FIT/HOLDOUT role;
4. passes the resulting bounded samples to the frozen AF-study producer;
5. returns deterministic `L3DAFST1`, artifact SHA-256 and summary.
`study_context_sha256` remains caller-retained and explicit. This layer does not
derive body/lens/focal/non-focus state from solver metadata and does not decide
whether different focus observations belong to one physically valid domain.
No Project DB access, solver execution, metadata inference, thresholding,
interpolation or extrapolation is introduced.

View file

@ -0,0 +1,117 @@
# Calibration Publication v2
**PASS / FROZEN — additive heterogeneous calibration publication boundary.**
## Authority and scope
Calibration Science v2 is the frozen scientific authority. This document
defines only the additive heterogeneous publication bridge:
```text
caller-owned validated group evidence
-> Calibration Tooling v2
-> L3DCALB2
-> Calibration Bootstrap v2
-> existing SparseCalibrationScope(image_id -> calibration_id)
```
It does not change `L3DCALB1`, Calibration Tooling v1, or Calibration Bootstrap
v1. It does not run a solver, define optical applicability thresholds, resolve
READY outside publication, or add persistence schema.
## L3DCALB2 format
The artifact is deterministic, fixed-width, and little-endian. Integers use
the stated unsigned width. Binary64 fields contain IEEE-754 bits; producers
canonicalize negative zero and importers reject non-finite values. Native
structures, padding, locale text, and trailing bytes are forbidden. The whole
artifact is at most 600,000 bytes, with 1..4,096 groups and 1..4,096 entries;
the byte-size bound may impose a lower combined maximum. SHA-256 fields must be
nonzero.
```text
magic 8 bytes = ASCII "L3DCALB2"
format_version u32 = 2
model_kind u32 = PINHOLE(1)
model_version u32 = 1
group_count u32, 1..4096
entry_count u32, 1..4096
for each group in strictly increasing
(group_identity_sha256, group_version) order:
group_identity_sha256 32 bytes
group_version u32, nonzero
optical_state_sha256 32 bytes
target_sha256 32 bytes
solver_executable_sha256 32 bytes
solver_configuration_sha256 32 bytes
initialization_evidence_sha256 32 bytes
validation_evidence_sha256 32 bytes
member_count u32, nonzero
for each member in strictly increasing selected_item_index order:
selected_item_index u32
image_id u64, nonzero
representation_sha256 32 bytes
width, height u32, u32, nonzero
fx, fy, cx, cy f64, f64, f64, f64
k1, k2, p1, p2 f64, f64, f64, f64
support_images u32, nonzero
support_observations u32, nonzero
reprojection_rmse_px finite nonnegative f64
maximum_parameter_delta finite nonnegative f64
validation_flags u32 = 0x0f
```
The pinhole parameters and four validation bits have exactly the v1 meaning.
V2 introduces no new solver threshold. Positive focal lengths and principal
points inside the declared dimensions are required. Every selected item index
from zero through `entry_count - 1` occurs exactly once across the artifact,
and every `image_id` occurs exactly once. Group order and member order are
validated, not silently normalized.
## Provenance and publication
Each complete serialized group record, including its exact membership and all
group-local evidence, is independently SHA-256 hashed. That group digest is the
`IMPORTED_TRUSTED` provenance fingerprint for each calibration in that group.
The independently supplied whole-artifact SHA-256 protects transport and binds
the combined publication without replacing group-local provenance.
Bootstrap validates the whole artifact and then verifies every entry against
the immutable selected execution's exact item index, `image_id`, and current
representation asset SHA-256 before its first write. It creates or reuses the
existing content-addressed Sparse calibrations per entry, creates or reuses one
scope over every selected image across all groups. The original Bootstrap v2
import attaches that complete scope and retains its existing public behavior.
The separately named `publish_unattached` primitive returns the exact
selected-item/image/calibration mapping without attaching the scope; it exists
only so Workflow v2 can establish v26 compatibility before READY. A
pre-publication error attaches no scope. A later failure may retain immutable
calibration evidence but cannot make the execution READY. Exact retries reuse
calibrations and the scope and converge on the same attachment.
## Heterogeneous Workflow v2 composition
Workflow v2 consumes the L3DCALB2 artifact and a complete caller-owned binding
for every selected item. Each binding repeats the selected execution's durable
`item_index -> capture_id` relation. Capture identity is never recovered from
an image ID, path, SHA-256, filename, artifact group identity, or numeric group
ID.
Before unattached publication, Workflow v2 requires complete observed v26
geometric state for every selected Capture. Automatic resolution reports zero
exact candidates as `CALIBRATION_REQUIRED` and multiple exact candidates as
`SELECTION_REQUIRED`. Explicit existing applicability must be exactly
compatible. Explicit publication creates/reuses deterministic profile metadata
from the immutable artifact calibration and its group-local provenance, then
creates/reuses applicability from the declared exact-state exemplar. It does
not create scientific Capture identity or authorize interpolation.
For every selected item, the final resolved v26 sparse `calibration_id` must
equal that image's calibration member in the complete unattached scope. Only
after all items pass that equality does Workflow v2 attach the scope and return
`READY`. Invalid evidence or a wrong assignment is a distinct non-ready error.
Any earlier failure leaves the selected execution unattached; exact retries are
deterministic and converge through the immutable Bootstrap/profile/
applicability/selection APIs. No Project DB schema change is required.

View file

@ -0,0 +1,543 @@
# Calibration Science v2 — heterogeneous optics and adaptive acquisition
**Status: PASS/FROZEN — generic additive scientific contract.**
Calibration Science v1 remains `PASS/FROZEN`. Science v2 does not weaken,
reinterpret or retroactively replace v1 evidence. It defines the next
scientific generation required by the product requirement that a Lardon3D
project may contain photographs from heterogeneous cameras, lenses, focal
configurations and normal autofocus operation.
No device-specific autofocus envelope is validated by this document. Numeric
focus-domain limits require dedicated physical evidence.
```text
CALIBRATION_SCIENCE_V2=PASS/FROZEN
```
This freezes the generic heterogeneous-optics, adaptive-exposure and
applicability contract only. It does not validate a device-specific optical
state, focus envelope or calibration. In particular,
`A6000_E_PZ_16_50_AF_APPLICABILITY=BLOCKED_BY_PHYSICAL_VALIDATION` remains
unchanged until retained physical evidence satisfies the applicable rules.
## 1. Product/science problem
A photogrammetry product must not require the operator to remember a rigid
exposure recipe or split one physical subject into artificial projects merely
because acquisition state changed.
The required user experience is:
```text
user frames and captures
-> Lardon3D records/observes acquisition state
-> Lardon3D measures actual image quality
-> Lardon3D resolves exact calibration applicability
-> Capture remains usable when quality is good
-> reconstruction becomes READY only when calibration coverage is complete
```
A normal supported camera body or rectilinear lens is equipment data. A new
brand/model name alone must not require source-code modification.
Source changes are justified only by a genuinely unsupported transport,
encoded representation, geometric camera model or required device-state
mechanism.
## 2. Compatibility with Science v1
The following remain frozen compatibility contracts:
```text
CALIBRATION_SCIENCE_V1=PASS/FROZEN
CALIBRATION_TOOLING_V1=PASS/FROZEN
CALIBRATION_BOOTSTRAP_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_V1=PASS/FROZEN
```
Historical S21 and A6000 Engine Bay campaigns remain:
```text
CALIBRATION_UNAVAILABLE
BLOCKED_BY_KNOWN_CALIBRATION_DATA
```
Science v2 does not authorize retro-calibration by inference.
## 3. Fundamental separation of states
Science v2 separates three concepts that must never be conflated.
### 3.1 Capture existence
A Capture is durable acquisition evidence. Unsupported or unresolved optics do
not erase it.
### 3.2 Photo quality
Quality describes whether the decoded representation is useful image evidence.
It is evaluated from the image itself and may include:
- sharpness;
- defocus;
- motion blur;
- black/white clipping;
- usable shadow/highlight information;
- contrast;
- texture;
- noise or local signal-to-noise evidence when implemented;
- downstream feature usability.
Photo Quality may recommend `GOOD`, `SUSPECT` or `REJECT` without assigning any
calibration identity.
### 3.3 Calibration readiness
Calibration readiness answers whether a selected scientific representation has
exact compatible geometric calibration evidence.
Therefore this is valid:
```text
QUALITY=GOOD
CALIBRATION=CALIBRATION_REQUIRED
```
The image remains in the project.
## 4. Heterogeneous selected executions
A single project and one selected scientific execution may contain images from:
- different camera bodies;
- different lenses;
- different focal configurations of one zoom;
- different validated focus states or focus applicability domains;
- different validated aperture applicability states;
- different acquisition sessions.
The scientific requirement is per selected image:
```text
selected image
-> exactly one compatible calibration
```
It is not:
```text
selected execution
-> one global calibration
```
A truthful READY state requires complete calibration coverage of the exact
selected-image set.
The current Sparse calibration scope already represents members as
`image_id -> calibration_id`; v2 should reuse that per-image model unless
repository inspection proves a real missing invariant.
## 5. No silent substitution
Science v2 preserves:
```text
NO_SILENT_SCIENTIFIC_SUBSTITUTION
```
The following are forbidden unless a later explicit validated v2 transfer model
authorizes them:
- another camera body's calibration;
- another lens's calibration;
- a nearby focal calibration;
- a guessed focus calibration;
- a guessed aperture compatibility;
- a guessed stabilization/crop/pipeline equivalence;
- fuzzy metadata matching that silently changes scientific identity;
- interpolation or extrapolation outside a validated model/domain.
If exact compatibility cannot be proven:
```text
CALIBRATION_REQUIRED
```
If more than one valid choice remains unresolved:
```text
SELECTION_REQUIRED
```
## 6. Exposure and photometric controls
Purely photometric controls are adaptive acquisition variables.
Science v2 does not reject an image merely because of the numeric value of:
- shutter speed;
- ISO;
- white balance;
- exposure compensation;
- other controls proven not to change image geometry.
Examples such as `1/125`, `ISO 800` or `f/8` from historical campaigns are
observations, not universal validity thresholds.
For shutter/ISO/white balance, actual decoded-image evidence decides whether
the result is usable. A faster shutter with higher ISO may be preferable to a
blurred low-ISO image; a slower shutter may be valid when the camera is stable.
Missing photometric metadata must not cause fabricated values.
## 7. Aperture
Aperture is not automatically classified as purely photometric because some
lenses can exhibit focus shift or geometric changes with aperture.
The product goal remains that the operator does not manually manage scientific
grouping.
Science v2 must allow either:
1. one calibration applicability proven valid across an aperture domain; or
2. distinct automatically classified aperture applicability states.
Unknown compatibility produces `CALIBRATION_REQUIRED`, not Capture rejection.
No aperture-domain threshold is frozen without physical validation.
## 8. Focal length and zoom
Multiple focal configurations may coexist in one project and one selected
execution.
Exact calibrated focal states are the baseline v2 mechanism.
Electronic focal metadata may select an exact compatible state only when its
identity and trust rules are satisfied.
Manual lenses or missing metadata remain normal product paths through explicit
bounded state assignment.
Calibration at one focal state is never silently borrowed for another focal
state.
A future continuous focal transfer/interpolation model is allowed only if it is
a separately versioned Science v2 model with fit-domain evidence, hold-out
validation, deterministic evaluation and hard rejection outside its validated
domain.
## 9. Autofocus and focus applicability
Autofocus is a normal acquisition mode in Science v2.
Normal product use must not require the user to lock manual focus for every
project merely to satisfy calibration.
Science v2 supports the following scientific concepts:
### 9.1 Exact focus state
A calibration may apply to one exact observed/declared focus state.
This remains useful for manual lenses, fixed-focus systems and strict
measurements.
### 9.2 Validated focus applicability domain
A calibration may apply across a bounded autofocus/focus domain only when
physical evidence proves that applicability.
Required evidence includes:
- exact camera body and objective identities;
- exact focal/geometric pipeline state;
- explicit focus-state observation representation and provenance;
- repeated physical calibration samples across the intended focus domain;
- independent hold-out focus states;
- comparison in image-space geometric error, not only parameter percentage;
- bounded deterministic applicability;
- explicit rejection outside the validated domain.
Historical EXIF/MakerNote values such as Sony `FocusPosition2` or derived
`FocusDistance2` may inform experiment design. They are not by themselves
physical calibration evidence.
The implemented generic Stage E foundation represents a validated discrete
domain as:
```text
existing exact calibration applicability
+ positive domain format/version
+ nonzero retained-evidence/provenance SHA-256
+ exact complete exemplar state for every non-focus geometry field
+ 1..64 explicit opaque observed focus tokens
```
The 64-token limit is an operational persistence/API bound, not a scientific
focus range. Domain membership is exact token equality. Tokens have no numeric
distance, order, magnitude, adjacency or EXIF interpretation, and the generic
machinery performs no interpolation or extrapolation. The target configuration
and every non-focus geometric-state and provenance field must exactly match the
exemplar. Unknown/incomplete focus remains `CALIBRATION_REQUIRED`.
An exact applicability and its attached domain retain one applicability and
calibration identity. Candidate enumeration deduplicates that identity if both
the exact exemplar state and a domain member match. Distinct overlapping
applicabilities remain ambiguous and produce `SELECTION_REQUIRED`.
This generic representation does not itself validate a real device domain.
In particular:
```text
A6000_E_PZ_16_50_AF_APPLICABILITY=BLOCKED_BY_PHYSICAL_VALIDATION
```
### 9.3 Discrete focus bands
If one calibration is not valid across the complete autofocus range, v2 may
use multiple validated discrete focus bands or exact focus states.
Automatic classification is required where trusted observed state permits it.
Ambiguous overlap must not silently choose a calibration.
### 9.4 Continuous focus transfer models
Continuous interpolation across focus is not assumed.
If future physical evidence justifies it, such a model must be:
- explicitly versioned;
- deterministic;
- bounded to a validated domain;
- fitted from retained physical evidence;
- tested on excluded hold-out focus states;
- rejected outside the domain;
- incorporated into scientific identity.
No extrapolation is silent.
## 10. Stabilization, crop, orientation and processing pipeline
Only state capable of changing geometry belongs to calibration applicability.
Examples requiring explicit treatment unless equivalence is proven:
- electronic stabilization;
- geometric crop;
- perspective correction;
- resize;
- computational geometry correction;
- in-camera lens correction that changes pixel coordinates;
- orientation normalization.
Purely cosmetic color processing does not create a new geometric state after
coordinate equivalence has been proven.
Unknown geometric state is incompatible until resolved.
## 11. Observed optical state and provenance
Science v2 requires a bounded, versioned observed state sufficient to decide
calibration applicability.
Conceptually it may contain:
```text
camera_body_identity
lens_identity
focal_state
focus_state_or_domain_observation
aperture_state_when_geometrically_relevant
stabilization_and_geometric_pipeline_state
decoded_oriented_width_height
crop_resize_transform_identity
representation/decode_pipeline_identity
observation_provenance
```
The exact public ABI and persistence layout are implementation-contract work.
Important rules:
- unknown is explicit;
- unknown is never replaced with a guessed default;
- electronic metadata is evidence with defined trust, not automatic truth for
every scientific field;
- manual assignment is explicit and durable where metadata cannot identify a
state;
- exact retry is idempotent.
## 12. Automatic calibration resolution
For each selected image:
```text
load exact Capture and representation
-> load/derive trusted observed optical state
-> enumerate validated compatible calibration applicability
-> exactly one: calibration resolved
-> none: CALIBRATION_REQUIRED
-> multiple unresolved: SELECTION_REQUIRED
```
A selection must remain reviewable and durable.
Calibration readiness does not rewrite Photo Quality evidence.
## 13. Artifact / Tooling / Bootstrap evolution
Science v2 does not require a new artifact merely because a project is
heterogeneous.
Implementation must first inspect and reuse existing primitives.
Current architecture already has:
```text
SparseCalibrationScope member = image_id -> calibration_id
L3DCALB1 entry = per-image intrinsics
```
However Tooling/Workflow v1 binds one global optical-state/provenance bundle.
If heterogeneous independent calibration groups cannot be represented without
losing provenance, an additive `L3DCALB2` / Tooling v2 / Bootstrap v2 may be
defined.
Any v2 artifact must preserve per-group:
- optical/applicability identity;
- target identity;
- solver executable/configuration identity;
- initialization evidence;
- validation evidence;
- model/dimensions;
- exact published intrinsics;
- exact selected-image membership.
V1 artifact semantics remain unchanged.
## 14. READY invariant
A selected execution is READY only if:
- every selected image is covered exactly once;
- every calibration is scientifically compatible with that image's observed
geometric state;
- the final calibration scope covers the exact selected image set;
- no unresolved or ambiguous selected image remains;
- publication/attachment follows the existing atomicity/idempotence contract.
A project may retain any number of other non-selected or
`CALIBRATION_REQUIRED` Captures.
## 15. Device independence
Normal equipment onboarding is data-driven.
Required product properties:
```text
NEW_CAMERA_REQUIRES_CODE_CHANGE=NO
NEW_LENS_REQUIRES_CODE_CHANGE=NO
MULTIPLE_LENSES_PER_CAMERA=SUPPORTED
ZOOM_MULTIPLE_FOCALS=SUPPORTED
MULTIPLE_OPTICAL_CONFIGURATIONS_PER_PROJECT=SUPPORTED
```
A genuinely different geometric model, unsupported file format or unsupported
transport may require new code. A different manufacturer/model string does not.
## 16. Acquisition control philosophy
Science v2 defines what must be observed and proven; it does not require one
specific control transport.
Future adapters should follow:
```text
if hardware permits control:
Lardon3D may choose/control settings automatically
else if state can be observed:
Lardon3D verifies/classifies automatically
else:
request only the minimum explicit user action required
```
HDMI, USB remote control and device-specific adapters remain separate future
implementation layers.
## 17. A6000 + E PZ 16-50 current evidence
Historical A6000 engine-bay images provide operational design evidence only.
Observed facts include:
- E PZ 16-50mm F3.5-5.6 OSS;
- heavy real use of 16-20 mm focal lengths;
- AF-S across the historical JPEG set;
- many distinct focus positions even within the 16 mm / f/8 subset;
- exposure values varied with scene conditions.
These observations demonstrate that a locked-focus single-state workflow is
not an acceptable general product UX for this use case.
They do not validate any reusable calibration or autofocus envelope.
Current device-specific state:
```text
A6000_E_PZ_16_50_AF_APPLICABILITY=BLOCKED_BY_PHYSICAL_VALIDATION
```
## 18. Required future physical study
Before claiming a real validated autofocus domain, capture dedicated ChArUco
evidence across representative focus states and repeats while preserving the
other relevant geometric state.
The study must determine whether the lens supports:
- one validated focus envelope;
- multiple discrete focus bands;
- exact focus-only calibration; or
- a future explicitly validated continuous transfer model.
Numeric domains and thresholds must come from the Science v2 validation design
and retained physical evidence. They are not inferred from historical scene
photos.
## 19. Implementation dependency
Current order:
```text
Calibration Science v2 design
-> heterogeneous optical-state persistence/applicability foundation
-> v2 Tooling/Bootstrap/artifact only if required
-> heterogeneous Workflow v2 READY proof
-> physical AF/optical applicability study
-> dedicated calibrated real campaign
-> real Sparse SfM
```
Do not start Sparse SfM or Dense/MVS as part of this design tranche.
## 20. STOP conditions
Stop rather than guess if:
- a required geometric state cannot be observed or explicitly assigned;
- a proposed compatibility relation lacks physical validation;
- an AF/focal/aperture transfer model needs an invented threshold;
- a new schema would force inference into historical rows;
- heterogeneous calibration would require silently changing a FROZEN v1
scientific meaning;
- a new camera requires a genuinely different geometric model not covered by
the current pinhole/distortion science.
Uncertainty remains visible as `CALIBRATION_REQUIRED` or
`SELECTION_REQUIRED`.

View file

@ -124,3 +124,23 @@ optique, acquérir au moins 40 vues, hacher, résoudre trois fois, vérifier
Science v1, passer le bundle à Calibration Tooling dans un projet dédié et Science v1, passer le bundle à Calibration Tooling dans un projet dédié et
vérifier `CALIBRATION → READY`. La preuve s'arrête avant Sparse SfM ; S21 ne vérifier `CALIBRATION → READY`. La preuve s'arrête avant Sparse SfM ; S21 ne
participe jamais. participe jamais.
## Current lifecycle update
The preflight decision above has now been implemented.
```text
CALIBRATION_EVIDENCE_SOLVER_V1=IMPLEMENTED/VALIDATED
```
`tools/calibration_evidence_solver/` contains the external OpenCV 5.x solver selected by this
preflight. Its deterministic synthetic CPU1 self-test passes. It remains external to Lardon3D
runtime linkage and Project DB mutation.
The current missing implementation boundary is no longer the solver. It is the higher-level
calibration workflow coordinator that binds an immutable physical session plus the complete solver
bundle to the exact selected execution, constructs `Lardon3DCalibrationToolingEvidence`, invokes the
frozen Tooling/Bootstrap path and reaches `READY` without manufacturing evidence.
The original preflight text is retained as decision history; this section owns its later lifecycle
status.

View file

@ -0,0 +1,109 @@
# Calibration Tooling v1
## Status
```text
CALIBRATION_TOOLING_V1=PASS/FROZEN
CALIBRATION_TOOLING_PLANARITY_ALIGNMENT=PASS/FROZEN
L3DCALB1_VERSION=1
```
## Authority
Calibration Science v1 is the scientific authority. This document specializes
the bounded operational bridge implemented by
`include/lardon3d/calibration_tooling.h` and `src/calibration_tooling.c`.
The bridge connects:
```text
Calibration Science v1 evidence
-> Calibration Tooling v1
-> L3DCALB1 v1
-> Calibration Bootstrap v1
```
It does not acquire images, run a solver, infer optical state, create
reconstruction results, add a Project DB schema version, add a Task kind, or
start Sparse SfM.
## Planarity alignment
Calibration Science v1 requires a rigid planar physical target and rejects a
warped board. It defines no numeric target-flatness tolerance.
The canonical external session records categorical physical evidence:
```text
planarity PASS <planarity-evidence-sha256>
```
The Science v1 value `0.20 mm` applies to the allowed range of the ten measured
30.000 mm squares. It is not a target-flatness threshold.
`Lardon3DCalibrationToolingEvidence.target_flatness_mm` is retained only to
preserve the existing public structure layout. In v1 it MUST be IEEE-754 NaN.
Any finite value is rejected so a caller cannot invent a physical measurement
or silently create a new scientific threshold.
The immutable session containing the categorical planarity attestation is bound
by the higher-level workflow through `initialization_evidence_sha256`.
This corrective alignment changes neither Calibration Science v1 nor
`L3DCALB1` v1 nor Calibration Bootstrap v1.
## Bounded evidence
Tooling continues to validate the frozen Science v1 requirements, including:
- exact ChArUco 9 x 7 / DICT_5X5_100 target identity;
- 30.000 mm squares and 21.000 mm markers;
- ten physical square measurements and instrument resolution;
- measured free white border of at least 30 mm, supplied explicitly by the session;
- immutable target, optical-state, solver and evidence digests;
- accepted and rejected view evidence;
- field-region, distance and angle diversity;
- corner quality, clipping, residual and hold-out evidence;
- deterministic repeated parameters;
- representative coordinate equivalence;
- exact selected-image order and representation SHA-256;
- the exact eight-parameter pinhole model;
- zero active extra distortion coefficients.
Successful solver exit alone is never sufficient acceptance.
## Artifact and Bootstrap
For valid evidence Tooling produces exactly the existing fixed-width,
little-endian `L3DCALB1` v1 artifact and may invoke only the frozen Calibration
Bootstrap importer.
A failed validation reaches neither artifact publication nor Project DB
mutation. Exact successful retries converge through the existing immutable
Bootstrap contract.
## Current next boundary
The external Calibration Evidence Solver v1 is implemented and validated.
The missing product boundary is the higher-level calibration workflow
coordinator. It must consume:
```text
session.l3dcal
session.l3dcal.bundle/detection.json
session.l3dcal.bundle/solve.json
session.l3dcal.bundle/evidence.json
session.l3dcal.bundle/producer.json
```
together with the exact selected execution and optical state, then construct
the bounded `Lardon3DCalibrationToolingEvidence`.
`producer.json` binds the exact solver executable SHA-256, canonical solver-configuration SHA-256, exact session SHA-256, OpenCV build identity, CPU1 policy and optical-state SHA-256.
`detection.json` also retains the exact per-view Science v1 decisions used by the solver: frame region, distance band, hold-out membership, target occupancy, normal angle, measured distance, target-quadrant mask, corner/residual counts, high-residual count, reprojection RMSE and maximum residual. The coordinator consumes these published values and does not reconstruct them from a later solve.
The coordinator must verify regular bounded files, immutable digests,
session/bundle identity, categorical planarity PASS, selected-execution image
binding and optical-state equality. It must never manufacture missing evidence.

View file

@ -0,0 +1,239 @@
# Calibration Workflow
## Status
```text
CALIBRATION_WORKFLOW=PASS/FROZEN
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_V2=PASS/FROZEN
CURRENT_WORKFLOW_NEXT=ADAPTIVE_CAPTURE_SETTINGS_AND_AUTOFOCUS_V2_FOUNDATION
```
## Authority
Calibration Science v1, Calibration Tooling v1 and Calibration Bootstrap v1
remain FROZEN scientific and import authorities.
This workflow is bounded orchestration only. It does not introduce a solver,
Project DB schema version, Task kind, Sparse SfM execution or reconstructed
scientific evidence.
## Frozen flow
```text
physical calibration acquisition
-> session.l3dcal
-> external Calibration Evidence Solver v1
-> immutable solver bundle
-> campaign-state evidence
-> Calibration Workflow
-> Calibration Tooling v1
-> L3DCALB1 v1
-> Calibration Bootstrap v1
-> selected execution READY
```
## Input Boundary v1
`CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN`.
The implementation is exposed through:
```text
include/lardon3d/calibration_workflow.h
src/calibration_workflow.cpp
```
Input Boundary v1 performs no Project DB mutation.
It accepts only bounded regular files and rejects special files and symlinks
before potentially blocking reads. File access follows the nonblocking,
close-on-exec regular-file discipline.
The bounded input set is:
```text
session.l3dcal
session.l3dcal.bundle/detection.json
session.l3dcal.bundle/solve.json
session.l3dcal.bundle/evidence.json
session.l3dcal.bundle/producer.json
L3DCAL_CAMPAIGN_STATE_V1
```
It verifies:
- regular bounded files;
- SHA-256 identities;
- strict session syntax;
- structurally valid canonical JSON bundle members;
- exact session SHA binding through `producer.json`;
- decoder/version consistency;
- exact optical-state SHA equality;
- exact optical-state token equality;
- campaign-state identity consistency.
Malformed JSON, oversize files, symlinks, FIFOs, session digest mismatch and
optical-state mismatch are rejected.
Input Boundary v1 does not:
- open or mutate Project DB;
- construct `Lardon3DCalibrationToolingEvidence`;
- call Calibration Tooling;
- produce `L3DCALB1`;
- invoke Calibration Bootstrap;
- change selected-execution state.
## Evidence Materialization v1
`CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN`.
The implementation is additive:
```text
src/calibration_workflow_materialize.cpp
tests/test_calibration_workflow_materialize.cpp
```
It consumes only inputs that first pass Input Boundary v1 and performs no
Project DB access or mutation.
The caller owns bounded arrays for materialized views and coordinate checks.
On success the output borrows those arrays and retains:
- exact target generator SHA-256 and physical target measurements;
- measured white border and categorical planarity evidence;
- exact optical-state SHA-256;
- exact solver executable and configuration SHA-256;
- exact accepted/rejected per-view classifications and rejection reasons;
- hold-out assignment, frame region, distance band and target coverage;
- retained per-view residual counts and metrics;
- coordinate-equivalence checks derived from the retained session points;
- all three exact full-solve parameter vectors;
- exact fit parameter vector;
- support image/observation counts;
- global RMSE, maximum residual and high-residual fraction;
- hold-out RMSE and maximum residual;
- maximum parameter delta and `validation_flags=0x0f`.
The stage consumes published solver evidence; it does not reclassify views,
rerun calibration, average repeated solves or manufacture missing values.
`initialization_evidence_sha256` is the exact `session.l3dcal` SHA-256.
`validation_evidence_sha256` is deterministic and domain separated:
```text
SHA256(
ASCII("L3DCAL_WORKFLOW_VALIDATION_V1\n")
|| detection_sha256_raw32
|| solve_sha256_raw32
|| evidence_sha256_raw32
|| producer_sha256_raw32
)
```
This boundary deliberately does not construct per-campaign
`Lardon3DCalibrationToolingEntry` rows. Those rows require Project DB proof of
the selected image identities, representation bytes/dimensions and explicit
Capture optical assignments, which belongs to Selected Execution Binding v1.
## Campaign optical-state evidence
Project DB v23 retains exact explicit optical configuration identity, including
body, objective and focal state, but Calibration Science v1 requires a broader
scientific key including focus, stabilization and processing/decode state.
No equality may be inferred between those domains.
`L3DCAL_CAMPAIGN_STATE_V1` therefore provides immutable external evidence for
the complete Science v1 optical state. A later coordinator stage must verify
this evidence against both the calibration session and each selected Capture's
explicit Project DB optical configuration.
Absence or disagreement remains `CALIBRATION_UNAVAILABLE`.
## Selected Execution Binding v1
```text
CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN
```
The implementation is additive:
```text
src/calibration_workflow_bind.cpp
tests/test_calibration_workflow_bind.cpp
```
This boundary is read-only. It may read Project DB and managed representation
bytes, but does not attach a calibration scope, invoke Calibration Tooling or
Bootstrap, or transition the selected execution to `READY`. It proves:
- exact selected-execution stage, completion, item order and Capture mapping;
- exact explicit v23 optical configuration, including campaign-origin facts
where present;
- exact selected-image/Capture relation and READY image asset identity;
- managed representation size and SHA-256 through project-relative `openat`
descent that rejects absolute paths, dot components, symlinks, non-directory
components and non-regular final files;
- grayscale OpenCV decoded width/height equal to the accepted materialized
calibration geometry; and
- deterministic selected-item-order `Lardon3DCalibrationToolingEntry`
construction from the published solve values, without averaging or solver
recomputation.
Entries are staged internally and published to caller storage only after every
selected item passes. Exact retries are read-only and deterministic.
## Tooling / Bootstrap READY v1
The final public composition validates and materializes input, performs the
read-only selected-execution binding, then invokes only FROZEN Calibration
Tooling. Tooling produces L3DCALB1 v1 and invokes FROZEN Bootstrap. Success
requires the returned scope to be the exact scope attached to READY; exact
retries converge through immutable importer semantics.
The software workflow is PASS/FROZEN. It does not establish physical evidence:
historical S21/A6000 campaigns remain CALIBRATION_UNAVAILABLE and
BLOCKED_BY_KNOWN_CALIBRATION_DATA.
## Additive heterogeneous Workflow v2
`CALIBRATION_WORKFLOW_V2=PASS/FROZEN`. This additive composition leaves every
v1 API, artifact and workflow meaning unchanged. It composes v26 Capture
geometric state/applicability with the L3DCALB2 publication path for one
heterogeneous selected execution.
The workflow first verifies the exact durable selected-item-to-Capture mapping
for every caller binding. It never recovers Capture identity from an image ID,
path, SHA-256, filename or operational group ID. Every Capture must have a
complete observed geometric state. Missing or incomplete state reports
`CALIBRATION_REQUIRED`; multiple exact compatible applicability candidates
report `SELECTION_REQUIRED`.
L3DCALB2 is first published through its additive unattached primitive. This
fully validates artifact bytes and selected image/representation bindings and
may create reusable immutable calibrations and a complete scope, but cannot
transition the execution to READY. Workflow v2 then binds each returned
per-image calibration to an exact v26 applicability/selection and verifies the
resolved `sparse_calibration_id` equals that scope member. Only after every
selected item passes may the existing scope-attachment transaction set READY.
Invalid artifact evidence and wrong optical assignments are distinct non-ready
errors. No pre-final failure attaches a scope. Exact retries reuse immutable
calibrations and the same complete scope deterministically.
## Current next boundary
```text
validated input -> materialized evidence -> selected-execution binding
-> FROZEN Calibration Tooling -> FROZEN Calibration Bootstrap -> READY
```
It must preserve the binding's exact provenance and use only the FROZEN
Tooling/Bootstrap import path. No failure before import may mutate Project DB.

View file

@ -1,48 +1,71 @@
# Sous-système Candidate Pair # Candidate Pair subsystem
## Vision ## Status
Le sous-système Candidate Pair répond uniquement à la question :
> « Quelles paires d'images valent la peine d'être présentées au Matcher ? »
Il ne répond **PAS** à :
> « Ces images ont-elle réellement des correspondances ? »
et ne contient **aucune** validation géométrique.
## Frontières
```text
CURRENT_PROJECT_DB_SCHEMA=v25
CANDIDATE_PAIR_MODEL=v1
CANDIDATE_PAIR_MODEL_STATUS=IMPLEMENTED
CANDIDATE_PAIR_TASK=candidate_pair.generate/1
CANDIDATE_PAIR_TASK_STATUS=IMPLEMENTED
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_A6000_PRE_SFM=PASS/FROZEN
``` ```
The Candidate Pair scientific model remains the Project DB v8 model. The durable Candidate Pair Task
was added in Project DB v9. Later schema versions through v25 are additive and do not redefine
Candidate Pair identity.
This document owns the Candidate Pair subsystem contract. Resource policy is governed by the Resource
Governor and the canonical resource documents; Candidate Pair declares bounded demand and preserves
deterministic scientific output.
## Purpose
The Candidate Pair subsystem answers one question:
> Which image pairs are worth presenting to the Matcher?
It does not answer whether two images actually have descriptor correspondences and it performs no
geometric verification.
The downstream Matcher is implemented and consumes persisted Candidate Pairs, but Matcher science and
persistence are outside this subsystem.
```text
Visual Index Visual Index
|
v
Candidate Pair Generator Candidate Pair Generator
|
v
Candidate Pair persistence Candidate Pair persistence
|
Matcher — HORS SCOPE de ce ticket v
Matcher
``` ```
Le Matcher est un consommateur des paires persistées. Il n'est pas ## Core invariants
implémenté dans ce sous-système.
## Invariants fondamentaux | Invariant | Contract |
| --- | --- |
| Symmetry | `(A,B)` and `(B,A)` are the same scientific pair |
| Canonical order | Persist with `image_id_a < image_id_b` |
| No self-pairs | `image_id_a != image_id_b`, implied by the SQL ordering check |
| Persistent uniqueness | `UNIQUE(image_id_a, image_id_b)` |
| Bounded query result | `top_k <= LARDON3D_VISUAL_INDEX_TOP_K_MAX = 256` |
| Determinism | Same inputs and options produce the same pair decisions in the same canonical order |
| Idempotence | Repeating generation does not duplicate persisted pairs |
| Persistent model | Candidate Pair rows were introduced by Project DB v8 |
| Durable execution | `candidate_pair.generate/1` was introduced by Project DB v9 |
| Invariant | Description | Candidate Pair identity is the canonical unordered image pair. Retrieval score, Visual Index provenance,
|-----------|-------------| Task ID, timestamps and operational resource choices do not enter that identity.
| **Symétrie** | A,B == B,A |
| **Ordre canonique** | Stockage avec `image_id_a < image_id_b` |
| **Self-pairs interdits** | `image_id_a != image_id_b` (implicite via CHECK SQL) |
| **Unicité persistante** | `UNIQUE(image_id_a, image_id_b)` |
| **Résultat borné** | `top_k <= LARDON3D_VISUAL_INDEX_TOP_K_MAX = 256` par requête |
| **Déterminisme** | Mêmes entrées/configuration → mêmes paires dans le même ordre |
| **Idempotence** | Répétition sans duplication |
| **Persistance durable** | Paires persistées dans Project DB v8 |
## Modèle persistant ## Persistent model
### Table `candidate_pairs` (Project DB v8) ### `candidate_pairs` — Project DB v8
```sql ```sql
CREATE TABLE candidate_pairs( CREATE TABLE candidate_pairs(
@ -57,17 +80,24 @@ CREATE INDEX candidate_pairs_image_a_idx ON candidate_pairs(image_id_a);
CREATE INDEX candidate_pairs_image_b_idx ON candidate_pairs(image_id_b); CREATE INDEX candidate_pairs_image_b_idx ON candidate_pairs(image_id_b);
``` ```
The executable schema in `src/project_db.c` remains authoritative if prose and SQL excerpts ever
diverge.
### Public Project DB API
The Candidate Pair persistence surface includes:
- `lardon3d_project_db_create_candidate_pair()`;
- `lardon3d_project_db_load_candidate_pair()`;
- `lardon3d_project_db_find_candidate_pair()`;
- `lardon3d_project_db_list_candidate_pairs()`.
Creation canonicalizes the image order and persistence enforces uniqueness.
## Single-source generation
### API ### API
- `lardon3d_project_db_create_candidate_pair()` — INSERT avec canonicalisation
- `lardon3d_project_db_load_candidate_pair()` — SELECT par ID
- `lardon3d_project_db_find_candidate_pair()` — SELECT par (image_a, image_b)
- `lardon3d_project_db_list_candidate_pairs()` — SELECT paginé ORDER BY id
## Génération single-source
### Prototype
```c ```c
Lardon3DVisualIndexResult lardon3d_candidate_pair_generate( Lardon3DVisualIndexResult lardon3d_candidate_pair_generate(
const char *project_path, Lardon3DProjectDb *database, const char *project_path, Lardon3DProjectDb *database,
@ -76,99 +106,117 @@ Lardon3DVisualIndexResult lardon3d_candidate_pair_generate(
Lardon3DCandidatePairGenStats *stats); Lardon3DCandidatePairGenStats *stats);
``` ```
### Algorithme ### Algorithm
1. Charger le FeatureSet source For one source Feature Set:
2. Obtenir `source_image_id`
3. Interroger le Visual Index avec `query_options`
4. Pour chaque candidat retourné :
- Exclure les self-pairs
- Canonicaliser l'ordre (image_a < image_b)
- Chercher si la paire existe déjà (`find_candidate_pair`)
- Créer si absente (`create_candidate_pair`)
5. Retourner les statistiques `{generated, skipped, queried}`
### Statistiques 1. load the source Feature Set;
2. obtain its `source_image_id`;
3. query the Visual Index with the supplied options;
4. for every returned candidate:
- reject self-pairs;
- canonicalize the image order;
- find an existing Candidate Pair;
- create the pair only when absent;
5. return bounded generation statistics.
The generator does not perform descriptor matching and does not perform geometric verification.
### Statistics
```c ```c
typedef struct { typedef struct {
uint32_t generated_count; // paires nouvellement créées uint32_t generated_count;
uint32_t skipped_count; // paires déjà existantes (idempotence) uint32_t skipped_count;
uint32_t queried_count; // candidats retournés par le Visual Index uint32_t queried_count;
} Lardon3DCandidatePairGenStats; } Lardon3DCandidatePairGenStats;
``` ```
## Score et provenance `generated_count` counts newly persisted pairs. `skipped_count` counts pairs already present.
`queried_count` counts candidates returned by the Visual Index query.
Le score de retrieval et la provenance Visual Index ne sont **PAS** ## Retrieval score and provenance
persistés dans la table `candidate_pairs` pour les raisons suivantes :
- L'identité Candidate Pair est纯粹 géométrique : (image_a, image_b) Retrieval score and Visual Index provenance are intentionally not stored in `candidate_pairs`.
- Le score dépend de la configuration du Visual Index et peut changer
- Le Matcher calculera ses propres scores de matching
- La séparation des responsabilités est plus nette
Le score reste accessible via le Visual Index si nécessaire. Reasons:
## Déterminisme - Candidate Pair identity is only the canonical image pair;
- retrieval score depends on Visual Index configuration;
- a later Visual Index execution may score the same pair differently;
- Matcher owns descriptor-level matching evidence;
- keeping retrieval evidence out of Candidate Pair identity preserves subsystem separation.
### Déterministe A generation fingerprint describes the generation request. It does not change the identity of an
already persisted Candidate Pair row.
- Mêmes entrées → mêmes paires ## Determinism
- Même ordre de sélection top-K
- Mêmes décisions de déduplication
### Non déterministe ### Deterministic inputs and decisions
- `created_at` (timestamp Unix, informatif uniquement) For identical immutable inputs and options, the subsystem preserves:
- `candidate_pair_id` (AUTOINCREMENT, identifiant technique)
### Tie-breaks - the same Visual Index query contract;
- the same top-K selection semantics;
- the same canonical image ordering;
- the same self-pair rejection;
- the same deduplication decisions;
- the same publication order for owner-published results.
En cas d'égalité de score dans le Visual Index, l'ordre est déterministe ### Non-scientific values
selon l'implémentation LSH (ordre des Feature Sets).
## Invalidation These values are not Candidate Pair scientific identity:
### Événements et impact - `created_at`;
- `candidate_pair_id`;
- Task ID;
- resource reservation ID;
- admitted CPU count;
- admitted batch size.
| Événement | Ce qui devient invalide | Ce qui reste réutilisable | Ce qui doit être recalculé | `candidate_pair_id` is a durable technical identity allocated by SQLite. It is not a scientific
|-----------|------------------------|--------------------------|---------------------------| fingerprint.
| Nouvelle image | Rien (incrémental) | Paires existantes | Nouvelles requêtes Visual Index |
| Nouveau FeatureSet | Rien | Paires existantes | Requête depuis ce FeatureSet |
| FeatureSet remplacé | Paires basées sur ce FeatureSet | Autres paires | Nouvelle requête depuis ce FeatureSet |
| Visual Index reconstruit | Toutes les paires (nouvelle config) | Rien | Tout recalcul |
| Configuration top-K modifiée | Rien (borné par requête) | Paires existantes | Nouvelles requêtes avec nouveau top_k |
| Filtre modifié | Rien | Paires existantes | Nouvelles requêtes avec nouveau filtre |
| Relance après interruption | Rien | Paires déjà persistées | Suite du traitement |
### Politique ## Generation fingerprint
L'invalidation est aussi locale que possible. On ne supprime jamais ### API
toutes les paires du projet suite à une modification locale.
### Vérification par fingerprint
Le fingerprint permet de vérifier si une génération doit être recalculée :
```c ```c
unsigned char fp_courant[32], fp_enregistre[32]; void lardon3d_candidate_pair_generation_fingerprint(
lardon3d_candidate_pair_generation_fingerprint(..., fp_courant); uint64_t visual_index_id, uint64_t source_feature_set_id,
// Si fp_courant != fp_enregistre → recalcul nécessaire const Lardon3DVisualIndexQueryOptions *query_options,
unsigned char fingerprint[32]);
``` ```
### Réutilisation ### Included fields
- Même fingerprint → résultat réutilisable The generation fingerprint includes:
- Différent fingerprint → recalcul nécessaire
- Les paires existantes sont conservées même si le fingerprint change
## Batch projet - `visual_index_id`;
- `source_feature_set_id`;
- `query_options->top_k`;
- `query_options->minimum_evidence_count`;
- `query_options->scanset_filter`;
- `query_options->exclude_same_asset`.
### Granularité ### Excluded fields
La génération batch traite un ensemble de FeatureSets de manière It excludes:
bornée et déterministe.
- `created_at`;
- `candidate_pair_id`;
- operational CPU/batch admission;
- processing order of unrelated source Feature Sets.
### Reuse meaning
The same fingerprint means the same generation request may be reused.
A different fingerprint means the generation request must be evaluated again. Existing canonical
Candidate Pair rows are not silently deleted merely because a different generation request is run;
idempotent persistence may reuse rows that remain selected.
## Project batch generation
### API ### API
@ -181,210 +229,352 @@ Lardon3DVisualIndexResult lardon3d_candidate_pair_generate_batch(
uint64_t *last_feature_set_id); uint64_t *last_feature_set_id);
``` ```
### Algorithme ### Ordering and bounds
1. Lister les FeatureSets par pages de 64 The project batch path:
2. Pour chaque FeatureSet :
- Appeler `lardon3d_candidate_pair_generate()`
- Accumuler les statistiques
- Mettre à jour le curseur
3. Retourner les totaux et le dernier FeatureSet traité
### Ordre de traitement - pages Feature Sets in bounded pages;
- processes source Feature Sets in increasing `feature_set_id`;
- never assumes IDs are contiguous;
- keeps top-K bounded by the Visual Index contract;
- keeps query/result memory bounded;
- returns the last processed Feature Set for restart.
Feature Sets traités en ordre croissant de `feature_set_id`. A pair selected from multiple sources is persisted once because canonical pair identity is unique.
### Déduplication The historical single-source and project-batch APIs remain valid. They are not, by themselves, the
complete current resource-execution description of the durable Task.
Une paire produite depuis plusieurs sources n'existe qu'une fois. ## Durable Task
La deduplication est assurée par `find avant create`.
### Bornes
- Un seul FeatureSet traité à la fois
- Top-K borné par requête
- Mémoire bornée : allocation `top_k * sizeof(candidate)` par requête
- Pagination bornée (64 FeatureSets par page)
### Reprise
Le curseur `after_feature_set_id` permet la reprise après interruption.
La fonction retourne le dernier FeatureSet traité.
## Tâche durable
### Task Kind ### Task Kind
`candidate_pair.generate` v1 — **IMPLEMENTED**. ```text
candidate_pair.generate/1
### Unité de travail
Un membership FeatureSet source du Visual Index et sa requête associée. Chaque
séquence traite un lot borné de memberships (1 à 64 selon le contrat Governor),
en ordre croissant d'ID sans supposer des IDs contigus.
### Checkpoint
Curseur `after_feature_set_id` persisté dans `candidate_pair_generate_tasks`.
Checkpoint sauvé après chaque lot via
`lardon3d_project_checkpoint_candidate_pair_generate_task()`.
### Reprise
Reprise idempotente : le champ `after_feature_set_id` est rechargé depuis la
DB, et les paires déjà persistées sont ignorées par `find avant create`.
À l'ouverture du projet, la tâche est automatiquement restaurée via la
registry production et resoumise à la queue.
Les anciens snapshots v1 produits avec l'estimation opérationnelle exacte
128 Kio fixes, 64 Kio par item, lot 164, CPU 1, IO 1 et GPU 0 sont normalisés
éphémèrement par la registry à la forme courante CPU64 avant admission. La
forme historique immédiatement précédente CPU12/256 Kio fixes/64 Kio par item
est elle aussi reconnue exactement. Le
snapshot durable original reste la source du reconstructeur ; aucun checkpoint
d'estimation seule n'est stagé, promu ou publié sous le même résumé. Une panne
pré-terminale répète donc cette normalisation exacte. Aucun autre snapshot,
curseur ou paramètre scientifique n'est réinterprété.
### Intégration Task/Queue/Governor
La tâche utilise le runtime générique via le pattern standard :
- Estimation opérationnelle (256 Kio fixes, 8 Mio par item, lot 164).
La Queue conserve un callback ; jusqu'à soixante-quatre participants CPU
admis peuvent calculer une fenêtre interne bornée sans modifier l'identité
scientifique.
- CPU et lot sont essayés ensemble (`1/1`, `2/2`, puis les paliers sûrs) :
un CPU supplémentaire avec un lot d'un seul membership ne peut exercer aucun
participant supplémentaire et ne constitue donc pas une mesure de scaling.
- Réservation CPU + IO avant exécution
- `lardon3d_task_sequence_break()` entre chaque lot pour réadmission Governor
- Callback terminal checkpoint après `COMPLETED`/`FAILED`/`CANCELLED`
- Reconstruction depuis `Lardon3DProjectDbCandidatePairGenerateTask`
### API
```c
Lardon3DTask *lardon3d_project_create_candidate_pair_generate_task(
Lardon3DAppState *state, uint64_t visual_index_id,
const Lardon3DVisualIndexQueryOptions *query_options, uint64_t *task_id);
bool lardon3d_project_enqueue_candidate_pair_generate(
Lardon3DAppState *state, uint64_t visual_index_id,
const Lardon3DVisualIndexQueryOptions *query_options, uint64_t *task_id);
bool lardon3d_candidate_pair_generate_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding);
``` ```
## Concurrence Status: **IMPLEMENTED**.
### Garantie actuelle ### Durable unit
La Queue conserve un callback actif. À l'intérieur de la Task, jusqu'à soixante-quatre The durable cursor is `after_feature_set_id` in `candidate_pair_generate_tasks`.
threads CPU admis calculent en parallèle une fenêtre d'au plus deux sources par
thread. Chaque participant possède un handle DB de lecture privé. Le thread
propriétaire publie ensuite seul et dans l'ordre canonique des sources. Voir le
[contrat de parallélisme interne](internal_parallelism.md).
### Atomicité The Task consumes a bounded ordered set of Visual Index source memberships. A sequence handles an
admitted bounded batch, publishes the canonical pair decisions, persists the cursor, checkpoints and
returns through `lardon3d_task_sequence_break()` before the next Governor admission.
Les workers internes ne créent aucune paire. Après leur jointure, le ### Checkpoint and restart
propriétaire applique seul `find avant create`. La contrainte UNIQUE reste une
protection persistante, pas un mécanisme d'ordonnancement parallèle.
### Limites The Task checkpoints through:
Le pattern `find + create` n'est pas atomique entre les deux appels. Le chemin ```text
de Task n'introduit aucun writer concurrent ; la sémantique existante reste lardon3d_project_checkpoint_candidate_pair_generate_task()
inchangée pour les autres appelants éventuels.
## Bornes et ressources
### Top-K
`top_k <= 256` (LARDON3D_VISUAL_INDEX_TOP_K_MAX)
### Mémoire
Allocation de requête bornée par le top-K, plus un résultat de propositions
borné par le même maximum. La Task limite sa fenêtre à 24 sources et annonce
256 Kio fixes plus 64 Kio par item au Governor.
### Complexité
- O(top_k) par requête (lectures Visual Index)
- O(1) par paire (DB write)
- Pas de structure O(N²)
## Fingerprint de génération
### Composants
Le fingerprint identifie une génération Candidate Pair unique :
```c
void lardon3d_candidate_pair_generation_fingerprint(
uint64_t visual_index_id, uint64_t source_feature_set_id,
const Lardon3DVisualIndexQueryOptions *query_options,
unsigned char fingerprint[32]);
``` ```
### Éléments inclus Restart:
- `visual_index_id` : Visual Index utilisé 1. restores the generic Task snapshot;
- `source_feature_set_id` : Feature Set source 2. loads the typed Candidate Pair Task payload;
- `query_options->top_k` : nombre de candidats par requête 3. restores `after_feature_set_id`;
- `query_options->minimum_evidence_count` : filtre minimum 4. reconstructs the production binding through the Task Kind registry;
- `query_options->scanset_filter` : filtre ScanSet 5. resubmits through the normal Queue/Governor path;
- `query_options->exclude_same_asset` : exclusion même asset 6. reuses already persisted Candidate Pairs idempotently.
### Éléments exclus (volontairement) A crash may therefore repeat work after the last durable cursor, but it must not invent a second
scientific pair identity.
- `created_at` : informatif, pas fonctionnel ### Historical resource descriptors
- `candidate_pair_id` : identifiant technique
- Ordre des Feature Sets traités en batch
### Stabilité Older durable snapshots are accepted only through exact compatibility shapes already recognized by the
registry.
Le fingerprint est stable pour mêmes entrées et configuration. Historical forms include the exact earlier descriptors documented by the implementation, including:
Un changement de configuration produit un fingerprint différent.
### Relation avec l'invalidation
Un fingerprint différent signifie que le travail doit être recalculé.
Le même fingerprint signifie que le résultat peut être réutilisé.
### Relation avec la reprise
Le fingerprint permet de vérifier qu'une reprise utilise la même
configuration que l'originale.
## Limites connues
1. Score non persistant (par design)
2. La compaction de segments Visual Index n'est pas implémentée
3. Aucun Matcher consommateur des paires
## Statut
**IMPLEMENTED** — génération single-source, persistance,
canonicalisation, idempotence, batch projet, fingerprint et
réutilisation/invalidation.
**IMPLEMENTED** — tâche durable `candidate_pair.generate` v1
via le runtime/Queue générique, avec estimation immuable,
checkpoint par curseur, reprise idempotente et intégration
dans la registry production.
## Relation avec le pipeline
Le Candidate Pair Generator est l'étape E du pipeline de reconstruction :
```text
128 KiB fixed
64 KiB per item
batch 1..64
CPU1
IO1
GPU0
``` ```
Feature Store (C)
and the later exact historical CPU12 / 256 KiB fixed / 64 KiB-per-item form.
Visual Index (D)
Those shapes are restart compatibility evidence. They are not the current resource model and must not
Candidate Pair Generator (E) ← CE DOCUMENT be copied into new Task creation.
Matching (F) — HORS SCOPE The original durable snapshot remains the source supplied to reconstruction. Compatibility
normalization is ephemeral and does not rewrite the persisted checkpoint or Candidate scientific
identity.
## Current resource contract
### Current Task estimate
The current validated Candidate Pair Task declares approximately:
```text
fixed RAM 256 KiB
per-item RAM 8 MiB
batch range 1..64
GPU demand 0
IO demand bounded by the existing Task estimate
CPU demand reducible and bounded by the host compute pool
```
The exact implementation constants remain authoritative in source. This documentation records the
current validated capability and intentionally does not retain the obsolete 24-source / 64-KiB-per-item
description as current policy.
### Coupled CPU and batch admission
Candidate Pair generation has independent source work, but additional CPU cannot exercise additional
participants if the admitted source batch remains one.
For this Task, CPU and batch scaling are therefore coupled during adaptation. Conceptually:
```text
CPU1 / batch1
CPU2 / batch2
then larger safe coupled rungs
```
subject to:
- the Task's declared maximums;
- the host compute pool;
- current Governor pressure;
- measured usefulness;
- current admission policy.
This coupling fixes an operational scaling defect. It does not modify Candidate Pair scientific
identity, query options, top-K behavior, publication order or persistence.
### Canonical resource principles
```text
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
The interactive host reserve is preserved first. After that reserve and all safety constraints are
satisfied, safe and useful compute capacity should not be left idle merely to preserve an old
single-thread measurement.
Reference-host values are observations, not portable constants.
### Atomicity is not serialism
One source query and one Candidate Pair publication decision remain bounded scientific/transactional
units.
That does not imply that independent source preparation must run serially.
```text
PER_ITEM_ATOMICITY_REQUIRES_CROSS_ITEM_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
```
## Internal concurrency
The Task Queue still owns one active callback. Candidate Pair uses bounded parallelism inside that
callback.
The validated shape is:
```text
one admitted Candidate owner Task
-> bounded source window
-> bounded CPU participants
-> private read-side preparation
-> join
-> owner publishes in canonical source order
```
Participants do not create Candidate Pair rows.
Each participant uses its allowed private read-side state. After participants join, the owner alone
performs the canonical `find` / create publication sequence.
The SQL `UNIQUE(image_id_a, image_id_b)` constraint remains a persistent integrity guard, not a
parallel scheduling primitive.
No second global scheduler, global worker pool or parallel SQLite writer subsystem is introduced.
## Persistence race boundary
The public pattern:
```text
find candidate pair
-> create candidate pair when absent
```
is not a general atomic compare-and-insert primitive across arbitrary concurrent writers.
The production durable Candidate Task avoids introducing competing pair writers: participant work is
read/preparation only and owner publication is serialized.
Other callers must not infer a stronger concurrency guarantee from the Task's owner-only publication
model.
## Resource complexity
### Query bound
```text
top_k <= LARDON3D_VISUAL_INDEX_TOP_K_MAX = 256
```
### Memory
Current Task admission uses the current estimate:
```text
256 KiB fixed + 8 MiB per admitted item
```
with batch bounded to `1..64`.
Actual admission can be reduced by the Governor and host compute pool. Swap, zram and external scratch
never enlarge admitted RAM.
Candidate Pair currently has no authoritative scratch consumer.
### Algorithmic shape
The subsystem does not allocate an O(N^2) project pair matrix.
Per source, bounded work is dominated by:
- bounded Visual Index query;
- bounded top-K filtering;
- canonical pair lookup/publication.
Project traversal is paged and restartable.
## GPU policy
Candidate Pair currently remains CPU.
The validated GPU audit classified it as:
```text
CANDIDATE_GPU=REJECTED_WITH_MEASURED_REASON
```
The workload is dominated by Visual Index access, filtering, branching and deterministic ordered SQLite
publication, and no validated GPU primitive currently preserves the complete Candidate contract with a
useful measured advantage.
This rejection does not authorize CPU serialism. Safe useful CPU parallelism remains required by the
canonical resource policy.
## Real A6000 evidence
The retained real A6000 pre-SfM execution exercised the current Candidate path before Matcher, GV and
Tracks.
It produced:
```text
Candidate Pairs = 38,420
Match Results = 38,420
```
The later checkpoint is:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Candidate replay at the retained checkpoint created no new Candidate work. The proof continued through
Matcher, Geometric Verifier v3 and Tracks, then stopped before real Sparse SfM.
This evidence validates the operational Candidate path on that project. It does not change Candidate
Pair v1 identity or the Project DB v8/v9 historical contracts.
## Relationship to current Project DB
The Candidate Pair model was introduced in Project DB v8 and its durable Task payload in v9.
The current schema head is v25:
```text
v22 selected scientific execution foundation
v23 generic optical-context overlay
v24 raw.develop.batch/1 persistence
v25 features.extract.batch/1 persistence
```
Those later additive migrations do not reinterpret Candidate Pair rows.
## Known limits
Current limits and non-goals include:
- retrieval score is not persisted in `candidate_pairs` by design;
- Visual Index segment compaction remains separate work;
- no generic DAG dependency scheduler is introduced by Candidate Pair;
- no Candidate GPU backend is currently validated;
- no scratch/spill path is authoritative for Candidate Pair;
- arbitrary concurrent pair writers are not provided by the Task owner-publication model.
The downstream Matcher is implemented; it is not a missing Candidate Pair feature.
## Pipeline relationship
```text
Feature Store
|
v
Visual Index
|
v
Candidate Pair Generator <- this document
|
v
Matcher
|
v
Geometric Verification
|
v
Tracks
```
Candidate Pair selects plausible image pairs. Matcher computes descriptor-level correspondence evidence.
Geometric Verification validates geometry. Track Builder creates multi-view observation tracks. These
scientific responsibilities remain separate.
## Summary
```text
CURRENT_PROJECT_DB_SCHEMA=v25
CANDIDATE_PAIR_MODEL=v1
CANDIDATE_PAIR_MODEL_STATUS=IMPLEMENTED
CANDIDATE_PAIR_TASK=candidate_pair.generate/1
CANDIDATE_PAIR_TASK_STATUS=IMPLEMENTED
CANDIDATE_PAIR_PERSISTENCE_VERSION=v8
CANDIDATE_PAIR_TASK_PERSISTENCE_VERSION=v9
CANDIDATE_CURRENT_FIXED_RAM=256_KiB
CANDIDATE_CURRENT_PER_ITEM_RAM=8_MiB
CANDIDATE_CURRENT_BATCH_RANGE=1..64
CANDIDATE_CPU_BATCH_ADAPTATION=COUPLED
CANDIDATE_GPU=REJECTED_WITH_MEASURED_REASON
CANDIDATE_SCRATCH_CONSUMER=NO
PER_ITEM_ATOMICITY_REQUIRES_CROSS_ITEM_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_A6000_PRE_SFM=PASS/FROZEN
``` ```

View file

@ -1,60 +1,169 @@
# Feature Store v1/v2 # Feature Store v1/v2
## Rôle et modèle ## Status
**IMPLEMENTED.** Le Feature Store est la mémoire visuelle locale persistante. ```text
Un `FeatureSet` logique appartient à une `image_id` et identifie exactement CURRENT_PROJECT_DB_SCHEMA=v25
`orb` version 1 plus une configuration canonique. Il référence un
`FeatureAsset` physique immutable. Deux images logiques portant le même contenu
et la même configuration gardent deux `feature_set_id`, mais partagent le même
asset lorsque leurs octets sont identiques. Les futurs matches référenceront
`feature_set_id + feature_index`; ces deux valeurs sont immuables après
publication.
`feature_set_id` et `feature_asset_id` sont des identités SQLite FEATURE_STORE_STATUS=IMPLEMENTED
`AUTOINCREMENT`. Une identité validée n'est jamais réattribuée. SQLite ne FEATURE_FILE_V1=FROZEN
contient que les relations, empreintes, tailles et paramètres ; les tableaux de FEATURE_FILE_V2=IMPLEMENTED
points et descripteurs restent hors DB.
## Extracteur et configuration HISTORICAL_FEATURE_TASK=features.extract/1
CURRENT_FEATURE_BATCH_TASK=features.extract.batch/1
PROJECT_DB_V25_FEATURE_BATCH=IMPLEMENTED/VALIDATED
L'extracteur production est ORB d'OpenCV 5, derrière une façade C. Aucune PER_IMAGE_FEATURE_RESULT=ATOMIC
exception C++ ni `cv::Mat` ne traverse cette frontière. La registry statique PER_IMAGE_ATOMICITY_REQUIRES_CROSS_IMAGE_SERIALISM=NO
expose `features.extract`, version 1. Une tâche traite exactement une image. OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
La configuration v1 contient trois `uint32` : `max_features` (1..8192), RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
`pyramid_levels` (1..16) et `fast_threshold` (1..255). Son fingerprint SHA-256 SERIALISM_REQUIRES_PROOF=CANONICAL
porte sur les 24 octets canoniques `L3DORBP1`, version et trois entiers
little-endian. Il ne dépend ni du padding, ni de la locale. Le contrat ORB v1
produit 32 octets binaires par point.
Au démarrage, avant la création du worker Queue, Lardon3D configure la baseline REAL_A6000_PRE_SFM=PASS/FROZEN
OpenCV process-wide depuis le compute-pool réel. `features.extract` publie la ```
borne positive `int` acceptée par OpenCV ; le Resource Governor réduit toujours
l'admission au budget hôte. Douze threads demeure une cohorte de validation,
pas un plafond portable. La Queue conserve un callback actif ; aucun pool
Lardon3D supplémentaire n'est créé pour ORB.
Cette limite est une configuration opérationnelle, jamais un paramètre The Feature Store is the persistent local visual-memory layer for Lardon3D.
scientifique : elle n'entre ni dans le fingerprint ORB v1, ni dans l'identité
FeatureSet, ni dans le Feature File. L'audit contrôlé OpenCV 5.0.0 aux limites
1, 2, 4, 8 et 12 exige le même count, le même ordre et les mêmes valeurs
binary32 des six champs keypoint persistés, les mêmes lignes/octets descriptor
et le même SHA-256 du Feature File. `class_id` est une donnée interne
`cv::KeyPoint` non publiée ; elle ne fait pas partie du record Feature File v1/v2
gelé et ne peut donc définir une identité scientifique.
La limite de 100 000 000 pixels est vérifiée après `cv::imread` : l'API utilisée The scientific identity of a Feature Set remains per image and immutable after publication. Project DB
ne fournit pas de sonde de dimensions multi-format fiable sans décodage. Le pic v25 adds an operational selected-execution batch Task, but does not change Feature Set identity,
mémoire du décodage peut donc précéder le rejet. Lardon3D ne revendique pas de Feature File formats, ORB descriptor semantics or the historical single-image Task contract.
bornage pré-décodage et n'embarque pas un parseur JPEG/PNG parallèle.
## Role and identity model
A logical `FeatureSet` belongs to one `image_id` and identifies:
- one extractor kind;
- one extractor version;
- one canonical parameter fingerprint;
- one exact source image content identity;
- one immutable published Feature Asset.
Two logical images with identical source content and identical extractor configuration keep distinct
`feature_set_id` values. They may share the same immutable physical Feature Asset when the resulting
Feature File bytes are identical.
Downstream correspondence identity references:
```text
feature_set_id + feature_index
```
Both components are immutable after Feature Set publication.
`feature_set_id` and `feature_asset_id` are SQLite `AUTOINCREMENT` identities. A committed published
identity is never reassigned to another object. SQLite stores logical relations, hashes, sizes,
extractor metadata and light metrics; keypoint arrays and descriptor arrays remain outside SQLite.
## Extractor registry and scientific configuration
The historical production ORB extractor uses OpenCV 5 behind a C boundary. No C++ exception or
`cv::Mat` crosses the public C API.
The historical registered Task Kind is:
```text
features.extract/1
```
That Task processes one image as one scientific unit.
The current selected-execution operational Task Kind is:
```text
features.extract.batch/1
```
It processes multiple independently selected images under one durable owner while preserving one
immutable Feature result per image.
### ORB v1 configuration
The ORB v1 configuration contains three `uint32_t` fields:
- `max_features`, range `1..8192`;
- `pyramid_levels`, range `1..16`;
- `fast_threshold`, range `1..255`.
Its SHA-256 parameter fingerprint is computed from the canonical 24-byte domain containing:
```text
L3DORBP1
version
max_features
pyramid_levels
fast_threshold
```
with fixed little-endian integer encoding.
The fingerprint does not depend on C struct padding, locale, Task ID, resource reservation,
admitted CPU count or admitted batch size.
ORB v1 produces 32-byte binary descriptors.
## OpenCV execution control
OpenCV thread count is process-wide operational state, not scientific identity.
Before Queue execution, Lardon3D establishes the validated OpenCV baseline from the actual host
compute pool. The active heavy callback applies the count required by its admitted contract and
restores the baseline on every exit path.
The Queue retains one active callback. This prevents unrelated heavy Tasks from racing the same
process-wide OpenCV configuration.
Historical `features.extract/1` may use the admitted OpenCV CPU count inside one image extraction.
For `features.extract.batch/1`, cross-image participants are the primary concurrency mechanism.
Independent participants prepare different images; Lardon3D does not blindly multiply a full OpenCV
thread team inside every participant.
The admitted CPU count is resource policy and never enters:
- ORB v1 fingerprint;
- Feature Set identity;
- Feature File bytes;
- source-image identity.
The controlled OpenCV 5.0.0 validation at 1, 2, 4, 8 and 12 threads required identical:
- feature count;
- keypoint order;
- binary32 values of all six persisted keypoint fields;
- descriptor row order and bytes;
- complete Feature File SHA-256.
Twelve threads are reference-host validation evidence, not a portable product ceiling.
`cv::KeyPoint::class_id` is not persisted by the frozen Feature File record and therefore cannot
define scientific identity.
## Decode bound
The 100,000,000-pixel limit is checked after `cv::imread`.
The API used by this implementation does not provide a reliable multi-format dimension probe without
decoding. Therefore decode peak memory can precede rejection.
Lardon3D does not claim pre-decode memory bounding and does not maintain a parallel JPEG/PNG dimension
parser for this purpose.
## Feature File v1 ## Feature File v1
Le fichier est little-endian et exige IEEE-754 binary32. Sa taille maximale est Feature File v1 is little-endian and requires IEEE-754 binary32.
16 Mio et son nombre maximal de points 8192.
| Offset | Taille | Champ | Limits:
```text
maximum file size 16 MiB
maximum feature count 8192
ORB descriptor U8 x 32
```
### Header
| Offset | Size | Field |
| ---: | ---: | --- | | ---: | ---: | --- |
| 0 | 8 | magic `L3DFEAT\0` | | 0 | 8 | magic `L3DFEAT\0` |
| 8 | 4 | format version = 1 | | 8 | 4 | format version = 1 |
@ -63,98 +172,456 @@ Le fichier est little-endian et exige IEEE-754 binary32. Sa taille maximale est
| 20 | 4 | descriptor dimension = 32 | | 20 | 4 | descriptor dimension = 32 |
| 24 | 4 | descriptor type = U8 | | 24 | 4 | descriptor type = U8 |
| 28 | 4 | keypoint record size = 24 | | 28 | 4 | keypoint record size = 24 |
| 32,36 | 4+4 | largeur, hauteur décodées | | 32, 36 | 4 + 4 | decoded image width and height |
| 40,48,56 | 8+8+8 | offsets keypoints/descriptors, taille totale | | 40, 48, 56 | 8 + 8 + 8 | keypoint offset, descriptor offset, total size |
| 64 | 32 | SHA-256 de l'asset image source | | 64 | 32 | source image asset SHA-256 |
| 96 | 32 | fingerprint paramètres | | 96 | 32 | parameter fingerprint |
| 128 | 16 | `orb\0` puis zéros réservés | | 128 | 16 | `orb\0` followed by reserved zero bytes |
| 144 | 4 | extractor version = 1 | | 144 | 4 | extractor version = 1 |
| 148 | 12 | réservés, zéro obligatoire | | 148 | 12 | reserved, must be zero |
Chaque keypoint contient six mots de 32 bits : `x`, `y`, `size`, orientation, Each keypoint contains six 32-bit words:
response en binary32, puis octave signé. `x/y` sont en pixels de l'image telle
que décodée par OpenCV, origine en haut à gauche. `size` est le diamètre du
voisinage en pixels. L'orientation est en degrés dans `[0,360)`. L'entrée `i`
du bloc keypoints correspond exactement aux 32 octets du descriptor `i`.
Le validateur contrôle magic, version, réservés, type, dimension, bornes, 1. `x` binary32;
offsets, taille exacte, multiplications, SHA-256 externe et cohérence DB. Une 2. `y` binary32;
version future est distinguée d'une corruption lorsque le fichier et son record 3. `size` binary32;
DB sont cohérents. Le reader utilise `pread`, accepte au plus 256 éléments par 4. orientation binary32;
appel et ne charge jamais le fichier entier. 5. response binary32;
6. signed octave.
Le chemin DB est validé sous sa forme canonique exacte dérivée du SHA-256 avant `x` and `y` are image pixels in the OpenCV-decoded image with top-left origin.
toute ouverture. Une absence retourne `NOT_FOUND`; troncature, hash divergent,
header ou métadonnées DB divergents retournent `CORRUPT`. Une version future
retourne distinctement `UNSUPPORTED_VERSION`.
## Publication et persistance `size` is the neighborhood diameter in pixels.
Layout : `assets/features/<2 hex>/<sha256 complet lowercase>`. Le SHA-256 porte Orientation is in degrees in `[0,360)`.
sur le Feature File complet. Le protocole est : temporaire local, écriture,
`fsync`, hash, `link` atomique sans écrasement, validation complète lors d'une
adoption concurrente, `fsync` du répertoire, puis transaction SQLite. Un échec
du dernier `fsync` est enregistré `PUBLISHED_NOT_DURABLE`. Un échec SQLite après
publication laisse un fichier orphelin et aucune ligne logique partielle.
Un nouvel essai qui revalide l'asset et réussit le `fsync` promeut explicitement
sa durabilité vers `DURABLE` dans la transaction DB.
Le sous-schéma introduit en v5 sépare `feature_assets`, `feature_sets` et Keypoint record `i` corresponds exactly to descriptor row `i`.
`feature_extract_tasks`. L'unicité logique porte sur image, kind, version et
fingerprint. Une tâche est persistée avant enqueue, passe par la queue et le
Governor (CPU, IO, 576 Mio conservateurs, lot 1), puis checkpointée initialement
et terminalement. La reprise recommence l'image entière : il n'existe pas de
fausse reprise intra-ORB. La publication est idempotente.
Pause et annulation sont coopératives avant/après l'appel ORB ; cet appel n'est ## Feature File v1 validation
pas interruptible. L'image gérée est rehashée avant extraction. Une image
uniforme produit légitimement un Feature Set READY vide.
Lardon3D ne garantit pas des octets ORB identiques entre versions d'OpenCV, The reader validates:
plateformes ou backends. L'idempotence porte sur l'environnement courant et le
contrat `extractor_version`; une évolution qui change durablement la sémantique
ou les octets exige d'auditer et, si nécessaire, d'incrémenter cette version.
## Statut - magic;
- format version;
- reserved-zero bytes;
- descriptor type;
- descriptor dimension;
- count bounds;
- offset bounds;
- exact total size;
- checked multiplication;
- external SHA-256;
- Project DB metadata consistency.
**IMPLEMENTED** — ORB réel, formats v1/v2, assets content-addressed, DB v7, A future format version is distinguished from corruption when both file and DB record are otherwise
publication atomique, reader borné, task kind production et reprise automatique. coherent.
**NOT_YET_WIRED** — commande de réconciliation/scrub des orphelins, contrôle fin The reader uses `pread`, reads at most 256 features per call and does not require loading the entire
du backend parallèle OpenCV, orientation EXIF, lancement automatique de Feature File.
l'extraction après import et planification multi-image/DAG.
**IMPLEMENTED** — Visual Index v1 consomme ce reader sans changer le format. The Project DB path must equal the canonical path derived from the stored SHA-256 before the file is
opened.
**IMPLEMENTED** — paires candidates, Matcher v1 et Match Store v1 consomment Outcomes include:
les Feature Sets persistés.
**IMPLEMENTED** — vérification géométrique et tracks. **Sparse SfM Gate A - missing file -> `NOT_FOUND`;
PASS**, the Sparse SfM v16 persistence model is **FROZEN** after Gate B; - truncation -> `CORRUPT`;
Gate C geometry and the synchronous in-memory Gate D incremental core are - hash mismatch -> `CORRUPT`;
**IMPLEMENTED / PASS**. Final per-component Gate E BA is **PASS / FROZEN**; - header/DB disagreement -> `CORRUPT`;
Gate F project/task orchestration is **PASS / FROZEN**. Gate G is - coherent unsupported future version -> `UNSUPPORTED_VERSION`.
**PASS / FROZEN**.
## Extension v2 multi-descriptor ## Publication and persistence
**IMPLEMENTED v1A.** ORB continue d'écrire exactement le v1 U8×32 historique. Physical layout:
SIFT et RootSIFT écrivent le v2 F32×128. Le header v2 de 176 octets contient,
aux offsets 0..64, magic, version, taille header, count, dimension, type, taille
scalaire, taille record, dimensions image, capabilities, offsets des blocs et
taille totale ; puis SHA source à 72, fingerprint à 104, kind sur 16 octets à
136, version extracteur à 152 et vingt octets réservés nuls.
Les valeurs durables sont `U8=1` et `F32=2`, de tailles 1 et 4. Chaque binary32 ```text
est encodé little-endian sans dump de struct. Writer et reader rejettent NaN et assets/features/<first-2-hex>/<full-lowercase-sha256>
Inf. Les lectures `descriptors_u8` et `descriptors_f32` refusent un type ```
incompatible, utilisent `pread` et restent limitées à 256 features. La limite
v1 reste 16 Mio ; la limite v2 est 64 Mio.
La façade d'extraction transporte aussi `descriptor_bytes`. La publication The SHA-256 covers the complete Feature File.
exige exactement `count × dimension × scalar_size`; elle refuse donc un buffer
tronqué ou surdimensionné avant toute lecture ou création de fichier.
ProjectDb v7 ajoute les métriques légères de couverture. Le détail multipasse, The publication protocol is:
la grille et RootSIFT sont canoniques dans
`precision_feature_pipeline.md`. ```text
local temporary file
-> write
-> fsync file
-> hash complete Feature File
-> atomic no-overwrite link/publication
-> fully validate any concurrently existing identical asset
-> fsync parent directory
-> SQLite transaction
-> logical READY state
```
If the final parent-directory `fsync` fails after publication, durability is recorded as
`PUBLISHED_NOT_DURABLE`.
If SQLite fails after the physical file is published, the valid file remains an orphan and no partial
logical Feature Set row is invented.
A later exact retry may revalidate the physical asset, complete a successful directory `fsync` and
promote durability to `DURABLE` in the Project DB transaction.
## Historical Project DB Feature foundation
The Feature Store persistence foundation separates:
- `feature_assets`;
- `feature_sets`;
- `feature_extract_tasks`.
Logical uniqueness is based on image, extractor kind, extractor version and parameter fingerprint.
The historical `features.extract/1` Task is persisted before enqueue, admitted by Queue/Governor,
executes one complete image and checkpoints at its durable boundaries.
Its historical conservative execution shape includes:
```text
one image
one atomic ORB extraction/publication
no intra-image restart
```
The existing conservative single-image resource descriptor remains valid for those Tasks and their
restart compatibility. It must not be generalized into a rule requiring all future independent images
to execute serially.
Pause and cancellation are cooperative before and after the OpenCV call. The OpenCV extraction itself
is not interruptible.
The managed source image is rehashed before extraction.
A uniform image may legitimately publish a READY Feature Set with zero features.
## Selected-execution Feature batch — Project DB v25
Project DB v25 adds only:
```text
feature_extract_batch_tasks
```
and the durable Task Kind:
```text
features.extract.batch/1
```
The v24 -> v25 migration is transactional, additive and DDL-only.
It does not:
- create Feature Sets;
- convert historical `features.extract/1` Tasks;
- infer an `image_id`;
- infer a Feature Set;
- change ORB v1 parameters;
- change Feature File v1/v2;
- change Feature Set identity.
### Durable owner identity
One `features.extract.batch/1` owner is bound to:
- one immutable selected execution;
- one monotonic `next_item_index`;
- the exact ORB extractor kind;
- the exact ORB extractor version;
- the exact ORB parameters;
- the exact ORB parameter fingerprint.
The selected execution order is authoritative.
### Execution shape
The current operational path is:
```text
one admitted owner Task
-> bounded selected-item window
-> bounded independent image participants
-> each participant prepares one per-image Feature result
-> participants perform no SQLite publication
-> join
-> owner validates or reuses the exact READY Feature Set
-> owner publishes in selected-item order
-> owner advances the durable Feature cursor
-> generic Task progress/checkpoint follows
```
Each participant therefore computes an ordinary per-image scientific Feature result.
The owner-only publication stage preserves deterministic selected-item order without requiring
independent image preparation to run serially.
### Atomicity and concurrency
The scientific atomic result remains one Feature Set for one image.
```text
PER_IMAGE_FEATURE_RESULT=ATOMIC
PER_IMAGE_ATOMICITY_REQUIRES_CROSS_IMAGE_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
```
Cross-image preparation may be concurrent when the Governor admits a useful bounded window.
This is not a generic DAG scheduler and does not add a second global worker pool. The Task Queue still
owns one active callback; the batch callback contains the bounded participant work.
### Crash and restart
An immutable Feature Set can become durable before the Feature-batch cursor or generic Task checkpoint
advances.
Therefore, after a crash:
```text
durable Feature Set may be ahead of Task checkpoint
```
but:
```text
Task checkpoint must never invent a Feature Set that is not durable
```
Restart revalidates and reuses the exact READY Feature Set and then converges the cursor. It does not
derive identity from path, processing order or Task-local position.
### Resource adaptation
For the selected-execution Feature batch, increasing admitted CPU while the item window remains one
cannot exercise additional independent images.
The current validated resource model may therefore couple CPU and batch growth where required by the
Feature-batch rung contract.
That behavior is Task-specific operational policy, not a universal Governor rule.
The canonical repository policy remains:
```text
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
Preserve the interactive host reserve and all RAM/CPU/IO/GPU safety constraints first. Within that
safe envelope, use the maximum useful validated execution width.
Per-item atomicity is not evidence that unrelated items must be serialized.
## GPU boundary
Feature Extraction currently remains CPU.
The current production GPU audit classifies Feature Extraction as:
```text
FEATURE_GPU=REJECTED_WITH_MEASURED_OR_IMPLEMENTATION_EVIDENCE
```
On the validated OpenCV 5.0.0 host:
- no usable ORB/SIFT Vulkan/OpenCL extraction seam is validated;
- CUDA is unavailable in the installed OpenCV build;
- no GPU path has proven byte-identical Feature Files for this boundary.
Therefore Feature Extraction receives no production GPU path merely because the host has a GPU.
This does not authorize avoidable CPU serialism. Cross-image CPU parallelism remains valid where its
Task contract has been proven.
## Environment and reproducibility
Lardon3D does not claim ORB Feature File bytes are identical across arbitrary:
- OpenCV versions;
- platforms;
- extractor backends.
Idempotence is defined inside the supported environment and extractor-version contract.
Any change that modifies durable Feature semantics or bytes requires explicit review and, when
necessary, an extractor-version change. It must not silently reuse the old scientific identity.
## Feature File v2 and multi-descriptor support
**IMPLEMENTED v1A.**
ORB continues to write the historical Feature File v1 U8x32 representation.
SIFT and RootSIFT write Feature File v2 F32x128.
### v2 header
Feature File v2 has a 176-byte header.
Its fields include:
- magic;
- explicit version;
- header size;
- feature count;
- descriptor dimension;
- descriptor type;
- scalar size;
- keypoint-record size;
- decoded image dimensions;
- capabilities;
- keypoint/descriptor block offsets;
- total size;
- source SHA-256 at offset 72;
- parameter fingerprint at offset 104;
- 16-byte extractor kind at offset 136;
- extractor version at offset 152;
- twenty reserved zero bytes.
Stable descriptor type values are:
```text
U8 = 1
F32 = 2
```
with scalar sizes 1 and 4.
Every binary32 value is explicitly encoded little-endian; persistent output is not a C/C++ struct
dump.
Writer and reader reject NaN and Inf.
The typed descriptor readers:
- use `pread`;
- refuse incompatible descriptor type;
- return at most 256 features per call.
File-size limits remain:
```text
Feature File v1 16 MiB
Feature File v2 64 MiB
```
The extraction facade also carries `descriptor_bytes`.
Publication requires exactly:
```text
feature_count * descriptor_dimension * scalar_size
```
and rejects both truncated and oversized descriptor buffers before file creation/publication.
The detailed multipass, grid and RootSIFT scientific contracts remain in
[`precision_feature_pipeline.md`](precision_feature_pipeline.md).
## Downstream consumers
The Feature Store currently feeds implemented downstream stages:
```text
Feature Store
|
v
Visual Index
|
v
Candidate Pair
|
v
Matcher
|
v
Geometric Verification
|
v
Tracks
```
Visual Index consumes the bounded Feature reader without changing Feature File format.
Candidate Pair consumes Feature Set identities indirectly through Visual Index membership.
Matcher consumes persisted Feature Sets and their descriptors.
Geometric Verification and Tracks consume later correspondence products; they do not rewrite Feature
Store identity.
Sparse SfM Gates C through G are implemented and PASS/FROZEN, but real known-calibration Sparse SfM
was not executed in the retained historical S21/A6000 campaigns.
## Real A6000 evidence
The retained real A6000 selected execution contains:
```text
Feature Sets 689
Candidate Pairs 38,420
Match Results 38,420
```
The v25 selected Feature-batch path completed all selected images.
The retained checkpoint is:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
The later GV/Tracks continuation required:
```text
Feature replay 0
Candidate replay 0
Matcher replay 0
```
and continued from retained immutable upstream products.
This proves the current Feature-batch operational path and restart/reuse boundary on that campaign.
It does not create calibration for that campaign and does not constitute real Sparse SfM execution.
## Current unfinished work
The following remain separate from the Feature Store scientific contract:
- global orphan-file reconciliation/scrub;
- EXIF orientation handling where still required;
- generic dependency/DAG scheduling;
- future extractor/backend work only after explicit equivalence/version review.
Multi-image Feature execution itself is no longer an unfinished item:
`features.extract.batch/1` is implemented and validated.
## Summary
```text
CURRENT_PROJECT_DB_SCHEMA=v25
FEATURE_STORE_STATUS=IMPLEMENTED
FEATURE_FILE_V1=FROZEN
FEATURE_FILE_V2=IMPLEMENTED
HISTORICAL_FEATURE_TASK=features.extract/1
CURRENT_FEATURE_BATCH_TASK=features.extract.batch/1
PROJECT_DB_V25_FEATURE_BATCH=IMPLEMENTED/VALIDATED
FEATURE_RESULT_IDENTITY=PER_IMAGE
PER_IMAGE_FEATURE_RESULT=ATOMIC
PER_IMAGE_ATOMICITY_REQUIRES_CROSS_IMAGE_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
FEATURE_GPU=REJECTED_WITH_MEASURED_OR_IMPLEMENTATION_EVIDENCE
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_A6000_FEATURE_SETS=689
REAL_A6000_PRE_SFM=PASS/FROZEN
```

View file

@ -1,180 +1,408 @@
# Geometric Verification # Geometric Verification Model
## Scope ## Status
Geometric Verification Model est le contrat persistant placé après le Matcher.
Sa représentation stocke les identités scientifiques Geometric Verifier v1/v2
historiques et v3 courantes, sans changement de schéma : `verifier_version` et
`parameter_fingerprint` appartiennent déjà à l'identité exacte. Il stocke un
résultat terminé, compact et immutable. Il n'est ni un moteur de calcul ni une
tâche.
Aucun RANSAC, USAC, MAGSAC, calcul d'inliers ou backend géométrique n'appartient à ce ticket.
## Position in reconstruction pipeline
La chaîne d'ownership est :
```text ```text
Feature Set → Candidate Pair → Match Result → Geometric Verification Result GEOMETRIC_VERIFICATION_MODEL=IMPLEMENTED
PROJECT_DB_GEOMETRIC_VERIFICATION=v12
HISTORICAL_VERIFIER_V1=VALID
HISTORICAL_VERIFIER_V2=VALID
CURRENT_PRODUCTION_VERIFIER_V3=FROZEN
CURRENT_PROJECT_DB_SCHEMA=v25
REAL_S21_GV_V3=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
``` ```
Le masque indexe exclusivement l'ordre des entrées du Match File canonique du Match Result. Il This document owns the **persistent Geometric Verification Result model**.
n'indexe directement ni les features, ni la Candidate Pair, ni un ordre temporaire de backend.
## Scientific ownership It does not own the numerical estimator implementation. The current executable scientific verifier is
documented in `geometric_verifier.md`.
Le parent scientifique est `match_result_id`. L'API accepte uniquement un Match Result existant, The persistence model was deliberately version-ready from Project DB v12: `verifier_version` and
`MATCHED`, avec `match_count` strictement positif. `NO_MATCH` et les erreurs runtime ne peuvent pas `parameter_fingerprint` already belong to exact result identity. Therefore historical verifier v1/v2
produire de résultat géométrique. and current v3 results coexist without a schema reinterpretation.
## Parent Match Result ## Pipeline position
Le Match Store reste propriétaire de la validation du Match File. La création consulte le parent ```text
et son `match_count` en DB ; elle ne relit pas l'asset. Un load valide aussi l'existence et l'état du Feature Set
parent afin qu'une ligne corrompue ne soit jamais rendue comme résultat valide. -> Candidate Pair
-> Match Result
-> Geometric Verification Result
-> Track Builder
-> Track Model
```
The inlier mask indexes the canonical Match File entry order.
It does not directly index:
- Feature Store physical order;
- Candidate Pair order;
- temporary backend order.
## Scientific parent
The exact parent is:
```text
match_result_id
```
A Geometric Verification Result may be created only for a valid `MATCHED` parent with positive
`match_count`.
`NO_MATCH` and runtime failures do not produce a scientific geometric result.
## Persistent identity ## Persistent identity
L'identité demandée et unique est : Exact identity:
```text ```text
(match_result_id, verifier_kind, verifier_version, parameter_fingerprint) (
match_result_id,
verifier_kind,
verifier_version,
parameter_fingerprint
)
``` ```
Le fingerprint est le SHA-256 opaque de 32 octets déjà standard dans le projet. Il représentera No selection by timestamp or "latest" is permitted.
un encodage de paramètres versionné, stable, à ordre de champs explicite et, pour les nombres
binaires, little-endian. Aucun timestamp, résultat, PID, durée ou identifiant matériel n'y entre. The fingerprint is an opaque canonical SHA-256 scientific parameter identity.
It excludes:
- Task ID;
- PID;
- elapsed time;
- CPU count;
- batch size;
- GPU identity;
- hardware identity.
## Verifier kind ## Verifier kind
Le modèle supporte uniquement `FUNDAMENTAL`, valeur persistante stable 1. Aucun comportement fictif The persistent supported model kind is:
`ESSENTIAL` ou `HOMOGRAPHY` n'est réservé dans l'API publique.
## Persistent states ```text
FUNDAMENTAL = 1
```
- `GEOMETRIC_REJECTED=1` : calcul scientifique terminé, critère non satisfait ; Do not reserve fictitious `ESSENTIAL` or `HOMOGRAPHY` values in prose without an explicit versioned
- `GEOMETRIC_VERIFIED=2` : calcul scientifique terminé, critère satisfait. implementation decision.
`FAILED`, `RUNNING`, `PAUSED` et `CANCELLED` appartiennent au Task Runtime. REJECTED peut conserver ## Scientific states
un nombre d'inliers non nul.
## Model representation Completed scientific states are:
FUNDAMENTAL utilise neuf colonnes SQLite `REAL`, en ordre ligne-major `m00` à `m22`. SQLite ```text
convertit les valeurs numériques en binary64 sans exposer une ABI C. VERIFIED exige les neuf GEOMETRIC_REJECTED = 1
valeurs présentes et finies. REJECTED exige les neuf valeurs NULL. Le modèle n'impose ni rang 2, GEOMETRIC_VERIFIED = 2
ni déterminant, ni normalisation ou échelle canonique ; ces règles relèvent du futur verifier. ```
## Inlier representation Runtime states such as RUNNING, FAILED, PAUSED or CANCELLED belong to Task Runtime, not this model.
Le masque est un BLOB SQLite obligatoire de taille exacte `ceil(match_count / 8)`. Pour l'entrée A rejected result may still contain non-zero inlier support.
`i`, `byte_index=i/8`, `bit_index=i%8` et le masque vaut `1u << bit_index`. Le bit 0 est donc le bit
de poids faible de l'octet 0. Cette convention est indépendante de l'endianness CPU et de l'ABI.
Les bits de padding du dernier octet valent zéro et le popcount est exactement `inlier_count`.
Le masque existe pour REJECTED comme pour VERIFIED. Avec 8192 matches, il mesure au maximum ## Fundamental matrix representation
1024 octets. Un BLOB SQLite évite les milliers de lignes secondaires et la publication, le hash,
le nettoyage et la récupération d'un asset externe d'environ 1 Kio. Une liste `uint32_t` serait
jusqu'à 32 fois plus grande au cas dense et aurait un encodage supplémentaire à versionner.
## Invariants A verified Fundamental result contains nine SQLite `REAL` values:
- `0 <= inlier_count <= parent.match_count <= 8192` ; ```text
- longueur, padding et popcount du masque sont canoniques ; m00 ... m22
- REJECTED possède un masque cohérent et aucun modèle ; ```
- VERIFIED possède un masque cohérent et exactement neuf valeurs finies ;
- kind, version et fingerprint ont une sérialisation stable ;
- une ligne publiée est complète et immutable.
Exemple : pour 100 matches, une identité FUNDAMENTAL v1, v2 ou v3/fingerprint in row-major order.
X peut publier REJECTED avec 23 inliers, un masque de 13 octets et aucun modèle.
Une autre identité peut publier VERIFIED avec 67 inliers, le même format de
masque et une matrice 3×3 finie.
## Persistence semantics The persistent representation is binary64 through SQLite numeric semantics, not a C ABI struct dump.
Une création valide puis insère identité, état, masque et modèle dans une transaction courte. Le A verified row requires nine finite values.
calcul futur se fera entièrement avant cette transaction. SQLite fournit l'atomicité ; aucun asset
ou journal secondaire n'est créé. A rejected row contains no model.
Rank/canonicalization/scientific-estimator rules belong to the versioned verifier contract.
## Inlier mask
The mask is a required SQLite BLOB of exact size:
```text
ceil(match_count / 8)
```
Bit convention for Match File entry `i`:
```text
byte = i / 8
bit = i % 8
mask[byte] & (1u << bit)
```
The mask is LSB-first inside each byte.
Padding bits in the final byte are zero.
The mask popcount must equal `inlier_count`.
The mask exists for both verified and rejected scientific results.
With the current Match File bound of 8192 matches, the mask is at most 1024 bytes.
## Persistent invariants
For every row:
```text
0 <= inlier_count <= parent.match_count <= 8192
mask size is canonical
padding bits are zero
mask popcount == inlier_count
```
Additionally:
```text
REJECTED -> no Fundamental matrix
VERIFIED -> exactly nine finite matrix coefficients
```
A published row is immutable.
## Publication
Numerical estimation completes before the short Project DB publication transaction.
Publication inserts:
- exact parent;
- exact verifier identity;
- completed state;
- canonical mask;
- optional verified Fundamental model.
No external asset is required because the bounded mask/model fit naturally in SQLite.
Rollback leaves no partial scientific result.
## Reuse ## Reuse
Le reuse cherche uniquement l'identité exacte, jamais le résultat le plus récent. Une identité Exact reuse uses only the full persistent identity.
existante retourne une erreur de contrainte à `create`; le runtime fera `find`, validera puis
réutilisera. `INSERT OR REPLACE` est interdit, même si le nouveau contenu semble identique.
## Invalidations Existing exact result:
Un nouveau Match Result possède un nouvel ID et ne réutilise donc aucun ancien résultat ```text
géométrique. La FK emploie `ON DELETE CASCADE` : supprimer explicitement le parent supprime ses find
enfants et ne crée pas d'orphelin. Aucun moteur d'invalidation parallèle n'est nécessaire. -> validate
-> reuse
```
## Project DB schema Never:
Project DB v12 ajoute `geometric_verification_results`, une contrainte UNIQUE sur l'identité et un ```text
index de pagination `(match_result_id, geometric_verification_result_id)`. Les CHECK SQL portent INSERT OR REPLACE
les bornes scalaires, tailles locales et nullabilité modèle/état. La cohérence avec le parent, le latest result
padding, le popcount et la finitude restent validés en C. closest fingerprint
same parent with different version
```
## API A new scientific verifier version creates another result identity.
L'API publique implémente : ## Parent deletion
- `lardon3d_project_db_create_geometric_verification_result()` ; The parent FK uses delete-cascade semantics.
- `lardon3d_project_db_load_geometric_verification_result()` ;
- `lardon3d_project_db_find_geometric_verification_result()` ;
- `lardon3d_project_db_list_geometric_verification_results()`.
La liste est bornée à 256 entrées, filtrée par parent puis ordonnée par ID croissant avec curseur. Explicit deletion of a Match Result deletes its dependent geometric results.
Le résultat en mémoire contient son `created_at` et son masque dans une capacité fixe de 1024
octets : aucun ownership dynamique ni fonction de destruction. Les fonctions copient fingerprint,
masque et neuf coefficients ; l'appelant conserve ses entrées.
Parent absent retourne `NOT_FOUND`; parent NO_MATCH ou parent incohérent retourne `CONSTRAINT` à No parallel invalidation engine is required.
la création. Masque, modèle ou arguments locaux invalides retournent `INVALID_ARGUMENT`; duplicate
identity retourne `CONSTRAINT`. Un loader qui rencontre une ligne ou un parent incohérent retourne
`CORRUPT`, sans résultat partiel.
## Resource bounds ## Schema
Un résultat contient au plus 1024 octets de masque et 72 octets de valeurs numériques, plus de Project DB v12 introduced `geometric_verification_results`.
petites métadonnées. Une page est bornée. Le loader vérifie les entiers et tailles SQLite avant
tout cast ou copie. Il n'existe ni cache global, ni lecture non bornée, ni Content Store associé.
Le Match File parent mesure au plus 98 336 octets ; le futur job peut donc rester une petite unité.
## Error ownership The current schema head is v25.
Seuls les résultats scientifiques terminés sont persistés. OOM, exception, annulation, timeout, Later schema additions do not redefine the v12 row format or identity.
device lost, I/O transitoire ou panne de thread appartiennent à l'exécution de tâche. État du modèle
et état d'exécution sont deux contrats distincts.
## Recovery semantics ## Public API
Après commit, le résultat est complet et réutilisable après réouverture. Avant commit, le rollback The model provides bounded create/load/find/list APIs for Geometric Verification Results.
ne laisse aucune ligne partielle. Un loader rejette toute ligne incohérente comme corruption au
lieu de réparer ou d'interpréter au mieux.
## Verifier execution contract The list API is paged and ordered by increasing ID.
L'exécution prend un Match Result et son Match File borné. L'accès nécessaire existe via In-memory result storage remains bounded: the inlier mask has fixed maximum capacity and no result-owned
`lardon3d_feature_reader_keypoints()`, borné à 256 keypoints par appel ; l'intégration devra relier heap destructor is required for the core row object.
les deux Feature Sets et les indices du Match File sans modifier le Feature Store. Le verifier
estimera hors transaction, dérivera état/masque/modèle, publiera en une courte transaction,
checkpoint puis libérera les buffers. Une paire est l'unité atomique. Task Runtime et Resource
Governor décideront admission, threads et lots ; zram/swap ne sont jamais un budget.
Un backend reste hors identité seulement s'il est scientifiquement transparent. Sinon son Exact function declarations in the public headers remain authoritative.
algorithme ou contrat doit apparaître dans kind/version/fingerprint avant publication. Toute seed
influençant le résultat doit avoir une politique déterministe versionnée ou être couverte par le
fingerprint. Aucun nombre de threads ou hardware ID n'est un paramètre scientifique par défaut.
## Explicitly out of scope ## Error semantics
GPU, Vulkan, OpenCL, shader et nouvelle orchestration restent hors périmètre de ce contrat de Creation distinguishes invalid local arguments from parent/identity constraints.
persistance.
## Versioning Loaders return corruption rather than a partially interpreted result if:
Project DB schema version 12 décrit le stockage. `verifier_version` décrit indépendamment le - parent is missing or invalid;
contrat scientifique. Changer un algorithme n'impose une migration DB que si la représentation - stored mask length is wrong;
persistante change. - padding is non-canonical;
- popcount disagrees;
- model/state nullability is inconsistent;
- a verified matrix contains non-finite values.
Scientific rejection is not a database/runtime failure.
Runtime OOM, exception, cancellation, estimator failure or device failure are not persisted as
`GEOMETRIC_REJECTED`.
## Current verifier lineage
The model stores all supported versions through the same identity fields.
### v1
Historical Fundamental verifier v1 remains immutable and valid.
### v2
Historical Fundamental verifier v2 remains immutable and valid.
V2 added the distinct-canonical-observation preflight in the scientific execution contract.
### v3
Current production verifier is Fundamental v3.
Production fingerprint:
```text
6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
```
V3 preserves the persistent model and adds its versioned scientific preflight before the unchanged
eligible estimator path.
No Project DB migration was needed for v3 because v12 already stores verifier version and fingerprint.
## Task relationship
The production Task Kind is:
```text
geometric_verifier.run/1
```
Project DB v13 adds only its typed durable Task payload.
The Task:
```text
pages Match Results
-> validates eligibility
-> computes or reuses exact GVR identity
-> owner publishes in canonical parent order
-> advances typed cursor
-> checkpoints
-> sequence_break
```
Task/runtime state remains separate from GVR scientific state.
## Current resource boundary
One Match Result is the scientific atomic item.
Current validated outer-parallel Task execution may prepare independent parents concurrently.
The owner publishes the contiguous canonical prefix.
Current validated bounds include:
```text
useful CPU participants <= 8
safe parent/window size <= 16
per-item reservation approximately 8 MiB
GPU = 0
```
The internal USAC/MAGSAC scientific solver remains `isParallel=false`.
These operational values do not enter GVR identity.
## Real S21 v3 evidence
Retained S21 proof:
```text
REAL_S21_GV_V3=PASS/FROZEN
Match Results 172,741
Applicable MATCHED 172,275
Verified GVRs 24,065
Rejected GVRs 148,210
non-applicable 466
duplicate mappings 0
```
The source Matcher project was retained unchanged and GV ran only from the Match Result boundary.
Restart/idempotence evidence preserved the complete GVR result set.
No Track/Sparse work belonged to the original GV-only boundary.
## Real A6000 v3 evidence
Retained current A6000 pre-SfM continuation:
```text
Match Results 38,420
Applicable GVRs 37,805
Verified GVRs 10,952
Rejected GVRs 26,853
duplicate mappings 0
```
Fingerprint:
```text
6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
```
The continuation then built Tracks and stopped before real Sparse SfM.
Checkpoint:
```text
REAL_A6000_PRE_SFM=PASS/FROZEN
```
## Out of scope
This persistence model does not define:
- RANSAC/USAC/MAGSAC implementation;
- GPU kernels;
- Task scheduling;
- Track construction;
- Essential pose;
- triangulation;
- Sparse SfM;
- Homography competition.
Those belong to their versioned scientific/runtime contracts.
## Summary
```text
GEOMETRIC_VERIFICATION_MODEL=IMPLEMENTED
PROJECT_DB_GEOMETRIC_VERIFICATION=v12
PROJECT_DB_GEOMETRIC_VERIFIER_TASK=v13
CURRENT_PRODUCTION_VERIFIER=FUNDAMENTAL_V3
CURRENT_VERIFIER_FINGERPRINT=6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
REAL_S21_GV_V3=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
CURRENT_PROJECT_DB_SCHEMA=v25
```

View file

@ -1,468 +1,486 @@
# Geometric Verifier v1 / v2 / v3 # Geometric Verifier v1 / v2 / v3
## Scope ## Status
Ce document décrit l'exécution scientifique qui transforme un Match Result `MATCHED` en résultat ```text
Fundamental `GEOMETRIC_REJECTED` ou `GEOMETRIC_VERIFIED`. Le contrat persistant reste défini par HISTORICAL_GEOMETRIC_VERIFIER_V1=FROZEN
[`geometric_verification.md`](geometric_verification.md). Tracks, pose, Essential, compétition HISTORICAL_GEOMETRIC_VERIFIER_V2=FROZEN
Homography, triangulation et SfM sont hors périmètre. CURRENT_GEOMETRIC_VERIFIER_V3=PASS/FROZEN
V1 et v2 restent des identités scientifiques historiques et immutables : leurs versions, CURRENT_VERIFIER_KIND=FUNDAMENTAL
fingerprints, lignes et résultats existants ne sont jamais réinterprétés. V2 conserve l'estimator, CURRENT_VERIFIER_VERSION=3
les paramètres, l'ordre et l'acceptance v1, mais ajoute avant USAC le support minimal par CURRENT_VERIFIER_FINGERPRINT=6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
observations canoniques distinctes décrit ci-dessous. V3 est la policy de production courante :
après les mêmes validations intégrales, elle compose ce support v2 avec la preuve exacte de GEOMETRIC_VERIFIER_GPU=NOT_JUSTIFIED
faisabilité d'acceptation `match_count >= min_inlier_count`. Chaque policy possède sa version et son REAL_S21_GV_V3=PASS/FROZEN
fingerprint distincts ; le schéma les stocke déjà depuis v12 et la tête courante REAL_A6000_PRE_SFM=PASS/FROZEN
Project DB v23 ne nécessite aucune migration GV. ```
This document owns the scientific execution that turns one valid `MATCHED` Match Result into one
completed Fundamental Geometric Verification Result.
The persistent row model is owned by `geometric_verification.md`.
Tracks, Essential pose, triangulation and Sparse SfM are downstream.
## Version lineage
### v1
Verifier v1 is a frozen historical scientific identity.
Its existing fingerprints and GVR rows remain immutable.
### v2
Verifier v2 preserves the v1 estimator/acceptance path but adds a bounded preflight requiring enough
distinct canonical observations on both sides before estimator execution.
It has its own version/fingerprint.
### v3
Verifier v3 is the current production policy.
It preserves v2 validation and additionally rejects estimator-ineligible parents when:
```text
match_count < min_inlier_count
```
because acceptance is mathematically impossible in that case.
At equality, the parent remains estimator-eligible.
V3 does not relabel or mutate v1/v2 rows.
Project DB v12 already stores verifier version/fingerprint, so no GVR schema migration is needed.
Current project schema head is v25.
## Inputs ## Inputs
Le parent DB fournit les deux Feature Set IDs, le compte, le chemin, la taille et le SHA-256 du The exact Match Result supplies:
Match File. Le reader Feature Store ouvre séparément chaque Feature Set validé et expose les
keypoints par plages d'au plus 256. Le verifier n'a besoin d'aucun descriptor : charger les blocs
ORB ou SIFT/RootSIFT serait inutile et est interdit dans le chemin normal.
Les keypoints persistants portent des coordonnées `binary32`. `x/y` sont exprimés en pixels de - Candidate Pair relation;
l'image exactement décodée par OpenCV lors de l'extraction, avec origine en haut à gauche et - Feature Set A/B identities;
positions subpixel possibles. Les dimensions décodées sont disponibles dans les métadonnées du - Match File path/size/SHA;
Feature File. - `match_count`.
## Fundamental matrix contract Feature readers provide keypoints for the two immutable Feature Sets.
Le seul modèle v1 est une matrice Fundamental 3×3. Une sortie acceptée doit être unique, finie, The verifier does not need descriptor blocks in its normal geometry path.
de norme non nulle et canonique avant publication. V1 ne projette pas la matrice vers le rang 2.
Feature coordinates are persistent binary32 decoded-image pixels with top-left origin.
They are converted to binary64 `Point2d` for geometric computation.
## Input ordering ## Input ordering
L'entrée `i` de l'estimator correspond exactement à l'entrée `i` du Match File : Estimator row `i` corresponds exactly to Match File entry `i`.
`feature_index_a` sélectionne le Feature Set A et `feature_index_b` le Feature Set B. Le Match
File impose déjà des indices A strictement croissants ; le verifier ne trie et ne filtre pas les
correspondances. Toute corruption d'index est une erreur d'exécution, jamais un rejet scientifique.
V2 distingue le nombre brut de lignes des observations canoniques distinctes. Les identités sont The verifier does not reorder or deduplicate Match File rows.
`A=(feature_set_id_a, feature_index_a)` et
`B=(feature_set_id_b, feature_index_b)`. Après validation intégrale du parent, du Match asset, des
Feature Sets et Feature assets, v2 exige au moins sept A distincts **et** sept B distincts. Sinon il
publie `GEOMETRIC_REJECTED`, `inlier_count=0`, masque intégralement nul de longueur exactement
`ceil(match_count/8)`, sans modèle et sans appel USAC.
Ce préflight ne modifie jamais l'évidence Matcher : aucune déduplication, tri, contrainte The published inlier bit `i` always maps back to Match File entry `i`.
one-to-one, unicité de coordonnées, limite de multiplicité, analyse de rang/conditionnement/
colinéarité ou compétition Homography n'est appliquée. Des IDs distincts ayant les mêmes
coordonnées restent des observations distinctes. Le Match File canonique rendant A strictement
croissant, une insuffisance A implique en pratique moins de sept lignes valides ; B peut en revanche
être insuffisant malgré un grand nombre de lignes brutes.
V3 exécute ensuite USAC seulement si `match_count >= min_inlier_count`. Lorsque cette inégalité Out-of-range Feature indices or corrupt assets are runtime/input failure, not scientific rejection.
échoue, au plus `match_count` bits du masque pourraient être inliers : l'acceptation est donc
mathématiquement impossible. V3 publie alors le même rejet zéro borné sans appel estimator. La
borne vient du paramètre durable, jamais d'une constante `16`. À l'égalité, l'entrée reste éligible.
Ce contrat n'ajoute aucune règle `N<20`, rang, coordonnées, homographie, déduplication ou retry ;
toute entrée qui franchit les deux préflights appelle l'USAC inchangé et toute exception inattendue
reste un échec Task sans publication.
## Coordinate representation ## Canonical observation identity
Le stockage source reste `binary32`. Sur 1024 points, bruit 0,75 px et 50 % d'outliers, Point2f et For preflight counting:
Point2d ont produit le même masque et la même qualité, en 3,58 et 3,55 ms. La production convertit
vers Point2d pour rendre le calcul et la sortie binary64 explicites, pour 256 Kio au maximum.
Aucune mise à l'échelle par résolution ni conversion de repère n'est appliquée implicitement.
## Algorithm candidates ```text
A = (feature_set_id_a, feature_index_a)
B = (feature_set_id_b, feature_index_b)
```
OpenCV 5 installé expose `FM_RANSAC`, `USAC_DEFAULT`, `USAC_ACCURATE`, `USAC_PROSAC` et V2/v3 require at least seven distinct A observations and seven distinct B observations.
`USAC_MAGSAC`. La shortlist Gate A est FM_RANSAC comme baseline, puis USAC_DEFAULT,
USAC_MAGSAC et USAC_ACCURATE. PROSAC est `NOT_APPLICABLE` en v1 : la distance descriptor est
persistée mais l'ordre canonique suit l'index de query, pas un classement de qualité benchmarké.
## Benchmark methodology Failure publishes a zero-inlier `GEOMETRIC_REJECTED` result with a correctly sized all-zero mask and
no Fundamental model.
Un corpus synthétique déterministe avec Fundamental ground truth couvrira bruit, outliers, This preflight does not:
résolutions, tailles, géométries saines, faibles et adversariales. Les méthodes seront comparées
par précision/recall du masque, erreurs épipolaires, échecs, repeatability, temps et ressources.
Le benchmark lourd restera hors build et suite par défaut. Aucune fixture photo réelle ne sera
revendiquée sans fixture non sensible présente dans le dépôt.
La campagne Gate A du 9 août 2026 utilise OpenCV 5.0.0, Clang 22.1.8, une seed fixe et 32 - rewrite Matcher evidence;
répétitions. Elle couvre 7 à 8192 points, 0 à 100 % d'outliers, bruit 0 à 1,5 px, 1280×720 à - enforce one-to-one matching;
4000×3000, baseline faible/large, concentration, quasi-colinéarité, planéité, rotation dominante - deduplicate coordinates;
et duplications. Aucune fixture photo réelle représentative n'existe dans le dépôt. - perform a rank test;
- perform collinearity analysis;
- run Homography competition.
| Algorithme | P/R 1024, 30 % | P/R 8192, 70 % | Médiane/p95/pire 8192 | Seed locale | Stable 32× | Distinct Feature IDs with identical coordinates remain distinct observations.
|---|---:|---:|---:|---|---|
| FM_RANSAC | 0,998/0,720 | 0,993/0,413 | 316,4/318,9/319,7 ms | non | oui observé |
| USAC_DEFAULT | 0,996/0,960 | 0,997/0,959 | 43,0/43,2/44,8 ms | preset non | oui |
| USAC_MAGSAC | 0,993/0,965 | 0,994/0,962 | 11,4/12,0/12,1 ms | preset non | oui |
| USAC_ACCURATE | 0,996/0,960 | 0,996/0,961 | 30,5/32,0/32,1 ms | preset non | oui |
| MAGSAC params v1 | 0,997/0,957 | 0,996/0,894 | 10,8/11,0/11,4 ms | oui | oui |
FM_RANSAC est rejeté pour son recall et son pire temps. DEFAULT et ACCURATE n'améliorent pas assez ## v3 acceptance-feasibility preflight
la qualité pour leur coût. La production emploie des UsacParams explicites : la seed par appel
prime sur la variation du cas extrême liée à la seed fixe. À bruit 0,75 px/50 % d'outliers, les
seuils 0,5/1,0/1,5/2,0/3,0 donnent des recalls 0,535/0,811/0,961/0,990/1,000 et des precisions
0,996/0,988/0,990/0,986/0,985. Le compromis retenu est 1,5 px.
## Determinism V3 additionally checks the durable configured `min_inlier_count`.
USAC expose `cv::UsacParams::randomGeneratorState`, un entier par appel, ainsi que les paramètres When:
de sampling, score, optimisation locale et polishing. Cette API est préférable à une mutation de
`cv::theRNG()` process-global. FM_RANSAC restera une baseline scientifique tant que son contrôle
RNG et sa repeatability n'ont pas été mesurés.
Les cinq candidats ont donné un hash modèle+masque identique sur 32 appels et dans trois processus ```text
distincts. La garantie v1 reste intra-environnement : mêmes octets, ordre, configuration, seed, match_count < min_inlier_count
OpenCV 5.0.0 et architecture. Aucun bit-exact cross-version ou cross-architecture n'est promis. ```
## Random seed policy the maximum possible support cannot satisfy acceptance, so V3 publishes the canonical zero rejection
without calling USAC.
La policy v1 calcule SHA-256 sur `L3DGVSE1`, le SHA-256 du Match File puis le fingerprint. Les The threshold comes from configuration, never a hidden constant.
quatre premiers octets sont décodés little-endian et les 31 bits faibles alimentent
`randomGeneratorState`. La policy est version 1.
## Parameter fingerprint ## Scientific model
Le fingerprint v1/v2/v3 est SHA-256 des 84 octets suivants. Les entiers sont little-endian ; les doubles The only production model is a 3x3 Fundamental matrix.
sont leurs bits IEEE-754 binary64 écrits comme `uint64_t` little-endian. NaN/Inf sont refusés et
le seul champ autorisant zéro signé, `min_inlier_ratio`, normalise `-0.0` en `+0.0`. Aucun octet ne
provient d'un dump de structure.
| Offset | Taille | Champ | Accepted output must be finite, non-zero and canonical.
|---:|---:|---|
| 0 | 8 | domaine ASCII `L3DGVFP1` |
| 8 | 4 | version encodage = 1 |
| 12 | 4 | kind FUNDAMENTAL = 1 |
| 16 | 4 | verifier version = 1, 2 ou 3 |
| 20 | 4 | algorithme USAC_MAGSAC explicite = 1 |
| 24 | 8 | threshold binary64 |
| 32 | 8 | confidence binary64 |
| 40 | 4 | max iterations |
| 44 | 4 | minimum inlier count |
| 48 | 8 | minimum inlier ratio binary64 |
| 56 | 4 | seed policy version |
| 60 | 4 | canonicalisation version |
| 64 | 1 | représentation Point2d = 2 |
| 65 | 1 | sampler uniforme = 0 |
| 66 | 1 | score MAGSAC = 2 |
| 67 | 1 | isParallel = 0 |
| 68 | 1 | LO inner = 1 |
| 69 | 4 | LO iterations = 5 |
| 73 | 4 | LO sample size = 14 |
| 77 | 1 | neighbor grid = 1 |
| 78 | 1 | COV polisher = 3 |
| 79 | 4 | polisher iterations = 3 |
| 83 | 1 | réservé nul |
Le vector golden v1 de la configuration historique commence par les 84 octets hexadécimaux The current verifier does not add Essential/Homography model competition.
`4c33444756465031...0300000000` et donne le SHA-256
`ddb44bb070c62be66c405946e89cbb49c084f8f30a21d6f408dc239225b7bbd0`. Pour un Match File SHA
composé de 31 octets nuls puis `01`, cette configuration donne la seed décimale `1910542150`.
V2 conserve cet encodage et place `2` au champ `verifier version`; son fingerprint diffère donc
obligatoirement même lorsque les sept paramètres numériques sont identiques. La seed dérivée suit
ce fingerprint v2 et appartient à cette nouvelle identité. Pour la configuration production, le
fingerprint v2 est `7868a893437ee611a10008a093286997212fa8bd80b2afd2bb1d11f04f01c5ae` ;
le même Match SHA golden donne la seed décimale `1528046088`.
V3 conserve encore exactement les 84 octets et place `3` au champ version. Pour la configuration
production, son fingerprint est
`6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c` ; le même Match SHA golden
donne la seed décimale `188721673`.
Les politiques de
ressources, hardware, PSI, lot, worker et réservation CPU ne sont ni des champs ni des entrées.
## Acceptance policy
Un modèle candidat qui échoue à cette policy publie REJECTED avec son masque et son compte
d'inliers, sans matrice. La production exige `inlier_count >= 16` et
`inlier_count / match_count >= 0,20`. Les cas 100 % faux produisent 10/64, 14/256, 26/1024 et
28/4096 inliers, ratio maximal 0,15625. Les scènes saines produisent 45/64, 129/256 et 297/1024 ;
la faible baseline produit 126/256.
## Fundamental matrix canonicalization
La production adopte cette canonicalisation version 1. Les neuf valeurs doivent être finies. La
norme de Frobenius est calculée avec une accumulation `hypot` résistante au débordement ; zéro est
refusé. Le premier coefficient de valeur absolue strictement maximale gagne, donc un tie conserve
le plus petit index ligne-major. Après division, le signe rend ce pivot positif et les zéros signés
sont normalisés à `+0.0`. Sur 8192/70 %, les singular values sont
3,392e-2, 1,374e-4 et 5,915e-24. OpenCV fournit déjà rank-2 à précision numérique. V1 ne calcule
aucune SVD en production, n'impose aucun seuil de rang et n'effectue aucune post-projection rank-2.
Les validations production portent uniquement sur la forme 3×3 unique, la finitude et la norme.
## Inlier mask generation
Le masque OpenCV est validé en type, taille et valeurs, puis converti sans réordonnancement vers
le bitset LSB-first du modèle. Les frontières 7/8/9, 63/64/65 et 8191/8192 sont testées.
Le core conserve strictement l'ordre d'entrée du Match File. Les tests utilisent des indices B
permutés et des masques non contigus ; le bit `i` publié reste l'élément `i` du fichier, jamais
l'index de feature. Les tailles 1, 2, 7, 8, 9, 63, 64, 65, 8191 et 8192, le padding nul et le
popcount sont couverts avec le Model v1 inchangé.
## Scientific rejection
En v1, un nombre brut de matches inférieur au minimum réel produit le rejet zéro historique. En
v2, moins de sept observations canoniques distinctes sur A ou B produit le rejet zéro décrit dans
`Input ordering`. V3 conserve ce test et rejette également quand le nombre brut est strictement
inférieur au `min_inlier_count` configuré. L'absence de modèle sur entrée valide ou l'échec de
l'acceptance policy produit un résultat scientifique REJECTED cohérent.
Le minimum USAC observé est sept. Sept à quinze observations supportées peuvent produire une
hypothèse mais ne franchissent pas nécessairement le support d'acceptation production.
## Execution failure
Match/Feature asset absent ou corrompu, index hors bornes, exception OpenCV, OOM, masque malformé,
matrice non finie ou invariant interne invalide échoue dans le Task Runtime. Aucun résultat
scientifique n'est publié dans ces cas.
Le core traduit parent absent/NO_MATCH et Feature Set absent en erreur d'exécution `NOT_FOUND` ;
asset absent, tronqué, hash divergent, ownership ou index incohérent en `CORRUPT` ; exception ou
sortie estimator malformée/non finie en `ESTIMATOR_ERROR` ; `bad_alloc` en `OUT_OF_MEMORY` ; et
échec Model en `DATABASE_ERROR`. Les seams test-only couvrent erreur estimator, mask/matrice
malformés, NaN, OOM et publication. Aucun de ces chemins ne crée de résultat scientifique.
## Resource bounds
Une unité atomique est un Match Result, au maximum 8192 correspondances. Le Match File est borné
à 98 336 octets et le bitset à 1024 octets. Aucun cache global ni préchargement de projet complet
n'est utilisé.
À 8192 matches, les allocations directement contrôlées maximales sont 98 304 octets d'entries,
393 216 octets de keypoints A/B, 262 144 octets de Point2d A/B, deux cartes v2 de présence de
8192 octets, 1024 octets de bitset, environ 8192 octets de mask OpenCV et 72 octets de modèle, soit
environ 761 Kio hors petits objets et scratch OpenCV. Les cartes appartiennent à une paire et sont
libérées au retour ; leur borne Feature Store est opérationnelle et n'ajoute aucune limite
scientifique. Aucun descriptor ni matrice A×B n'est lu. Massif mesure 2,445 Mio de heap au pic
du test E2E complet, incluant SQLite, OpenCV, fixtures Feature Store et toutes les séquences de test.
La forme sérielle historique réservait 4 Mio fixes. La Task outer-parallel
courante réserve 8 Mio par item admis afin de couvrir aussi la pile enfant
bornée de 4 Mio, l'objet préparé, les readers et le scratch opaque. Avec un lot
maximal 16, cette charge reste bornée à 128 Mio et ne limite pas la cardinalité
du dataset.
## CPU policy
Le parallélisme scientifique interne OpenCV reste explicitement désactivé :
`UsacParams::isParallel=false` appartient au fingerprint FROZEN. Le verifier ne
change pas `cv::setNumThreads()` par paire.
Le parallélisme courant porte uniquement sur des Match Results indépendants.
Le Governor peut admettre CPU1..8 pour une fenêtre/lot d'au plus 16 ; le
callback Queue compte comme participant, crée au plus `cpu_threads-1` enfants,
les joint, puis publie seul dans l'ordre. La fenêtre participant a été validée
sûre jusqu'à 16, mais CPU8 est le maximum utile mesuré. Ce choix opérationnel
n'entre ni dans le fingerprint, ni dans le GVR.
## GPU policy
Aucun backend Vulkan n'est implémenté avant profil du chemin CPU final. La décision attendue est
`NOT_JUSTIFIED` si les unités restent sub-millisecondes ou de quelques millisecondes.
Verdict Gate A : `NOT_JUSTIFIED`. Les cas usuels prennent 0,3 à 5,6 ms et le pire MAGSAC local
mesuré reste à 11,4 ms. Aucun backend Vulkan de vérification n'est implémenté.
## Task Runtime
L'audit Gate C conclut que le checkpoint générique v1 est insuffisant : il conserve l'état,
la progression, le compteur de séquences et les temps, mais aucun payload propre au kind. Le
reconstructeur doit retrouver les paramètres scientifiques immuables et le curseur sans les
inventer depuis un fingerprint irréversible.
`geometric_verifier_tasks` est donc la seule raison de Project DB v13. Elle doit conserver
`task_id`, `after_match_result_id`, les sept paramètres de configuration v1 et le fingerprint
calculé à la création pour validation à la reconstruction. Aucun `cv::Mat`, buffer, état RNG,
paramètre Governor ou donnée hardware n'y appartient. La tâche calcule hors transaction et publie
chaque résultat par transaction courte avant avancement du curseur.
Le payload v22 ne duplique pas `verifier_version`. À la reconstruction, le fingerprint exact est
comparé aux encodages supportés v1, v2 et v3 des sept paramètres durables : cela restaure sans
ambiguïté chaque policy, sans migration ni nouveau système d'identité. Toute nouvelle tâche
sélectionne v3 ; le payload et l'ABI publique de configuration restent inchangés. Une tâche v3
démarre avec une identité neuve et ne reprend ni ne ré-étiquette une tâche v1/v2.
Le Task Kind production est `geometric_verifier.run` version 1. Il pagine les Match Results par ID
strictement croissant avec une page de `batch + 1`, et traite des lots Governor
1..16 avec CPU utile 1..8. Les préparations éligibles peuvent s'exécuter en
parallèle, mais la publication, l'avancement du curseur contigu et le checkpoint
restent owner-only et ordonnés. Les parents autres que `MATCHED` avec
`match_count > 0` sont seulement traversés par le curseur. Une unité éligible
appelle le core, qui reuse l'identité exacte avant toute lecture d'asset.
WHY GENERIC TASK PERSISTENCE IS INSUFFICIENT: aucun champ de payload métier dans le snapshot v1.
REQUIRED DURABLE FIELDS: configuration scientifique v1, fingerprint et dernier Match Result
publié puis checkpointé.
WHY EXISTING DB CANNOT STORE THEM: `tasks` et `checkpoints` ne portent que le résumé générique ;
aucune table v12 ne possède une ligne 1:1 adaptée à ce Task Kind.
## Checkpoint/recovery
La pagination suit `match_result_id` croissant sans supposer des IDs contigus. Le résultat est
publié avant que `after_match_result_id` avance en mémoire ; le curseur n'est persisté qu'après le
lot. Après chaque lot non terminal, `task_sequence_break()` rend la réservation au Governor.
Le test de crash publie puis interrompt avant checkpoint du curseur, ferme runtime et DB, recharge
le checkpoint antérieur et reconstruit le Task Kind. Le parent est revu, son résultat exact est
réutilisé, puis le curseur progresse.
## Cancellation
Pause et annulation sont coopératives avant chaque parent et entre lots. Une petite estimation
OpenCV engagée finit et publie avant l'arrêt ; aucun résultat scientifique CANCELLED n'est créé.
Les résultats déjà publiés restent durables.
## Backend policy
Un backend n'est transparent pour l'identité que si ses sorties scientifiques sont équivalentes
selon le contrat. V1 possède une seule implémentation CPU de production.
## Core publication and reuse
Le core charge les métadonnées DB, relâche les mutex internes après chaque API, lit les assets et
calcule sans transaction longue, puis appelle une publication Model v1 courte. Une identité exacte
VERIFIED ou REJECTED est retournée avant toute lecture Feature/Match et sans appel estimator. Une
contrainte concurrente déclenche un unique `find` de l'identité, jamais un overwrite ou une
récursion. Changer un paramètre scientifique produit un autre fingerprint et un autre résultat.
Les tests E2E utilisent le vrai Project DB (migré séquentiellement jusqu'à v15 à
l'ouverture), deux Feature Files à 8192 points, des Match Files hashés, le vrai
MAGSAC et le Model v1. VERIFIED est rechargé après close/reopen avec modèle et
masque bit-identiques ; REJECTED conserve son support et est également réutilisé.
## Production algorithm ## Production algorithm
UsacParams explicites, sampler uniforme, score MAGSAC, non parallèle et seed locale par appel. The production estimator uses explicit OpenCV USAC/MAGSAC parameters rather than a hidden preset.
Les champs LO et polishing effectifs sont encodés explicitement ; aucun preset enum caché.
## Production parameters Scientific choices include:
FUNDAMENTAL version 1 ; seuil 1,5 px ; confiance 0,999 ; 5000 itérations ; 16 inliers ; ratio 0,20 ; ```text
seed policy 1 ; canonicalisation 1 ; Point2d. Tous les champs scientifiques appartiennent au model FUNDAMENTAL
fingerprint version 1. algorithm USAC_MAGSAC
point representation Point2d
threshold 1.5 px
confidence 0.999
max iterations 5000
minimum inlier count 16
minimum inlier ratio 0.20
sampler uniform
score MAGSAC
isParallel false
LO iterations 5
LO sample size 14
polisher COV
polisher iterations 3
```
## Validation The internal scientific estimator remains serial:
Gate A couvre corpus, comparaison, seed et repeatability. Gate B couvre fingerprint/seed golden, ```text
canonicalisation, mapping bit à bit, frontières d'acceptation, E2E DB, reuse, corruption, UsacParams::isParallel=false
publication, 8192 matches et ASan/UBSan. Gate C couvre Task, publication avant curseur et reprise. ```
La référence A6000 v3 complète contient 37 805 parents `MATCHED`. Le préflight v3 rejette à zéro Outer Task-level concurrency is separate.
9 368 parents avec `N<16`, puis le support distinct A/B rejette 117 parents supplémentaires avec
`N>=16`. Les 28 320 autres parents appellent USAC sous leur seed v3 exacte ; ils terminent tous par
un modèle ou une absence de modèle, sans exception estimator. Cette exécution démarre une tâche v3
neuve et ne reprend ni ne ré-étiquette la tâche v2 historique 1385.
Gate D a exécuté 1000 parents configurés dans la vraie Task, puis les reprises et variantes de ## Random seed
configuration du test : environ 2001 traversées réutilisées en 5,870 s, soit environ 341/s. Ce
run valide pagination, checkpoints, Governor et reuse ; il n'est pas une mesure de latence MAGSAC
et n'en revendique ni médiane ni p95. Le RSS pic observé est 25 964 Kio pour le processus de test
complet. `MemAvailable` passe de 10 702 988 à 10 692 916 Kio ; `pswpin/pswpout` restent 0/0 ; en
fin de run, PSI avg10 vaut 0,34 % CPU, 0 % mémoire et 0 % I/O. Le chemin calculé reste couvert par
le vrai E2E MAGSAC Gate B et ses bornes, sans campagne scientifique répétée.
TSan couvre core, Task, sequencing et Governor (4/4), avec uniquement la suppression OpenCV The seed is derived locally from immutable scientific input.
existante. Le build CPU-only couvre la suite normale (31/31). La suite normale ne contient ni
benchmark lourd ni stress. Le clean build Clang/Clang++ et la campagne normale finale passent The frozen seed domain is based on:
32/32 avec ORB Vulkan matériel sur Radeon 780M RADV PHOENIX.
```text
L3DGVSE1
Match File SHA-256
verifier parameter fingerprint
```
The resulting seed is supplied to the per-call USAC parameter object.
Global `cv::theRNG()` mutation is forbidden.
## Parameter fingerprint
The verifier fingerprint is SHA-256 over the frozen canonical 84-byte encoding.
Domain:
```text
L3DGVFP1
```
It explicitly encodes:
- verifier kind;
- verifier version;
- algorithm;
- threshold;
- confidence;
- iteration bound;
- minimum inlier count;
- minimum inlier ratio;
- seed-policy version;
- canonicalization version;
- Point2d selection;
- sampler;
- score;
- `isParallel`;
- LO settings;
- neighbor mode;
- polisher settings;
- reserved zero byte.
Integers are little-endian.
Binary64 values use explicit IEEE-754 bits encoded little-endian.
No C struct dump participates.
Current v3 production fingerprint:
```text
6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
```
Historical v1/v2 fingerprints remain distinct and valid.
## Acceptance
A candidate is verified only when the frozen support policy is satisfied:
```text
inlier_count >= 16
inlier_count / match_count >= 0.20
```
Otherwise the result is scientific `GEOMETRIC_REJECTED`.
A scientific rejection is a completed valid result.
Runtime failure publishes no scientific GVR.
## Fundamental canonicalization
The nine coefficients must be finite.
Frobenius norm is computed robustly; zero norm is rejected.
The pivot is the first row-major coefficient having the strictly greatest absolute value.
The matrix is normalized and signed so that pivot is positive.
Signed zeros are normalized to `+0.0`.
No production SVD/rank-2 post-projection is performed by this verifier version.
## Inlier mask
OpenCV output mask is validated for type, length and values.
It is converted without reordering to the persistent LSB-first GVR bitset.
Padding bits are zero and popcount matches `inlier_count`.
## Runtime failure versus rejection
Scientific rejection includes valid cases such as:
- insufficient canonical support;
- v3 impossible acceptance support;
- estimator returns no acceptable model;
- inlier support below frozen acceptance.
Runtime/input failure includes:
- missing/corrupt Match or Feature asset;
- out-of-range Feature index;
- malformed estimator mask;
- non-finite invalid model;
- OOM;
- unexpected OpenCV exception;
- Project DB publication failure.
Runtime failure publishes no fake rejected result.
## Resource bounds
One scientific atomic item is one Match Result with at most 8192 correspondences.
Current outer-parallel Task reservation uses approximately:
```text
8 MiB per admitted parent
batch/window <= 16
useful CPU participants <= 8
GPU 0
```
The window is safe to 16; CPU8 is the retained useful maximum from measurement.
These are operational resource values, not fingerprint fields.
No descriptors or dense A-by-B matrix are loaded by the normal verifier.
## Outer parallel execution
Current validated shape:
```text
one admitted owner Task
-> bounded independent Match Result preparation
-> up to admitted CPU participants
-> join
-> owner publishes canonical contiguous prefix
-> owner advances cursor
-> checkpoint
-> sequence_break
```
Participant preparation writes no GVR rows.
Only the owner publishes after join.
This preserves deterministic parent ordering and restart semantics.
The inner USAC solver remains scientifically serial.
## GPU policy
Current classification:
```text
GEOMETRIC_VERIFIER_GPU=NOT_JUSTIFIED
```
Measured CPU units remained small enough that no production GPU backend was justified.
No Vulkan/OpenCL/CUDA verifier backend is currently part of the production identity.
CPU outer parallelism remains valid and should not be disabled merely because GPU is rejected.
## Durable Task
Task Kind:
```text
geometric_verifier.run/1
```
Project DB v13 adds the typed task payload required because generic checkpoint v1 does not contain the
scientific verifier parameters/cursor.
Durable payload includes the immutable scientific configuration and:
```text
after_match_result_id
```
The fingerprint is revalidated on reconstruction.
The durable payload does not contain:
- `cv::Mat`;
- RNG engine state;
- Governor feedback;
- hardware identity;
- CPU mask;
- transient buffers.
## Restart
Pagination follows increasing `match_result_id` and does not assume contiguous IDs.
A GVR is published before the durable cursor advances.
Crash after publication but before checkpoint may replay that parent.
Restart finds/revalidates the exact GVR identity and reuses it.
No overwrite is performed.
## Historical resource normalization
The exact historical serial resource shape remains accepted only for restart compatibility.
It may be normalized ephemerally to the current outer-parallel capability.
The original checkpoint is not rewritten.
Neighboring resource shapes are rejected rather than guessed.
## Real S21 GV v3 ## Real S21 GV v3
`REAL_S21_GV_V3=PASS/FROZEN` au 31 août 2026. La preuve part d'une copie reflink entière du projet Retained S21 proof:
Matcher S21 gelé à 2 826 Feature Sets, 172 741 Candidate Pairs et 172 741 Match Results. Le SHA-256
DB source vaut avant et après
`9f5ee4877bca25db3d4929be06d8e6ff4fa1c29e11249e4125266a833f09f3e0` ; le projet source n'est
jamais ouvert en écriture. La copie de travail reprend exclusivement à la frontière Match Result,
par la Task, la Queue et le Resource Governor AUTO de production, puis s'arrête avant Track
Builder. Avant GV, la Task Matcher 2831 est `COMPLETE`, progression 100,
`sequence_count=21629`, curseur 172 741 ; son checkpoint SHA-256 vaut
`636f4f4a20f27308d90142c495c9f6ffc04b4c0dfcca0fdc75cfeb5366ab50b1`. Le projet contient alors
zéro GVR, Track Set, Track ou Sparse Reconstruction.
La Task 2832 consomme le curseur complet de 172 741 Match Results. Parmi eux, 172 275 parents ```text
`MATCHED` applicables produisent exactement 172 275 identités v3 : 24 065 REAL_S21_GV_V3=PASS/FROZEN
`GEOMETRIC_VERIFIED` et 148 210 `GEOMETRIC_REJECTED`. Les 466 autres Match Results sont traversés
sans GVR conformément au contrat. Le fingerprint est
`6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c`. La Task termine
`COMPLETE`, progression 100, `sequence_count=21592`, curseur 172 741 et zéro mapping dupliqué. Son
checkpoint final SHA-256 vaut
`3e6bed97cee9c96f229ef19a3c905d19d4693cdbf43b30319edcdeed6c4e378e`. Le wall propre à
l'enqueue/attente GV vaut 3 221,757986763 s ; le wall du runner incluant l'audit intégral amont
vaut 3 252,89 s.
L'audit relit les 172 741 mappings Candidate/Match et les 172 275 Match Files : SHA, taille, Match Results 172,741
header, entrées, ordre et curseur Matcher restent valides. Le digest `L3DMRD1` demeure Applicable MATCHED 172,275
`e5128a2e599ff593c4f79850e067254b1f249d19e8480a44973306b1af250f70`. Feature Sets, Candidate Verified 24,065
Pairs et Match Results gardent respectivement 2 826, 172 741 et 172 741 lignes ; aucune Task Rejected 148,210
Feature, Candidate ou Matcher n'est rejouée. Track Set, Track, Track Builder Task, Sparse SfM Task non-applicable 466
et Sparse Reconstruction restent tous à zéro. duplicate mapping 0
```
Le Governor enregistre 21 593 admissions, exclusivement backend fixe, sans changement de contrat. The v3 fingerprint is the current production fingerprint.
Le dernier contrat est GREEN, CPU 1, GPU 0, I/O 1, batch 8, hôte 4 Mio et GPU 0. Les masques sont
compute `0-5,8-13` et reserve `6,7,14,15`. Sur l'échantillonnage coalescé de 21 590 changements,
le minimum `MemAvailable` vaut 8 907 714 560 octets, le RSS/HWM processus maximal 45 690 880
octets, le PSI mémoire maximal 0,90 %, le PSI I/O maximal 50,17 % et les deltas swap-in/out sont
0/0. Les réserves 3 Gio/2 Gio alors en vigueur pour ce run historique restent
respectées ; aucune voie GPU GV n'est créée.
La seconde reprise complète crée la Task 2833, traverse le même curseur et crée zéro GVR. Les A complete replay produced zero new GVRs.
172 275 lignes avant/après sont égales sur toutes leurs colonnes par `EXCEPT` dans les deux sens,
avec zéro différence, les mêmes IDs 1..172275 et les mêmes comptes accepté/rejeté. Sur une copie
reflink séparée, SIGKILL interrompt la Task 2834 après un préfixe checkpointé : l'état durable reste
`RUNNING/PENDING`, puis la registry de production reprend cette même Task (`inspected=1`,
`resumed=1`) jusqu'à `COMPLETE`, progression 100 et curseur 172 741. L'égalité complète des GVR
avec le projet terminé reste zéro différence dans les deux sens ; aucun résultat n'est perdu ou
dupliqué et aucun travail amont/aval n'est exécuté.
Les builds normaux Vulkan et portable sans Vulkan passent. Les 14 tests focalisés GV, Task, A SIGKILL/restart proof resumed the same Task and converged to the same complete GVR set.
checkpoint, Project DB/Project, registry, Queue et Governor passent dans chaque configuration ; le
test runner ciblé passe aussi sous ASan/UBSan. `REAL_S21_GV_V3`, `RESTART_IDEMPOTENCE`,
`DETERMINISM`, `GOVERNOR_ADMISSION` et `DOWNSTREAM_STOP` sont donc `PASS/FROZEN`. Ce gel porte sur
la preuve réelle de la policy v3 déjà gelée ; il ne rouvre ni algorithme, seuil, RNG, fingerprint,
Project DB v22, Matcher/Governor v2, Track Builder ou Sparse SfM.
## Maintenance outer-parallel — preuve représentative réelle The run stopped before Track Builder at the original GV-only checkpoint.
**IMPLEMENTED / VALIDATED / REVIEWED.** La maintenance sépare la préparation Later S21 Track evidence exists separately.
scientifique de la publication sans modifier la policy v3. Une préparation
valide tout l'input immuable et produit un objet opaque borné ; elle n'écrit
jamais Project DB. Après jointure, le callback propriétaire publie ces objets
en ordre de `match_result_id`, détruit chacun exactement une fois et checkpoint
le seul préfixe contigu. Les pannes de création partielle, calcul, publication,
annulation et reprise ne peuvent donc ni publier un suffixe devant un trou, ni
laisser un enfant/handle vivant à la libération de réservation.
Le corpus représentatif réel contient 4 113 parents, dont 4 102 applicables, ## Real A6000 v3
578 `GEOMETRIC_VERIFIED` et 3 524 `GEOMETRIC_REJECTED`. CPU1/2/4/8/12
conservent les mêmes IDs 1..4102, toutes les colonnes scientifiques et le digest
`9401ef6168804b6f1d51f4cdf64cd6b33cbebd2934e5294c8feacc87f9c8ce86`.
Les walls Task complets sont 67,521078032/48,859141912/39,158236068/
35,170176868/34,251675780 s, soit 60,7514/83,9556/104,7545/116,6329/
119,7606 parents/s. Le gain CPU8→CPU12 vaut seulement 2,68 %, sous le seuil de
5 %. La capacité production est donc CPU utile 8, batch 16 et fenêtre sûre 16.
Les tests focalisés finaux passent 8/8, les répétitions de stress 60/60, The retained A6000 pre-SfM continuation contains:
ASan/UBSan 3/3 et TSan 3/3, avec contrôles C17 GCC/Clang. La preuve S21
historique ci-dessus reste le run complet CPU1/batch8 acquis ; aucun rerun
complet de 3 221 s n'est revendiqué pour cette maintenance bornée. Le manifest
retenu de cette preuve a le SHA-256
`52a4412299c74050a66d5690122a793c9451c79faf47e32b6e65a5958f804856`.
Il compare littéralement les 4 102 GVR applicables — ordre/IDs 1..4102,
statut, compteur/masque d'inliers, présence et octets binary64 du modèle — et
vérifie intégrité DB, clés étrangères, absence de replay amont et absence de
travail Tracks/Sparse. Le run S21 complet acquis couvre déjà la policy v3 et
sa persistance FROZEN ; la maintenance ne change que la préparation externe et
la publication owner-only. Cette combinaison réelle bornée + tests ciblés de
panne/checkpoint/reprise discrimine donc le changement sans payer un second run
scientifique intégral ni prétendre l'avoir exécuté.
La validation globale fraîche qui englobe ce delta passe aussi dans le graphe ```text
Clang portable Vulkan-off 931/931 et sa suite 64/64, puis le graphe Vulkan-on Match Results 38,420
939/939 et sa suite 65/65. Le TSan global reste volontairement Vulkan-disabled Applicable GVRs 37,805
et couvre les deux cibles GV dans sa matrice 14/14 plus répétitions ; il utilise Verified GVRs 10,952
uniquement les suppressions externes OpenCV/TBB documentées par le projet. Rejected GVRs 26,853
duplicate mappings 0
```
## Out of scope Current fingerprint:
Tracks, model competition, classification planaire ou faible parallaxe, Essential, calibration, ```text
pose, triangulation, bundle adjustment, SfM et Vulkan RANSAC. 6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
```
The Task completed the full cursor.
Restart traversed the cursor and created zero new GVRs.
The continuation then reused/built the frozen Track Set and stopped before real Sparse SfM.
```text
REAL_A6000_PRE_SFM=PASS/FROZEN
```
## Validation boundaries
The scientific verifier validation covers:
- canonical fingerprint/seed;
- bit-exact mask mapping;
- acceptance boundaries;
- corruption;
- publication/reuse;
- maximum Match File cardinality;
- restart;
- deterministic canonicalization;
- outer-parallel owner publication.
TSan qualification must preserve the external OpenCV/TBB boundary described by the concurrency/global
maintenance documents.
Do not claim Vulkan verifier validation: there is no production Vulkan verifier backend.
## Summary
```text
CURRENT_GEOMETRIC_VERIFIER=FUNDAMENTAL_V3
CURRENT_VERIFIER_VERSION=3
CURRENT_VERIFIER_FINGERPRINT=6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
GEOMETRIC_VERIFIER_TASK=geometric_verifier.run/1
PROJECT_DB_GEOMETRIC_VERIFICATION=v12
PROJECT_DB_GEOMETRIC_VERIFIER_TASK=v13
INNER_USAC_PARALLEL=false
OUTER_PARALLEL=VALIDATED
USEFUL_CPU_MAX=8
SAFE_WINDOW_MAX=16
PER_ITEM_RAM=8_MiB
GPU=NOT_JUSTIFIED
REAL_S21_GV_V3=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
CURRENT_PROJECT_DB_SCHEMA=v25
```

View file

@ -1,223 +1,459 @@
# Vue d'ensemble de l'architecture Lardon3D # Lardon3D Architecture Overview
## Finalité et flux global ## Purpose
Lardon3D est un moteur de reconstruction géométrique persistante et incrémentale, Lardon3D is a persistent, incremental, resource-aware photogrammetry engine for Linux.
piloté par une TUI ncursesw. Le terminal reste le centre de contrôle : il gère
les projets, lance les opérations, présente leur progression et permet leur The TUI is the operational control center for projects, acquisition, Tasks, durable progress,
annulation. Le viewer sera un composant graphique séparé mais intégré à resource state, optical configuration and optional external-storage control. Rich visualization and
l'interface pour un usage confortable sur un seul écran. live acquisition remain separate product areas and must consume validated snapshots rather than
mutable worker buffers.
## Current authority
```text ```text
TUI / Projet CURRENT_PROJECT_DB_SCHEMA=v25
CURRENT_PRODUCTION_TASK_KINDS=16
Task RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
Estimate REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
Task Queue
Resource Governor / Reservation
callback worker admis
Résultat atomique
Viewer live
``` ```
## Composants actuels The current Project DB head is additive:
```text
v22 Selected scientific execution foundation
v23 Generic optical-context overlay
v24 raw.develop.batch/1 persistence
v25 features.extract.batch/1 persistence
```
Earlier schema versions remain valid historical contracts where their documentation says so. Current
documentation must not present an older version as the active head merely because that version owns a
specific historical scientific or persistence boundary.
## Global execution model
```text
TUI / Project
|
v
Task
|
v
immutable resource estimate / capability
|
v
bounded Task Queue
(one active callback)
|
v
Resource Governor
(admission + reservation)
|
v
admitted owner callback
|
+--> bounded internal participants when justified
|
v
deterministic owner publication
|
v
durable checkpoint / restart boundary
|
v
validated snapshot consumers
```
The Queue owns bounded dispatch and backpressure. The Governor owns production resource admission.
The active Task owns its execution state and any bounded internal participants. Scientific stores own
their own identities and artifact validation.
No layer may silently absorb another layer's authority.
## Canonical resource objective
Lardon3D first preserves the interactive host reserve required for normal workstation use. Safe and
useful capacity beyond that reserve belongs to the active workload.
The objective is:
```text
MAXIMUM_SAFE_USEFUL_THROUGHPUT
```
This does not mean maximizing utilization percentages for appearance. It means using the largest
currently safe and useful CPU, RAM, I/O and validated accelerator capability while preserving
scientific identity, deterministic publication, host responsiveness and pressure limits.
The companion rule is:
```text
SERIALISM_REQUIRES_PROOF
```
Per-item atomicity does not imply cross-item serialization. Ordered or owner-only durable publication
does not imply serial preparation. Independent work should expose bounded concurrency when exact
science and persistence semantics can be preserved.
A CPU1 or batch1 path may remain when serialism, a measured scaling knee, memory, I/O, GPU execution,
exclusive state or another concrete constraint justifies it. Historical conservative descriptors are
not portable performance ceilings.
On the current validation host, the normal observed outcome is approximately:
```text
16 logical CPUs total
4 logical CPUs reserved for interactive host use
12 logical CPUs available to the compute pool
~3 GiB MemAvailable hard reserve
Radeon 780M UMA available to validated and useful GPU backends
```
These values are reference-host observations, not product constants.
## Current components
### Project ### Project
Gestion persistante des projets : création, ouverture, fermeture, structure
de répertoires. Chaque projet regroupe configuration, images originales,
manifeste, résultats, exports et journaux.
**Statut :** IMPLEMENTED Persistent project lifecycle, stable identity, directory layout and Project DB ownership.
### Import **Status:** IMPLEMENTED
Import asynchrone et annulable d'images dans un projet. Copie individuelle
des fichiers admissibles et maintenance d'un manifeste cohérent.
**Statut :** IMPLEMENTED ### Project Database
### Import Task SQLite owns durable logical identities, relations, typed Task payloads, scientific metadata and
Premier type métier persistant. Il s'exécute par lots bornés dans le runtime et references to external immutable artifacts.
la Queue génériques, cible explicitement un ScanSet et peut être reconstruit
puis repris.
**Statut :** IMPLEMENTED The current schema head is v25. The scientific meaning of historical rows remains owned by the
versioned contracts that created them.
### ScanSet et Image Catalog **Status:** CURRENT / v25
Catalogue SQLite persistant séparant acquisition, image logique et asset
physique SHA-256. Les parcours persistants sont paginés ; l'ancien catalogue
mémoire depuis `manifest.tsv` reste une façade legacy pour la TUI.
**Statut :** IMPLEMENTED ### Import and ScanSets
### Profils optiques et calibrations Import materializes managed immutable assets and logical images under explicit ScanSets. Provenance
from external source paths remains distinct from managed asset identity.
Project DB v23 ajoute un overlay générique distinct pour profils de boîtiers, **Status:** IMPLEMENTED
objectifs électroniques ou manuels, configurations optiques, affectations aux
campagnes/Captures et calibrations explicitement compatibles. La migration
n'infère ni ne backfill aucune donnée S21/A6000 ; ces appareils restent des
preuves de validation, pas des identités produit.
**Statut :** IMPLEMENTED — OVERLAY ADDITIF v23 ### Capture / Asset Provenance
### Image View Capture identity remains distinct from file, Asset, `image_id`, SHA-256, path, Task ID and campaign
Vues triées et filtrées du catalogue pour la TUI. Ne modifie pas le group ID. RAW and JPEG siblings may belong to one physical Capture without becoming one file or one
catalogue, le manifeste ou les images. scientific image identity.
**Statut :** IMPLEMENTED **Status:** PASS / FROZEN
### TUI observatoire / centre de contrôle ### Bounded acquisition discovery and campaign execution
Le thread principal possède ncurses, l'entrée et le rendu. Un modèle pur reçoit Discovery and planning are bounded and deterministic. Automatic grouping requires the documented
des copies bornées de l'unique Queue et du Governor, coalescées autour d'une strong evidence; otherwise explicit caller confirmation remains `CALLER_EXPLICIT`.
seconde, et présente progression durable, ETA honnête, pipeline, ressources,
profils optiques et SSD. Les dimensions validées sont full ≥100×30, compact de
référence 72×20, minimum 60×15, puis le fallback `Terminal trop petit`. Les
couleurs s'accompagnent toujours de libellés textuels et `F10 SSD` reste visible
au minimum supporté.
Ouvrir, fermer ou changer de projet détruit/joint l'unique Queue avant de Durable campaign execution uses the existing Task, Queue, Governor and Project DB recovery model.
fermer Project DB, puis recrée une Queue vide et rebranche l'observation. Les
ABI historiques Task/Resource/AppState/layout restent inchangées ; les vues
riches utilisent des structures et fonctions additives décrites dans
[Runtime](runtime.md).
**Statut :** CURRENT / VALIDATED OPERATIONAL **Status:** PASS / FROZEN
### Photo Quality Triage
Quality analysis is an operational selection layer, not a scientific identity. It produces explicit
GOOD / SUSPECT / REJECT recommendations and keeps human overrides distinct.
**Status:** PASS / FROZEN
### Selected scientific execution
The selected-execution snapshot binds the retained acquisition groups to explicit scientific
representations without inferring identity from paths, filenames or metadata.
**Status:** PASS / FROZEN
### Optical profiles and calibration selection
Project DB v23 provides generic camera-body profiles, lens profiles, optical configurations,
campaign/Capture assignments, calibration profiles and explicit compatible calibration selection.
Electronic metadata aliases are exact. Manual lenses without EXIF are normal. Missing or ambiguous
identity remains unresolved rather than guessed.
**Status:** IMPLEMENTED / VALIDATED
### RAW development
The historical `raw.develop/1` path remains valid. Project DB v24 adds
`raw.develop.batch/1`, allowing bounded independent RAW preparation with deterministic owner-only
publication in selected-item order.
Per-Capture publication remains atomic while cross-Capture preparation may be concurrent.
**Status:** IMPLEMENTED / VALIDATED
### Feature Store ### Feature Store
Extraction ORB réelle par tâche persistante, Feature Sets logiques et assets
binaires content-addressed lisibles par plages bornées.
**Statut :** IMPLEMENTED Feature Store publishes immutable content-addressed Feature Files and bounded typed readers for ORB,
SIFT and RootSIFT data.
The historical `features.extract/1` task remains valid. Project DB v25 adds
`features.extract.batch/1`, which prepares independent selected images with bounded participants and
publishes Feature Sets deterministically through the owner callback.
**Status:** IMPLEMENTED / VALIDATED
### Visual Index ### Visual Index
Index de retrieval ORB LSH persistant en segments immuables. Il indexe les
Feature Sets par lots bornés et retourne des candidats inter-ScanSets sans
matching géométrique.
**Statut :** IMPLEMENTED The ORB LSH Visual Index is persistent and segmented. It indexes homogeneous Feature Sets in bounded
updates and retrieves candidate relationships without performing geometric validation.
### Task **Status:** IMPLEMENTED
Moteur de tâches avec états, progression, pause/reprise coopérative,
annulation, checkpoints et estimations de ressources.
**Statut :** IMPLEMENTED
### Task Queue
File FIFO avec worker unique, sélection de la première tâche admissible,
backpressure et bornage du nombre de tâches en attente.
**Statut :** IMPLEMENTED
### Hardware Profile
Détection des capacités matérielles statiques : cœurs CPU, RAM, GPU/VRAM.
**Statut :** IMPLEMENTED
### Resource Snapshot
Capture instantanée des ressources disponibles : RAM libre, charge CPU,
VRAM disponible.
**Statut :** IMPLEMENTED
### Resource Governor
Arbitrage centralisé des budgets (RAM, GPU, CPU, IO), calcul de lots
adaptatifs, réservations opaques et historique borné de métriques.
**Statut :** IMPLEMENTED
### Contrôleur SSD externe optionnel
Frontière physique UDisks2 pour la paire de labels `LARDON_SWAP` et
`LARDON_SCRATCH`, avec identité Drive+UUID stable, état borné, leases scratch
et drain sûr. Il ne formate, ne répare ni ne force jamais l'hôte et ne remplace
pas le Resource Governor. La TUI exécute ses actions dans un seul thread
joinable et le Governor enregistre l'état physique puis orchestre seul les
leases scratch de production. Les seize Task kinds courants n'en consomment
aucun ; capacité visible ne signifie donc pas usage.
**Statut :** CURRENT / VALIDATED OPERATIONAL
### Candidate Pair ### Candidate Pair
Sous-système de génération et persistance de paires d'images candidates pour
le matching géométrique. Répond uniquement à « quelles paires valent la peine
d'être explorées ? » sans validation géométrique.
**Statut :** IMPLEMENTED Candidate Pair generation answers which image pairs are worth exploring. Scientific pair identity and
canonical ordering remain independent from the amount of execution parallelism used to prepare them.
### Match Result Candidate currently uses a bounded coupled CPU/batch ladder because additional CPU cannot exercise
Persistance d'un calcul descriptor-level réussi entre deux Feature Sets additional independent pair work while the admitted item window remains one.
liés à une Candidate Pair. Identité déterministe par
`(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)`.
Validation d'appartenance Feature Set → image. Les correspondances vivent dans
le Match Store v1 content-addressed; les échecs restent dans le Task Runtime.
**Statut :** IMPLEMENTED **Status:** IMPLEMENTED
### Matcher ### Matcher
Matching de descripteurs entre deux Feature Sets via BFMatcher OpenCV
(ORB Hamming, SIFT/RootSIFT L2) avec Lowe ratio test. Produit un
Match File content-addressed et un Match Result dans Project DB.
Déterministe, idempotent, borné.
**Statut :** IMPLEMENTED Matcher produces deterministic Match Results from Feature Sets.
ORB supports the validated Vulkan hot path with exact CPU fallback. SIFT and RootSIFT remain CPU
OpenCV L2 paths. GPU use is selected only when a backend is both validated and useful.
**Status:** IMPLEMENTED
### Geometric Verification
Geometric Verifier v3 performs Fundamental USAC/MAGSAC verification with bounded restartable Task
execution. Scientific solver parallelism remains controlled by its contract while cross-parent
preparation can use bounded Governor-admitted participants.
**Status:** IMPLEMENTED / VALIDATED
### Track Model and Track Builder
Track Model v1 and Track Builder publish immutable Track Sets with deterministic identity and bounded
recovery semantics.
Retained real-data evidence includes:
```text
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
```
**Status:** PASS / FROZEN
### Sparse SfM ### Sparse SfM
Les primitives géométriques calibrées Gate C, le noyau incrémental synchrone
Gate D et le Bundle Adjustment final par composante Gate E sont implémentés.
Gate E traite en mémoire une copie du résultat Gate D immutable, sur CPU avec
un thread Ceres, et publie atomiquement chaque candidate acceptée. Il n'intègre
ni persistance, ni Task Runtime, ni Resource Governor.
**Statut :** GATES C/D/E — PASS / FROZEN Calibrated Sparse SfM capability is implemented through Gates C-G:
Gate F relie ces noyaux au Task Runtime durable, à la Queue/Governor/Reservation - Gate C: calibrated geometric primitives;
et à la publication Project DB v17 atomique et idempotente. - Gate D: incremental reconstruction core;
- Gate E: final per-component Bundle Adjustment;
- Gate F: durable Task orchestration and atomic Project DB publication;
- Gate G: Governor admission and resource integration.
**Statut :** GATE F — PASS / FROZEN. Gate G est **PASS / FROZEN**. Phase H v1 adds incremental enrichment from immutable predecessor snapshots without redefining Gate F
scientific identity.
La phase de pipeline H `incremental_reconstruction.run` v1 enrichit un snapshot **Status:** C-G PASS / FROZEN; PHASE H V1 PASS / FROZEN
publié et produit un nouveau snapshot complet avec provenance prédécesseur dans
Project DB v18. Elle reste scientifiquement distincte de Gate F et ne modifie
ni F0 ni les contrats Gate D/E.
**Statut :** PHASE H V1 — PASS / FROZEN. Implemented capability must remain distinct from execution on a particular real campaign. The
historical S21 and A6000 Engine Bay campaigns remain `CALIBRATION_UNAVAILABLE` for known-calibration
Sparse SfM. The retained A6000 proof intentionally stopped before Sparse SfM and Dense/MVS.
## Résultats et publication live ### MVS boundary
Les traitements fonctionnent par séquences adaptatives : lire un lot borné, MVS-M1 provides the validated bounded external OpenMVS boundary and deterministic COLMAP/PLY exchange
calculer, écrire un résultat atomique, libérer la mémoire, puis traiter le contracts.
lot suivant. La stabilité du système hôte et la réactivité de la TUI ont
priorité sur le débit maximal.
Le viewer consomme des snapshots de résultats validés et publiés Durable dense publication, full Dense/MVS orchestration, mesh refinement, texturing and export remain
atomiquement. Il ne lit jamais un fichier intermédiaire et ne partage pas future work.
directement les buffers de travail d'un worker. Une interruption doit laisser
le dernier snapshot validé exploitable et permettre la reprise à une
frontière de séquence connue.
## Invariants fondamentaux **Status:** PASS / FROZEN boundary
- Aucun callback de tâche n'est lancé sans réservation active validée. ### Task Runtime
- La Queue ne décide jamais des ressources.
- Le Resource Governor est l'unique propriétaire des budgets.
- Les réservations sont libérées exactement une fois.
- ncurses appartient exclusivement au thread principal.
- Les estimations de ressources sont immuables.
- Les buffers et files sont strictement bornés.
## Limites actuelles Tasks own lifecycle state, progress, cooperative pause/cancel behavior, sequence boundaries,
checkpoints and typed durable reconstruction.
- File à worker unique avec FIFO stable et bypass des seuls `WAIT` ressources. **Status:** IMPLEMENTED
- Absence de DAG de dépendances.
- Absence de priorités. ### Task Queue
- Absence de pools de workers multiples (CPU/GPU/IO).
- La TUI legacy ne sélectionne pas encore explicitement ses ScanSets. The Queue provides bounded FIFO dispatch with one active callback, stable scanning and resource-WAIT
- La réconciliation globale des assets/checkpoints orphelins n'est pas implémentée. bypass behavior. It does not own resource policy.
- La compaction des segments Visual Index n'est pas implémentée.
- Viewer et publication live non implémentés. **Status:** IMPLEMENTED
### Task Kind Registry
The production registry currently contains 16 Task kinds. Historical documents may legitimately
record smaller inventories at their checkpoint.
**Status:** IMPLEMENTED
### Hardware Profile and Resource Snapshot
Hardware Profile detects static host capability. Resource Snapshot captures current observable
capacity and pressure signals required for admission.
Unavailable optional telemetry remains unknown; it must not be fabricated.
**Status:** IMPLEMENTED
### Resource Governor
The Governor is the sole production authority for CPU, RAM, GPU, I/O admission and process-local
scratch leases where explicitly supported.
Pressure may throttle future admission. When pressure clears, useful resources must be eligible to
ramp back up; throttling is not a permanent lower ceiling.
UMA GPU allocations are charged exactly once against host RAM. Swap, zram and external scratch never
become admitted RAM.
**Status:** IMPLEMENTED / VALIDATED
### Optional external SSD controller
The UDisks2/GDBus controller owns the reviewed physical lifecycle for the exact
`LARDON_SWAP` / `LARDON_SCRATCH` device contract. It is not a second scheduler or Governor.
The Governor remains the sole production entry for scratch leases. The current 16 production Task
kinds have zero authoritative scratch consumption; visible storage capacity is not fabricated usage.
**Status:** CURRENT / VALIDATED OPERATIONAL
### TUI observatory and control center
ncurses input and rendering remain on the main thread. Runtime observation is bounded and coalesced.
The TUI exposes project state, Tasks, durable progress, resource state, optical configuration and SSD
control.
Validated layout boundaries are:
```text
full layout >= 100x30
reference compact 72x20
minimum supported 60x15
```
Below the minimum, only the bounded terminal-too-small fallback is rendered.
Opening, closing or switching a project is a Queue/DB lifetime boundary: destroy and join the sole
Queue first, including finished callbacks, then close Project DB, recreate one empty Queue and rebind
observers.
**Status:** CURRENT / VALIDATED OPERATIONAL
## Publication and restart model
Long-running processing follows bounded sequence boundaries:
```text
admit bounded work
-> prepare bounded items
-> join participants
-> publish deterministic atomic results
-> persist typed cursor/state
-> checkpoint generic Task progress
-> release transient buffers
-> sequence break / re-admission
```
The exact order varies by Task contract, but durable progress must never claim work that has not
reached its authoritative publication boundary.
A crash may leave generic Task progress behind an already durable immutable result where the
documented retry contract permits exact reuse. Recovery must converge from durable identity; it must
not guess identity from paths, timestamps, basenames or similar metadata.
## Viewer and live boundaries
Viewer, coverage analysis, live localization and capture guidance remain future product areas.
The architecture requirement is already clear: visual consumers use validated immutable snapshots.
They do not access mutable worker buffers, own scientific identity or become a second execution
runtime.
Device-specific acquisition transports such as A6000 HDMI/USB or S21/mobile integration belong at the
acquisition adapter boundary. Heavy reconstruction remains PC-side.
## Core invariants
- No Task callback executes without a valid active reservation.
- Queue/runtime do not own resource policy.
- The Resource Governor is the sole production resource authority.
- ncurses remains owned by the main thread.
- Scientific and operational identities remain distinct.
- Task estimates and installed sequence contracts remain immutable for their defined lifetime.
- Memory, buffers, queues, files, descriptors, processes, threads and internal participants are bounded.
- Per-item atomicity does not imply cross-item serialization.
- Owner-only publication does not imply serial preparation.
- Validated and useful GPU backends are preferred when eligible and Governor-safe.
- UMA GPU memory is charged exactly once against host RAM.
- Swap, zram and scratch do not enlarge RAM admission.
- Failure and cancellation clean operation-owned transient resources.
- Atomic scientific publication never exposes partial results as complete.
- FROZEN scientific decisions are not reopened by resource-policy evolution.
## Current limitations and future scope
The following remain outside the current implemented execution model:
- general dependency DAG scheduling;
- multiple active Task callbacks / inter-Task worker pools;
- general CPU/GPU/I/O worker-pool architecture;
- multi-GPU scheduling;
- global orphan-artifact reconciliation;
- Visual Index compaction / larger-index evolution;
- authoritative Task scratch consumers;
- durable dense / mesh publication;
- full Dense/MVS orchestration;
- mesh refinement, texturing and final export workflow;
- viewer;
- offline coverage analysis;
- suggested supplementary viewpoints;
- live camera localization;
- live coverage overlay;
- A6000/S21 live acquisition integration;
- capture guidance;
- video ingestion and deterministic keyframe extraction.
Deferring inter-Task parallelism does not authorize accidental serialization inside the one active
Task. Bounded internal concurrency remains the current mechanism for independent work under
`SERIALISM_REQUIRES_PROOF`.
## Current summary
```text
CURRENT_PROJECT_DB_SCHEMA=v25
CURRENT_PRODUCTION_TASK_KINDS=16
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
RAW_BATCH_PATH=raw.develop.batch/1
FEATURE_BATCH_PATH=features.extract.batch/1
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
SPARSE_SFM_CAPABILITY=C_G_PASS_FROZEN
REAL_A6000_SPARSE_SFM_EXECUTED=NO
REAL_A6000_DENSE_MVS_EXECUTED=NO
TUI=CURRENT_VALIDATED_OPERATIONAL
VIEWER=FUTURE
LIVE_CAPTURE_GUIDANCE=FUTURE
```
Detailed contracts remain owned by the specialized architecture documents. This overview summarizes
current repository state and must not replace those normative documents.

View file

@ -1,80 +1,86 @@
# Persistance et base de données Lardon3D # Lardon3D Persistence and Project Database
## Vision ## Current authority
Lardon3D stocke les métadonnées de reconstruction dans Project DB SQLite, The current Project DB schema is **v25**.
tandis que les données numériques massives restent dans des fichiers/binaires
adaptés. Le schéma courant est v23 ; les sections v7 ci-dessous documentent la
fondation historique sans prétendre être la tête de migration.
## Principes fondamentaux ```text
CURRENT_PROJECT_DB_SCHEMA=v25
### Séparation logique/binaire v22 selected scientific execution foundation PASS/FROZEN
- État logique, relations, index → base persistante légère v23 generic optical-context overlay IMPLEMENTED/VALIDATED/REVIEWED
- Données numériques massives → fichiers/artefacts binaires adaptés v24 raw.develop.batch/1 persistence IMPLEMENTED/VALIDATED
v25 features.extract.batch/1 persistence IMPLEMENTED/VALIDATED
### Cycle de publication
```
lot calculé
→ artefact temporaire
→ validation
→ publication atomique
→ transaction de métadonnées
→ état READY
``` ```
### Règle de reprise Project DB evolves additively. Older schema versions remain valid historical contracts when a
Une reprise ne considère jamais un artefact partiellement publié comme valide. section explicitly documents the state published by that version. They must not be rewritten as if
they had always contained later overlays.
## Concepts de domaine The detailed schema, migration ledger and identity contracts are owned by
[Project Database](project_database.md). This document owns the persistence model and publication,
checkpoint, recovery and artifact-boundary rules.
Les éléments suivants sont des concepts de domaine, PAS des tables SQL imposées : ## Persistence model
- project Lardon3D keeps queryable logical state in SQLite Project DB while large numerical payloads remain in
- scan_set bounded external files or artifacts designed for their format.
- image
- feature_set
- visual_signature
- candidate_pair
- verified_pair
- track
- observation
- camera
- camera_body_profile
- lens_profile
- optical_configuration
- optical_calibration_profile
- pose
- point3d
- reconstruction_layer
- measurement
- document_source
- geometric_constraint
- artifact
- checkpoint
## Invariants The split is intentional:
- Chaque publication est atomique - identity, lifecycle state, relations, durable Task payloads and publication metadata -> SQLite;
- Les artefacts partiels ne sont jamais considérés comme valides - large descriptors, match payloads, checkpoints and other numerical artifacts -> bounded files;
- La reprise commence à la dernière frontière connue - a file path is a storage locator after identity resolution, never scientific identity by itself.
## Checkpoint durable de tâche v1 Project DB does not serialize runtime-only objects such as mutexes, condition variables, callbacks,
userdata pointers, worker threads, Governor reservations or live execution contracts.
### État durable ## Core invariants
Le modèle durable versionné contient uniquement l'identifiant stable, le nom, - durable publication is explicit and ordered;
l'estimation immuable, l'état observé, l'état de reprise, la progression, le - partial artifacts are never accepted as complete scientific outputs;
message, les horodatages et le compteur de séquences. Il ne contient aucun gros - recovery begins from the last durable boundary the owning contract can prove;
artefact numérique. Une future version pourra référencer des identifiants - SQLite state and external files are not falsely described as one distributed transaction;
d'artefacts publiés et validés sans incorporer leur contenu. - immutable scientific identity is not inferred from path, basename, timestamp or operational IDs;
- large collections are read and written through bounded interfaces;
- retry behavior must converge only from identities already established by the owning contract;
- Project DB schema migration never silently creates new scientific meaning for historical rows.
Les mutex, conditions, callbacks, userdata, workers, gouverneur, réservations et ## Logical and binary publication
contrats d'exécution sont transitoires et ne sont jamais sérialisés.
### Normalisation après arrêt de processus The generic publication shape is:
| État observé | État restauré | ```text
bounded computation
-> operation-owned temporary output
-> format/content validation
-> atomic file publication where required
-> Project DB transaction
-> READY / published durable state
```
The exact ordering is owned by the subsystem. Some contracts publish an immutable file before the
SQLite transaction and therefore explicitly admit an orphan-file window. Others publish metadata and
files through a more specialized ordered protocol. No document may strengthen a subsystem guarantee
beyond the actual implementation.
Failure cleanup may remove only resources owned by the current operation. Shared immutable assets are
not deleted merely because a later metadata transaction fails.
## Durable Task checkpoint v1
### Durable state
The versioned Task checkpoint contains bounded logical Task state only. It includes the stable Task
identity, name, immutable estimate, observed state, recovery state, progress, message, timestamps and
sequence count. It does not embed large scientific artifacts.
Task kind and typed business payload are separate concerns. The generic checkpoint format v1 does not
become a miniature Project DB and does not replace typed Project DB persistence.
### Process-stop normalization
| Observed state | Restored state |
|---|---| |---|---|
| `TASK_PENDING` | `TASK_PENDING` | | `TASK_PENDING` | `TASK_PENDING` |
| `TASK_RUNNING` | `TASK_PENDING` | | `TASK_RUNNING` | `TASK_PENDING` |
@ -83,162 +89,355 @@ contrats d'exécution sont transitoires et ne sont jamais sérialisés.
| `TASK_FAILED` | `TASK_FAILED` | | `TASK_FAILED` | `TASK_FAILED` |
| `TASK_CANCELLED` | `TASK_CANCELLED` | | `TASK_CANCELLED` | `TASK_CANCELLED` |
Une rupture de séquence n'est pas un état : elle est observée comme A sequence break is not a persistent Task state. If execution stopped while a sequence was active,
`TASK_RUNNING`. Son `sequence_count` est durable, mais la reprise revient à recovery returns the Task to `TASK_PENDING`; its durable sequence count remains retained and a new
`TASK_PENDING` et exige une nouvelle admission. Governor admission is required before execution resumes.
### Stockage minimal ### Standalone checkpoint file
Le codec v1 est indépendant de la future Project Database. Le fichier est de The v1 codec is bounded and field-encoded with explicit magic/version/size/checksum semantics. It does
taille fixe et bornée, encodé champ par champ, avec magie, version, taille et not serialize native structs or native padding.
checksum de payload.
La publication écrit un fichier temporaire unique dans le même répertoire,
effectue `fsync`, renomme atomiquement puis synchronise le répertoire parent.
La lecture distingue absence, corruption, version inconnue et erreur d'I/O.
La sauvegarde distingue trois frontières : Standalone publication uses a unique temporary file in the same directory, synchronizes the file,
renames atomically and then synchronizes the parent directory.
- avant `rename`, toute erreur retourne `IO_ERROR`, supprime le temporaire et The result distinguishes three important boundaries:
laisse l'ancien checkpoint publié inchangé ;
- après un `rename` réussi, le nouveau checkpoint est publié et visible et
n'est jamais présenté comme rollbackable ;
- si le `fsync` du répertoire échoue après ce `rename`, le résultat est
`PUBLISHED_NOT_DURABLE` : le fichier visible est valide, mais sa présence sous
ce nom après un crash ou une coupure n'est pas garantie. `OK` garantit que le
contenu et l'entrée de répertoire ont tous deux été synchronisés avec succès,
sous réserve des garanties fournies par le système de fichiers et le stockage.
Les tailles persistantes sont refusées avant conversion lorsqu'elles dépassent - before successful rename: failure leaves the previously published checkpoint unchanged;
`SIZE_MAX`. Les secondes sont des entiers non signés v1 : les timestamps - after successful rename: the new visible checkpoint is published and is not presented as rolled back;
négatifs ne sont pas sérialisables et une valeur lue doit être représentable - parent-directory sync failure after rename: result is `PUBLISHED_NOT_DURABLE`; the visible file is
par le `time_t` local avant conversion. Le format reste donc lisible entre valid, but name persistence across crash/power loss is not guaranteed by Lardon3D.
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
`time_t`.
## Project Database v7 — fondation historique `OK` means both content and directory-entry synchronization completed, subject to the guarantees of the
filesystem and storage stack.
SQLite contient l'état logique interrogable et les références aux fichiers ; Persistent sizes are rejected before conversion if they exceed the local representable domain. The v1
les checkpoints et artefacts volumineux restent externes. L'enregistrement du timestamp representation likewise requires values representable by the local `time_t` before runtime
résumé de tâche et de sa référence checkpoint est une transaction unique. Un conversion.
artefact est d'abord publié et vérifié comme fichier régulier, puis seulement
marqué `READY` en DB. Le chemin inverse est interdit.
### Protocole checkpoint projet ## Project-owned checkpoint protocol
Le protocole réel n'est pas une transaction distribuée : The project checkpoint protocol coordinates a Task file and SQLite metadata without claiming a true
filesystem+SQLite transaction.
1. capture locale du snapshot sous le mutex de tâche puis déverrouillage ; The current project-level ordering is:
2. publication atomique du fichier sous
`.lardon3d/checkpoints/<task_id>.chk` ;
3. transaction SQLite sur `tasks` et `checkpoints` avec chemin relatif.
Une erreur avant publication ne modifie pas la DB. `PUBLISHED_NOT_DURABLE` est ```text
conservé comme tel en DB. Si la publication réussit puis que SQLite retourne capture bounded Task snapshot
`BUSY` ou une erreur, le fichier valide reste sur disque, la DB conserve son -> publish .chk.next
ancienne vérité et le nouveau fichier est un orphelin à réconcilier plus tard. -> record Task/checkpoint summary in SQLite
Il n'est pas supprimé et aucune atomicité FS+SQLite n'est revendiquée. -> promote .chk.next to canonical .chk under .chk.lock
```
L'inventaire distingue checkpoint récupérable durable, récupérable mais publié The canonical location is:
non durable, absent, invalide, version inconnue et erreur d'I/O. Aucune réparation
ou suppression silencieuse n'est effectuée.
Le format checkpoint reste en version 1 et ne contient pas de `task_kind`. Le ```text
schéma SQLite v4 conserve `task_kind` et `task_kind_version` dans le résumé .lardon3d/checkpoints/<task_id>.chk
logique interrogable. La migration v1→v2 laisse ces deux colonnes à `NULL` : une ```
tâche legacy reste inspectable mais ne peut pas être reconstruite ou resoumise.
Un kind inconnu ou une version non supportée est diagnostiqué sans exécuter de
code.
## Statut The advisory `.chk.lock` exists only as process synchronization; it is not recovery data.
**IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication Recovery obtains the lock, reloads the relevant Project DB record because it may have changed while
atomique et restauration sûre d'une tâche isolée. waiting, and then selects a codec/version-valid checkpoint whose stored summary matches the Project DB
summary exactly for the fields Project DB owns.
**IMPLEMENTED** — Project Database v7 pour identité, tâches typées, ScanSets, A valid canonical `.chk` has priority. A valid matching `.chk.next` may be promoted when the canonical
images logiques, Feature Sets/assets SHA-256, Visual Index segmenté, file does not match. Stale or corrupt `.next` files never override a valid canonical checkpoint.
checkpoints et artefacts génériques.
**IMPLEMENTED** — registry statique bornée et reconstruction explicite avec A missing, corrupt, future-version or summary-mismatched checkpoint makes that Task non-recoverable; it
ownership du userdata. does not make unrelated project state invalid.
**IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au ## Project DB foundation
redémarrage.
**IMPLEMENTED** — `import.images` persiste son chemin source absolu et son Project DB is SQLite with explicit schema versioning and sequential transactional migrations.
`scanset_id`, puis publie un checkpoint après chaque lot validé. Le catalogue
SQLite rend le rejeu idempotent à la granularité du contenu dans un ScanSet.
Le chemin source absolu est l'intention durable v1 : il doit rester accessible The retained configuration uses:
après redémarrage et un projet déplacé ne rend pas une source externe portable.
Une source absente ou devenue non-répertoire fait échouer proprement la
reconstruction. Après import terminé, l'image dépend de l'asset géré, plus de la
source. Le SHA-256 est calculé pendant la copie avec un tampon fixe de 64 Kio.
L'asset est publié sans écrasement sous
`assets/images/<prefix>/<sha256>`, puis seulement enregistré `READY` dans une
transaction SQLite. Un asset concurrent déjà présent n'est adopté qu'après
rehash complet et vérification de taille. Si SQLite échoue après publication,
le fichier reste orphelin pour une future réconciliation ; aucune transaction
FS+SQLite n'est revendiquée.
Les identités publiées `scanset_id`, `image_id` et `asset_id` utilisent les ```text
séquences SQLite `AUTOINCREMENT` : une valeur issue d'une transaction validée foreign_keys=ON
n'est jamais réattribuée à un autre objet, même après suppression de la ligne. journal_mode=DELETE
Une valeur réservée par une transaction annulée n'est pas une identité publiée. synchronous=FULL
busy_timeout=5000
```
`manifest.tsv` reste supporté par l'ancien chemin d'import/catalogue. Le chemin `DELETE` journal mode matches the current single-owner Project DB model and avoids persistent WAL/SHM
persistant entretient une projection best-effort par hardlinks pour la TUI, files. The timeout bounds waiting on an external lock.
mais SQLite est le commit logique canonique : la correction de la reprise ne
dépend plus de l'ordre de `readdir()` ni du nom de fichier.
La migration v3 vers v4 ne transforme pas les lignes du manifeste historique The project identity is duplicated intentionally across `project.ini` and the `project` table and must
en images cataloguées : elles ne contiennent pas toutes les preuves exigées par match. A divergence is an error, not an opportunity to invent a new identity.
le modèle v4. Le marqueur durable `legacy_image_catalog_pending` rend cette
situation visible. Une tâche récupérable peut repeupler le catalogue par rejeu
si sa source existe encore ; sinon les fichiers et le manifeste restent une
projection legacy, explicitement non cataloguée. Une tâche v3 déjà terminée
n'est pas rejouée automatiquement.
**IMPLEMENTED** — reprise automatique sélective à l'ouverture : pagination Published catalog and scientific IDs use SQLite integer identities according to their owning schema.
bornée, validation checkpoint/kind, reconstruction production et enqueue sans Where `AUTOINCREMENT` is part of the contract, a committed published identity is not later reused for a
claim persistant supplémentaire. different object. An ID allocated only inside a rolled-back transaction is not a published identity.
Les records sont parcourus par task ID croissant. Un checkpoint ## Historical Project DB v7 foundation
`PUBLISHED_NOT_DURABLE` présent, valide et cohérent peut être repris ; le résumé
conserve cet avertissement jusqu'au prochain checkpoint durable. Une tâche
terminale n'appartient pas à la requête de reprise.
**NOT_YET_WIRED** — réconciliation des fichiers orphelins et retry piloté par Project DB v7 is the historical persistent project/runtime foundation. It covers the durable project
l'utilisateur pour les sources indisponibles. Les checkpoints existants sont identity, Tasks, checkpoint references, generic artifacts, ScanSets, logical images, image assets,
kind-owned aux frontières métier ; aucun timer générique ne peut les remplacer. Feature Store metadata and the segmented Visual Index foundation.
**NOT_YET_WIRED** — migration de la vue TUI en mémoire vers la pagination Important v7-era persistence rules remain current unless a later contract explicitly supersedes them:
SQLite, scrub des assets et réconciliation globale des orphelins.
**IMPLEMENTED** — Feature Store externe versionné, immutable, borné et relié - Task summary plus checkpoint reference is a single SQLite transaction;
transactionnellement à ses métadonnées SQLite après publication. - large checkpoint and scientific files remain external;
- an artifact file is published and validated before Project DB marks it `READY`;
- Project DB stores bounded metadata and references, not large descriptor/posting payloads;
- `AUTOINCREMENT` is used where published catalog/scientific IDs must not be recycled after committed
deletion;
- the persistent import source path records durable execution intent, while imported scientific data
ultimately depends on the managed immutable asset rather than the original external source path.
**IMPLEMENTED** — Visual Index externe segmenté, memberships transactionnels ### Generic artifact orphan window
et tâche `visual_index.update` récupérable.
**IMPLEMENTED** — migrations additives et séquentielles jusqu'à Project DB For generic file-first publication, a successful file publication followed by SQLite `BUSY` or another
v23. Les versions v16 à v22 restent l'histoire scientifique et de persistance SQLite failure leaves a valid orphan file on disk while Project DB retains its previous truth.
gelée ; v23 ajoute uniquement l'overlay optique générique.
Les neuf relations v23 séparent profils de boîtier et alias, profils d'objectif That file is not silently deleted. Global orphan reconciliation is a separate maintenance capability.
et alias, configurations optiques, affectations de configuration aux groupes The absence of such reconciliation does not justify pretending the file and SQLite update were atomic.
de campagne et aux Captures, profils de calibration et sélection explicite par
Capture. Une configuration référence exactement un boîtier et un objectif ; un
objectif manuel sans EXIF est normal. La compatibilité d'une
calibration est exacte sur la configuration optique et ses dimensions/champs
scientifiques. Aucun profil S21, A6000 ou Meike n'est inséré ou déduit par la
migration : les tables nouvelles restent vides tant qu'un caller ne fournit
pas explicitement les données.
La migration v22→v23 est une transaction additive. Elle ne réinterprète ni les ## Import and managed source assets
Captures, ni les Images, ni les résultats scientifiques historiques. Une copie
S21 et une copie A6000 ont atteint v23 avec intégrité et clés étrangères
valides, comptes scientifiques inchangés et tables optiques vides. Les détails
normatifs sont dans [Project Database](project_database.md).
**NOT_YET_WIRED** — reprise ordonnée par dépendances/DAG et réconciliation `import.images` persists its source path and ScanSet identity and checkpoints after validated bounded
globale des artefacts orphelins. work. Managed image assets are content-addressed by SHA-256 after a complete bounded copy/hash pass.
A concurrently existing asset is adopted only after the implementation verifies the expected content
and size according to the owning contract. SQLite publication follows file publication.
`manifest.tsv` remains a historical/legacy projection. SQLite is the canonical logical commit for the
persistent catalog. The `legacy_image_catalog_pending` marker means legacy data may remain outside the
current catalog model; it does not claim that historical manifest rows were silently converted into
fully proven catalog identities.
## Selective Task recovery
Project open performs bounded selective recovery using Project DB pages and the Task Kind Registry.
A recoverable Task must have:
- a valid Project DB Task record;
- a supported Task kind and version;
- a valid coherent checkpoint boundary required by that kind;
- reconstructable typed business persistence where the kind requires it.
The recovery scan copies records outside the SQLite mutex before business reconstruction and enqueue.
A full Queue window stops the scan without mutating the unvisited records.
Project-level schema, migration and project-identity errors are fatal to opening the project. Per-Task
legacy, unknown-kind, unsupported-version, missing-checkpoint, invalid-checkpoint, unavailable-source or
reconstruction errors are isolated to the affected Task where the owning contract permits.
## External scientific artifacts
### Feature Store
Feature files are immutable external artifacts with versioned bounded readers. Project DB stores their
identity and publication metadata only after the external file is valid according to the Feature Store
contract.
Feature descriptors are not duplicated into SQLite.
### Visual Index
Visual Index postings remain in bounded immutable external segments. Project DB stores index identity,
segment metadata and memberships. Durable update Tasks retain the bounded cursor required to resume
publication without rebuilding already accepted segments.
### Match and later scientific payloads
Match payloads and other large scientific representations follow the same architectural principle:
SQLite owns durable identity, relations and bounded metadata; specialized external formats own large
numerical payloads where the subsystem contract requires them.
## Project DB v16-v22 retained scientific foundation
The scientific and persistence contracts introduced through v16-v22 remain historical PASS/FROZEN
foundations. Later schema versions are additive overlays and do not reinterpret those rows.
In particular:
- v16 publishes the immutable Sparse SfM persistence model;
- v17 adds the durable typed Sparse SfM Task payload for Gate F;
- v18 adds Phase H v1 incremental-reconstruction identity/persistence;
- v19 adds Capture / Asset Provenance v1;
- v20 adds durable acquisition-campaign Task persistence;
- v21 adds Photo Quality Triage persistence;
- v22 adds selected scientific execution, explicit Capture SOURCE-asset mapping and durable selected
RAW development persistence.
Detailed tables, identities and migration invariants are defined in
[Project Database](project_database.md).
## Project DB v23 - generic optical-context overlay
Project DB v23 is an additive optical-context overlay above the v22 scientific foundation.
It separates four identities:
- camera body profile;
- lens profile;
- optical configuration;
- optical calibration profile.
The migration creates the optical relations empty. It does not inspect EXIF, path, basename, SHA-256,
dimensions, device name or historical calibration to backfill identity.
Electronic metadata aliases, when present, use exact stored identity semantics. A manual lens without
EXIF is a normal explicit profile and does not require a fabricated metadata alias.
Calibration selection is explicit and requires exact compatible optical configuration. No silent
interpolation, substitution or inferred calibration identity is introduced by persistence.
Historical S21/A6000 copies migrated through the optical overlay without changing their existing
scientific rows; empty optical tables remain an honest state until explicit data is supplied.
## Project DB v24 - RAW batch persistence
Project DB v24 adds only the typed durable relation required by `raw.develop.batch/1`:
```text
raw_development_batch_tasks(task_id, selected_execution_id)
```
The migration is additive and DDL-only. It does not create Capture, Asset, Image or selected-execution
identity and does not rewrite historical `raw.develop/1` Tasks.
The selected execution remains the durable scientific ordering authority. The batch Task may prepare
independent RAW items concurrently under Governor admission, but after all participants join, the owner
publishes selected representations in deterministic selected-item order.
The durable ordering remains:
```text
prepare bounded independent RAW items
-> join participants
-> owner publishes exact selected item
-> selected item/cursor commit
-> generic Task progress/checkpoint
```
A crash may therefore leave generic Task progress behind already durable selected scientific state; it
must never move generic progress ahead of unpublished selected state. Recovery resumes from durable
selected-execution identity/cursor and exact already-published representations rather than guessing
from paths or files.
The retained real A6000 run completed this RAW-batch path for all 689 selected RAW representations.
## Project DB v25 - Feature batch persistence
Project DB v25 is the current schema head. It adds only the typed durable relation required by
`features.extract.batch/1` through `feature_extract_batch_tasks`.
The v25 row binds one durable Feature-batch Task to:
- one immutable selected execution;
- the monotone selected-item prefix/cursor;
- the exact ORB extractor kind/version/parameters/fingerprint domain required by the Task contract.
The migration is additive and DDL-only. It creates no Feature Set, converts no historical
`features.extract/1` Task and infers no Image or Feature Set identity.
Selected images may be prepared concurrently without SQLite access. After participants join, the owner
publishes Feature Sets in selected order and advances the durable Feature cursor only when the exact
READY Feature Set is already durable.
A crash may leave generic Task checkpoint/progress or the Feature cursor behind an immutable Feature
Set that was already published. Recovery must revalidate and reuse that exact result; it must not infer
a replacement identity.
The retained real A6000 proof completed the v25 path with 689 Feature Sets and then continued through
Visual Index, 38,420 Candidate Pairs, 38,420 Match Results, Geometric Verification and Tracks without
replaying acquisition, RAW or Feature work.
This persistence result does **not** imply that real Sparse SfM or Dense/MVS was executed. Those counts
remain zero in `REAL_A6000_PRE_SFM=PASS/FROZEN`.
## Capture / Asset / Image identity boundary
Persistence must preserve these distinctions:
```text
Capture != file
Capture != Asset
Capture != image_id
Capture != SHA-256
Capture != path
Capture != basename
Capture != Task ID
Capture != campaign group ID
```
`asset_id` identifies a managed immutable asset record. SHA-256 identifies immutable bytes according to
the asset contract. `image_id` identifies a scientific image representation. `capture_id` identifies a
physical acquisition representation in Project DB. Task and campaign-group IDs remain operational
identities.
Retry and recovery may use an explicit persisted mapping between these domains only where a canonical
contract defines it. They may not reconstruct missing identity from coincidental equality or metadata.
## Acquisition-campaign crash/restart ordering
For durable campaign execution, the important persistence boundary is conceptually:
```text
S3-E returns capture_id
-> persist group_id -> capture_id mapping and campaign cursor
-> advance generic Task progress/checkpoint
-> next group may execute
```
The retained pre-return S3-E crash window remains intentional. If a Capture is created internally but
the process dies before the caller receives and durably retains its `capture_id`, campaign persistence
does not guess that identity from path, digest, basename, timestamp, metadata or `image_id`.
## Concurrency and ownership
A Project DB connection is serialized by its internal mutex. Each public compound operation owns its
entire transaction; a public transaction is not left open across calls.
External artifact I/O is not performed while holding the Project DB mutex where the owning contract
separates those operations. SQLite-owned strings and records are copied into caller-owned bounded
storage before returning.
Closing Project DB concurrently with an active Project DB call is forbidden by owner lifetime rules.
Application project/session teardown must first destroy and join the Queue so Task callbacks and leases
are finished, then close Project DB.
## Schema migration discipline
Project DB migrations are sequential, transactional and additive unless an explicit future human ticket
authorizes a different migration.
Current known sequence:
```text
v1 -> ... -> v22
v22 -> v23 generic optical-context overlay
v23 -> v24 RAW batch Task persistence
v24 -> v25 Feature batch Task persistence
```
A migration failure rolls back both its new schema objects and its schema-version publication marker.
A retry therefore starts from the previous complete known schema.
Future schema versions beyond v25 are rejected by the current implementation and require explicit human
authorization before code or documentation may treat them as current.
## Current status
```text
PERSISTENCE_DOCUMENT=CURRENT
CURRENT_PROJECT_DB_SCHEMA=v25
TASK_CHECKPOINT_V1 IMPLEMENTED
PROJECT_CHECKPOINT_PROTOCOL IMPLEMENTED/VALIDATED
PROJECT_DB_V7_FOUNDATION IMPLEMENTED
PROJECT_DB_V16_TO_V22_FOUNDATION PASS/FROZEN
PROJECT_DB_V23_OPTICAL_OVERLAY IMPLEMENTED/VALIDATED/REVIEWED
PROJECT_DB_V24_RAW_BATCH IMPLEMENTED/VALIDATED
PROJECT_DB_V25_FEATURE_BATCH IMPLEMENTED/VALIDATED
REAL_A6000_PRE_SFM PASS/FROZEN
```
Current intentionally unfinished persistence-adjacent work includes global orphan-file reconciliation,
asset scrub/reconciliation and a general dependency/DAG recovery model. Those are separate future
capabilities; they do not change the current v25 schema authority.

File diff suppressed because it is too large Load diff

View file

@ -1,158 +1,498 @@
# Pipeline Feature + Matcher sensible aux ressources # Lardon3D Resource-Aware Pipeline
## Contrat portable ## Status
Une unité lourde ne démarre qu'avec une réservation active. Elle termine son This document describes the current production resource model for the selected pre-SfM pipeline and
petit travail courant sans être tuée sur une mesure instantanée, publie le its resource-sensitive execution paths.
résultat atomiquement, checkpoint, libère ses buffers, puis repasse par le
Governor avant la séquence suivante. Les files restent bornées et le swap n'est
jamais ajouté au budget de travail.
Le mode normal est interactif : il réserve de la RAM et des threads logiques au ```text
desktop. Les signaux d'admission combinent `MemAvailable`, charge CPU, PSI CPU, CURRENT_PROJECT_DB_SCHEMA=v25
PSI mémoire, PSI I/O et deltas `pswpin`/`pswpout`. Un seuil dépassé empêche une CURRENT_PRODUCTION_TASK_KINDS=16
nouvelle admission ; il ne rompt pas une réservation saine déjà active. RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Le Governor maintient trois zones. GREEN emploie le lot adapté normal. La zone The scientific contracts of Feature Store, Candidate Pair, Matcher, Geometric Verification and Track
de prudence RAM entre 3 et 4 Gio, un PSI au seuil ou un premier intervalle avec Builder remain owned by their specialized documents. This document owns the operational view:
swap actif produit YELLOW et interdit toute croissance. Deux observations de admission, bounded work, internal concurrency, publication boundaries, pressure response and
pression consécutives, ou `MemAvailable` sous la réserve dure de 3 Gio, reference-host measurements.
produisent RED et suspendent toute admission. Le premier snapshot swap établit
seulement la baseline.
La récupération possède deux phases distinctes : trois observations saines ## Canonical execution rule
font `RED → YELLOW`, puis trois nouvelles observations saines font
`YELLOW → GREEN`. Après RED, le plafond de lot reste 1. En GREEN, trois
observations saines sont nécessaires à chaque palier `1 → 2 → 4 → 8`. Une
nouvelle pression réinitialise cette progression. Cette mémoire est
process-local, bornée et protégée par le mutex du Governor.
Gate G gèle le rafraîchissement initial : lorsqu'il existe du travail PENDING A heavy production unit starts only while an active Resource Governor reservation authorizes it.
en `WAIT` de ressources, le worker unique de la Task Queue dort au plus 500 ms
avant de rescanner la file et de recapturer les ressources. Un signal explicite
le réveille plus tôt. Cette cadence ne remplace pas les 50 ms existantes d'une
tâche déjà active qui attend sa réadmission à une frontière de séquence.
**Gate G — PASS / FROZEN.** Cette réévaluation bornée, la The normal sequence is:
fraîcheur des snapshots et l'identité GPU sélectionnée sont raccordées aux
chemins de production existants et leur validation finale est terminée.
Les snapshots emploient `CLOCK_MONOTONIC` et leur âge maximal est 1000 ms. Une ```text
capture complète impossible est une erreur opérationnelle, tandis qu'une PSI immutable Task payload
ou télémétrie swap optionnelle absente reste inconnue. Compute Governor v2 -> Governor admission
observe maintenant le RSS/HWM courant dans un buffer borné, uniquement comme -> bounded preparation / computation
diagnostic du processus : il ne le confond ni avec la réservation Task ni avec -> deterministic owner publication
un coût attribuable. Le modèle cible un hôte Linux natif non contraint ; cgroups, -> durable cursor / checkpoint
limites systemd/RLIMIT, multi-GPU, historique/monitoring RSS long terme, -> release sequence-local buffers and reservation
redimensionnement d'admission depuis le RSS et consommation Task du scratch -> next admission
restent différés. Le contrôleur SSD optionnel gère le cycle de vie physique ; ```
son état est enregistré auprès du Governor, qui est l'unique orchestrateur des
leases scratch de production. Aucun des quatorze kinds actuels ne les consomme, The Queue keeps one active Task callback. That does not require a Task callback itself to perform all
et ni le registre, ni les leases, ni le swap ne créent un budget RAM. independent work serially. Where a Task owns multiple independent units, bounded internal participants
may prepare those units concurrently when the scientific and persistence contracts permit it.
Canonical policy:
```text
MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF
```
Per-item atomicity does not imply cross-item serialization. Owner-only or ordered durable publication
does not imply serial preparation.
A long-running CPU1 or batch1 path is acceptable only when a concrete dependency, measured scaling
knee, RAM limit, I/O limit, validated GPU path or another documented operational constraint proves
that more concurrency would not be safely useful.
## Host reserve and pressure model
Lardon3D preserves the interactive host reserve first, then gives the active workload the safe and
useful remaining capacity.
On the current validation host, the normal observed outcome is approximately:
```text
16 logical CPUs total
4 logical CPUs reserved for interactive host use
12 logical CPUs available to the compute pool
~3 GiB MemAvailable preserved as the hard RAM reserve
Radeon 780M UMA available to validated and useful GPU backends
```
These values are evidence for the current host, not portable product constants.
The Governor may use `MemAvailable`, CPU pressure, memory PSI, I/O PSI, active `pswpin` / `pswpout`
deltas, selected-GPU state, and Task-declared fixed, per-participant and transient costs.
Swap, zram and external scratch never enlarge admitted RAM. UMA GPU allocations are charged exactly
once against host RAM.
Pressure may reduce admission. When pressure clears, safe and useful capacity must be re-admitted
rather than leaving the process permanently throttled.
## Queue and sequence boundaries
The Task Queue remains bounded and has one active callback.
When pending work exists but all candidates receive `WAIT`, the Queue worker performs the validated
bounded wait and retries normal admission with a fresh snapshot. A running sequential Task may also
cross `sequence_break`, which releases the current reservation and requires fresh admission for the
next sequence.
The current architecture separates:
```text
cross-Task dispatch -> one active Queue callback
inside-Task independent work -> bounded participants when justified
durable publication -> owner-only where required
```
No second scheduler, uncontrolled worker pool, detached-thread system or parallel persistence
subsystem is introduced.
## Current Task inventory
The production registry contains 16 Task kinds.
The two additive kinds beyond the historical fourteen-kind maintenance inventory are:
```text
raw.develop.batch/1
features.extract.batch/1
```
Historical documents that correctly recorded fourteen kinds at their checkpoint remain historical
evidence. Fourteen is not the current production count.
## Feature Extraction ## Feature Extraction
ORB est déjà une tâche durable par image : source validée, extraction, ### Scientific atomicity
publication Feature Store, métadonnées DB, checkpoint terminal et libération du
buffer. Le batch vaut donc une image et la granularité de reprise est une image.
Le worker unique et la file bornée fournissent la backpressure actuelle.
Le démarrage configure une baseline OpenCV issue du compute-pool réellement A Feature Set remains an immutable per-image scientific result.
disponible avant la création de Queue. L'unique callback lourd applique ensuite
temporairement le compte CPU immuable admis pour sa séquence, dans The historical production path remains valid:
`1..compute-pool`, et restaure la baseline sur toute
sortie, y compris après une mutation suivie d'un échec de vérification. La tâche ```text
réserve donc le nombre réellement appliqué au lieu d'annoncer artificiellement features.extract/1
un thread pendant qu'une primitive interne en utilise davantage. Une mutation one image
process-wide concurrente par plusieurs workers n'est pas supportée ; Queue -> feature preparation
conserve un seul callback actif. -> Feature File publication
-> Project DB metadata
-> terminal Task checkpoint
```
That path remains important for legacy Tasks and exact restart compatibility.
### Current selected-execution batch path
Project DB v25 adds the current operational path:
```text
features.extract.batch/1
```
The durable owner is bound to one immutable selected execution, the exact ORB domain and a monotone
`next_item_index`. The selected item order remains authoritative.
The batch path is:
```text
one admitted owner Task
-> bounded independent selected-image participants
-> each participant prepares one per-image Feature result without SQLite access
-> join all admitted participants
-> owner validates / reuses the exact READY Feature Set
-> owner publishes in selected-item order
-> owner advances the durable Feature cursor
-> generic Task checkpoint follows
```
A crash may leave the generic checkpoint or Feature-batch cursor behind an already immutable Feature
Set. Restart revalidates and reuses that exact result; it does not infer a new identity.
Per-image Feature atomicity is therefore preserved while cross-image preparation may be concurrent.
### Feature CPU control
OpenCV thread count is process-wide state, so it must remain controlled.
The runtime establishes the validated baseline from the available compute pool before Queue
execution. The active heavy callback temporarily applies the admitted count required by its current
contract and restores the baseline on every exit path.
The Queue still has one active callback, so unrelated Tasks do not race process-wide OpenCV thread
configuration.
For `features.extract.batch/1`, cross-image participants are the concurrency mechanism. Additional
internal OpenCV threads inside every participant are not multiplied blindly.
The current coupled Feature-batch admission exists because increasing CPU while the admitted item
window remains one cannot exercise additional independent images. CPU and batch may therefore move
together for this Task where the validated rung contract requires it.
This is an explicit operational exception, not a universal Governor rule.
## RAW selected-execution batch
Project DB v24 provides `raw.develop.batch/1`.
Its pattern is analogous at the execution boundary:
```text
one admitted owner Task
-> bounded independent RAW participants
-> join
-> deterministic owner-only publication by selected_item_index
-> durable selected-execution cursor advancement
```
Per-Capture RAW publication remains atomic. Cross-Capture preparation need not be serial.
The retained real A6000 proof completed all 689 selected RAW representations through this path.
## Visual Index
Visual Index remains a bounded CPU path.
Its scientific identity and segment publication are unchanged. Internal parallelism may be used only
within its validated contract; no GPU path is promoted merely to make the accelerator busy.
The current GPU audit rejected Visual Index as a useful production GPU candidate. That rejection is a
measured backend decision, not permission to leave useful CPU capacity idle.
## Candidate Pair generation
Candidate Pair generation processes bounded Visual Index input and publishes deterministic canonical
pairs.
Current resource behavior distinguishes per-pair scientific identity from cross-item execution.
The validated Candidate path may use coupled CPU/batch rungs because additional CPU cannot exercise
additional independent source work while the admitted item window remains one.
The current operational model therefore permits:
```text
bounded source/item window
+ bounded CPU participants
-> deterministic Candidate preparation
-> owner publication
```
The exact current memory model and batch limits are owned by [`candidate_pair.md`](candidate_pair.md).
A historical CPU1/batch1 descriptor or older window estimate is not a permanent product ceiling.
## Matcher ## Matcher
`matcher.run` v1 est une tâche durable. Son unité atomique est une Candidate `matcher.run` v1 is durable. Its scientific atomic unit is one Candidate Pair.
Pair et son lot vaut 1, 2, 4 ou 8 paires. La tâche page la DB par
`candidate_pair_id`, sans supposer des IDs continus, et ne conserve jamais la
liste entière. Chaque paire publie immédiatement son Match Result avant que le
curseur ne soit avancé en mémoire.
Project DB v10 porte le Match Result publié. La migration transactionnelle The normal Task pages Project DB by `candidate_pair_id`; it does not assume contiguous IDs and does
v10→v11 ajoute uniquement `matcher_tasks`, qui porte la configuration et ce not retain the whole Candidate Pair set in memory.
curseur durable.
Après chaque lot, la tâche persiste le curseur, checkpoint, puis appelle The durable Matcher sequence is:
`lardon3d_task_sequence_break()`. Pause et annulation sont vérifiées avant
chaque paire et entre les lots. Un crash après publication mais avant le ```text
checkpoint revoit la paire : le Matcher réutilise alors le Match Result et ne bounded Candidate Pair page
recalcule pas les descripteurs. -> match one or more admitted pairs
-> publish each exact Match Result
-> advance durable cursor
-> checkpoint
-> sequence_break
```
The current adaptive batch rungs are:
```text
1 -> 2 -> 4 -> 8
```
A published Match Result may precede its generic checkpoint after a crash. Restart finds and reuses
the exact durable Match Result rather than recomputing or inventing identity.
### ORB Vulkan
The validated ORB Matcher Vulkan backend is GPU-first when eligible.
The Radeon 780M is UMA, so backend memory is host RAM and is charged once.
The current production Vulkan contract retains:
```text
normal useful inflight depth = 1
validated private safety capacity = 2
helpers = 0
```
Depth 2 was measured and rejected as the normal useful setting because its improvement remained below
the accepted deadband. It remains a validated private capacity, not a production default.
The backend preserves canonical Match Result identity and exact CPU fallback. An ineligible pair or
backend failure is recomputed completely on CPU; no partial GPU result is published.
SIFT and RootSIFT remain CPU OpenCV L2 paths. Their Vulkan feasibility work did not reach production
eligibility and therefore receives no authoritative production GPU reservation.
## Geometric Verification ## Geometric Verification
Project DB v12 stocke un résultat borné à 1024 octets de masque et neuf The current production verifier lineage is Geometric Verifier v3.
binary64. `geometric_verifier.run` v1 traite chaque Match Result comme unité
scientifique atomique, publie par une transaction courte puis checkpoint son
curseur par lots 1..16 avant
`task_sequence_break()`. Sa ligne durable appartient à Project DB v13. Le job
peut employer jusqu'à huit participants utiles et seize participants sûrs, par
lots au plus seize, avec 8 Mio par parent et sans slot GPU. Le propriétaire
publie le préfixe canonique dans l'ordre. Admission, pression, lots et
slow-start restent exclusivement décidés par Runtime et Governor ; l'USAC
scientifique interne conserve `isParallel=false`.
## GPU et files One Match Result remains the scientific atomic input.
La Radeon 780M est UMA : toute mémoire GPU compte aussi comme pression RAM. The bounded execution shape is:
Vulkan 1.4.354 énumère la 780M RADV et une file compute dédiée. Le backend ORB
top-2 de production possède un contexte lazy réutilisable et jusqu'à deux jobs
privés en vol sur cette file, sans helper hôte. Chaque slot mappé vaut 640 Kio.
Le backend part de zéro et retient exactement un slot après une initialisation
ou séquence AUTO normale depth 1. Il n'alloue le second que sous un contrat
privé de sûreté/benchmark depth 2 déjà admis, puis le libère avant de franchir
la prochaine admission depth 1. Les
publications restent strictement ordonnées et le fallback CPU reste exact. Le
CPU reste le fallback portable si Vulkan est absent, incompatible ou désactivé
pour la session.
La feasibility SIFT/RootSIFT a borné son prototype Vulkan à 8,125 Mio de ```text
payload lazy, mais n'a pas franchi la Gate de production. Le Governor ne réserve one admitted owner Task
donc aucun slot ni budget GPU pour SIFT/RootSIFT ; leur estimation CPU publiée -> bounded independent GVR preparation
reste inchangée. -> join
-> owner publishes the canonical prefix in order
-> durable cursor / checkpoint
```
## Profil interactif 8845HS mesuré The validated path may use up to eight useful participants and sixteen safe participants, with
admitted windows up to sixteen Match Results.
- budget CPU Lardon3D observé : 12 threads logiques, 4 réservés au desktop ; The internal USAC/MAGSAC scientific solver keeps its validated `isParallel=false` behavior. Cross-item
- réserve dure `MemAvailable` : 3 Gio ; parallelism belongs outside that per-item solver and does not modify scientific thresholds or identity.
- zone de prudence `MemAvailable` : de 3 à 4 Gio ;
- Feature workers : 1 ; batch : 1 image ;
- Matcher workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Candidate Pairs ;
- Geometric Verifier : un callback propriétaire, jusqu'à 8 participants utiles,
lots adaptatifs jusqu'à 16 Match Results ;
- profondeur de la Task Queue : 64 tâches légères, un seul callback actif ;
- PSI CPU avg10 : nouvelle admission suspendue à 20 % ;
- PSI mémoire avg10 : nouvelle admission suspendue à 1 % ;
- PSI I/O avg10 : seuil existant 80 %.
Le benchmark Matcher 8192 mesure environ 70 ms ORB et 135 ms SIFT à 12 threads, The retained real A6000 continuation completed:
contre 68 ms et 127 ms à 16 threads : le profil interactif abandonne environ
37 % de latence isolée pour réserver quatre threads logiques au desktop.
Le run soutenu Geometric Verifier traverse environ 2001 parents réutilisés en ```text
5,870 s via Task, DB, checkpoints et Governor. Le processus de test culmine à Match Results 38,420
25 964 Kio RSS ; `MemAvailable` reste au-dessus de 10,69 Gio et les compteurs Applicable GVRs 37,805
swap restent nuls. PSI avg10 final vaut 0,34 % CPU et 0 % mémoire/I/O. Cette Verified GVRs 10,952
mesure valide le chemin resource-aware et la reprise ; elle ne prétend pas être Rejected GVRs 26,853
une distribution de latence estimator-only. ```
## Limites with deterministic restart and no duplicate mappings.
Les pools multi-workers restent hors périmètre. La capacité CPU portable est ## Track Builder
désormais bornée par le compute-pool de l'hôte et la limite intrinsèque du kind,
jamais par un plafond global 12.
SIFT/RootSIFT et Feature Extraction Vulkan restent hors de ce contrat.
Swap, zram et disque externe ne sont jamais ajoutés au budget RAM. Aucun chemin
scratch/spill Task n'appartient à Gate G core. La commande SSD optionnelle est
une intégration opérationnelle additive, pas une admission scientifique.
La validation B3 du modèle Sparse SfM v16 a utilisé des processus frais, un Track Builder consumes an immutable GVR scope.
fixture synthétique de 100 000 landmarks et 500 000 observations, cinq passes
de paging sur 50 000 landmarks, et n'a observé ni OOM, ni swap storm, ni dérive Its compact current memory model supersedes the historical rejected S21 envelope that attempted to
RSS applicative. Cette preuve concerne la persistance bornée, pas le solveur. reserve approximately 18.204 GiB.
The retained real S21 proof completed:
```text
Tracks 912,447
Track observations 2,495,768
```
The retained real A6000 proof completed:
```text
Tracks 130,714
Track observations 318,944
```
Both proofs preserve deterministic restart semantics and no authoritative scratch consumption.
Track Builder resource details remain owned by [`track_builder.md`](track_builder.md).
## GPU policy
GPU use is capability- and evidence-driven.
Canonical rule:
```text
validated AND useful backend -> preferred when eligible and Governor-safe
unvalidated backend -> never promoted for utilization appearance
measured non-useful backend -> may remain CPU
```
Current examples:
```text
ORB Matcher Vulkan validated and preferred
SIFT / RootSIFT Matcher CPU
Candidate CPU
Feature CPU
Visual Index CPU
Geometric Verification CPU
```
This inventory may evolve only through measured, validated backend work.
## External SSD and scratch
The optional external SSD controller is a physical-lifecycle boundary using the reviewed
UDisks2/GDBus contract.
Its state is registered with the Resource Governor. The Governor is the sole production orchestrator
for scratch leases.
At the current checkpoint:
```text
16 production Task kinds
0 authoritative scratch-consuming Task kinds
```
Scratch availability is therefore a capability, not fabricated usage.
A future Task may use scratch only after an explicit Task-specific contract defines eligibility,
lease lifetime, path ownership, cleanup, cancellation, failure handling, capacity accounting and
restart behavior.
Scratch and swap never become RAM.
## Reference-host measurements
The following measurements are evidence, not portable constants.
### CPU reserve
On the Ryzen 7 8845HS reference host:
```text
16 logical CPUs total
~4 logical CPUs reserved for interactive use
~12 logical CPUs available to Lardon3D compute
```
Historical isolated Matcher measurements observed only a small additional gain from 12 to 16 logical
threads. That observation supports the reference-host reserve; it does not create a global 12-thread
product ceiling.
### Geometric Verifier historical resource-aware run
A retained resource-aware GV run traversed approximately 2001 reused parents in about 5.870 seconds
through Task, Project DB, checkpoints and Governor.
The test process peaked around 25,964 KiB RSS, `MemAvailable` remained above approximately 10.69 GiB
and swap deltas remained zero.
This measurement validates the bounded path and restart behavior. It is not a universal latency
model.
### A6000 current proof
The later A6000 proof recorded 2,714 Governor admissions while continuing Match Result -> GV -> Tracks.
Its final GV admitted window was 16, with the reference-host compute pool at 12 logical CPUs and four
reserved for host use. Swap-in/out deltas remained zero.
The checkpoint is:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Sparse SfM and Dense/MVS were not executed by this proof.
## Recovery and deterministic publication
Parallel preparation never weakens publication ordering.
For every path where durable order matters:
```text
prepare independent work
-> join
-> validate exact immutable result
-> owner-only deterministic publication
-> durable typed cursor
-> generic Task checkpoint
```
A checkpoint may lag an already published immutable scientific result. Restart must reuse or validate
that result through its exact scientific identity; it must not fabricate a replacement identity.
Cancellation is cooperative at the Task's documented boundaries. All admitted participants are
bounded and joined before sequence completion or owner cleanup.
## Limits and deferred work
The following remain outside this document's current production contract:
- cross-Task worker pools;
- multiple simultaneous active Queue callbacks;
- general DAG scheduling;
- multi-GPU scheduling;
- cgroup/systemd constrained-runtime capacity accounting;
- generic resource residency/cache management;
- authoritative Task scratch consumers;
- unvalidated GPU ports;
- dense/MVS resource orchestration.
Deferring cross-Task parallelism is not permission to serialize independent units inside one active
Task.
The Queue/Governor plus bounded internal participants are the current production architecture.
## Current summary
```text
CURRENT_PROJECT_DB_SCHEMA=v25
CURRENT_PRODUCTION_TASK_KINDS=16
raw.develop.batch/1 CURRENT
features.extract.batch/1 CURRENT
PER_ITEM_ATOMICITY_REQUIRES_CROSS_ITEM_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
VALIDATED_GPU_BACKENDS_PREFERRED_WHEN_USEFUL=YES
UMA_ACCOUNTED_ONCE_AGAINST_HOST_RAM=YES
SWAP_ZRAM_SCRATCH_AS_RAM=FORBIDDEN
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
SPARSE_SFM_EXECUTED_IN_A6000_PROOF=NO
DENSE_MVS_EXECUTED_IN_A6000_PROOF=NO
```

File diff suppressed because it is too large Load diff

View file

@ -1,314 +1,505 @@
# Exécution et runtime Lardon3D # Lardon3D Runtime
## Modèle d'exécution ## Status
### Threads
- Thread principal : entrée, modèle de vue et rendu TUI ncursesw (exclusif)
- Thread worker Queue : exécution sérielle des tâches métier
- Participants internes : uniquement ceux du contrat Task admis, joints par le
callback propriétaire avant publication
- Opération SSD : au plus un thread joinable, uniquement pendant une opération
UDisks bornée ; il ne rend rien et ne devient ni Queue ni scheduler
### Synchronisation
- Mutex pour les accès partagés
- Variables de condition pour la coordination
- Atomicité des opérations critiques
## Cycle de vie d'une tâche
```text ```text
1. Création (PENDING) CURRENT_PROJECT_DB_SCHEMA=v25
2. Soumission à la file CURRENT_PRODUCTION_TASK_KINDS=16
3. Sélection FIFO/adaptative par la Queue
4. Réservation obligatoire TASK_QUEUE_WORKERS=1
5. Exécution (RUNNING) INTERNAL_PARALLELISM=BOUNDED
- Pause/reprise coopérative INTER_TASK_PARALLELISM=NOT_IMPLEMENTED
- Annulation coopérative
- Séquences adaptatives RUNTIME_OBSERVER=CURRENT/VALIDATED_OPERATIONAL
6. Complétion (COMPLETED) ou Échec (FAILED) RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
7. Nettoyage des ressources SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_A6000_PRE_SFM=PASS/FROZEN
``` ```
## Synchronisation The runtime coordinates the main thread, Task Queue, Resource Governor, Project DB lifecycle, bounded
Task-internal participants, runtime observation and optional external-SSD controller.
### Mutex It does not introduce a second scheduler.
- Protection des données partagées
- Accès exclusif aux ressources critiques
### Variables de condition ## Thread model
- Coordination entre threads
- Notification de changement d'état
- Attente passive ; timeout borné seulement pour réévaluer un `WAIT` ressources
### Atomicité ### Main thread
- Opérations indivisibles
- État cohérent garanti
## Gestion des erreurs Owns:
### Rollback - input;
- Rollback des transactions locales avant publication - ncurses;
- Nettoyage complet des ressources possédées par l'opération - TUI model binding;
- Une publication fichier réussie suivie d'un échec DB peut laisser un orphelin - project open/close orchestration;
valide ; aucune transaction distribuée fichier+SQLite n'est revendiquée - bounded polling of runtime/SSD state.
### Récupération ncurses remains main-thread-only.
- Reprise à la dernière frontière connue
- Ignorance des artefacts partiels
- Validation avant publication
## Limites actuelles ### Task Queue worker
- Worker Queue unique (pas de pools inter-Tasks multiples) The single Queue worker owns one active heavy Task callback at a time.
- Pas de parallélisme inter-Tasks ; certains kinds possèdent des participants
internes bornés, comptés par leur contrat Governor
- Reprise automatique limitée aux tâches indépendantes reconstructibles
## Reprise durable A Task callback may create bounded internal participants only when its admitted Task contract permits
them.
Un snapshot ne conserve que l'état logique d'une tâche. `RUNNING` et `PAUSED` Those participants join before owner publication.
sont normalisés vers `PENDING`; aucun worker, callback brut, pointeur, contrat
ou réservation n'est restauré. Le propriétaire fournit un nouveau callback et
resoumet la tâche. Les états terminaux sont conservés.
`started_at` désigne le début de la tentative d'exécution courante, pas le ### SSD operation thread
premier démarrage historique. Un checkpoint `RUNNING` restauré en `PENDING`
conserve temporairement l'horodatage de la tentative interrompue pour
l'observation ; lors de `lardon3d_task_start()`, `started_at` est remplacé par le
nouveau démarrage et `finished_at` est remis à zéro. `finished_at` n'est fixé
qu'à la terminaison de cette tentative.
**IMPLEMENTED** — snapshot, codec v1 et restauration isolée. The SSD controller may own at most one bounded joinable operation thread while executing a synchronous
UDisks operation.
**IMPLEMENTED** — l'import `import.images` se sauvegarde à chaque fin de lot et It does not become:
se reconstruit explicitement avec un userdata neuf lié au projet rouvert. Son
intention durable contient `source_path + scanset_id`; le hash/copie et la
transaction catalogue restent hors mutex Task et hors mutex DB pendant l'I/O.
**IMPLEMENTED** — `project_open()` inventorie par pages de 8, restaure puis - a Task worker;
resoumet automatiquement les tâches production valides. Il retourne après - a Queue;
l'enqueue et n'attend jamais leur terminaison. - a scheduler;
- an ncurses owner.
L'ordre d'initialisation production est : politique driver, profil matériel, ## Task lifecycle
Governor, backend, Queue/worker, contrôleur SSD optionnel et binding
Governor, puis TUI. L'ouverture DB/projet et la reprise synchrone sont pilotées
ensuite depuis le thread principal. Une fermeture ne peut commencer qu'après le
retour de `project_open()`. Le worker peut consommer pendant le scan ; chaque
tâche exécutée est néanmoins réadmise normalement.
**NOT_YET_WIRED** — reprise ordonnée par dépendances/DAG. Les kinds de Conceptual lifecycle:
production reconstructibles checkpointent déjà à leurs frontières métier ;
aucun timer autosave générique ne doit avancer devant leur publication durable.
**IMPLEMENTED** — reprise sélective des kinds reconstructibles via Project DB, ```text
Task Kind Registry et Queue. Les dépendances/DAG restent différées ; il create PENDING
n'existe pas de scheduler global distinct à restaurer. -> persist typed intent where required
-> enqueue
-> Queue selects
-> Resource Governor admits/reserves
-> RUNNING
-> bounded sequence work
-> Task-specific durable publication
-> generic checkpoint
-> optional sequence_break/re-admission
-> COMPLETED | FAILED | CANCELLED
-> terminal callback
-> destruction
```
## Accès Project Database Pause/cancel are cooperative.
**IMPLEMENTED** — une connexion SQLite opaque sérialisée par mutex interne ; A sequence break is not a Task state.
les opérations multi-tables sont transactionnelles et bornées.
**IMPLEMENTED** — le cycle de vie projet ouvre/crée `project.db`, vérifie ## Durable restart
l'identité et ferme la connexion. Ouvrir, fermer ou changer de projet est une
frontière exacte : l'observateur et la vue optique libèrent leurs borrows, puis
l'unique Queue est annulée, jointe et détruite, callbacks terminaux inclus,
avant la fermeture de Project DB. Une seule Queue vide est ensuite recréée et
les observateurs sont rebondés. Il n'existe jamais deux schedulers simultanés.
L'historique terminal et l'espace d'IDs Queue sont ainsi propres à la session ;
les mêmes Task IDs durables de deux projets restent indépendants et aucun
historique fourni n'est affiché lorsqu'aucun projet n'est chargé.
**IMPLEMENTED** — la registry reconstruit explicitement callback/userdata hors A generic snapshot stores logical Task state, not live execution machinery.
mutex DB pour un kind connu ; elle ne soumet aucune tâche.
**IMPLEMENTED** — la queue accepte un identifiant restauré préassigné s'il On restoration:
n'entre en collision avec aucune tâche connue. L'import production peut donc
être reconstruit puis soumis explicitement.
**IMPLEMENTED** — la resoumission automatique utilise la registry production, ```text
conserve le task ID et laisse le worker obtenir une nouvelle réservation. RUNNING -> PENDING
Kinds inconnus, tâches legacy, checkpoints invalides et sources absentes ne PAUSED -> PENDING
bloquent pas l'ouverture. ```
**IMPLEMENTED** — `visual_index.update` reprend à la dernière membership Terminal states remain terminal.
commitée. Un segment temporaire n'est jamais visible et un rejeu exclut les
Feature Sets déjà membres.
## Durée de vie terminale et fermeture Queue Restart never restores:
Une Task terminale reste vivante jusqu'au retour complet de son callback - worker thread;
terminé. Queue la retire alors de la liste active et la détruit hors de son - callback pointer;
mutex ; seule une histoire de 64 snapshots reste observable. Les appels déjà - userdata pointer;
enregistrés avant `task_queue_destroy()` sont attendus. Le propriétaire doit - CPU affinity;
empêcher tout nouvel appel dès le début de la destruction, règle nécessaire à - live reservation;
toute API C adressée par pointeur brut. - GPU handle;
- scratch lease;
- adaptive feedback history.
Un callback terminé peut consulter les vues Queue tant que le propriétaire la Task Kind Registry reconstructs fresh runtime binding from exact durable kind/version and typed payload.
maintient vivante. Il ne peut pas détruire cette Queue, retirer son propre
record ni attendre une opération dépendante de son retour.
## Observatoire TUI actuel Every resumed Task is re-admitted by the Resource Governor.
**CURRENT / VALIDATED OPERATIONAL.** Ce statut décrit l'implémentation et ses ## Project-open recovery
tests courants. L'audit global qui contient cette frontière est désormais
`PASS/FROZEN` après revue indépendante ; le statut TUI reste volontairement
opérationnel et n'interdit pas ses évolutions futures sous un ticket distinct.
### Séparation modèle, observation et rendu `project_open()` discovers durable Tasks in bounded pages, validates their checkpoint/typed identity,
reconstructs eligible bindings and submits them to the existing Queue.
Le modèle `tui_model` est pur et testable sans terminal. Le renderer reçoit It returns after enqueue; it does not wait for those Tasks to finish.
seulement des copies bornées et n'interroge ni Queue, ni Governor, ni Project
DB, ni contrôleur SSD. Toutes les fonctions ncurses, l'entrée clavier et le
rendu demeurent sur le thread principal.
L'observateur runtime emprunte Queue et Governor et conserve une seule copie Unknown kinds, unsupported versions, legacy-untyped Tasks, invalid checkpoints or Task-specific
cohérente. Les captures ordinaires sont coalescées pendant au moins une seconde non-reconstructible input remain inspectable and do not cause guessed execution.
monotone ; un échec conserve la dernière vue bornée en la marquant stale.
Il observe au plus 129 Tasks : les 64 pending possibles, l'unique active et les
64 snapshots terminaux récents. L'ordre Queue place le travail vivant du plus
récent au plus ancien, puis l'histoire par terminaison décroissante ; une Task
active ne peut donc pas être masquée par un vieux préfixe historique. Il
n'existe ni scan DB par frame, ni lecture `/proc` volumineuse, ni historique
non borné.
Les ABI historiques restent exactes : `Lardon3DTaskSnapshot`, Generic dependency/DAG recovery remains unimplemented.
`Lardon3DResourceSnapshot`, `Lardon3DAppState` et
`lardon3d_layout_draw()` ne sont pas étendus en place. Les surfaces additives
`Lardon3DTaskObservation`, `lardon3d_task_queue_observe()`,
`Lardon3DResourceObservation`, `Lardon3DRuntimeSnapshot` et
`lardon3d_layout_draw_runtime()` portent les nouveaux champs. De même,
`lardon3d_tui_run()` reste le symbole historique ; l'application utilise
`lardon3d_tui_run_with_ssd_operation()` avec un owner SSD conservé hors de
`Lardon3DAppState`.
### Progression et ETA ## Initialization order
Une Task typée publie `completed/total` seulement après son propre commit Production startup establishes the safe driver/runtime policy before heavy worker/backend activity.
métier durable. Quand ces compteurs sont connus, la TUI les affiche toujours et
en dérive le pourcentage sans utiliser le message ou le nom. Une Task marquée
`COMPLETED` avec un préfixe durable incomplet est une erreur d'intégrité
visible, jamais 100 %. Quand les comptes typés sont inconnus, le lifecycle peut
être terminal mais la progression scientifique reste indéterminée. Le
pourcentage générique non typé, lorsqu'il est utile, porte explicitement le
libellé runtime.
Le débit est un EWMA borné. La première observation établit seulement le Conceptually:
préfixe de reprise et ne contribue pas au taux ; une reprise de RUNNING remet
également la fenêtre temporelle à zéro. Deux intervalles strictement positifs
sont nécessaires avant un débit et une ETA connus. Une absence de progrès,
une pression Governor, une régression ou une preuve insuffisante produit
respectivement `STALLED`, `THROTTLED`, reset ou `INDETERMINATE/CALCULATING`.
Seule une complétion cohérente vaut exactement 100 % et ETA zéro ; aucune fausse
précision n'est affichée.
### Pipeline et ressources ```text
driver policy
-> hardware profile
-> Resource Governor
-> optional backend metadata
-> Task Queue / worker
-> optional SSD controller + Governor binding
-> TUI
```
La synthèse utilise les étapes Acquisition, RAW, Quality, Features, Visual Project open/recovery is then driven from the main thread.
Index, Candidate, Matcher, GV, Tracks, Sparse SfM et future Dense. Les états
sont `NOT_READY`, `READY`, `QUEUED`, `RUNNING`, `THROTTLED`, `BLOCKED`,
`COMPLETE`, `FAILED` et `NOT_APPLICABLE`. Dense reste explicitement
`NOT_APPLICABLE` tant qu'aucun Task kind de production n'existe ; une étape
future n'est jamais devinée active depuis un nom ou un message.
Le panneau ressources expose CPU actif/admis/disponible et sa raison, GPU The exact source initialization sequence remains authoritative.
présent/mémoire/busy/backend lorsqu'ils sont connus, RAM/MemAvailable/réserve,
swap total/utilisé et deltas actifs, lot/inflight/helpers/I/O, scratch et
pression Governor GREEN/YELLOW/RED. Le contrat installé de l'exacte Task active
est l'autorité pour CPU et lot. Un dernier diagnostic privé seulement indexé
par kind peut appartenir à une autre Task ou séquence : backend, inflight,
helpers, utilisation ou raison restent donc `UNKNOWN` sans association exacte
Task+séquence. La mémoire UMA est comptée une seule fois et ni swap ni scratch
ne sont ajoutés à la capacité RAM.
### Dimensions, couleurs et clavier ## Project lifetime boundary
Le layout complet demande au moins 100×30. Le layout compact est validé à la Changing/closing project is an exact ownership boundary.
frontière 72×20 et reste supporté jusqu'au minimum 60×15. En dessous, le rendu
se réduit au message borné `Terminal trop petit`; un resize recalcule la classe
sans faire travailler un worker. Les rôles sémantiques sont healthy vert,
warning jaune, error rouge, GPU cyan, CPU bleu, SSD magenta, plus dim/bold.
Les libellés textuels demeurent l'autorité lorsqu'il n'y a pas de couleur ou
pas assez de paires terminal.
Les écrans courants sont accueil, projets, import, viewer futur, tâches, Before Project DB close:
ressources, optique, SSD et aide. `F1..F7` naviguent respectivement vers aide,
projets, import, viewer, tâches, ressources et optique. Le segment littéral
`F10 SSD` est réservé au début du footer et reste visible à 60 colonnes dans
tous les modes pertinents. Les footers dérivent du même mode que le handler :
- saisie active : Enter valide, Échap annule, F10 reste disponible ; ```text
- import actif : `X` demande l'annulation et F10 reste disponible ; `q` et views release DB borrows
Échap sont affichés comme désactivés ; -> Queue ingress closes
- mode idle : `q`, Échap/navigation et les commandes propres à l'écran sont -> Queue cancels/joins/destroys
annoncés seulement lorsqu'ils sont réellement traités ; -> terminal callbacks finish
- Tasks : flèches/`j`/`k`, `P` pause, `R` reprise, `C` annulation ; -> Project DB closes
- Optique : Tab change de panneau, flèches/`j`/`k` sélectionnent, `[` revient à ```
la première page et `]` charge la suivante ; `B/L/C/V/A/G/K/E` déclenchent
les opérations indiquées et `R` retente explicitement un bind/chargement.
### Workflow optique A fresh empty Queue can then be created for the next project.
La TUI consomme les API v23 décrites dans No terminal callback may dereference a closed Project DB.
[Project Database](project_database.md), sans SQL direct ni édition d'une ligne
immuable. Elle inspecte une affectation Capture, effectue seulement des lookup
metadata exacts, liste les profils de boîtier/objectif/configuration et accepte
un objectif manuel sans électronique ni alias — le Meike de test est un cas
normal, pas une branche produit. « Modifier » signifie créer un nouveau profil
ou une nouvelle configuration immuable, puis l'assigner explicitement à un
groupe de campagne encore éligible ou à un Capture non affecté. Les
calibrations listées doivent être compatibles avec l'exacte configuration et
la sélection reste explicite ; absence, ambiguïté, incompatibilité, BUSY, I/O
et corruption sont rendues sans profil fabriqué. Les pages ont 16 lignes,
rapportent un compte page-local et un indicateur « suite » exact.
### SSD F10 et lifetime application Queue terminal history belongs to the current runtime session and does not leak between projects.
La TUI affiche les huit états physiques `ABSENT`, `DETECTED`, `ENABLING`, ## Project Database
`ENABLED`, `IN_USE`, `DRAINING`, `SAFE_TO_UNPLUG` et `ERROR`, avec identité
stable, modèle/télémétrie lorsqu'ils sont connus, swap, scratch, mount, usage,
leases, drain et raison. `UNKNOWN` n'est jamais remplacé par zéro ou par une
supposition ; `SAFE_TO_UNPLUG` est mis en évidence comme endpoint sûr. F10
choisit exclusivement l'une des capacités exactes
`can_enable`, `can_disable` ou `can_cancel_drain` publiée par le contrôleur ; un
état incomplet, une paire de remplacement ou un résultat malformé n'accorde
aucune autorité. L'opération synchrone UDisks s'exécute dans au plus un thread
joinable, tandis que le main continue de rendre et de poller sans blocage.
La validation qui alimente ces capacités est fail-closed par état : toute Project DB uses an opaque serialized SQLite connection with bounded transactional operations.
autorité exige Drive et deux partitions détectés, identités Drive+UUID exactes,
extents positifs connus et faits mount/activité/drain cohérents. `ABSENT` ne
peut transporter aucun fait actif, `DETECTED` partiel n'a aucune action et un
hazard `ERROR` déconnecté ne peut que retenir l'identité originale sans
allocation. Seule la reconnexion complète de ce tuple peut autoriser son drain.
Après chaque observation ou résultat validé, l'adaptateur enregistre une copie Current schema head:
bornée de l'état physique auprès du Governor. Une copie malformée devient
`ERROR` et interdit les nouvelles allocations ; l'observation ressources lit
cet état Governor-owned, tandis que les détails/permissions F10 restent dans
le snapshot physique. La génération source peut saturer à `UINT64_MAX` : une
update publique égale ne réaccorde jamais une autorité stale ; seule la
complétion du wrapper exact déjà engagé réconcilie son lease adressé. À l'arrêt,
l'ordre est : destruction/join de la Queue et
libération de chaque lease Task, fermeture du projet/DB, join puis unregister
vérifié de l'adaptateur SSD, destruction du contrôleur, puis destruction du
Governor. Les tests utilisent un provider factice et n'exécutent aucune vraie
mutation SSD.
## Invariants ```text
CURRENT_PROJECT_DB_SCHEMA=v25
```
- ncurses appartient exclusivement au thread principal Current additive selected-execution overlays include:
- Aucune tâche ne démarre sans réservation active
- Les réservations sont libérées exactement une fois
- Les buffers sont strictement bornés
- Le Resource Governor reste l'unique propriétaire de l'admission ; ni Queue,
ni contrôleur SSD ne constituent un second orchestrateur de ressources
## Statut : CURRENT / VALIDATED OPERATIONAL ```text
v24 raw.develop.batch/1
v25 features.extract.batch/1
```
La TUI/runtime et son raccordement SSD sont implémentés, testés et relus dans The v23 optical model remains valid but is no longer the schema head.
leur tranche. Le statut global est
`GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`. Les builds portables/Vulkan, ## Current production Task inventory
sanitizers, contrôles de concurrence et ABI frais sont acquis ; l'unique revue
finale indépendante a conclu PASS sans finding bloquant. Production currently has sixteen Task Kinds.
Important selected-execution additions:
```text
raw.develop.batch/1
features.extract.batch/1
```
The Queue still has one active heavy callback; batch Task Kinds obtain throughput from bounded
participants inside that callback.
Per-item atomicity does not require cross-item serial execution.
## Resource Governor
Every production Task goes through the Resource Governor, including fixed-resource Tasks.
The canonical host policy is:
```text
preserve defined interactive reserve
then maximize safe useful throughput
```
```text
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
Reference-host CPU counts are evidence, not portable constants.
Pressure may reduce a later admission; healthy recovery may ramp useful width again.
Swap/zram/scratch do not enlarge admitted RAM.
UMA GPU memory is charged once against host RAM.
## Internal parallelism
Validated Task Kinds may execute:
```text
one Queue owner callback
-> bounded participants
-> join
-> deterministic owner publication
```
Current examples include:
- Feature selected batch;
- Visual Index;
- Candidate Pair;
- Matcher CPU work;
- Geometric Verifier outer parallel preparation;
- RAW selected batch.
This is not inter-Task parallelism and does not create another global worker pool.
## Current Matcher backend policy
ORB Matcher normal production is Governor-owned AUTO.
Eligible ORB work prefers the validated Vulkan backend.
Fallback is complete CPU recomputation.
Normal Vulkan contract:
```text
inflight = 1
helpers = 0
useful batch <= 8
```
Depth 2 remains validated private safety/benchmark capacity but was rejected as normal useful policy.
SIFT/RootSIFT Matcher remains CPU.
## Runtime observation
The runtime observer borrows Queue/Governor state and publishes one bounded coherent snapshot for the
TUI.
It does not retain Task userdata.
Ordinary snapshots are rate-limited/coalesced.
On observation failure, the previous bounded view may be retained and explicitly marked stale.
Task observation includes live/pending/recent-terminal entries only within fixed capacity.
No unbounded Project DB scan is performed per frame.
## Durable progress and ETA
Typed Tasks publish exact `completed/total` only after their Task-specific durable prefix is committed.
The TUI must not infer exact scientific progress from:
- Task name;
- message text;
- generic percentage.
When exact counters exist, they are authoritative.
Throughput/ETA needs enough positive-time progress observations.
No-progress/pressure/restart cases become explicit states such as stalled, throttled or indeterminate
rather than fabricated precision.
A terminal Task with inconsistent durable progress is visible as an integrity problem rather than
silently forced to 100%.
## Pipeline observation
Current observable stages include:
```text
Acquisition
RAW
Quality
Features
Visual Index
Candidate
Matcher
GV
Tracks
Sparse SfM
Dense
```
Sparse SfM capability exists.
Dense has no production Task Kind and remains not applicable/unimplemented at the current checkpoint.
Historical S21/A6000 real campaigns have not executed Sparse SfM because known calibration is
unavailable for those campaigns.
## TUI resource observation
Resource UI may display known bounded values for:
- active/admitted/available CPU;
- GPU presence/backend/busy/memory;
- RAM and `MemAvailable`;
- host reserve;
- swap state and active deltas;
- batch/inflight/helpers;
- IO;
- scratch;
- Governor pressure.
The installed contract of the exact active Task is authoritative for that sequence.
A diagnostic indexed only by Task Kind cannot automatically be attributed to another Task/sequence.
Unknown values remain unknown.
## TUI layout
The validated layout classes remain bounded.
Current thresholds include:
```text
full layout >= 100x30
compact boundary = 72x20
supported minimum = 60x15
```
Below the supported minimum, the UI uses its too-small-terminal fallback.
Repository documentation is English. Any remaining non-English executable UI literal is legacy runtime
text and must be changed only in the explicitly scoped UI-language remediation pass; documentation does
not redefine executable behavior by pretending that source literal has already changed.
## Navigation
The current TUI provides screens for the implemented runtime surfaces, including:
- home;
- projects;
- import;
- tasks;
- resources;
- optics;
- SSD;
- help;
- viewer placeholder/future surface.
Key bindings and exact executable labels remain owned by the TUI source and its tests.
Documentation should describe behavior rather than preserve stale localized literals as authority.
## Optical workflow
The TUI uses the public optical APIs introduced by the v23 overlay.
It does not write optical SQLite rows directly.
It supports explicit inspection/selection and immutable profile creation.
Manual lenses without electronic metadata are valid data.
No workflow may fabricate:
- "unknown" lens identity;
- calibration compatibility;
- metadata match;
- focal/lens substitution.
Calibration selection remains explicit and exact.
## SSD F10 boundary
The SSD UI reflects controller capability/state rather than inventing actions.
The controller owns physical detection, pairing, mount/swap/scratch state and bounded UDisks operations.
The Resource Governor owns scratch lease admission.
A state that is incomplete, stale or physically inconsistent grants no control/lease authority.
Scratch is storage capacity, never RAM.
## Global shutdown
Ownership shutdown preserves:
```text
Task Queue / Task leases
-> project close
-> SSD operation join / Governor unregister
-> SSD controller
-> Resource Governor
```
A real outstanding scratch lease can block unregister and must remain an observable error.
Do not abandon a live lease pointer.
## Error/recovery model
Runtime operations use local rollback and explicit publication boundaries.
File asset publication plus SQLite is not treated as one distributed transaction.
A successfully published physical asset followed by DB failure may leave a valid orphan.
Recovery validates known durable representations; it does not guess or silently repair scientific
identity.
## Current real checkpoint
Current retained A6000 checkpoint:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
It exercised current runtime/Queue/Governor behavior through:
```text
selected RAW batch
selected Feature batch
Visual Index
Candidate Pair
Matcher
Geometric Verifier v3
Track Builder
```
The final continuation recorded deterministic restart/reuse and stopped with:
```text
Sparse SfM Tasks 0
Sparse Reconstructions 0
Dense/MVS 0
```
This is a current real runtime checkpoint, later than the historical global-maintenance checkpoint.
Both remain valid for the boundaries they prove.
## Current limits
Current runtime intentionally does not provide:
- multiple concurrent heavy Queue callbacks;
- generic inter-Task DAG scheduling;
- generic Task priorities beyond current Queue policy;
- generic autosave ahead of Task-specific durable publication;
- Dense/MVS production Task Kind;
- live capture/viewer reconstruction loop;
- generic scratch-consuming Task Kind.
Those are future product/implementation decisions, not silently missing state.
## Summary
```text
CURRENT_PROJECT_DB_SCHEMA=v25
CURRENT_PRODUCTION_TASK_KINDS=16
TASK_QUEUE_WORKERS=1
ACTIVE_HEAVY_CALLBACKS=1
INTER_TASK_PARALLELISM=NOT_IMPLEMENTED
INTERNAL_PARALLELISM=BOUNDED
GENERIC_DAG=NOT_IMPLEMENTED
DENSE_TASK_KIND=NOT_IMPLEMENTED
CURRENT_SCRATCH_CONSUMING_TASK_KINDS=0
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_A6000_PRE_SFM=PASS/FROZEN
```

File diff suppressed because it is too large Load diff

View file

@ -1,268 +1,628 @@
# Registry des types métier de tâches # Task Kind Registry
## Feature kinds v1A ## Status
La registry statique conserve `features.extract` v1 pour ORB et ajoute ```text
`features.extract.sift` v1 et `features.extract.rootsift` v1. Les reconstructeurs CURRENT_PROJECT_DB_SCHEMA=v25
chargent la table dédiée, revalident le fingerprint et ne capturent aucun CURRENT_PRODUCTION_TASK_KINDS=16
`AppState`.
## Contrat TASK_KIND_REGISTRY_STATUS=IMPLEMENTED
COMPUTE_GOVERNOR_V2=PASS/FROZEN
ORB_VULKAN_ASYNC_EXECUTION=PASS/FROZEN
Une instance possède un **task ID** stable. Son **task kind** décrit son RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
comportement métier, son **task state** décrit son état d'avancement, la SERIALISM_REQUIRES_PROOF=CANONICAL
**checkpoint version** décrit le codec générique et la **task kind version** ```
versionne les paramètres nécessaires au reconstructeur. Ces identités ne sont
pas interchangeables.
Le kind v1 est une chaîne ASCII de 1 à 64 caractères au format The production registry contains exactly sixteen version-1 Task Kinds.
`[a-z0-9][a-z0-9._-]*`. La version est un entier non nul. Aucun kind n'est
déduit d'un nom, callback ou pointeur et aucune normalisation n'est effectuée.
## Registry et ownership The registry is a bounded static dispatch table. It maps durable Task Kind identity to a reconstruction
function. It is not a scheduler, dependency graph, plugin loader, resource-policy database or
scientific identity resolver.
La registry est une vue bornée à 64 descriptors sur un tableau statique Project DB v25 is the current schema head. Earlier schema versions remain authoritative for the Task
immutable. Le lookup est linéaire, déterministe, sans allocation et sûr en payloads they introduced.
lecture concurrente. Elle ne charge aucun code dynamiquement.
Un descriptor contient exactement le kind, sa version et un reconstructeur. ## Identity contract
Le reconstructeur produit callback, userdata et destructeur optionnel. Avant le
transfert, la registry nettoie le userdata sur toute erreur ; après restauration
réussie, `Lardon3DTask` en devient propriétaire et le détruit une fois après la
fin de l'exécution. Le constructeur métier n'est jamais appelé sous mutex DB.
La registry peut normaliser une ancienne estimation opérationnelle connue. Le A durable Task has several distinct identities and versions:
reconstructeur reçoit toujours le snapshot durable original afin de valider le
mode exact ; la registry applique ensuite l'estimation effective uniquement à
la copie privée transmise à la restauration de `Task`. Cette normalisation est
éphémère et déterministe : elle ne stage, ne promeut et ne publie aucun
checkpoint contenant seulement une estimation différente sous le même résumé.
Une panne pré-terminale peut donc répéter la normalisation exacte. Cette couture
ne peut modifier ni identité, paramètres scientifiques, progression ou curseur
métier, et toute forme voisine est rejetée.
## Inventaire production et entrées runtime - stable Task ID;
- Task Kind string;
- Task Kind version;
- generic checkpoint codec version;
- Task state;
- scientific input identity owned by the Task-specific payload.
`src/task_kinds.c::lardon3d_task_kind_registry_production()` enregistre les These fields are not interchangeable.
16 kinds v1 du profil de production courant. La colonne « reprise » nomme le
reconstructeur du binding ; « callback » nomme l'entrée runtime privée dans le
même fichier. Le détail chiffré des capacités est centralisé dans l'[audit des
16 kinds](resource_governor.md#audit-des-15-kinds-de-production).
| Kind v1 | Source, reprise et callback | Réconciliation pré-admission courante | A Task Kind v1 string is ASCII, 1 to 64 characters, with the form:
| --- | --- | --- |
| `raw.develop` | `raw_development_task.cpp`; `lardon3d_raw_development_task_reconstruct`; `run` | Aucune |
| `raw.develop.batch` | `raw_development_task.cpp`; `lardon3d_raw_development_batch_task_reconstruct`; `run_batch` | Association v24 Task → exécution sélectionnée ; le curseur reste dans l'exécution |
| `photo_quality.triage` | `photo_quality_task.cpp`; `lardon3d_photo_quality_task_reconstruct`; `run` | Aucune |
| `acquisition_campaign.run` | `acquisition_campaign_task.cpp`; `lardon3d_acquisition_campaign_task_reconstruct`; `run` | Forme courante ou forme v22 exacte vérifiée contre la requête immuable → capacité courante en mémoire |
| `import.images` | `import_task.c`; `lardon3d_image_import_reconstruct`; `run_image_import` | Aucune |
| `features.extract` | `feature_task.c`; `lardon3d_feature_extract_reconstruct`; `run` | Formes CPU12/CPU1 historiques exactes → demande OpenCV portable ; runtime borné au compute-pool |
| `features.extract.batch` | `feature_task.c`; `lardon3d_feature_extract_batch_reconstruct`; `run_batch` | Association v25 à l'exécution sélectionnée, domaine ORB exact et curseur typé ; CPU/lot bornés 1..12 |
| `features.extract.sift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | CPU12/CPU1 historiques exacts → demande OpenCV portable ; runtime borné au compute-pool |
| `features.extract.rootsift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | Même réconciliation SIFT ; aucune voie GPU validée |
| `visual_index.update` | `visual_index_task.c`; `lardon3d_visual_index_update_reconstruct`; `run` | Formes CPU12/CPU1 historiques exactes → CPU/lot 1..16 |
| `candidate_pair.generate` | `candidate_pair_task.c`; `lardon3d_candidate_pair_generate_reconstruct`; `run` | Formes CPU12 et CPU1 historiques exactes → CPU/lot 1..64 |
| `matcher.run` | `matcher_task.c`; `lardon3d_matcher_task_reconstruct`; `run` | Signatures CPU/Vulkan historiques exactes → formes courantes en mémoire |
| `geometric_verifier.run` | `geometric_verifier_task.c`; `lardon3d_geometric_verifier_task_reconstruct`; `run` | Forme sérielle CPU1/batch8 exacte → CPU utile 1..8, lot 1..16 |
| `track_builder.run` | `track_builder_task.cpp`; `lardon3d_track_builder_task_reconstruct`; `run` | Aucune |
| `sparse_sfm.run` | `sparse_sfm_task.cpp`; `lardon3d_sparse_sfm_task_reconstruct`; `run` | Aucune |
| `incremental_reconstruction.run` | `incremental_reconstruction_task.cpp`; `lardon3d_incremental_reconstruction_task_reconstruct`; `run` | Aucune |
## Couture privée Compute Governor v2 ```text
[a-z0-9][a-z0-9._-]*
```
**COMPUTE_GOVERNOR_V2 — PASS / FROZEN.** Le descriptor C public The Task Kind version is a non-zero integer.
reste limité à kind, version et reconstructeur. L'enveloppe de capacités est
intégrée sans changement d'ABI dans le `struct Lardon3DTask` opaque et les
coutures privées `src/task_internal.h` / `src/resource_governor_internal.h`.
Les coutures d'admission sont `src/task_queue.c::select_admissible()`,
`src/task.c::lardon3d_task_sequence_break()` et, côté Governor,
la sélection multi-capacité sur un snapshot unique. La normalisation historique
exacte reste dans `src/task_kind_registry.c::normalize_known_legacy_estimate()`.
Cette enveloppe n'est ni une identité scientifique, ni un nouveau payload No Task Kind is inferred from:
Project DB, ni un nouveau scheduler. Le Governor possède l'admission de tous
les kinds, y compris les formes entièrement fixes. Le contrat choisi est
immutable pendant une séquence et seule la suivante peut être adaptée. Une Task
sans alternative reçoit automatiquement une capacité égale à son estimation
durable. Le Governor conserve un état borné par kind/backend et un dernier
diagnostic ; ni l'enveloppe ni ce choix ne sont persistés.
La politique CPU hôte reste privée au Governor : masque permis, groupes - Task display name;
package/core/SMT, compute-pool et résultat d'application du worker Queue. Le - callback address;
compute-pool borne l'admission de chaque kind. Feature/SIFT/RootSIFT utilisent - userdata address;
le maximum `int` positif comme borne de l'API OpenCV, puis consomment le compte - source file name;
immutable réellement admis ; les CPU12 durables ne sont plus que des signatures - persisted function pointer;
historiques exactes. Les kinds CPU1 justifiés restent fixes. Aucun ID CPU ou - Task ID;
choix d'affinité n'entre dans le descriptor, le checkpoint ou le Project DB. - scientific fingerprint.
Le feedback ne requalifie pas un succès de reprise en travail durable : les No normalization is performed on the persisted kind string.
kinds Feature/SIFT/RootSIFT comptent un item seulement après extraction et
publication durable propre. READY, collision `ALREADY_PRESENT` ou publication
incertaine compte zéro ; Visual Index compte pareillement zéro pour un segment
`PUBLISHED_NOT_DURABLE`.
L'état privé par kind/backend coordonne désormais une seule dimension d'essai. ## Registry ownership
Les CPU réductibles slow-startent par doubles successifs depuis 1, puis le
maximum exact de leur capacité, toujours bornés par le compute-pool. Après deux
observations de baseline, deux observations à au moins +5 % sont nécessaires
pour accepter le palier.
Une fois CPU stabilisé, seuls les kinds dont le callback consomme réellement
son lot peuvent ouvrir un essai de lot. `features.extract`, SIFT et RootSIFT
enregistrent une observation atomique réussie partagée entre Tasks ; Visual
Index, Candidate et Matcher enregistrent chaque séquence. Les autres formes ou
dimensions non adaptables restent égales à leur capacité fixe honnête.
## Persistance et legacy The registry contains at most 64 descriptors in a static immutable array.
Le checkpoint générique reste en version 1. Project Database v7 conserve le Lookup is:
kind/version ; les lignes migrées depuis v1 restent `NULL/NULL` et sont classées
`LEGACY_UNTYPED`. Un kind inconnu ou une version non supportée reste inspectable
mais inexécutable. Aucun type n'est inventé et aucun code n'est sélectionné par
adresse persistée.
## Statut - linear;
- deterministic;
- allocation-free;
- safe for concurrent readers.
**IMPLEMENTED** — identité typée immutable, registry statique, lookup, The registry never dynamically loads code.
migration DB v1→v2, classification recovery et restauration explicite testée.
**IMPLEMENTED** — le descriptor production `import.images`, version 1, charge A descriptor contains only:
le chemin source borné depuis la table dédiée et reconstruit callback et
userdata sans `AppState *` ancien.
**IMPLEMENTED** — `project_open()` utilise la registry production immutable - Task Kind string;
pour restaurer hors mutex DB et transférer chaque tâche acceptée à la queue. - Task Kind version;
- reconstruction function.
**NOT_YET_WIRED** — réconciliation orpheline et dépendances/DAG. Les kinds The public descriptor does not contain resource-policy state, CPU topology, GPU identity, scratch
reconstructibles checkpointent déjà à leurs frontières métier ; la Registry ne state or persisted scheduler configuration.
possède pas un timer autosave et ne doit pas devancer leurs curseurs durables.
**IMPLEMENTED** — `features.extract` version 1 reconstruit une extraction ORB ## Reconstruction ownership
depuis `image_id` et ses paramètres bornés.
**IMPLEMENTED** — `visual_index.update`, version 1, recharge A reconstruction function returns:
`visual_index_id + after_feature_set_id` et reconstruit un contexte neuf.
**IMPLEMENTED** — `candidate_pair.generate`, version 1, recharge - callback;
`visual_index_id + after_feature_set_id + top_k + minimum_evidence_count - callback userdata;
+ scanset_filter + exclude_same_asset` depuis `candidate_pair_generate_tasks` - optional userdata destructor;
et reconstruit un contexte boundé. La restauration reconnaît le snapshot - Task-specific private binding state where required.
opérationnel sériel historique v1 exact et la forme CPU12 immédiatement
antérieure, puis les remplace éphémèrement par la demande courante CPU64 avec
8 Mio par item avant admission. Le checkpoint historique et le curseur typé
restent inchangés ; une forme voisine est rejetée.
**PASS / FROZEN — Compute Governor v2.** `matcher.run`, version 1, Before ownership transfer, the registry destroys any newly allocated userdata on failure.
recharge la configuration Matcher, l'identité Feature Set et le curseur
`after_candidate_pair_id`. Il traite une Candidate Pair atomique à la fois dans
des lots bornés à douze, checkpoint le curseur et repasse par le Governor entre
les lots. La table durable `matcher_tasks` est introduite par Project DB v11,
après le Match Result v10. Son reconstructeur accepte les formes courantes
CPU12/GPU0 et CPU1/GPU1/640 Kio à lot `1..12`, les signatures historiques
CPU8/GPU0 et Vulkan à lot maximal 8, puis les formes CPU12
pré-estimation-par-paire. La normalisation reste en mémoire. Une forme voisine
échoue au lieu de servir d'indice de backend ; le payload Project DB ne change
pas et ne persiste aucune identité matérielle. Les nouvelles Tasks ORB normales
ont une signature de classe `MIXED`, dont les autres champs restent une demande
de ressources réelle ; elle seule reconstruit la politique Governor `AUTO`.
Toutes les formes ORB de classe `CPU`, anciennes ou courantes, reconstruisent
un CPU fixe pour préserver les overrides explicites et une compatibilité sûre ;
une forme Vulkan restaurée reste fixe Vulkan. Un build portable reconstruit la
même politique `MIXED` mais n'expose que sa capacité CPU. Les snapshots tout à
zéro Candidate/SIFT/RootSIFT sont explicitement corrompus ; seules leurs
signatures historiques complètes exactes sont acceptées. Seule la forme AUTO
restaurée établit la disponibilité Vulkan partagée. Restaurer ensuite CPU,
Vulkan ou une signature historique fixe n'écrit rien dans cet état : la
co-restauration est indépendante de l'ordre. Aucun nouvel état de
backend n'est persisté.
Pour une Task AUTO, la Registry reconstruit aussi l'enveloppe privée Vulkan After a successful `Lardon3DTask` restoration, the Task owns the userdata and destroys it exactly once
CPU1/GPU1, lot opérationnel `1..8`, helpers 0 et inflight 1. La signature after execution lifetime ends.
durable historique reste à lot `1..12`; la signature 640 Kio
reste la forme depth-1 minimale et n'est pas mutée; l'admission normale facture
exactement 640 Kio une seule fois sur UMA. Le choix inflight est immutable dans
la séquence et ne devient ni payload, ni fingerprint, ni indice de reprise.
Vulkan explicite reste depth 1. La capacité privée de sûreté/benchmark peut
forcer deux slots et 1,25 Mio sans changer la reconstruction normale.
Le backend ne mappe pas le maximum de l'enveloppe à sa création : il retient
exactement un slot à depth 1 et deux seulement sous une séquence depth 2 admise,
puis libère le second avant l'admission suivante. La signature durable 640 Kio
reste donc inchangée sans sous-facturer une allocation depth 2 forcée.
L'A/B forcé ABBA a mesuré 54,661652238 paires/s à depth 1 et 55,797311953 à A business-specific constructor is never called while the Project DB mutex is held.
depth 2, soit +2,077617 %, sous le deadband 5 %, avec digest identique, quatre
séquences de fallback local par exécution et zéro panne/discard. La Registry conserve donc
`DEPTH_MAX_VALIDATED_SAFETY=2` pour les seules coutures privées, mais la
capacité AUTO normale suit `DEPTH_MAX_USEFUL=1` : depth 2 est
**REJECTED_WITH_MEASURED_REASON**, sans nouvelle signature durable.
La télémétrie privée de `matcher.run` conserve les classes de fallback par Project-open recovery copies the durable record out of the DB boundary before registry lookup and
séquence et compte aussi les items exacts local-ineligible/backend-failure/other Task-specific reconstruction.
après leur publication durable. Ce détail opérationnel n'ajoute aucun kind,
champ durable ou identité et empêche le regroupement batch de devenir un
comparateur scientifique. Le commit immédiat par item reste acquis si une
paire suivante avorte, tandis que le feedback de séquence n'est pas enregistré;
la déduplication actuelle vit seulement avec la Task reconstruite en mémoire.
Les logs batch 2/4 antérieurs à ce compteur restent préliminaires et prouvent
seulement l'invalidité du comparateur par séquences. Les huit runs item-valides
`forced-batch{2,4,8,12}-items{,-b}.stdout.jsonl` conservent chacun 4113 paires,
six items locaux, zéro panne/autre et le même digest. Les débits combinés sont
54,180767704, 66,094373197, 74,784998723 et 76,755814095 paires/s. Les gains
jusqu'à batch 8 dépassent 5 %, celui de 8 à 12 vaut seulement +2,635308425 % :
la Registry expose `BATCH_MAX_USEFUL=8` en AUTO normal et réserve batch 12 aux
preuves privées (`REJECTED_WITH_MEASURED_REASON`).
Le S21 final confirme l'enveloppe Registry en production : `matcher.run` v1
reste le même kind durable, AUTO choisit Vulkan pour 21 630 admissions et
termine 172 741/172 741 résultats à batch 8/inflight 1/helpers 0. Le passage
transitoire 8 → 1 → 2 → 4 → 8 ne modifie ni signature durable, ni
fingerprint, ni digest scientifique. Aucun backend ou champ persistant n'est
ajouté par cette adaptation.
La reconstruction AUTO ne sonde ni n'initialise Vulkan sur le thread ## Exact legacy estimate normalization
`project_open()`. Elle expose la capacité depuis les seules métadonnées runtime
build/backend/GPU ; le Governor possède le dimensionnement exact et
l'admission UMA sur son snapshot. Le premier begin appartient au worker Queue après son
affinité. La politique Mesa sûre est déjà établie avant les pthreads et
l'initialisation du driver ; aucun sweep/latch auxiliaire n'appartient donc au
contexte Task. Une paire localement inéligible n'initialise pas le backend. Une
panne réelle produit des paires CPU complètes et rend le backend indisponible
aux admissions AUTO suivantes sans réécrire le snapshot durable.
Le contrôle de benchmark `synchronous` du runner réel n'étend pas le descriptor The registry may recognize exact historical operational resource descriptors for restart compatibility.
ni le reconstructeur. Il est compilé hors du binaire production, attaché
seulement au contexte éphémère d'une nouvelle Task et refusé par le runner si
une Task Matcher doit être reprise. La Registry continue donc à reconstruire
uniquement la politique AUTO/fixe déduite de la signature durable, jamais un
pipeline de benchmark.
**IMPLEMENTED** — ORB, SIFT et RootSIFT acceptent leurs formes CPU12/CPU1 The reconstruction function always receives the original durable snapshot.
historiques complètes et les normalisent vers la demande OpenCV portable
`INT_MAX`. Le Governor borne l'exécution au compute-pool ; les sorties testées
à 1/2/4/8/12 restent égales. Cette compatibilité opérationnelle n'altère ni
fingerprint, Feature Set, checkpoint durable, ni politique scientifique.
**IMPLEMENTED** — `geometric_verifier.run`, version 1, recharge la configuration If an exact historical descriptor is recognized, the registry may replace only the private effective
Fundamental immuable, en revalide le fingerprint et reprend `after_match_result_id`. resource estimate passed to the restored runtime Task.
Project DB v13 ajoute uniquement `geometric_verifier_tasks`, car le checkpoint
générique v1 ne possède aucun payload propre au kind.
La forme historique série exacte (4 Mio fixes, CPU1, batch 1..8) est normalisée This normalization is:
en mémoire vers 8 Mio par item, CPU utile 8 et batch maximal 16. Cette évolution
ne touche ni fingerprint, GVR, ordre, curseur ni checkpoint historique ; une
forme voisine est refusée.
**IMPLEMENTED** — `track_builder.run`, version 1, reconstruit un scope explicite - ephemeral;
depuis son payload Project DB v15 et son asset little-endian validé. Le callback - deterministic;
réutilise l'orchestration Gate C et le reconstructeur refuse toute corruption, - exact-shape only;
version, fingerprint, checksum, tri, unicité ou L3DTSIS1 incohérents. - non-persistent.
**PASS / FROZEN** — `sparse_sfm.run`, version 1, recharge le It never rewrites:
payload scientifique explicite Project DB v17, restaure l'estimation générique
persistée et rejoue D puis E depuis les références Track Set/calibration. Le
fingerprint F0 est recalculé ; le checkpoint générique v1 reste inchangé.
**PASS / FROZEN** — `incremental_reconstruction.run`, - the generic checkpoint;
version 1, recharge le payload Project DB v18 composé du prédécesseur, du Track - Task ID;
Set d'extension, du scope de calibration et du fingerprint H. La tâche atomique - Task Kind/version;
recalcule depuis ces entrées après redémarrage, passe par la Queue et le - Task progress;
Governor avec son estimation H immuable, et ne persiste aucun état de solveur. - Task-specific cursor;
Elle n'ajoute ni DAG ni dépendance implicite. - scientific parameters;
- scientific fingerprint;
- Project DB payload.
A neighboring or partially matching historical resource descriptor is rejected rather than guessed.
A crash before a later terminal checkpoint may therefore repeat the same exact normalization on the
next restart.
## Production inventory
The production registry is created by:
```text
src/task_kinds.c::lardon3d_task_kind_registry_production()
```
It contains exactly these sixteen version-1 kinds:
| # | Task Kind | Durable payload / introduction | Reconstruction |
| ---: | --- | --- | --- |
| 1 | `raw.develop` | Project DB v22 `raw_development_tasks` | `lardon3d_raw_development_task_reconstruct` |
| 2 | `raw.develop.batch` | Project DB v24 `raw_development_batch_tasks` | `lardon3d_raw_development_batch_task_reconstruct` |
| 3 | `photo_quality.triage` | Project DB v21 | `lardon3d_photo_quality_task_reconstruct` |
| 4 | `acquisition_campaign.run` | Project DB v20 | `lardon3d_acquisition_campaign_task_reconstruct` |
| 5 | `import.images` | historical typed import payload | `lardon3d_image_import_reconstruct` |
| 6 | `features.extract` | historical Feature Task payload | `lardon3d_feature_extract_reconstruct` |
| 7 | `features.extract.batch` | Project DB v25 `feature_extract_batch_tasks` | `lardon3d_feature_extract_batch_reconstruct` |
| 8 | `features.extract.sift` | typed SIFT Feature payload | `lardon3d_sift_extract_reconstruct` |
| 9 | `features.extract.rootsift` | typed RootSIFT Feature payload | `lardon3d_sift_extract_reconstruct` |
| 10 | `visual_index.update` | Visual Index Task payload | `lardon3d_visual_index_update_reconstruct` |
| 11 | `candidate_pair.generate` | Project DB v9 `candidate_pair_generate_tasks` | `lardon3d_candidate_pair_generate_reconstruct` |
| 12 | `matcher.run` | Project DB v11 `matcher_tasks` | `lardon3d_matcher_task_reconstruct` |
| 13 | `geometric_verifier.run` | Project DB v13 `geometric_verifier_tasks` | `lardon3d_geometric_verifier_task_reconstruct` |
| 14 | `track_builder.run` | Project DB v15 `track_builder_tasks` | `lardon3d_track_builder_task_reconstruct` |
| 15 | `sparse_sfm.run` | Project DB v17 `sparse_sfm_tasks` | `lardon3d_sparse_sfm_task_reconstruct` |
| 16 | `incremental_reconstruction.run` | Project DB v18 `incremental_reconstruction_tasks` | `lardon3d_incremental_reconstruction_task_reconstruct` |
The current detailed resource-capability inventory is owned by
[`resource_governor.md`](resource_governor.md), in its production sixteen-kind audit.
Historical documents that correctly recorded fewer kinds at their checkpoint remain historical
evidence. They must not be rewritten merely to make their old count equal the current count.
## Current additive Task Kinds
The two Task Kinds added after the historical fourteen-kind maintenance inventory are:
```text
raw.develop.batch/1
features.extract.batch/1
```
### `raw.develop.batch/1`
Project DB v24 binds one Task to one immutable selected execution.
The selected-execution cursor remains authoritative. Independent RAW items may be prepared within a
bounded admitted window, participants join, and the owner publishes the selected prefix in canonical
order.
The Task-specific restart path reuses exact already published RAW-derived representations and does not
infer identity from path, basename or processing position.
### `features.extract.batch/1`
Project DB v25 binds one Task to:
- one immutable selected execution;
- one monotonic `next_item_index`;
- exact ORB kind/version/parameters/fingerprint.
Independent selected images may be prepared concurrently without participant SQLite publication.
After join, the owner validates/reuses the exact READY Feature Set, publishes in selected-item order,
advances the typed cursor and then advances generic Task progress/checkpoint.
The historical `features.extract/1` kind remains valid and reconstructible.
```text
PER_IMAGE_FEATURE_RESULT=ATOMIC
PER_IMAGE_ATOMICITY_REQUIRES_CROSS_IMAGE_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
```
## Compute Governor v2 private seam
```text
COMPUTE_GOVERNOR_V2=PASS/FROZEN
```
The public registry descriptor remains limited to kind, version and reconstruct function.
Resource capability alternatives live in private runtime state, including the opaque `Lardon3DTask`
and private Task/Resource Governor seams.
The key production admission boundaries remain:
- Queue admissibility selection;
- `lardon3d_task_sequence_break()`;
- Resource Governor capability selection from one fresh snapshot.
This private capability envelope is not:
- scientific identity;
- a new Project DB payload;
- a Task Kind version;
- a scheduler;
- a dependency graph;
- durable hardware identity.
The selected resource contract is immutable for one admitted sequence. A later sequence may receive a
different admitted contract.
A Task with no valid alternatives receives the honest fixed capability represented by its contract.
## Host CPU policy
The Resource Governor owns host CPU policy.
It determines:
- allowed affinity mask;
- package/core/SMT topology where available;
- interactive reserve;
- compute pool;
- actual worker affinity;
- per-kind admission bounds.
CPU IDs never enter:
- Task Kind descriptor;
- generic checkpoint;
- Project DB Task payload;
- scientific fingerprint.
Reference-host observations such as a 12-logical-CPU compute pool are evidence for that host, not a
portable registry ceiling.
The canonical repository policy is:
```text
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
After the interactive reserve and all safety constraints are preserved, useful validated compute
capacity should not remain idle merely because an older durable resource descriptor was smaller.
## OpenCV kinds
The OpenCV-controlled kinds include:
```text
features.extract
features.extract.sift
features.extract.rootsift
```
Exact historical CPU1/CPU12 descriptors may be recognized for restart compatibility.
The effective current Task request can use the portable positive OpenCV upper bound, while the Resource
Governor limits actual admission to the host compute pool.
The callback applies the admitted OpenCV thread count and restores the process-wide baseline on every
exit path.
Controlled validation at 1/2/4/8/12 threads preserved the required scientific outputs for the
validated ORB/SIFT/RootSIFT contracts.
Those thread counts are operational evidence and do not change:
- extractor fingerprint;
- Feature Set identity;
- Feature File format;
- Task Kind version.
For `features.extract.batch/1`, cross-image participants are the primary concurrency mechanism.
OpenCV CPU teams are not multiplied blindly inside every participant.
## Candidate Pair normalization
`candidate_pair.generate/1` reconstructs:
- `visual_index_id`;
- `after_feature_set_id`;
- `top_k`;
- `minimum_evidence_count`;
- `scanset_filter`;
- `exclude_same_asset`.
Exact earlier CPU1 and CPU12 resource shapes may be normalized in memory.
The current validated Candidate capability is represented by the current source/runtime contract,
including:
```text
fixed RAM 256 KiB
per-item RAM 8 MiB
batch range 1..64
CPU/batch coupled during useful scaling
GPU none
```
The registry does not reinterpret the old durable checkpoint as if it had been created with those
current values.
## Matcher reconstruction
`matcher.run/1` reconstructs the immutable Matcher configuration and durable
`after_candidate_pair_id` cursor.
Project DB v11 introduced the typed Matcher payload after Match Result persistence in v10.
### Durable backend-class compatibility
The reconstruct path accepts only explicitly supported exact historical/current resource signatures.
These include the validated classes required for:
- current CPU operation;
- current ORB AUTO/MIXED policy;
- explicit Vulkan operation;
- exact historical CPU/Vulkan restart shapes.
A neighboring signature fails rather than being treated as a backend hint.
No hardware identity is persisted in `matcher_tasks`.
### ORB AUTO
Only the AUTO/MIXED durable class reconstructs the shared GPU-first policy.
On a portable build with no usable Vulkan backend, the same durable policy can reconstruct while
exposing only the CPU capability.
Restoring a fixed CPU or fixed Vulkan historical Task does not mutate shared AUTO availability state.
Backend probing/initialization does not occur on the `project_open()` thread. Actual backend begin
belongs to admitted Queue execution.
### Current useful ORB bounds
The current production AUTO policy retains:
```text
GPU-first when eligible
CPU fallback complete
normal Vulkan inflight depth = 1
validated private safety depth = 2
normal useful Vulkan batch <= 8
```
Depth 2 and Matcher batch 12 remain validated private/safety or benchmark capabilities where
applicable, but did not meet the useful-throughput deadband for normal AUTO policy.
Those performance decisions do not change:
- `matcher.run/1`;
- Matcher fingerprint;
- Match Result identity;
- Match File bytes;
- durable cursor.
## Geometric Verifier reconstruction
`geometric_verifier.run/1` reloads:
- immutable Fundamental verifier configuration;
- validated verifier fingerprint;
- `after_match_result_id`.
Project DB v13 adds only its typed Task payload.
The exact historical serial resource descriptor is recognized for compatibility and may be normalized
ephemerally to the current outer-parallel capability.
The current validated execution shape allows:
- up to 8 useful participants;
- a safe admitted window up to 16 Match Results;
- 8 MiB per admitted parent;
- owner-only ordered publication.
The scientific USAC/MAGSAC inner solver remains in its validated serial configuration. Outer
cross-item concurrency does not change verifier science.
## Track Builder reconstruction
`track_builder.run/1` reconstructs the explicit immutable scope from:
- Project DB v15 typed Task payload;
- validated little-endian scope asset;
- exact builder selector/fingerprint;
- exact GVR scope identity.
Corruption, unsupported version, fingerprint mismatch, checksum mismatch, non-canonical ordering,
duplicates or invalid scope identity make the Task non-reconstructible.
Track Builder restart does not invent a scope from current Project DB contents.
## Sparse SfM reconstruction
```text
sparse_sfm.run/1
```
is **PASS/FROZEN**.
It reloads the explicit Project DB v17 payload, restores the persisted generic Task estimate and
replays the frozen Sparse SfM D/E execution from explicit Track Set and calibration-scope references.
F0 is recomputed from the scientific payload.
The generic checkpoint remains version 1.
The historical fixed CPU1/batch1 Sparse SfM estimate is part of that frozen Task contract. It is not a
global argument for serializing unrelated modern Task Kinds.
## Incremental reconstruction
```text
incremental_reconstruction.run/1
```
is **PASS/FROZEN**.
It reloads the Project DB v18 payload containing:
- predecessor reconstruction;
- extension Track Set;
- calibration scope;
- Phase H fingerprint.
Restart recomputes from those immutable inputs.
No solver state, generic DAG or hidden dependency edge is persisted.
## Generic checkpoint and Project DB boundary
The generic checkpoint codec remains version 1.
Project DB stores Task Kind and Task Kind version in the generic Task summary.
Rows migrated from the oldest untyped schema remain:
```text
task_kind = NULL
task_kind_version = NULL
```
and are classified as:
```text
LEGACY_UNTYPED
```
They remain inspectable but cannot be reconstructed as a typed production Task.
Unknown kinds are classified separately from unsupported versions.
A future unknown Task Kind or unsupported Task Kind version must never trigger guessed code execution.
No function address is persisted.
## Project-open recovery
`project_open()` uses the immutable production registry to restore eligible Tasks.
The recovery flow is conceptually:
```text
load bounded Task page
-> copy durable record outside DB mutex
-> validate checkpoint and summary
-> classify Task Kind/version
-> reconstruct Task-specific binding
-> apply exact legacy resource normalization when allowed
-> restore Lardon3DTask
-> enqueue through normal Queue
```
A full Queue window stops further recovery without mutating unprocessed Tasks.
Task-local failures remain task-local unless the project schema/identity itself is invalid.
## Resource feedback is not scientific state
Private feedback may record bounded throughput observations per Task Kind/backend.
It may influence a later resource contract only where the capability contract permits adaptation.
It must not modify:
- scientific inputs;
- Task-specific durable cursor;
- Project DB identity;
- Task Kind/version;
- generic checkpoint codec;
- scientific fingerprint;
- canonical result ordering.
Only successfully completed durable work counts as useful work for adaptive feedback where that Task
contract requires it.
Uncertain publication does not become a successful scientific sample.
## Scratch boundary
The current production registry contains sixteen Task Kinds, but none has an authoritative
scratch-consuming scientific Task contract.
```text
CURRENT_PRODUCTION_TASK_KINDS=16
CURRENT_SCRATCH_CONSUMING_TASK_KINDS=0
```
The external SSD controller and Resource Governor scratch wrappers remain operational infrastructure.
Scratch, swap and zram do not enlarge RAM admission.
A future scratch consumer requires an explicit Task-specific eligibility, lifetime and cleanup
contract. Merely registering the SSD does not change a Task Kind.
## What the registry does not own
The registry does not own:
- Queue scheduling;
- resource admission;
- CPU topology;
- GPU selection;
- scratch allocation policy;
- Project DB migration;
- scientific fingerprint definitions;
- artifact path resolution;
- dependency/DAG planning;
- autosave timers;
- orphan reconciliation.
It only provides typed durable dispatch and reconstruction ownership.
## Current intentionally unfinished work
These remain outside the registry contract:
- global orphan-file reconciliation;
- generic dependency/DAG scheduling;
- any future new Task Kind not explicitly added to the production array;
- future resource-capability changes requiring new proof.
Existing reconstructible Task Kinds already checkpoint at their Task-specific durable boundaries.
The registry must not move those cursors forward with an independent autosave timer.
## Current checkpoint evidence
The current registry inventory includes both additive selected-execution kinds:
```text
raw.develop.batch/1
features.extract.batch/1
```
The retained A6000 checkpoint is:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
That proof exercised the current upstream/downstream registry through:
- selected RAW batch;
- selected Feature batch;
- Visual Index;
- Candidate Pair;
- Matcher;
- Geometric Verifier v3;
- Track Builder;
and stopped before real Sparse SfM.
The current production inventory remains exactly sixteen Task Kinds.
## Summary
```text
CURRENT_PROJECT_DB_SCHEMA=v25
CURRENT_PRODUCTION_TASK_KINDS=16
CURRENT_SCRATCH_CONSUMING_TASK_KINDS=0
TASK_KIND_REGISTRY_STATUS=IMPLEMENTED
TASK_KIND_REGISTRY_CAPACITY=64_DESCRIPTORS
GENERIC_CHECKPOINT_VERSION=1
RAW_BATCH_TASK=raw.develop.batch/1
FEATURE_BATCH_TASK=features.extract.batch/1
COMPUTE_GOVERNOR_V2=PASS/FROZEN
ORB_VULKAN_ASYNC_EXECUTION=PASS/FROZEN
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
REAL_A6000_PRE_SFM=PASS/FROZEN
```

File diff suppressed because it is too large Load diff

View file

@ -1,526 +1,450 @@
# Track Model v1 # Track Model v1
## Scope ## Status
Track Model v1 est le contrat persistant qui transforme les correspondances ```text
géométriquement vérifiées en structures multi-view cohérentes. Il stocke des TRACK_MODEL_V1=FROZEN
ensembles d'observations 2D liées à un même point physique supposé. Il ne TRACK_BUILDER_V1=PASS/FROZEN
calcule rien, ne triangule pas, ne contient aucune coordonnée 3D et ne résout
aucun conflit. Le Track Builder, la triangulation, le Sparse SfM et le Bundle CURRENT_PRODUCTION_VERIFIER=FUNDAMENTAL_V3
Adjustment sont des étapes séparées ; Gate E a gelé le Builder v1 sans SPARSE_SFM_CAPABILITY=IMPLEMENTED_THROUGH_GATE_G
implémenter ces étapes 3D. REAL_S21_SPARSE_SFM=NOT_EXECUTED
REAL_A6000_SPARSE_SFM=NOT_EXECUTED
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Track Model v1 is the persistent scientific contract for coherent multi-view 2D observation sets.
A Track is **not** a 3D point.
It contains no camera pose, triangulated coordinate, reprojection error or Bundle Adjustment state.
Track Builder v1 constructs Tracks from verified Geometric Verification Results. Sparse SfM consumes an
immutable Track Set later.
## Pipeline position
Current pipeline:
```text
Feature Set
-> Candidate Pair
-> Match Result
-> Geometric Verifier v3
-> Geometric Verification Result
-> Track Builder v1
-> Track Model v1
-> Sparse SfM capability
```
Sparse SfM Gates C through G are implemented and frozen.
The retained S21 and A6000 historical campaigns stop before real Sparse SfM because known calibration
data is unavailable for those campaigns.
Older Track Model text that called Sparse SfM "future" describes historical lifecycle, not current
implementation status.
## Track definition ## Track definition
Un **Track** est un ensemble d'observations 2D cohérentes d'un même point A Track is a coherent set of 2D observations believed to correspond to the same physical scene point
physique supposé, observé à travers plusieurs images. Chaque observation est across multiple images.
identifiée par `(feature_set_id, feature_index)`.
Un Track n'est **pas** un point 3D. Il ne contient aucune coordonnée 3D, Observation identity is exactly:
aucune erreur de reprojection, aucun statut de triangulation. La
triangulation appartient à une étape ultérieure.
La chaîne scientifique correcte est :
```text ```text
Matcher → Match Result → Geometric Verification → Track Builder v1
→ Track Model → Sparse SfM (futur)
```
Le Matcher ne produit pas les Tracks. Le Track Builder v1 les assemble à partir
des Geometric Verification Results.
## Observation identity
Une observation est identifiée par :
```
(feature_set_id, feature_index) (feature_set_id, feature_index)
``` ```
- `feature_set_id` : identifiant SQLite AUTOINCREMENT du Feature Set. Le `feature_set_id` identifies one immutable Feature Set.
Feature Set porte directement `image_id` comme colonne NOT NULL FK. L'image
est dérivable par `SELECT image_id FROM feature_sets WHERE feature_set_id=?`.
- `feature_index` : ordinal zero-based dans le tableau de keypoints du Feature
File, stable tant que le Feature Set existe. Un Feature Set publié est
immutable : aucune API de production ne modifie ses colonnes après INSERT.
L'identité `(feature_set_id, feature_index)` est suffisante. Il est inutile `feature_index` is the zero-based keypoint ordinal inside that immutable Feature File.
de porter `image_id` dans la table d'observations car il est dérivable via
`feature_sets.image_id`.
Note : `feature_sets` ne possède pas de colonne d'état. L'existence d'une The Feature Set directly owns `image_id`; image identity is therefore derivable and is not duplicated
ligne publiée dans la table constitue le contrat réel de disponibilité du in Track observation identity.
Feature Set.
## Scientific inputs ## Scientific input
Les Tracks sont construits exclusivement à partir de : Track Builder consumes only completed verified geometric results selected by one exact verifier
identity.
``` Current production lineage:
Geometric Verification Result
status == GEOMETRIC_VERIFIED (2)
```
correspondant exactement au VERIFICATION_SELECTOR du Track Set.
Pour chaque résultat vérifié, les entrées du Match File dont le bit
correspondant dans le masque d'inliers vaut 1 fournissent les correspondances
valides. La chaîne de dérivation est :
```text ```text
GVR → match_result_id verifier_kind = FUNDAMENTAL
→ candidate_pair + feature_set_id_a + feature_set_id_b verifier_version = 3
→ Match File entry[i] = (feature_index_a, feature_index_b, distance) verifier_fingerprint =
→ bit i du masque d'inliers = 1 6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
→ observation A: (feature_set_id_a, feature_index_a)
→ observation B: (feature_set_id_b, feature_index_b)
``` ```
Un `GEOMETRIC_REJECTED` ne produit aucun track. Un Match Result non vérifié Historical Track Sets created from Fundamental verifier v1 or v2 remain valid historical scientific
géométriquement ne suffit pas. objects.
## VERIFICATION_SELECTOR They must not be relabelled as v3.
Le VERIFICATION_SELECTOR définit la configuration de Geometric Verification For each selected `GEOMETRIC_VERIFIED` result, only Match File entries whose corresponding inlier-mask
éligible pour un Track Set. Il est stocké sur le Track Set et fait partie de bit is one contribute observation edges.
son identité de reuse.
``` A rejected GVR contributes no Track edge.
(
verifier_kind INTEGER, -- ex: 1 = FUNDAMENTAL ## Verification selector
verifier_version INTEGER,
parameter_fingerprint BLOB(32) A Track Set stores the exact verifier selector:
)
```text
verifier_kind
verifier_version
verifier_fingerprint
``` ```
Le Track Builder ne consomme que les GVR avec `status == GEOMETRIC_VERIFIED` The builder never selects verification evidence using:
correspondant exactement à ce tuple. Aucune sélection par timestamp, "latest"
ou ordre d'insertion n'est permise.
Valeur production : `(1, 1, SHA-256 de l'encodage canonique 84 octets)`. - timestamp;
- "latest";
- insertion order;
- approximate fingerprint match.
## INPUT_SCOPE The current default producer is v3, but the Track Model remains version-independent and can store valid
sets from explicitly supported historical selectors.
L'INPUT_SCOPE représente l'ensemble scientifique réel des entrées consommées ## Input scope
par une Track Generation donnée. Il est distinct du VERIFICATION_SELECTOR :
le selector dit quels GVR sont admissibles, le scope dit quels GVR ont
effectivement été consommés.
``` A Track Set also records the exact consumed GVR scope.
input_scope_hash BLOB(32) -- SHA-256 canonique
gvr_count INTEGER -- nombre de GVR consommés Canonical scope identity uses:
```text
domain: L3DTSIS1
items: geometric_verification_result_id
order: strictly increasing
encoding: uint64 little-endian
digest: SHA-256
``` ```
### INPUT_SCOPE_HASH Conceptually:
| Propriété | Valeur | ```text
|-----------|--------| SHA-256(L3DTSIS1 || id_0 || id_1 || ... || id_N)
| Domain/version | `L3DTSIS1` (8 octets ASCII) | ```
| Items | `geometric_verification_result_id` des GVR consommés |
| Canonical ordering | IDs triés par ordre croissant |
| Serialization | Chaque ID : 8 octets little-endian |
| Digest | SHA-256 |
| DB-local IDs | OUI — le reuse est scoped à une DB projet |
| Duplicate handling | Inutile — les IDs sont uniques par construction |
| Empty scope | Interdit — un Track Set sans GVR n'a pas de sens |
Le digest est calculé sur `L3DTSIS1` (8 octets) suivi des IDs sérialisés : The scope is Project-DB-local because SQLite GVR IDs participate directly.
`SHA-256(L3DTSIS1 || id_0 || id_1 || ... || id_N)` où chaque `id_i` est
8 octets little-endian et les IDs sont triés par ordre croissant.
Le `gvr_count` est stocké comme métadonnée de validation. Il permet de `gvr_count` is retained as validation metadata.
détecter un scope incomplet sans re-hasher. Il ne fait pas partie du hash
lui-même.
Le scope_hash est DB-local : il utilise les `geometric_verification_result_id` An empty scope is invalid.
SQLite. Deux DB distinctes avec les mêmes données produiront des IDs
différents. Le reuse est donc scoped à une seule DB projet.
## Track membership invariants ## Track membership invariants
1. **Minimum structurel** : un Track contient au moins 2 observations. ### Minimum size
Une seule observation ne constitue aucune relation multi-view. Le futur
Track Builder v1, la triangulation ou le Sparse SfM pourront appliquer des
critères plus stricts. Le Model ne fixe pas de plafond de reconstruction.
2. **One observation per image** : un Track ne contient pas deux observations A Track has at least two observations.
issues de la même image. Cette contrainte est validée par l'API lors de la
création. Le schéma v1 ne dénormalise pas `image_id` dans
`track_observations` ; l'API vérifie déterministement la relation via
`feature_sets.image_id` avant publication sous `BEGIN IMMEDIATE`.
**SQL** : non protégé (pas de colonne `image_id` dans `track_observations`). ### At most one observation per image
**API** : validation par jointure `feature_sets.image_id` avant INSERT.
3. **Observation unique across tracks** : dans un même Track Set, une One Track cannot contain two observations derived from the same image.
observation `(feature_set_id, feature_index)` n'appartient qu'à un seul
Track.
**SQL** : `PRIMARY KEY(track_set_id, feature_set_id, feature_index)` sur This is validated through `feature_sets.image_id`.
`track_observations`. Le `track_set_id` est dénormalisé depuis `tracks`.
**API** : validation que `track_set_id` correspond au `track_set_id` du
`track_id` parent.
4. **Feature Set existence** : chaque `feature_set_id` référencé existe dans ### Observation uniqueness inside one Track Set
la table `feature_sets`. La FK SQLite garantit la référence.
**SQL** : `REFERENCES feature_sets(feature_set_id)`. Within one Track Set:
5. **Feature index bounds** : `feature_index < feature_sets.feature_count` ```text
pour l'observation correspondante. (feature_set_id, feature_index)
```
**SQL** : `CHECK(feature_index >= 0)`. belongs to at most one Track.
**API** : validation de la borne supérieure via `feature_sets.feature_count`
(SQLite CHECK ne peut pas référencer une autre table). The persistence schema enforces this using the Track Set-scoped primary key.
### Feature Set existence
Every referenced Feature Set must exist.
### Feature index bound
For every observation:
```text
0 <= feature_index < feature_count
```
The upper bound is validated against the referenced Feature Set.
### Parent consistency
The denormalized Track Set ID carried by an observation must equal the Track Set of its parent Track.
## Track identity ## Track identity
Un Track persistant possède un identifiant opaque : Persistent Track identity is the opaque SQLite:
``` ```text
track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0) track_id
``` ```
Il n'a pas d'identité scientifique dérivée de son contenu en v1. Les raisons : Track Model v1 does not define a content-derived Track hash.
- un hash de membership rendrait les INSERTs dépendants de l'ordre ; Reproducibility and reuse are owned by the Track Set identity, builder configuration and exact input
- le contenu d'un track peut être reconstruit depuis les GVR sources ; scope.
- un `track_id` opaque suffit pour la persistance, le référencement et la
pagination ;
- la corruption est détectée par cohérence interne (doublons, images
manquantes, index hors bornes) plutôt que par re-hash.
La reproductibilité est assurée au niveau du Track Set (parent), pas du Track ## Track Set identity
individuel.
## Track Set / Generation A Track Set is one complete immutable generation.
Un **Track Set** est le parent obligatoire de tout Track persistant. Il Its reuse identity contains:
représente une génération complète de Track Building.
Champs : ```text
builder_kind
``` builder_version
track_set_id INTEGER PK AUTOINCREMENT builder_parameter_fingerprint
builder_kind TEXT(1..64) verifier_kind
builder_version INTEGER > 0 verifier_version
parameter_fingerprint BLOB(32) verifier_fingerprint
verifier_kind INTEGER -- VERIFICATION_SELECTOR
verifier_version INTEGER
verifier_fingerprint BLOB(32)
input_scope_hash BLOB(32)
gvr_count INTEGER >= 1
track_count INTEGER >= 0
created_at INTEGER >= 0
```
### Identité de reuse
```
(
builder_kind,
builder_version,
parameter_fingerprint,
verifier_kind,
verifier_version,
verifier_fingerprint,
input_scope_hash input_scope_hash
)
``` ```
`gvr_count` est stocké comme métadonnée de validation mais ne fait pas `gvr_count` validates the scope metadata but is not an independent reuse discriminator.
partie de l'identité de reuse. Le même `input_scope_hash` avec un `gvr_count`
différent indiquerait une corruption (hash cohérent mais nombre de sources
incohérent).
Un set existant avec cette identité exacte est réutilisé. `INSERT OR REPLACE` `INSERT OR REPLACE` is forbidden.
est interdit.
### Immutabilité An exact existing immutable set is reused.
Un Track Set publié est **immutable**. Aucune opération d'append, remove ou A scientifically different scope/configuration creates a new Track Set.
merge n'est permise sur un track ou un set existant.
L'invalidation scientifique (nouvelle entrée, nouveau scope, nouvelle ## Immutability
configuration) produit un nouveau Track Set. Le set précédent reste intact.
La suppression référentielle utilise `ON DELETE CASCADE` : supprimer un A published Track Set is immutable.
Track Set supprime ses tracks et observations.
### Justification No production operation:
- chaque rebuild crée un nouveau set, les anciens restent intacts ; - appends to it;
- plusieurs configurations peuvent coexister (expérimentation) ; - removes observations;
- l'invalidation est simple : supprimer un set supprime ses tracks via - merges existing Tracks;
CASCADE ; - rewrites memberships;
- la reproductibilité est portée par le fingerprint et le scope_hash ; - updates it to a newer verifier version.
- pas d'UPDATE/INSERT/MERGE sur des tracks existants ;
- cohérent avec tous les résultats publiés existants (Feature Sets, Match
Results, GVRs) qui sont immutables après publication.
Le Track Builder v1 construit en mémoire, puis publie un set complet New evidence creates a new generation.
dans une transaction. Aucun track n'est visible avant que le set entier soit
validé.
## Immutability / incrementality Historical generations remain queryable until explicitly deleted.
Un Track publié dans un set est **immutable**.
L'incrémentalité est gérée par création de nouveaux sets :
1. nouvelles images → nouveaux Match Results → nouveaux GVR → nouveau
Track Set ;
2. le set précédent reste valide et consultable ;
3. le futur Sparse SfM choisira quel set consommer.
Cette approche est cohérente avec la philosophie Lardon3D :
- résultats atomiques ;
- pas de destruction silencieuse ;
- reprise à frontière connue ;
- conservation de l'historique.
## Persistence ## Persistence
### Conceptual schema Track storage was introduced by Project DB v14.
```sql Durable Track Builder Task payload persistence was added in Project DB v15.
CREATE TABLE track_sets(
track_set_id INTEGER PRIMARY KEY AUTOINCREMENT
CHECK(track_set_id > 0),
builder_kind TEXT NOT NULL
CHECK(length(builder_kind) > 0 AND length(builder_kind) <= 64),
builder_version INTEGER NOT NULL CHECK(builder_version > 0),
parameter_fingerprint BLOB NOT NULL
CHECK(length(parameter_fingerprint) = 32),
verifier_kind INTEGER NOT NULL CHECK(verifier_kind > 0),
verifier_version INTEGER NOT NULL CHECK(verifier_version > 0),
verifier_fingerprint BLOB NOT NULL
CHECK(length(verifier_fingerprint) = 32),
input_scope_hash BLOB NOT NULL
CHECK(length(input_scope_hash) = 32),
gvr_count INTEGER NOT NULL CHECK(gvr_count >= 1),
track_count INTEGER NOT NULL CHECK(track_count >= 0),
created_at INTEGER NOT NULL CHECK(created_at >= 0),
UNIQUE(builder_kind, builder_version, parameter_fingerprint,
verifier_kind, verifier_version, verifier_fingerprint,
input_scope_hash)
);
CREATE TABLE tracks( Later schema migrations through v25 do not reinterpret Track Model v1.
track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0),
track_set_id INTEGER NOT NULL
REFERENCES track_sets(track_set_id) ON DELETE CASCADE,
observation_count INTEGER NOT NULL CHECK(observation_count >= 2)
);
CREATE INDEX tracks_set_idx Conceptual tables:
ON tracks(track_set_id, track_id);
CREATE TABLE track_observations( ```text
track_set_id INTEGER NOT NULL, track_sets
track_id INTEGER NOT NULL tracks
REFERENCES tracks(track_id) ON DELETE CASCADE, track_observations
feature_set_id INTEGER NOT NULL
REFERENCES feature_sets(feature_set_id),
feature_index INTEGER NOT NULL CHECK(feature_index >= 0),
position_in_track INTEGER NOT NULL CHECK(position_in_track >= 0),
PRIMARY KEY(track_set_id, feature_set_id, feature_index),
UNIQUE(track_id, position_in_track)
);
CREATE INDEX track_observations_lookup_idx
ON track_observations(feature_set_id, feature_index, track_set_id);
``` ```
### Schema invariants Publication is atomic for the complete Track Set under one transaction.
**SQL-enforced :** No Track from that set becomes visible before the complete generation validates and commits.
- `track_observations.PRIMARY KEY(track_set_id, feature_set_id, feature_index)` Rollback leaves no partial Track Set.
: dans un Track Set donné, une observation n'apparaît qu'une fois. Cela
garantit qu'une observation scientifique appartient à au plus un Track dans
ce set.
- `REFERENCES tracks(track_id) ON DELETE CASCADE` : l'observation appartient
à un track existant ; supprimer le track supprime l'observation.
- `REFERENCES feature_sets(feature_set_id)` : le Feature Set existe.
- `CHECK(observation_count >= 2)` : minimum structurel.
- `UNIQUE(builder_kind, builder_version, parameter_fingerprint,
verifier_kind, verifier_version, verifier_fingerprint,
input_scope_hash)` sur `track_sets` : identité de reuse, empêche les
doublons de set pour une même configuration et un même scope.
- `ON DELETE CASCADE` depuis `track_sets` : supprimer un set supprime tout.
- `CHECK(feature_index >= 0)` : borne inférieure de l'index.
- `UNIQUE(track_id, position_in_track)` : chaque position dans un track est
unique. L'ordre est déterminé par le Track Builder lors de la publication.
**API-enforced :** ## Ordering
- `track_set_id` dans `track_observations` correspond au `track_set_id` du Track Builder publishes deterministic canonical order.
`track_id` parent. Le schéma ne comporte pas de FK composite (aucun
précédent dans le codebase). L'API valide cette cohérence avant INSERT sous
`BEGIN IMMEDIATE`.
- Une seule observation par image par track. L'API valide via jointure à
`feature_sets.image_id`.
- `feature_index < feature_sets.feature_count`. L'API valide la borne
supérieure.
- `observation_count` cohérent avec le nombre réel d'observations insérées.
- `track_count` cohérent avec le nombre réel de tracks insérés.
- `position_in_track` contigu à partir de 0 pour chaque track.
### Note sur la dénormalisation `position_in_track` is contiguous from zero.
`track_set_id` dans `track_observations` dénormalise une clé grandparent, The exact builder contract owns edge ordering and conflict resolution; Track Model only persists the
après le même pattern utilisé par `visual_index_memberships.visual_index_id`. validated result.
Le pattern parent-key-in-UNIQUE est déjà répandu dans le codebase. La cohérence
repose sur le chemin d'écriture unique du Track Builder et la validation API
sous transaction.
`track_observations.track_set_id` n'a pas de FK directe vers `track_sets` No hash-table iteration order may define persistent scientific ordering.
pour éviter un second chemin CASCADE depuis `track_sets` vers
`track_observations` (le premier chemin passe par `tracks`). La cohérence
est garantie par l'API sous `BEGIN IMMEDIATE`.
## Provenance ## Deletion
### Track Set provenance Deleting a Track Set cascades to its Tracks and observations.
Chaque Track Set conserve : A Feature Set referenced by a Track observation cannot be silently removed while the reference remains
valid.
- `builder_kind`, `builder_version`, `parameter_fingerprint` : configuration Deletion semantics do not mutate other immutable Track Sets.
du Track Builder ;
- `verifier_kind`, `verifier_version`, `verifier_fingerprint` : configuration
du Geometric Verifier consommé ;
- `input_scope_hash`, `gvr_count` : ensemble réel des GVR consommés.
Ces champs suffisent pour identifier la configuration scientifique complète ## Pagination and resource bounds
ayant produit le set.
### Edge provenance (deferred) Track Model storage APIs are paged.
En v1, la provenance détaillée (quels GVR spécifiques ont contribué à quel The model does not impose an arbitrary scientific maximum Track length below the number of images that
track individuel) n'est pas persistée. Les raisons : could legitimately observe the same point.
- elle peut être reconstruite en comparant les memberships du set aux GVR It does not materialize a dense image-by-image covisibility matrix.
disponibles ;
- une table `track_set_sources` volumineuse complexifie la DB sans bénéfice
immédiat ;
- une future version du Track Builder pourra l'ajouter dans une migration
ultérieure.
## Invalidation Loading one Track loads that Track's observations; project-wide traversal remains paged.
### Invalidation scientifique Execution-memory strategy belongs to Track Builder, not Track Model.
Un nouveau scope, une nouvelle configuration de verifier ou un nouveau
builder produit un **nouveau** Track Set avec une identité différente. Le set
précédent reste intact et consultable. Aucune mutation silencieuse n'est
permise.
### Suppression référentielle
`ON DELETE CASCADE` s'applique :
- `track_sets``tracks``track_observations` : supprimer un set supprime
tous ses tracks et observations ;
- `feature_sets` → (pas de CASCADE vers `track_observations`) : la FK utilise
le comportement par défaut (NO ACTION). Supprimer un Feature Set référencé
par une observation est interdit tant que l'observation existe.
## Atomic publication
L'unité persistante est le Track Set complet. La publication est une seule
transaction `BEGIN IMMEDIATE` contenant l'INSERT du set, de tous ses tracks
et de toutes ses observations.
- aucun track n'est visible avant le COMMIT du set entier ;
- un rollback ne laisse aucune ligne partielle ;
- le `created_at` du set est le timestamp de la transaction ;
- le `track_count` et `gvr_count` sont validés contre les INSERTs réels.
Le Track Builder v1 utilise le Task Runtime pour le checkpoint/reprise et le
Resource Governor pour l'admission. Le Model ne contient aucune
logique d'exécution.
## Resource bounds
- **Pas de plafond de longueur arbitraire** : le Model ne fixe pas de
maximum sur le nombre d'observations par Track. Un projet avec N images
peut produire des tracks de longueur jusqu'à N.
- **Lecture paginée** : `list_tracks` et `list_track_sets` utilisent une
page de 64 entrées avec curseur.
- **Chargement borné** : load track by id charge les observations du track ;
la taille est bornée naturellement par le nombre d'images dans le scope.
- **Pas de chargement complet du graphe** : aucune API ne charge tous les
tracks et toutes les observations d'un projet en une seule fois.
- **Pas de matrice dense** : aucune matrice de co-visibilité N×N n'est
matérialisée par le Model.
## Corruption handling ## Corruption handling
Le loader doit détecter : A loader returns corruption rather than partial best-effort data when it detects conditions such as:
- track absent (`track_id` référencé mais inexistant) ; - missing parent Track or Track Set;
- observation invalide (`feature_set_id` inexistant) ; - missing Feature Set;
- duplicate observation dans un même track set ; - duplicate observation in one Track Set;
- deux observations de la même image dans un même track ; - repeated image inside one Track;
- `feature_index` hors bornes du Feature Set ; - out-of-range feature index;
- `observation_count` incohérent avec le nombre réel d'observations ; - inconsistent observation count;
- `track_set_id` dans `track_observations` ne correspondant pas au - inconsistent Track count;
`track_set_id` du `track_id` parent ; - inconsistent denormalized Track Set ID;
- `track_set` parent absent. - invalid/non-contiguous position ordering.
Toute corruption retourne `CORRUPT` sans résultat partiel. No loader repairs scientific identity in place.
## API ## Provenance
L'API publique implémente : Track Set provenance includes:
- `lardon3d_project_db_create_track_set()` — INSERT set + ses tracks + ```text
observations dans une seule transaction `BEGIN IMMEDIATE`. builder identity
- `lardon3d_project_db_load_track_set()` — SELECT par ID. verifier selector
- `lardon3d_project_db_find_track_set()` — SELECT par identité exacte. input scope hash
- `lardon3d_project_db_list_track_sets()` — SELECT paginé ORDER BY id, gvr count
page 64. ```
- `lardon3d_project_db_load_track()` — SELECT par ID avec observations.
- `lardon3d_project_db_list_tracks()` — SELECT par set, paginé ORDER BY
id, page 64.
- `lardon3d_project_db_find_track_by_observation()` — recherche par
`(feature_set_id, feature_index)` dans un set donné.
La création valide en C : existence des Feature Sets, bornes des Detailed per-edge provenance is not persisted by Track Model v1.
`feature_index`, unicité des observations, unicité image par track,
`observation_count` cohérent, `track_set_id` cohérent. L'INSERT est
transactionnel.
## Explicitly out of scope Adding such provenance later requires an explicit version/schema decision if persistent representation
changes.
- Track Builder algorithmique (union-find, connected components) ; ## Current production verifier lineage
- triangulation ;
- coordonnées 3D ; Fundamental verifier v1 and v2 are historical scientific identities.
- Essential matrix ;
Current new production verification uses Fundamental v3.
V3 adds bounded preflight rejection before the unchanged scientific estimator path and has its own
fingerprint.
Track Builder consumes only exact matching GVR identities.
Therefore:
```text
HISTORICAL_TRACK_SET_VERIFIER_V1=VALID
HISTORICAL_TRACK_SET_VERIFIER_V2=VALID
CURRENT_TRACK_SET_VERIFIER_V3=PRODUCTION
```
No historical Track Set is upgraded in place.
## Real S21 evidence
The retained S21 Track proof is:
```text
REAL_S21_TRACKS=PASS/FROZEN
Track Set observations = 2,495,768
Tracks = 912,447
minimum Track length = 2
maximum Track length = 42
mean Track length = 2.7352470883240341
```
Retained digest:
```text
c30eba192627bf73eaf21ff30d81038d8cc6bbf36a69226f88cdc8c37f7d74a1
```
The compact memory model supersedes the older historical 18.204 GiB envelope.
That checkpoint did not execute real Sparse SfM.
## Real A6000 evidence
The current retained A6000 checkpoint is:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Track output:
```text
Track Set 1
Tracks 130,714
Track observations 318,944
duplicate obs 0
repeated-image 0
orphan obs 0
```
The upstream v3 GV scope contained:
```text
Applicable GVRs 37,805
Verified GVRs 10,952
Rejected GVRs 26,853
```
Restart traversed the retained scope and reused the same Track Set without creating a duplicate
scientific generation.
No Sparse SfM Task or Sparse Reconstruction was created.
## Sparse SfM relationship
Track Model does not perform Sparse SfM.
Sparse SfM capability is nevertheless implemented through Gate G.
Correct current statement:
```text
TRACK_MODEL_OUTPUT=AVAILABLE
SPARSE_SFM_IMPLEMENTATION=AVAILABLE
REAL_HISTORICAL_CAMPAIGN_SPARSE_SFM=BLOCKED_BY_KNOWN_CALIBRATION_DATA
```
These are separate lifecycle facts.
## Out of scope
Track Model v1 does not own:
- Track Builder union/find algorithm;
- Fundamental estimation;
- Essential estimation;
- camera pose; - camera pose;
- bundle adjustment ; - triangulation;
- sparse reconstruction / Sparse SfM ; - 3D coordinates;
- reprojection error; - reprojection error;
- Bundle Adjustment;
- dense reconstruction; - dense reconstruction;
- Track optimization ou merge ; - metric scale;
- mutation de tracks existants ; - Track mutation/merge;
- co-visibilité (matrice ou calcul) ; - selection by "latest".
- sélection par timestamp ou "latest".
## Track rejected state ## Summary
Le Model v1 ne persiste pas d'état Track rejected. Le Model représente des ```text
Tracks structurellement valides (≥ 2 observations, cohérents). Le Track Builder TRACK_MODEL_V1=FROZEN
v1 décide quels candidats publier. Les candidats non publiés n'existent pas dans TRACK_BUILDER_V1=PASS/FROZEN
le Model ; cette séparation reste la frontière scientifique figée.
## Versioning CURRENT_PRODUCTION_VERIFIER=FUNDAMENTAL_V3
CURRENT_VERIFIER_FINGERPRINT=6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
Project DB v14 introduced the Track storage and v15 adds only durable Track PROJECT_DB_TRACK_MODEL=v14
Builder task payload persistence. `builder_version` et `verifier_version` PROJECT_DB_TRACK_TASK=v15
décrivent indépendamment les contrats scientifiques. CURRENT_PROJECT_DB_SCHEMA=v25
Changer un algorithme n'impose une migration DB que si la représentation
persistante change. REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
SPARSE_SFM_CAPABILITY=IMPLEMENTED_THROUGH_GATE_G
REAL_S21_SPARSE_SFM=NOT_EXECUTED
REAL_A6000_SPARSE_SFM=NOT_EXECUTED
```

View file

@ -1,207 +1,430 @@
# Visual Index v1 # Visual Index v1
## Problème et frontière ## Status
Le Visual Index transforme une collection homogène de `FeatureSet` READY en ```text
candidats de recherche. Il consomme exclusivement `feature_set_id` et les VISUAL_INDEX_V1=IMPLEMENTED
descripteurs ORB lus par le Feature Reader. Il ne fait ni matching final, ni VISUAL_INDEX_KIND=orb-lsh
ratio test, ni vérification géométrique. VISUAL_INDEX_VERSION=1
## Choix algorithmique CANDIDATE_PAIR=IMPLEMENTED
MATCHER=IMPLEMENTED
La v1 utilise un LSH binaire déterministe à six tables. Chaque table extrait VISUAL_INDEX_GPU=REJECTED_WITH_MEASURED_REASON
24 positions distinctes des 256 bits ORB. La position v1 est CURRENT_PROJECT_DB_SCHEMA=v25
`(41*table + 11*bit) mod 256`; 11 étant premier avec 256, les 24 positions REAL_A6000_PRE_SFM=PASS/FROZEN
d'une table sont distinctes. Une clé est `(table_id, key24)`. Des descripteurs proches en ```
Hamming ont une probabilité élevée de collision dans au moins une table, sans
conversion flottante.
Alternatives évaluées : Visual Index turns a homogeneous collection of immutable Feature Sets into bounded image-retrieval
candidates.
- le hash exact est très compact et déterministe, mais son rappel s'effondre It is a retrieval stage, not a Matcher and not a geometric verifier.
dès qu'un descriptor varie d'un bit ;
- le multi-index hashing avec sous-chaînes et multiprobes offre des garanties
Hamming intéressantes, mais le nombre de postings/probes nécessaire au
rappel utile d'ORB est trop élevé pour une v1 bornée ;
- FLANN-LSH masque son format, ses allocations et sa stabilité de
sérialisation, ce qui nuit à la reprise et à l'audit ;
- BoW/IVF donne un bon retrieval image, mais impose vocabulaire, entraînement,
identité et politique de mise à jour avant de pouvoir être incrémental ;
- HNSW et FAISS ajoutent une dépendance et un état mutable complexes sans
avantage décisif à quelques milliers d'images.
Ce LSH n'est pas un matcher. Il privilégie une base déterministe, segmentable Current downstream consumers are implemented:
et contrôlable. Une évolution de la sélection de bits exige une nouvelle
`visual_index_version`.
## Identité et configuration ```text
Feature Store
-> Visual Index
-> Candidate Pair Generator
-> Matcher
-> Geometric Verification
-> Tracks
```
Le kind est `orb-lsh`, version 1. Un index contient exclusivement des Feature Older text describing Candidate Pair or Matcher as future consumers is historical design context and is
Sets de même `descriptor_type`, dimension, `extractor_kind`, version et not current status.
`parameter_fingerprint`. Sa configuration v1 contient :
- `table_count=6` ; ## Algorithm
- `key_bits=24` ;
- `max_features_per_set` entre 1 et 1024, défaut 512 ;
- `max_bucket_postings` entre 1 et 4096, défaut 256 ;
- `max_segments=256` ;
- `max_feature_sets_per_segment=16`.
Le fingerprint de paramètres est SHA-256 des 32 octets canoniques Visual Index v1 uses deterministic binary LSH over ORB descriptors.
`L3DVICF1`, version et cinq entiers little-endian. Aucun padding, JSON, locale
ou endianness hôte n'intervient. `visual_index_id` est une identité SQLite
`AUTOINCREMENT`, jamais réutilisée après publication.
## Échantillonnage It uses six tables.
Au plus `max_features_per_set` features sont indexées. La sélection v1 retient Each table selects 24 distinct positions from the 256 ORB bits.
le préfixe de `feature_index` croissant. Les postings conservent l'indice
original. Un Feature Set vide est membre valide sans posting. Le build Task
peut lire en parallèle jusqu'à douze Feature Files, avec un reader et une
tranche de 256 descripteurs privés par participant effectivement admis. Chaque
Feature Set écrit dans une tranche privée de la capacité de postings déjà
réservée pour le segment ; le propriétaire compacte ensuite les tranches dans
l'ordre de sélection et applique seul l'ordre total persistant.
## Segments introduits en Project Database v6, conservés en v7 The frozen v1 position rule is:
Un index logique possède des segments immuables READY. Chaque update publie un ```text
segment de un à seize nouveaux Feature Sets, puis ajoute atomiquement segments position = (41 * table + 11 * bit) mod 256
et memberships. `UNIQUE(visual_index_id,feature_set_id)` assure l'idempotence. ```
Une recherche copie au début la liste bornée des segments READY, relâche le
mutex DB, puis lit ce snapshot. Un segment commité au milieu sera visible à la
requête suivante.
SQLite conserve les tables `visual_indexes`, `visual_index_segments`, A posting key is:
`visual_index_memberships` et `visual_index_update_tasks`. Les gros postings
restent hors DB. La configuration et chaque membership sont immuables. La
compaction est `NOT_YET_WIRED`; au-delà de 256 segments une update est refusée avec
`LARDON3D_VISUAL_INDEX_LIMIT`. Avec seize membres par segment, la capacité v1 est donc
exactement 4096 Feature Sets par index. Le refus ne publie ni segment ni membership et
l'index existant reste requêtable.
Le DDL v6 exact est `schema_visual_v6` dans `src/project_db.c`. Il impose ```text
`AUTOINCREMENT` aux index/segments, les uniques (table_id, key24)
`(visual_index_id,generation)`, `(visual_index_id,sha256)` et ```
`(visual_index_id,feature_set_id)`, ainsi que les FKs vers index, Feature Set,
segment et tâche. Les CHECKS bornent tables 1..32, bits 8..32, sampling Changing this bit-selection policy requires a new Visual Index scientific version.
1..1024, bucket 1..4096, membres segment 1..16 et durabilité 0..1. La migration
entière reste sous `BEGIN IMMEDIATE` et possède une injection de rollback v6. ## Identity and configuration
Current kind/version:
```text
orb-lsh / 1
```
One index contains Feature Sets with homogeneous:
- descriptor type;
- descriptor dimension;
- extractor kind;
- extractor version;
- extractor parameter fingerprint.
Frozen v1 configuration contains:
```text
table_count = 6
key_bits = 24
max_features_per_set = 1..1024, default 512
max_bucket_postings = 1..4096, default 256
max_segments = 256
max_feature_sets_per_segment = 16
```
The canonical parameter fingerprint uses domain:
```text
L3DVICF1
```
with explicit little-endian fields.
No C struct padding, locale or host endianness enters the fingerprint.
## Sampling
At most `max_features_per_set` Feature entries are indexed.
V1 selects the increasing `feature_index` prefix.
Postings retain the original Feature index.
An empty Feature Set is a valid member and contributes no posting.
## Segment persistence
Project DB v6 introduced Visual Index persistence. v7 retained the model.
Later schema versions through v25 do not reinterpret Visual Index v1.
A logical index owns immutable READY segments.
One update publishes one segment containing between one and sixteen new Feature Sets.
Membership uniqueness is enforced on:
```text
(visual_index_id, feature_set_id)
```
A query snapshots the bounded READY segment list before asset reads.
A segment committed after that snapshot is visible to the next query, not retroactively injected into
the running query.
## Capacity
V1 currently allows:
```text
max_segments = 256
max_feature_sets_per_segment = 16
```
Therefore one v1 index can contain exactly up to:
```text
4096 Feature Sets
```
before another update returns the Visual Index limit.
This is an index-v1 capacity bound, not a project-wide image-count limit.
Compaction/base-delta redesign remains deferred.
## Segment File v1 ## Segment File v1
Le fichier est little-endian et ne sérialise aucune structure C. Layout : Segment File v1 is explicitly little-endian and does not serialize C structs.
| Offset | Taille | Champ | Magic:
|---:|---:|---|
| 0 | 8 | magic `L3DVIDX\0` |
| 8 | 4 | format version 1 |
| 12 | 4 | header size 128 |
| 16 | 4 | table count |
| 20 | 4 | key bits |
| 24 | 8 | posting count |
| 32 | 8 | member count |
| 40 | 8 | postings offset, 128 |
| 48 | 8 | total size |
| 56 | 32 | index parameter fingerprint |
| 88 | 32 | feature parameter fingerprint |
| 120 | 8 | réservés, zéro |
Chaque posting fait 24 octets : `table_id:u32`, `key24:u32`, ```text
`feature_set_id:u64`, `feature_index:u32`, réservé zéro `u32`. L'ordre est L3DVIDX\0
`table_id`, clé, Feature Set, feature index. Le fichier exact est SHA-256 et ```
vit sous `assets/visual-index/<2 hex>/<sha256 lowercase>`.
Publication : temporaire local, écriture, `fsync`, hash, `link` sans A posting contains:
écrasement, validation d'une adoption concurrente, `fsync` du répertoire, puis
transaction DB. Un échec après publication peut laisser un orphelin mais jamais
un segment READY partiel. La durabilité distingue `DURABLE` et
`PUBLISHED_NOT_DURABLE`.
Le reader vérifie le SHA avant le parsing. Un fichier au SHA et aux métadonnées cohérents ```text
mais portant une version future produit `UNSUPPORTED_VERSION`; les comptes et produits table_id:u32
d'offset invalides produisent `CORRUPT` avant allocation, conversion ou lecture de posting. key24:u32
feature_set_id:u64
feature_index:u32
reserved_zero:u32
```
## Recherche, score et bornes Canonical persistent ordering is:
L'API est centrée sur `(visual_index_id, query_feature_set_id)`. Elle accepte ```text
`ANY_SCANSET`, `SAME_SCANSET` ou `OTHER_SCANSETS`, l'exclusion du même asset, table_id
un minimum de preuves et `top_k` entre 1 et 256. Elle ne retourne jamais le key24
Feature Set ni l'image de requête. feature_set_id
feature_index
```
Une preuve est un `feature_index` de requête distinct ayant au moins une The complete Segment File is content-addressed by SHA-256 under the Visual Index asset tree.
collision avec le candidat. Plusieurs tables, postings ou descriptors du
candidat ne multiplient pas cette preuve. Le score final vaut
`evidence_count / sampled_query_feature_count` dans `[0,1]`. Le volume du
candidat ne peut donc pas augmenter le score sans preuve distincte. L'ordre est
score décroissant, preuves décroissantes, `image_id`, puis `feature_set_id`.
La burstiness est bornée par une contribution maximum par feature de requête ## Publication
et candidat. Une première passe additionne la fréquence d'un bucket sur tous
les segments du snapshot. Au-delà de `max_bucket_postings`, il est ignoré : un motif
très commun ne peut ni allouer une liste géante ni dominer le score. Le reader
lit au plus 256 postings par appel. L'accumulateur contient au plus 4096
candidats ; les nouveaux candidats sont ignorés après saturation, de manière
déterministe par l'ordre des postings. Aucun cache global n'existe et un seul
segment est ouvert à la fois.
Deux updates concurrentes peuvent sélectionner le même lot et construire le même asset. Publication follows the normal immutable-asset pattern:
La transaction SQLite et les contraintes uniques ne laissent publier qu'un segment et
un membership par Feature Set; l'autre update échoue/rejoue en no-op. Une query prend son
snapshot de métadonnées avant les lectures et ouvre/ferme un seul segment à la fois, y
compris avec 250 à 256 segments : le nombre de descripteurs de fichier reste borné.
## Tâche, reprise et ressources ```text
local temp
-> write
-> fsync
-> hash
-> no-overwrite publication/adoption validation
-> fsync directory
-> short Project DB transaction
```
`visual_index.update`, version 1, persiste `visual_index_id` et un curseur A physical file may remain orphaned if DB publication fails after the file is published.
`after_feature_set_id`. Une séquence traite au plus seize Feature Sets non
indexés, publie et commit un segment, puis checkpoint. Pause et annulation sont
coopératives entre lectures et avant publication ; un segment déjà READY reste
valide. La reprise recommence au dernier curseur commité et l'unicité des
memberships rend le rejeu idempotent.
**IMPLEMENTED — parallélisme interne borné.** La Queue exécute toujours un seul No partially committed READY segment is invented.
callback. L'estimation demande jusqu'à seize threads CPU, un slot I/O, GPU zéro,
8 Mio fixes et 2 Mio par Feature Set, lot 1..16. Le callback compte comme un
participant et crée au plus `cpu_threads - 1` enfants. Chaque enfant lit
exclusivement des Feature Files immuables et écrit une tranche privée ; il ne
touche ni au handle Project DB partagé, ni au fichier de segment, ni au curseur.
Tous les enfants sont joints avant tri, sérialisation, publication asset et
transaction SQLite.
La réduction emploie l'ordre total v1 Durability distinguishes:
`table_id,key24,feature_set_id,feature_index`. Le fichier, son SHA-256, le
chemin, les memberships, la génération, le fingerprint et les résultats de
requête sont donc exactement identiques à un build avec un participant. Une
erreur de lecture dans une tranche interdit toute publication ; une création
de thread refusée est remplacée par le calcul de cette tranche sur le callback,
sans changer la réduction. Le curseur n'avance qu'après la publication
transactionnelle du segment, puis le checkpoint existant reste le seul point
de reprise Task. `record_batch` reçoit le nombre de Feature Sets réellement
commités, la durée réelle et `peak_memory_bytes=0` (inconnue).
## Complexité et limites ```text
DURABLE
PUBLISHED_NOT_DURABLE
```
Pour `D` descriptors échantillonnés, construction et disque sont `O(6D)`. ## Reader validation
Une requête effectue `O(6Q log P + H)` par segment (`Q<=1024`, `H` hits bornés),
pas `O(images²)`. La mémoire build est bornée par les métadonnées, les tranches
de 256 descripteurs privées des participants et les postings d'un segment ; les
tranches privées partitionnent le buffer de postings existant et ne le
dupliquent pas. Chaque participant garde au plus un reader/FD de Feature File.
La mémoire query est bornée par 4096 candidats, 256 postings et 256 résultats.
À 3700 images et 512 features, environ 11,4
millions de postings sont produits. Un test structurel persiste 50 000 Feature Sets puis
confirme la pagination par 16 et le refus propre après 4096 memberships. Un index unique
ne couvre donc pas encore 50 000 images : le risque principal est le nombre de segments
et les seeks. Une compaction/base+delta ou une évolution v2 sera nécessaire, sans changer
les identités durables; elle est `NOT_YET_WIRED`.
Les fixtures de validation incluent un Feature Set vide, un crop réel, une rotation de The reader validates the asset SHA before trusting the format.
8 degrés, deux campagnes, un asset source partagé et une attaque de motif répétitif. Ces
tests valident le classement de candidats LSH, jamais une compatibilité géométrique.
## Frontière future It rejects:
Le Candidate Pair Generator pourra filtrer sur score et `evidence_count`, puis - invalid counts;
transmettre `feature_set_id + feature_index` au futur matcher. Le score Visual - invalid offsets;
Index ne constitue jamais une preuve géométrique. - overflow;
- malformed reserved fields;
- inconsistent DB metadata;
- unsupported future version.
A coherent future format version is `UNSUPPORTED_VERSION`, not generic corruption.
## Query
Query identity is centered on:
```text
(visual_index_id, query_feature_set_id)
```
Options include:
- ScanSet filter;
- same/other ScanSet policy;
- source-asset exclusion;
- minimum evidence count;
- `top_k` in `1..256`.
The source Feature Set/image is never returned as its own candidate.
## Evidence and score
One evidence unit is one distinct query `feature_index` that collides with the candidate.
Multiple tables or candidate postings do not multiply the same query-feature evidence.
Score:
```text
evidence_count / sampled_query_feature_count
```
Range:
```text
0..1
```
Canonical result order:
```text
score descending
evidence_count descending
image_id ascending
feature_set_id ascending
```
Visual Index score is retrieval evidence only.
It is not descriptor-match evidence and not geometric evidence.
## Burstiness bound
Bucket frequency is bounded across the retained query snapshot.
A bucket above `max_bucket_postings` is ignored.
This prevents common patterns from dominating score or creating unbounded posting accumulation.
The query accumulator is bounded to 4096 candidates and 256 returned results.
No global query cache is required.
## Durable Task
Task Kind:
```text
visual_index.update/1
```
Durable cursor:
```text
after_feature_set_id
```
One sequence handles a bounded admitted set of new Feature Sets, publishes a complete segment, commits
memberships, advances the cursor/checkpoint and returns through `sequence_break()` if more work remains.
Restart resumes from the durable cursor and membership uniqueness makes replay idempotent.
## Internal parallelism
The Queue owns one active heavy callback.
Visual Index may use bounded internal CPU participants inside that callback.
Current validated shape:
```text
CPU up to 16
batch/window 1..16
GPU 0
fixed RAM approximately 8 MiB
per-item RAM approximately 2 MiB
```
Each participant reads immutable Feature data into private work.
Participants do not publish the segment.
After join, the owner performs canonical total ordering, serialization, asset publication and Project DB
commit.
Thread-creation failure may fall back to owner computation of that slice without changing output.
## Determinism
The following must match the serial scientific result:
- posting set;
- posting order;
- Segment File bytes;
- SHA-256;
- membership set;
- generation ordering;
- query results.
Operational CPU width does not enter scientific identity.
## GPU policy
Current GPU classification:
```text
VISUAL_INDEX_GPU=REJECTED_WITH_MEASURED_REASON
```
The stage is dominated by posting construction, total ordering, hashing and deterministic publication,
and there is no validated production GPU seam that preserves the full contract with useful measured
benefit.
This does not authorize avoidable CPU serialism.
```text
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
## Current downstream relationship
Candidate Pair Generator is implemented and consumes Visual Index queries.
Matcher is implemented and consumes persisted Candidate Pairs.
Therefore current relationship is:
```text
Visual Index
-> Candidate Pair Generator
-> Candidate Pair persistence
-> Matcher
```
Visual Index does not pass raw `feature_set_id + feature_index` pairs directly into a hypothetical
future Matcher.
The persisted Candidate Pair boundary remains explicit.
## Real A6000 evidence
The retained A6000 proof contains:
```text
Feature Sets 689
Candidate Pairs 38,420
Match Results 38,420
```
Final continuation replayed no new Visual Index work.
Checkpoint:
```text
real-a6000-pre-sfm-2026-09-02
REAL_A6000_PRE_SFM=PASS/FROZEN
```
This confirms the current Visual Index path was already durably reusable before downstream GV/Tracks
continuation.
## Limits
Current v1 limits/non-goals include:
- no segment compaction;
- one index limited to 4096 Feature Sets;
- no GPU backend;
- no geometric meaning assigned to retrieval score;
- no dense project-wide pair matrix.
A future index version may change capacity or data structure only through an explicit versioned
scientific/persistence decision.
## Summary
```text
VISUAL_INDEX_V1=IMPLEMENTED
VISUAL_INDEX_KIND=orb-lsh
VISUAL_INDEX_VERSION=1
VISUAL_INDEX_CAPACITY=4096_FEATURE_SETS
VISUAL_INDEX_SEGMENT_MEMBERS=16
VISUAL_INDEX_TOP_K_MAX=256
VISUAL_INDEX_TASK=visual_index.update/1
VISUAL_INDEX_GPU=REJECTED_WITH_MEASURED_REASON
CANDIDATE_PAIR=IMPLEMENTED
MATCHER=IMPLEMENTED
CURRENT_PROJECT_DB_SCHEMA=v25
REAL_A6000_PRE_SFM=PASS/FROZEN
```

View file

@ -0,0 +1,625 @@
# Lardon3D — Documentation Inventory Audit
## Status
DOCUMENTATION_INVENTORY_AUDIT=PASS_WITH_FINDINGS
Audit branch: docs-audit
Reference checkpoints:
- global-maintenance-2026-09-01
- real-a6000-pre-sfm-2026-09-02
This audit concerns documentation only.
It does not reopen scientific contracts, alter executable behavior, redefine FROZEN evidence, or authorize implementation work.
## Classification vocabulary
Documents/findings use: CURRENT_AND_COMPLETE, CURRENT_BUT_INCOMPLETE, PARTIALLY_STALE, STALE, HISTORICAL_AND_VALID, DUPLICATED_AUTHORITY, AMBIGUOUS_AUTHORITY, NEEDS_REORGANIZATION.
Finding severity: BLOCKING_DOC, IMPORTANT, IMPROVEMENT.
A BLOCKING_DOC finding means documentation could make a future agent work from the wrong project state. It does not mean executable code is known to be incorrect.
## Current facts
Current Project DB head: v25.
Valid historical/additive foundations remain:
- v22 = selected scientific execution foundation
- v23 = generic optical-context overlay
- v24 = raw.develop.batch persistence
- v25 = features.extract.batch persistence
Current production inventory: 16 Task kinds.
Canonical resource policy:
- MAXIMUM SAFE USEFUL THROUGHPUT
- SERIALISM_REQUIRES_PROOF
Reference-host evidence:
- 16 logical CPUs total
- approximately 4 logical CPUs reserved for interactive host use
- approximately 12 logical CPUs available to compute
- approximately 3 GiB MemAvailable hard reserve
- Radeon 780M UMA
These are observations of the reference host, not portable product constants.
Real-data checkpoints:
- REAL_S21_TRACKS=PASS/FROZEN
- REAL_A6000_PRE_SFM=PASS/FROZEN
A6000 final pre-SfM evidence:
- Feature Sets: 689
- Candidate Pairs: 38,420
- Match Results: 38,420
- Applicable GVR: 37,805
- Verified GVR: 10,952
- Rejected GVR: 26,853
- Track Sets: 1
- Tracks: 130,714
- Track observations: 318,944
- Sparse SfM: NOT EXECUTED
- Dense/MVS: NOT EXECUTED
# BLOCKING_DOC findings
## DOC-B01 — Roadmap contains mutually exclusive current states
File: docs/roadmap/roadmap.md
The roadmap contains the acquired REAL_A6000_PRE_SFM=PASS/FROZEN result but also retains obsolete current-next material describing Project DB v24/v25 as validation in progress and instructing a resume from A6000 cursor 259 with 430 RAW representations remaining.
Those operations have already completed.
Required correction:
- preserve useful historical evidence;
- mark the cursor-259 and validation-in-progress material as historical;
- make the final A6000 pre-SfM proof authoritative;
- define one unambiguous CURRENT NEXT state.
## DOC-B02 — Project Database has competing current heads
File: docs/architecture/project_database.md
Different sections present v23, v24 and v25 as current state.
The actual current schema head is v25.
v22, v23 and v24 remain valid historical and additive contracts. They must not be erased or rewritten as though they never existed.
Required correction:
- state once near the top that current Project DB head is v25;
- preserve v22 selected execution, v23 optics and v24 RAW batch as historical additive foundations;
- update v25 Feature batch lifecycle to match acquired real evidence.
## DOC-B03 — Persistence advertises v23 as current
File: docs/architecture/persistence.md
The document currently states that the current schema is v23.
Required correction:
- current schema = v25;
- retain historical v7, v16, v22 and v23 contracts;
- add a concise v24 RAW batch and v25 Feature batch persistence summary.
## DOC-B04 — Resource-aware pipeline describes superseded serial Features
File: docs/architecture/resource_aware_pipeline.md
Current prose still describes Feature Extraction as one durable task per image with batch one and one worker as the complete operational model.
That description is incomplete after features.extract.batch/1, Project DB v25, bounded cross-image participants, coupled CPU/batch scaling and owner-only ordered publication.
The document also retains a fourteen-Task current inventory even though production now contains sixteen Task kinds.
Required correction:
- preserve per-image Feature scientific atomicity;
- document the v25 cross-image batch path;
- distinguish per-item atomicity from cross-item serialism;
- update current Task count to sixteen.
## DOC-B05 — Development testing guide uses obsolete API examples
File: docs/development/testing.md
The guide contains examples based on old-style names including task_estimate_t, task_t, task_create, TASK_STATE_IDLE and g_test_*.
These examples do not represent the current repository API and test style.
Because this is a developer instruction document rather than historical evidence, stale examples are dangerous.
It also retains repeated wipe-build and fixed -j8 assumptions that conflict with current delta-validation and host-aware parallelism policy.
Required correction:
- rewrite examples against actual current repository tests and public APIs;
- use current Meson targets and validation discipline;
- preserve exact sanitizer qualifications;
- avoid mandatory repeated full rebuilds when the delta does not justify them.
## DOC-B06 — Sparse SfM opening lifecycle is historical but presented as current
File: docs/architecture/sparse_sfm.md
The opening still describes numerical Sparse SfM as deferred to Gate C and later gates.
Later sections and other canonical documents establish Gates C, D, E, F and G as acquired.
Required correction:
- retain Gate A and Gate B history;
- label early lifecycle wording as historical;
- add a concise current lifecycle summary;
- distinguish implemented Sparse SfM capability from real Sparse SfM execution on current historical campaigns, which remains unexecuted because known calibration data is unavailable.
## DOC-B07 — README presents obsolete current DB state
File: README.md
README still presents Project DB v23 as the effective current head.
Required correction:
- current Project DB = v25;
- mention RAW batch v24 and Feature batch v25;
- register REAL_S21_TRACKS=PASS/FROZEN;
- register REAL_A6000_PRE_SFM=PASS/FROZEN;
- state clearly that Sparse SfM and Dense/MVS were not executed in the A6000 real pre-SfM proof.
# IMPORTANT findings
## DOC-I01 — AGENTS.md still describes v25 proof as unfinished
File: AGENTS.md
The resource policy and Source Comment Contract are strong and current.
However, lifecycle prose still describes the v25 Feature-batch tranche as not yet eligible for final closure.
Required correction:
- update the v25 lifecycle wording to match acquired evidence;
- register REAL_A6000_PRE_SFM=PASS/FROZEN;
- retain global-maintenance-2026-09-01 as valid historical review evidence;
- document the later A6000 checkpoint without erasing the maintenance checkpoint.
## DOC-I02 — Resource Boundary contains stale current schema and Task count
File: docs/architecture/resource_boundary.md
Present-tense material still contains combinations of current schema v24, Project Database v23 as current identity owner, fourteen current Task kinds and migrations only through v23.
The no-generic-Resource-System decision remains valid and must remain historical authority.
Required correction:
- update only stale present-tense current-state claims;
- current schema = v25;
- current production inventory = 16 Task kinds;
- preserve the original Gate G and no-new-subsystem decisions.
## DOC-I03 — Architecture overview weakens the canonical resource policy
File: docs/architecture/overview.md
The overview states that host stability and TUI responsiveness have priority over maximum throughput without the qualification introduced by the current canonical policy.
The current rule is to preserve the defined interactive host reserve and then maximize safe useful throughput.
Leaving additional safe and useful resources idle is not a stability strategy.
Required correction:
- align the overview with MAXIMUM SAFE USEFUL THROUGHPUT;
- preserve the interactive host reserve as the safety boundary;
- summarize v24 and v25 operational overlays without making reference-host values portable constants.
## DOC-I04 — Candidate resource sections contradict each other
File: docs/architecture/candidate_pair.md
The current Task section describes approximately 256 KiB fixed memory, 8 MiB per admitted item, batch 1..64 and coupled CPU/batch scaling.
A later resource section still describes historical values around a 24-source window and 64 KiB per item.
Required correction:
- reconcile the later resource section with the current validated Task capability;
- preserve Candidate scientific identity, scoring, ordering and persistence;
- keep historical estimates only when explicitly labelled historical.
## DOC-I05 — Feature Store omits the v25 selected-execution batch path
File: docs/architecture/feature_store.md
The scientific Feature File contract is strong, but operational prose still describes only the historical single-image features.extract/1 path as the production model.
Required correction:
- preserve features.extract/1;
- document features.extract.batch/1 as an additional current operational path;
- distinguish per-image scientific atomicity from cross-image execution concurrency;
- keep general DAG planning separately deferred.
## DOC-I06 — Task Kind Registry uses an obsolete 15-kind anchor
File: docs/architecture/task_kind_registry.md
The document correctly states that production contains 16 Task kinds but links to resource_governor.md#audit-des-15-kinds-de-production.
Required correction:
- synchronize the Resource Governor heading and Registry link around the current 16-kind inventory;
- do not alter historical audit counts that were correct at their checkpoint.
## DOC-I07 — Generic Task adaptation prose conflicts with coupled CPU/batch kinds
File: docs/architecture/task_system.md
Generic Compute Governor prose says CPU and batch are never tried together.
Feature Batch and Candidate now have legitimate coupled CPU/batch rungs because additional CPU cannot exercise additional independent work while the admitted item window remains one.
Required correction:
- retain independent-dimension adaptation as the generic rule;
- document explicit coupled cross-item exceptions where the dimensions are operationally inseparable for measurement;
- do not make coupled scaling universal.
## DOC-I08 — Build guide retains historical fixed -j8 examples
File: docs/development/build.md
Build examples repeatedly use -j8 while current engineering policy requires safe host-aware parallelism.
Required correction:
- describe build parallelism as host-derived;
- a reference-host example may use approximately -j12 when clearly labelled as reference-host evidence;
- no fixed job count becomes a portable product constant.
## DOC-I09 — Concurrency guide retains historical build-policy examples
File: docs/development/concurrency.md
The concurrency, lifetime and TSan qualification rules are valuable and mostly current, but the build example still uses fixed -j8 and some illustrative snippets do not represent exact current public API names.
Required correction:
- align build parallelism with host-aware policy;
- identify illustrative pseudo-code as illustrative when it is not exact repository API;
- preserve the external OpenCV and TBB TSan qualification;
- preserve the separate Vulkan validation boundary.
## DOC-I10 — Visual Index describes implemented downstream work as future
File: docs/architecture/visual_index.md
The final future section still describes Candidate Pair Generator and Matcher as future consumers.
Both are implemented.
Required correction:
- replace the stale future wording with the actual current downstream relationship or explicitly mark it as historical design context;
- do not change Visual Index scientific identity or capacity contracts.
## DOC-I11 — Track Model contains stale production selector and pipeline wording
File: docs/architecture/tracks.md
The Track Model scientific contract remains valid, but some prose still presents Geometric Verifier v1 as the production selector and Sparse SfM as future.
Current real Track evidence uses Geometric Verifier v3. Sparse SfM Gates C through G are implemented, while real known-calibration Sparse SfM on the historical real campaigns remains unexecuted.
Required correction:
- preserve Track Model v1 identity and persistence semantics;
- distinguish historical verifier-v1 examples from the current v3 production lineage;
- distinguish implemented Sparse SfM capability from real campaign execution.
# Document classification
## CURRENT_AND_COMPLETE
The following documents are currently strong enough that no major contract rewrite is justified by this audit:
- docs/architecture/calibration_science_v1.md
- docs/architecture/calibration_bootstrap.md
- docs/architecture/calibration_solver_preflight_v1.md
- docs/architecture/photo_quality_triage.md
- docs/architecture/task_queue.md
- docs/architecture/matcher.md
- docs/architecture/track_builder.md
- docs/performance/target_hardware.md
Small consistency edits may still be appropriate later.
## CURRENT_BUT_INCOMPLETE
The following documents have strong core contracts but need limited current-state reconciliation:
- docs/architecture/internal_parallelism.md
- docs/architecture/runtime.md
- docs/architecture/geometric_verification.md
- docs/architecture/geometric_verifier.md
## HISTORICAL_AND_VALID
The following documents must remain historical evidence and must not be mass-modernized:
- docs/architecture/global_maintenance_audit.md
- docs/architecture/foundation_review.md
- docs/concepts/matching_and_tracks.md
- docs/concepts/reconstruction_layers.md
Older Task counts, schema versions, measurements and decisions may be exactly correct for the checkpoint described by those documents.
An old version number is stale only when the prose claims that it is the current state.
# Documentation authority findings
## AUTH-01 — Current Project DB authority is ambiguous
Current-version claims are distributed across:
- README.md
- docs/architecture/project_database.md
- docs/architecture/persistence.md
- docs/architecture/resource_boundary.md
- docs/roadmap/roadmap.md
They must converge on one current-state fact:
Project DB current schema = v25
Detailed v22, v23, v24 and v25 contracts remain owned by the specialized architecture documents.
## AUTH-02 — Resource policy is duplicated across too many authorities
Resource policy currently appears in AGENTS, Governor, Resource Boundary, Resource Aware Pipeline, Internal Parallelism, Overview, Target Hardware, Build and Concurrency documentation.
Desired authority split:
- docs/architecture/resource_governor.md owns runtime resource policy;
- AGENTS.md owns engineering and agent obligations;
- docs/performance/target_hardware.md owns reference-host measurements;
- other documents summarize and link instead of redefining policy.
## AUTH-03 — Latest real checkpoint needs first-class documentation
The repository now contains the real checkpoint real-a6000-pre-sfm-2026-09-02.
Documentation must explain its relationship to global-maintenance-2026-09-01.
The newer checkpoint does not erase the maintenance checkpoint. The maintenance checkpoint remains historical review authority for unchanged frozen systems; the A6000 checkpoint adds later operational and real-data evidence.
## LINK-01 — Task Kind Registry anchor is stale
docs/architecture/task_kind_registry.md states sixteen kinds but points to a Resource Governor anchor named for fifteen kinds.
The heading and cross-link must be synchronized without altering historical fourteen-kind audit evidence.
# Repository language decision
Human authority has selected English as the canonical repository language.
The final repository target is:
- DOCUMENTATION_LANGUAGE=ENGLISH
- SOURCE_COMMENT_LANGUAGE=ENGLISH
- AGENT_CONTRACT_LANGUAGE=ENGLISH
- USER_INTERFACE_LANGUAGE=ENGLISH
All technical documentation, agent contracts, prompt files and production source comments are to converge on English during the remediation passes.
Translation must preserve scientific meaning, historical truth, FROZEN contracts, identities, numeric values, lifecycle state and evidence. A historical document may be translated but must not be silently modernized.
The canonical TUI language is English. Existing non-English UI strings are to be remediated in an explicitly scoped UI-language pass; this documentation audit does not itself authorize executable UI changes.
# Product documentation intentionally deferred
This documentation cleanup must not invent final contracts for:
- Viewer behavior;
- Sony A6000 live acquisition;
- Samsung S21 live acquisition;
- Coverage Analysis;
- Capture Guidance;
- suggested viewpoints;
- video and keyframe ingestion;
- final optics onboarding UX;
- optics profile import and export;
- final mesh, texture and export UX.
Those areas will be defined explicitly by the human during the PRODUCT_DEFINITION phase before the final prompt/ execution contract is frozen.
Already established human product intent includes:
- NEW_CAMERA_REQUIRES_CODE_CHANGE=NO
- NEW_LENS_REQUIRES_CODE_CHANGE=NO
- ELECTRONIC_LENS_WITH_METADATA=SUPPORTED
- MANUAL_LENS_WITHOUT_EXIF=SUPPORTED
- MULTIPLE_LENSES_PER_CAMERA=SUPPORTED
- ZOOM_MULTIPLE_FOCALS=SUPPORTED
- MULTIPLE_OPTICAL_CONFIGURATIONS_PER_PROJECT=SUPPORTED
- SILENT_CALIBRATION_SUBSTITUTION=FORBIDDEN
- SILENT_LENS_IDENTITY_INFERENCE=FORBIDDEN
- OPTICS_TUI_WORKFLOW=REQUIRED
- PROFILE_IMPORT_EXPORT=REQUIRED
These statements describe product intent only. They do not authorize implementation during this documentation audit.
# Documentation remediation order
## D1 — Current-state authority
Correct first:
- README.md
- AGENTS.md
- docs/roadmap/roadmap.md
- docs/architecture/project_database.md
- docs/architecture/persistence.md
Goal: one coherent current lifecycle and one explicit Project DB v25 head.
## D2 — Resource and runtime consistency
Then correct:
- docs/architecture/resource_boundary.md
- docs/architecture/resource_aware_pipeline.md
- docs/architecture/overview.md
- docs/architecture/candidate_pair.md
- docs/architecture/feature_store.md
- docs/architecture/task_kind_registry.md
- docs/architecture/task_system.md
Goal: current operational descriptions obey MAXIMUM SAFE USEFUL THROUGHPUT and SERIALISM_REQUIRES_PROOF without rewriting frozen science.
## D3 — Developer instructions
Then correct:
- docs/development/testing.md
- docs/development/build.md
- docs/development/concurrency.md
Goal: developer instructions match current APIs, build policy and validation discipline.
## D4 — Targeted scientific and current-state wording cleanup
Then inspect and correct only stale lifecycle wording in:
- docs/architecture/sparse_sfm.md
- docs/architecture/tracks.md
- docs/architecture/visual_index.md
- docs/architecture/geometric_verification.md
- docs/architecture/geometric_verifier.md
- docs/architecture/runtime.md
This phase must not reopen FROZEN science.
## D5 — Documentation index and links
Finally reconcile README navigation, historical/current labels and cross-document anchors.
# Finding summary
BLOCKING_DOC = 7
IMPORTANT = 11
Highest-priority files:
1. docs/roadmap/roadmap.md
2. docs/architecture/project_database.md
3. docs/architecture/persistence.md
4. docs/architecture/resource_aware_pipeline.md
5. docs/development/testing.md
6. docs/architecture/sparse_sfm.md
7. README.md
# Audit boundary
This report does not authorize:
- scientific threshold changes;
- new schema versions;
- new Task kinds;
- resource-policy redesign;
- Viewer implementation;
- live-capture implementation;
- Sparse SfM execution;
- Dense/MVS execution.
The next separate audit is SOURCE_COMMENT_AUDIT over include/lardon3d/** and src/**.
Source comments will later be classified as:
- EXCELLENT
- GOOD
- ACCEPTABLE
- UNDER_COMMENTED
- SEVERELY_UNDER_COMMENTED
- STALE_COMMENT
- MISLEADING_CONTRACT_COMMENT
The comment audit will focus on WHY, CONTRACT, INVARIANT, OWNERSHIP, IDENTITY, persistence ordering, restart, cancellation, concurrency, resource accounting and FROZEN boundaries.
No mass source-comment editing is authorized by this inventory.
# Remediation closure — 2026-09-02
The documentation finding-remediation pass is complete.
```text
D1=PASS
D2=PASS
D3=PASS
D4=PASS
D5=PASS
BLOCKING_DOC_RESOLVED=7/7
IMPORTANT_RESOLVED=11/11
AUTHORITY_FINDINGS_RESOLVED=3/3
LINK_FINDINGS_RESOLVED=1/1
```
D1 converged README, AGENTS, roadmap, Project DB and persistence on the current v25 lifecycle.
D2 reconciled resource/runtime documentation with sixteen production Task kinds,
`MAXIMUM_SAFE_USEFUL_THROUGHPUT`, `SERIALISM_REQUIRES_PROOF`, the v24 RAW batch path and the v25
Feature batch path.
D3 replaced stale developer API/build examples, removed fixed build-width policy and preserved the
qualified sanitizer/TSan/Vulkan evidence boundaries.
D4 reconciled Sparse SfM lifecycle, Track verifier lineage, Visual Index downstream status, current
Geometric Verifier v3 and runtime current-state wording without reopening frozen scientific contracts.
D5 adds `docs/README.md` as the navigation/authority map and removes the stale fifteen-kind
cross-document anchor dependency.
The historical `global-maintenance-2026-09-01` checkpoint remains authoritative evidence for the
unchanged boundaries it reviewed. The later `real-a6000-pre-sfm-2026-09-02` checkpoint adds real-data
operational evidence and does not erase the maintenance checkpoint.
No source code, schema, Task Kind, scientific threshold, Sparse SfM execution, Dense/MVS execution,
viewer or live-capture implementation is authorized by this closure.
## Language normalization boundary
Human authority selected English as the repository language.
The documents modified by the finding-remediation pass are English. Other untouched documentation may
still contain historical or current non-English prose. That remaining language-only normalization is
tracked separately and must not be mistaken for an unresolved current-state authority finding.
Translation of historical evidence must preserve the historical checkpoint exactly.
# Phase state
- DOCUMENTATION_INVENTORY_AUDIT=PASS_WITH_FINDINGS
- DOCUMENTATION_FINDING_REMEDIATION=PASS
- DOCUMENTATION_LANGUAGE_NORMALIZATION=IN_PROGRESS
- SOURCE_COMMENT_AUDIT=PASS
- SOURCE_COMMENT_REMEDIATION=PASS
- PRODUCT_DEFINITION=PASS/FROZEN
- PROMPT_TREE=NEXT

View file

@ -0,0 +1,99 @@
# Lardon3D — Source Comment Audit
## Status
```text
SOURCE_COMMENT_AUDIT=PASS
SOURCE_COMMENT_REMEDIATION=PASS
SOURCE_COMMENT_SCOPE=include/lardon3d/** + src/**
SOURCE_COMMENT_LANGUAGE=ENGLISH
SOURCE_CODE_BEHAVIOR_CHANGED=NO
```
The audit covered 148 C/C++ source/header files under `include/lardon3d/**` and `src/**`.
The initial static inventory reported:
```text
comment blocks 807
raw non-English lexical candidates 33
actual French comment blocks 29
English false positives 4
stale-contract candidates 0
raw unannotated-public declarations 324
```
## Remediation
All 29 actual French source comments were translated to English.
The four raw language-detector false positives were already English and were left unchanged.
The remediation touched comments only. A file-level lexical comparison removed comments entirely and verified that every non-comment source
byte remained identical before and after the declared replacements.
No executable UI/error string was changed in this pass. UI language is a separate explicitly scoped
implementation pass.
## Stale and misleading comments
The lexical stale-contract pass found zero candidate comments.
No scientific threshold, schema version, Task Kind, resource contract, persistence identity or FROZEN
boundary was changed by this audit.
## Public API adjacency heuristic
The first inventory reported 324 public declarations without an immediately adjacent comment.
That number is **informational only**, not 324 defects.
The repository Source Comment Contract requires comments where WHY, CONTRACT, INVARIANT, OWNERSHIP,
IDENTITY, persistence ordering, restart, cancellation, concurrency, resource accounting or a FROZEN
boundary is non-obvious. It does not require one redundant comment for every getter, thin wrapper,
paired create/load/list function or declaration already covered by a surrounding contract block.
Therefore absence of an adjacent comment alone is not a valid
`UNDER_COMMENTED`/`SEVERELY_UNDER_COMMENTED` finding.
Future audits must review semantic contract coverage rather than using declaration adjacency as a
blanket failure criterion.
## Source Comment Contract
Production source comments:
- are English;
- explain non-obvious WHY/CONTRACT/INVARIANT facts;
- preserve ownership and lifetime boundaries;
- preserve persistence/restart ordering;
- preserve concurrency/resource-accounting boundaries;
- preserve scientific identity and FROZEN constraints;
- do not paraphrase obvious code line-by-line;
- are updated when behavior changes.
## Validation
The remediation runner requires:
```text
branch = docs-audit
no unrelated worktree changes
all expected old comments match exactly once
file-level source with all comments removed remains byte-for-byte identical
no strong French-comment candidate remains
git diff --check passes
```
## Phase state
```text
DOCUMENTATION_FINDING_REMEDIATION=PASS
DOCUMENTATION_LANGUAGE_NORMALIZATION=IN_PROGRESS
SOURCE_COMMENT_AUDIT=PASS
SOURCE_COMMENT_REMEDIATION=PASS
PRODUCT_DEFINITION=PASS/FROZEN
PROMPT_TREE=NEXT
```

View file

@ -1,193 +1,293 @@
# Instructions de build # Build
## Prérequis ## Status
- **OS** : Linux (testé sur distributions récentes) ```text
- **Compilateur** : Clang (recommandé) ou GCC BUILD_SYSTEM=MESON_NINJA
- **Système de build** : Meson + Ninja PUBLIC_API_LANGUAGE=C17
- **Dépendances principales** : ncursesw, SQLite, OpenSSL, GIO/GLib, OpenCV, IMPLEMENTATION_LANGUAGES=C17_CXX17
LibRaw, libexif, libpng, libdeflate, Ceres ; Vulkan reste optionnel BUILD_PARALLELISM=HOST_AWARE
- **Langages** : API publiques C17 et implémentation mixte C17/C++17 RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
## Bootstrap des outils Meson is the build-system authority. Do not duplicate dependency-version truth
in this document when `meson.build` already enforces it.
Les commandes ci-dessous installent seulement le compilateur, Meson/Ninja, ## Requirements
`pkg-config` et ncurses. Les bibliothèques listées plus haut doivent aussi être
disponibles dans les versions acceptées par `meson.build`; Meson reste la source Lardon3D targets Linux.
de vérité et refuse explicitement une dépendance absente ou incompatible.
Primary toolchain:
```text
Clang or GCC
Meson
Ninja
pkg-config
```
Major dependencies currently include ncursesw, SQLite, OpenSSL, GIO/GLib,
OpenCV, LibRaw, libexif, libpng, libdeflate and Ceres. Vulkan remains optional
at configuration level.
Public APIs are C17. Implementation is mixed C17/C++17.
## Bootstrap examples
These commands install only the basic compiler/build front end; Meson remains
authoritative for the complete dependency set.
Debian/Ubuntu:
```sh ```sh
# Debian / Ubuntu
sudo apt install clang meson ninja-build libncursesw5-dev pkg-config sudo apt install clang meson ninja-build libncursesw5-dev pkg-config
```
# Fedora Fedora:
```sh
sudo dnf install clang meson ninja-build ncurses-devel pkg-config sudo dnf install clang meson ninja-build ncurses-devel pkg-config
```
# Arch Arch Linux:
```sh
sudo pacman -S clang meson ninja ncurses pkgconf sudo pacman -S clang meson ninja ncurses pkgconf
``` ```
## Build standard ## Standard build
First configuration:
```sh ```sh
# Première configuration CC=clang CXX=clang++ meson setup build
CC=clang meson setup build ```
# Arbre existant Existing tree:
```sh
meson setup --reconfigure build meson setup --reconfigure build
meson compile -C build -j8 meson compile -C build
``` ```
### Options utiles Do not hard-code `-j8` as project policy.
Meson/Ninja should use host-appropriate parallelism unless a specific
validation has a reason to constrain it.
## Build parallelism policy
The build is not governed by a portable fixed job count.
Canonical policy:
```text
preserve the interactive host reserve
then use maximum safe useful throughput
```
A reference host measurement such as 8 or 12 useful jobs is evidence for that
host at that time, not a repository constant.
If memory-heavy compilation or another active workload creates pressure,
reduce build width for that run. Do not convert the temporary reduction into a
global documentation rule.
## Reconfigure versus wipe
Prefer incremental reuse:
```sh ```sh
# Build de debug (défaut) meson setup --reconfigure build
meson setup build --wipe meson compile -C build
# Build de release
meson setup build --wipe --buildtype=release
# Build avec optimisations aggressive
meson setup build --wipe --buildtype=release -Db_lto=true
``` ```
Use `--wipe` only when a fresh configuration is actually required, such as:
- switching sanitizer configuration in the same directory;
- changing compiler family;
- changing a configuration whose cached state cannot be reused safely;
- reproducing a clean release/global-maintenance proof;
- recovering from a stale or corrupt build directory.
A normal edit/test loop should not wipe the build tree repeatedly.
## Release build
Use an explicit release directory or deliberate reconfiguration.
Example:
```sh
CC=clang CXX=clang++ meson setup build-release --buildtype=release
meson compile -C build-release
```
For LTO:
```sh
CC=clang CXX=clang++ meson setup build-release-lto --buildtype=release -Db_lto=true
meson compile -C build-release-lto
```
Separate directories avoid destroying a useful incremental debug tree.
## Vulkan configuration
Portable CPU-only proof:
```sh
CC=clang CXX=clang++ meson setup build-portable -Dvulkan_orb=disabled
meson compile -C build-portable
```
Vulkan-enabled proof:
```sh
CC=clang CXX=clang++ meson setup build-vulkan -Dvulkan_orb=enabled
meson compile -C build-vulkan
```
A Vulkan-on build is not automatically a proof that every scientific path uses
or should use the GPU.
Current production GPU promotion remains limited by each subsystem's validated
backend contract.
## Validation ## Validation
Normal configured tests:
```sh ```sh
# Tests unitaires
meson test -C build --print-errorlogs meson test -C build --print-errorlogs
# Vérification du style (whitespace)
git diff --check
# Vérification autonome d'un header C public modifié
cc -x c -std=c17 -fsyntax-only -Iinclude \
-include lardon3d/<header>.h /dev/null
``` ```
### Preuve fraîche de maintenance globale — 1er septembre 2026 Whitespace/style boundary:
Le [registre canonique](../architecture/global_maintenance_audit.md) conserve
le détail et les qualifications. Les résultats reproductibles acquis sont :
| Configuration fraîche | Compilateurs/options | Build | Suite |
| --- | --- | ---: | ---: |
| portable | Clang/Clang++ 22.1.8, C17/C++17, `-Dvulkan_orb=disabled` | 931/931 | 64/64 sériel |
| Vulkan | Clang/Clang++ 22.1.8, C17/C++17, `-Dvulkan_orb=enabled` | 939/939 | 65/65 sériel |
| ASan/UBSan portable | Clang/Clang++ 22.1.8, `address,undefined` | graphe complet | 64/64 avec LSan désactivé après attribution externe |
| TSan portable | GCC/G++ 16.2.1, Vulkan désactivé | cibles concurrentes | 14/14 + 220 répétitions |
La suite Vulkan comprend `orb-vulkan-backend` sur la Radeon 780M RADV PHOENIX
réelle. La cible de feasibility SIFT/RootSIFT, non enregistrée dans la suite,
a été compilée/exécutée séparément : zéro divergence de décision Lowe mais des
divergences d'index et de bits de distance, donc aucune promotion en backend
production. Les probes stricts GCC/Clang C17+C++17 passent 76/76 sur les
19 headers publics modifiés ou nouveaux, ainsi que le fixture ABI, le lien
application et `git diff --check`; `scan3d/` reste intact.
L'unique revue finale indépendante GPT-5.6 SOL/ULTRA a conclu PASS sans finding
bloquant. Elle a indépendamment rejoué le build portable, la suite complète
64/64, une matrice focalisée 15/15, les 76/76 probes de headers, l'ABI, les
négatifs de seams production, le SHA-256 du manifest GV retenu et le diff-check.
Le statut canonique est donc `GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN` ; les
qualifications sanitizer ci-dessous restent néanmoins partie de la preuve.
Une validation post-freeze a ensuite attribué le délai intermittent de
`test-feature-task` à la capture de la télémétrie hôte réelle par ses Governors
synthétiques. Le fixture utilise maintenant un `ResourceSnapshot` complet,
privé, par Governor et compilé pour cette seule cible ; production continue de
lire la télémétrie réelle et n'exporte aucun seam. Après correction du second
Governor relevé en revue, Feature passe 100/100, la matrice ordonnée 4/4 et les
suites finales portable/Vulkan 64/64 et 65/65 ; ASan/UBSan ciblé avec
`detect_leaks=0` et TSan passent. Le registre canonique conserve la régression
charge 5 `WAIT`/charge 0 `START` et la qualification exacte. Un timeout `task`
isolé dans une suite normale mixte après reconstruction large reste
non reproductible : le ciblé immédiat et sa matrice de revue 100/100 passent,
sans modification de Task ni de son timeout.
La première suite LSan complète est volontairement conservée comme non-PASS :
57 OK, 6 FAIL et 1 timeout. Cinq échecs partagent exactement la fuite externe
OpenCL de 3 808 octets/68 allocations ; les deux anomalies de 30 s n'ont aucun
diagnostic sanitizer. Le délai Feature, absent du suivi initial, a ensuite été
reproduit et corrigé comme décrit ci-dessus ; le délai Task reste non
reproductible. La suite entière passe 64/64 avec ASan/UBSan actifs et
`detect_leaks=0`, tandis qu'un sous-ensemble prouvé sans loader OpenCV/OpenCL
passe 20/20 avec LSan actif. Il est donc incorrect de résumer cette preuve par
« LSan 64/64 ».
Le log Clang complet a aussi été audité. La conversion publique Sparse SfM
`uint32_t → int` était matérielle et a été corrigée avec validation ciblée ; les
autres émissions sont soit des conversions baseline déjà bornées, soit des
tests/benchmarks, soit des headers OpenCV/Ceres externes. Les emplacements et
justifications exacts restent centralisés dans le registre afin de ne pas
dupliquer une seconde liste normative ici.
## Build ASan/UBSan (debug mémoire)
À exécuter pour tout ticket touchant la mémoire, les durées de vie ou les
allocations :
```sh ```sh
CC=clang meson setup build-asan --wipe \ git diff --check
-Db_sanitize=address,undefined ```
meson compile -C build-asan -j8
Public C header probe:
```sh
cc -x c -std=c17 -fsyntax-only -Iinclude -include lardon3d/<header>.h /dev/null
```
Use `docs/development/testing.md` for sanitizer and validation policy.
## ASan / UBSan build
Example dedicated directory:
```sh
CC=clang CXX=clang++ meson setup build-asan -Db_sanitize=address,undefined
meson compile -C build-asan
meson test -C build-asan --print-errorlogs meson test -C build-asan --print-errorlogs
``` ```
## Build TSan (concurrence) Do not claim an unqualified full LeakSanitizer pass from the retained global
maintenance checkpoint. The external OpenCL loader qualification documented in
the canonical audit remains part of that evidence.
À exécuter pour tout ticket touchant la concurrence (pthread, mutex, ## TSan build
variables de condition, états partagés) :
Use TSan only with the configuration that matches the intended proof.
The retained global maintenance concurrency proof used GCC/G++ with Vulkan
disabled, because the project TSan matrix and the Vulkan runtime validation are
separate evidence boundaries.
Example:
```sh ```sh
CC=clang meson setup build-tsan --wipe \ CC=gcc CXX=g++ meson setup build-tsan -Db_sanitize=thread -Db_lundef=false -Dvulkan_orb=disabled
-Db_sanitize=thread \ meson compile -C build-tsan
-Db_lundef=false
meson compile -C build-tsan -j8
meson test -C build-tsan --print-errorlogs meson test -C build-tsan --print-errorlogs
``` ```
## Variables d'environnement The exact target subset, suppression qualification and repetition evidence are
documented in `docs/development/concurrency.md` and the global maintenance
audit.
| Variable | Description | ## Current retained maintenance checkpoint
|---|---|
| `CC` | Compilateur C (défaut : gcc) |
| `CFLAGS` | Drapeaux de compilation supplémentaires |
| `LDFLAGS` | Drapeaux de liaison supplémentaires |
## Structure du build The canonical detailed evidence is:
```text
docs/architecture/global_maintenance_audit.md
GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN
```
The 2026-09-01 checkpoint retained:
```text
portable Clang/Clang++ build and suite
Vulkan-on Clang/Clang++ build and suite
ASan/UBSan qualified run
portable GCC/G++ TSan matrix
public-header C17/C++17 probes
ABI and application-link checks
independent review
```
Those exact historical counts belong to the audit and should not be duplicated
as a new current build contract.
## Build directory layout
A configured Meson tree typically contains:
```text ```text
build/ build/
├── src/ # objets et binaires src/
├── tests/ # binaires de tests tests/
└── compile_commands.json # pour LSP / clangd compile_commands.json
``` ```
## Dépannage Exact generated layout is Meson/Ninja output and may evolve.
### Erreur : ncursesw introuvable ## Environment
Common variables include:
| Variable | Purpose |
| --- | --- |
| `CC` | C compiler |
| `CXX` | C++ compiler |
| `CFLAGS` | additional C flags |
| `CXXFLAGS` | additional C++ flags |
| `LDFLAGS` | additional linker flags |
Prefer Meson options for project features rather than ad-hoc environment flags
that make builds difficult to reproduce.
## Troubleshooting
Check ncursesw discovery:
```sh ```sh
# Vérifier l'installation
pkg-config --libs ncursesw pkg-config --libs ncursesw
# Si absent, installer le paquet de développement ncursesw
``` ```
### Erreur : clang introuvable If Clang is unavailable, GCC is supported where the current Meson checks allow
it.
```sh For a slow build, first preserve the existing build tree and let Ninja use
# Utiliser gcc en alternative normal host-aware scheduling. Reduce concurrency only when actual host pressure
meson setup build --wipe or another active workload justifies it.
# ou installer clang
sudo apt install clang `ccache` may be used when available, but it is optional operational tooling and
not part of scientific identity.
## Rules
```text
NO_FIXED_GLOBAL_J8=YES
NO_REPEATED_UNCHANGED_WIPE=YES
HOST_AWARE_BUILD_PARALLELISM=YES
``` ```
### Build lent Build configuration is operational state. It must not silently redefine
scientific formats, fingerprints or persistence contracts.
```sh
# Réduire la parallélisation
meson compile -C build -j4
# ou utiliser ccache
CC="ccache clang" meson setup build --wipe
```

View file

@ -1,255 +1,412 @@
# Règles de concurrence # Concurrency
## Vue d'ensemble ## Status
Lardon3D utilise un modèle de concurrence à thread unique pour ncurses
et un modèle multi-thread pour le traitement. La séparation est stricte :
le thread ncurses ne fait jamais de travail métier, et les workers ne
touchent jamais ncurses.
## Modèle de concurrence
```text ```text
Thread principal (ncurses) NCURSES_OWNER=MAIN_THREAD_ONLY
├── Gestion des entrées ACTIVE_HEAVY_QUEUE_CALLBACKS=1
├── Affichage TUI TASK_CANCELLATION=COOPERATIVE
└── Orchestration INTERNAL_PARALLELISM=BOUNDED
OWNER_ONLY_PUBLICATION=CANONICAL_WHERE_REQUIRED
Worker thread TSAN_PORTABLE_PROJECT_MATRIX=QUALIFIED_PASS
├── Exécution des tâches TSAN_EXTERNAL_OPENCV_TBB=QUALIFIED
├── Calculs métier VULKAN_CONCURRENCY_VALIDATION=SEPARATE
└── Écritures de résultats
SSD operation thread (0 ou 1, joinable) RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
└── Un poll ou contrôle UDisks synchrone borné, sans ncurses ni Task SERIALISM_REQUIRES_PROOF=CANONICAL
``` ```
## Règles fondamentales Lardon3D separates UI ownership from heavy processing.
### 1. ncurses appartient au thread principal The main thread owns ncurses. The Task Queue owns one active heavy callback.
Individual validated Task Kinds may create bounded internal participants inside
that callback.
```c An SSD controller operation may also use at most one bounded joinable operation
// ✅ Correct : appel depuis le thread principal thread under its own ownership contract.
mvprintw(0, 0, "Progression: %d%%", progress);
// ❌ Interdit : appel depuis un worker ## Execution model
// mvprintw() dans un thread secondaire
```text
main thread
input
ncurses
TUI orchestration
Task Queue worker
one active heavy callback
admitted Task sequence
optional bounded internal participants
deterministic owner publication
SSD operation thread
zero or one bounded joinable controller operation
no ncurses
no Task callback
``` ```
### 2. Variables partagées protégées par mutex Internal participants are not a second global scheduler or Queue.
```c ## Fundamental rules
// ✅ Correct
pthread_mutex_lock(&queue->mutex);
queue->count++;
pthread_mutex_unlock(&queue->mutex);
// ❌ Interdit ### ncurses ownership
// queue->count++; sans protection
Only the main thread calls ncurses.
Workers publish observable state through protected data. They never call
`mvprintw`, `wrefresh`, or other ncurses APIs.
### Shared mutable state
Shared mutable state must have an explicit synchronization owner:
```text
mutex
condition variable
atomic primitive where the contract explicitly permits it
single-thread ownership
immutable-after-publication
``` ```
### 3. Variables de condition pour la synchronisation Do not rely on timing or "normally only one caller".
```c ### Condition variables
// Producteur (caller de la Task Queue)
pthread_mutex_lock(&queue->mutex);
queue->ready = true;
pthread_cond_signal(&queue->cond);
pthread_mutex_unlock(&queue->mutex);
// Consommateur (worker) Always test the predicate in a loop around `pthread_cond_wait()`.
pthread_mutex_lock(&queue->mutex);
while (!queue->ready) { A signal is not durable state; the protected predicate is.
pthread_cond_wait(&queue->cond, &queue->mutex);
} ### Cooperative cancellation
// traitement
pthread_mutex_unlock(&queue->mutex); Production Task cancellation is cooperative.
Do not use `pthread_cancel()` to stop a Task.
Task-specific non-preemptible operations finish their current atomic boundary
before pause/cancel is observed.
### Reservation before callback
No Task callback runs without the Resource Governor admission/reservation
required by its installed sequence contract.
Fixed-resource Tasks do not bypass the Governor.
### Terminal lifetime
Terminal callback completion precedes destruction of Task userdata.
Queue/Task ownership must ensure no observer dereferences freed userdata.
## Lock ordering
When multiple locks are required, the owning subsystem must define and preserve
one order.
Never add a reverse-order path to solve a local problem.
Avoid holding one subsystem mutex while calling into another subsystem that may
call back.
Where practical:
```text
copy bounded state under lock
release lock
perform I/O / expensive work
reacquire only for publication
``` ```
### 4. Pas de callback ncurses depuis un worker ## Queue ingress lifetime
```c The Queue owner closes ingress before destruction.
// ✅ Correct : le worker signale au thread principal
void worker_callback(task_t *task, void *userdata) {
shared_state_t *state = userdata;
pthread_mutex_lock(&state->mutex);
state->result_ready = true;
pthread_cond_signal(&state->cond);
pthread_mutex_unlock(&state->mutex);
}
// ❌ Interdit : appel ncurses depuis le worker Shutdown waits for:
// void worker_callback(...) {
// mvprintw(...); - active worker completion;
// } - registered in-flight API calls covered by the ownership contract;
- terminal callbacks.
This cannot make a raw C pointer safe if a caller begins a new call after the
object has already been freed. Callers must obey lifetime ownership.
## Bounded internal parallelism
A validated Task Kind may use internal participants while the Queue callback
remains the sole Task owner.
Required shape:
```text
one admitted Task owner
-> bounded participant count
-> bounded private work
-> join all participants
-> owner-only deterministic publication when required
-> Task-specific durable cursor
-> generic checkpoint
-> sequence_break
``` ```
## Primitives utilisées Participant count and memory must fit the admitted Resource Governor contract.
| Primitive | Usage | No participant may silently exceed the installed sequence contract.
|---|---|
| `pthread_mutex_t` | Protection des données partagées |
| `pthread_cond_t` | Synchronisation producteur/consommateur |
| `pthread_create()` | Création des workers |
| `pthread_join()` | Attente de fin des workers |
| `pthread_cancel()` | Non utilisé pour interrompre une Task ; annulation coopérative |
## Invariants de concurrence ## Atomicity does not imply serialism
1. **Un seul thread ncurses** : ncurses n'est jamais appelé depuis un Per-item scientific atomicity and cross-item execution width are separate.
worker. Toute mise à jour de l'UI passe par des variables partagées
protégées.
2. **Mutex hiérarchique** : si plusieurs mutex sont acquis, toujours dans ```text
le même ordre pour éviter les deadlocks. PER_ITEM_ATOMICITY_REQUIRES_CROSS_ITEM_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
3. **Annulation coopérative** : les workers vérifient périodiquement un
drapeau d'annulation. Une Task n'est pas interrompue brutalement.
4. **Réservation atomique** : la réservation du gouverneur est atomique.
Deux threads ne peuvent pas obtenir la même réservation.
5. **Pas de callback sans réservation** : aucun callback de tâche n'est
invoqué sans réservation active. Cet invariant est maintenu même en
présence d'erreurs.
6. **Retraite après callback** : la notification terminale finit avant la
destruction du userdata. Queue détruit la Task hors de son mutex et ne
conserve ensuite qu'un snapshot borné.
7. **Fermeture d'ingress** : le propriétaire empêche les nouveaux appels Queue
avant `destroy()`. La fermeture interne attend le worker et chaque appel
enregistré avant le close ; elle ne peut rendre sûr un appel démarré après
la libération d'un pointeur C brut.
8. **Parallélisme scientifique propriétaire** : lorsqu'un kind emploie des
participants internes, le callback Queue demeure l'unique propriétaire. Le
nombre de participants et leur mémoire sont admis par le Governor ; seul le
propriétaire publie le préfixe durable ordonné et joint tous les enfants.
9. **Lease SSD par objet** : un lease scratch appartient à l'adresse exacte de
l'objet fourni par le caller. Tous ses champs sont lus/écrits sous le mutex
du contrôleur. Le caller lui garantit un accès exclusif et ne le copie, ne le
déplace ni ne le présente simultanément à deux contrôleurs. En production,
acquire/release passent par les wrappers Governor ; le Governor relâche son
mutex avant l'appel contrôleur, et le contrôleur ne rappelle jamais le
Governor. À la saturation légale `generation == UINT64_MAX`, seule la fin
du wrapper exact déjà sérialisé peut réconcilier sa propre opération et le
compte fondé sur les adresses ; une update publique au même watermark ne
peut pas rendre une autorité stale.
10. **Owner SSD unique** : la TUI/main demande et poll l'opération ; au plus un
thread joinable exécute une opération bornée et ne touche jamais ncurses.
Le destroy le joint avant unregister. Une observation malformée enregistre
`ERROR` et ne confère aucune autorité de contrôle ou de lease.
11. **Frontière projet** : les vues libèrent leurs borrows, puis la Queue est
annulée/jointe/détruite avant Project DB. Une Queue vide est créée ensuite.
Aucun callback terminal ne peut donc déréférencer une DB déjà fermée et
l'histoire d'un projet ne fuit pas dans le suivant.
12. **Ordre d'arrêt global** : Queue et leases Task, puis fermeture projet,
join/unregister du binding SSD, contrôleur SSD, et enfin Governor. Un
unregister encore bloqué par un lease est un échec observable, jamais un
pointeur abandonné.
## Anti-patterns
### Deadlock
```c
// ❌ Risque de deadlock
pthread_mutex_lock(&mutex_a);
pthread_mutex_lock(&mutex_b); // attend mutex_b
// Dans un autre thread :
pthread_mutex_lock(&mutex_b);
pthread_mutex_lock(&mutex_a); // attend mutex_a → DEADLOCK
``` ```
**Solution** : toujours acquérir les mutex dans le même ordre. Current examples include selected RAW, selected Feature extraction, Candidate
Pair source work and outer Geometric Verification preparation.
### Race condition Serialization is valid only where the subsystem's scientific, persistence,
library or measured-throughput contract proves it necessary.
```c ## CPU/batch coupling
// ❌ Race condition
if (task->state == TASK_STATE_QUEUED) {
task->state = TASK_STATE_RUNNING;
}
// ✅ Correct CPU and batch/window are not globally independent dimensions.
pthread_mutex_lock(&task->mutex);
if (task->state == TASK_STATE_QUEUED) { For a Task whose additional participants cannot do useful work while the
task->state = TASK_STATE_RUNNING; admitted item window remains one, a Task-specific capability may couple those
} dimensions.
pthread_mutex_unlock(&task->mutex);
Current validated examples include:
```text
candidate_pair.generate/1
features.extract.batch/1
``` ```
### Use-after-free This is not a universal rule for all Task Kinds.
```c ## Project lifetime boundary
// ❌ Use-after-free
task_destroy(task);
task_callback(task); // task est libéré
// ✅ Correct : le callback est entièrement revenu avant la destruction Before closing a project:
task_callback(task);
task_destroy(task); ```text
views release Project DB borrows
-> Queue is cancelled/joined/destroyed
-> Project DB closes
-> fresh empty Queue may be created for the next project
``` ```
## Validation A terminal callback must never observe a Project DB already destroyed.
Les readers Visual Index sont sans état partagé mutable. Une query copie la Project-specific runtime history must not leak into the next project.
liste bornée des segments sous le mutex DB, puis effectue hash, lectures et
accumulation après déverrouillage. Un update ne rend le nouveau segment visible
qu'au commit memberships+segment ; une query en cours garde son snapshot.
Pour tout ticket touchant la concurrence, exécuter : ## Global shutdown boundary
The current shutdown order preserves ownership across:
```text
Task Queue and Task leases
-> project close
-> join/unregister SSD binding
-> SSD controller
-> Resource Governor
```
A scratch unregister blocked by a real outstanding lease is an observable
failure, not permission to abandon a live pointer.
## SSD lease ownership
A scratch lease belongs to the exact caller-owned lease object used for the
operation.
Its mutable fields are controlled under the SSD controller mutex.
The caller must not:
- copy a live lease;
- move a live lease;
- present the same lease object to two controllers;
- release through a different ownership path.
Production acquire/release uses the Resource Governor wrappers.
The Governor releases its own mutex before entering the controller, and the
controller does not callback into the Governor while holding its mutex.
Scratch remains storage capacity, never RAM admission.
## Visual Index readers
Visual Index query readers do not share mutable query state.
A query obtains a bounded segment snapshot under the Project DB boundary, then
performs hash/read/accumulation after release.
A concurrent update makes a new segment visible only at its canonical commit
boundary. An already running query continues with its retained snapshot.
## OpenCV process-wide state
OpenCV thread configuration is process-wide.
The active heavy Queue callback owns temporary mutation of that setting where a
Task contract requires it and restores the previous/baseline value on all exit
paths.
Internal participants must not independently race `cv::setNumThreads()`.
For Feature batch, cross-image participants are used while internal OpenCV
threading is controlled explicitly.
## Vulkan boundary
ORB Vulkan concurrency is validated separately from portable TSan.
The production AUTO contract currently uses:
```text
normal inflight depth = 1
private validated safety depth = 2
helpers = 0
```
Depth 2 is a private safety/benchmark capability and was rejected as the normal
useful setting by measured throughput.
A Vulkan backend failure produces complete CPU fallback before publication.
Partial GPU scientific output is never published.
## TSan policy
For project concurrency changes, use a dedicated TSan build that matches the
supported proof boundary.
The retained global maintenance matrix used:
```text
GCC/G++
Vulkan disabled
selected concurrent targets
deterministic repetitions
```
and completed the retained 14/14 target matrix plus 220 repetitions.
The narrow suppression file is:
```text
tests/tsan-opencv.supp
```
It covers external non-instrumented OpenCV/TBB objects only.
It must not suppress Lardon3D frames.
Therefore the correct retained claim is not "TSan proves all concurrency".
It is:
```text
portable project concurrency matrix passed under the documented qualification
external OpenCV/TBB reports are narrowly qualified
Vulkan concurrency has a separate validation boundary
```
## What TSan does not prove
TSan is useful for instrumented conflicting memory access and some
synchronization misuse.
It does not prove absence of:
- deadlock;
- lost wakeup caused by incorrect predicate design;
- lifetime bugs outside the exercised paths;
- races hidden inside non-instrumented external libraries;
- Vulkan driver/runtime correctness;
- scientific determinism.
Lock-order review, ownership reasoning and deterministic tests remain required.
## Sanitizer command policy
Do not encode fixed `-j8` as canonical validation.
Example configuration:
```sh ```sh
# Build TSan CC=gcc CXX=g++ meson setup build-tsan -Db_sanitize=thread -Db_lundef=false -Dvulkan_orb=disabled
CC=clang meson setup build-tsan --wipe -Db_sanitize=thread -Db_lundef=false meson compile -C build-tsan
meson compile -C build-tsan -j8
meson test -C build-tsan --print-errorlogs meson test -C build-tsan --print-errorlogs
``` ```
TSan détecte automatiquement : Use host-aware compile/test parallelism unless the proof itself requires
serialization.
- les accès concurrents conflictuels instrumentés ; Do not repeatedly wipe an unchanged TSan tree.
- certaines utilisations incohérentes des primitives de synchronisation.
Il ne prouve pas l'absence de deadlock, de signal perdu ou de bug dans une ## Concurrency review checklist
bibliothèque non instrumentée. Les invariants de lifetime et d'ordre de locks
restent donc soumis aux tests déterministes et à la revue.
### Preuve TSan globale courante Before closing a concurrency-sensitive change, verify:
La matrice fraîche emploie GCC/G++ 16.2.1 et désactive explicitement Vulkan. - ncurses remains main-thread-only;
Elle passe 14/14 cibles couvrant Task, Project, Queue, Governor, registre/leases - every shared mutable field has an explicit synchronization owner;
SSD, contrôleur SSD, observateur/TUI async, Candidate, Visual Index, Feature, - condition predicates are checked in loops;
Matcher et GV, puis 220/220 répétitions déterministes : **234/234** au total. - lock order remains consistent;
- no Task uses forced asynchronous cancellation;
- Queue callbacks have an active reservation;
- internal participants stay within the admitted contract;
- all children join on every exit path;
- owner-only publication remains ordered where required;
- project-close ordering prevents DB use-after-close;
- SSD lease ownership remains exact;
- Task userdata outlives terminal notification;
- appropriate deterministic concurrency tests pass;
- portable TSan qualification is preserved;
- Vulkan validation is reported separately;
- ASan/UBSan is run when the change also affects lifetime/memory.
La seule liste de suppressions est `tests/tsan-opencv.supp`, limitée aux objets ## Current retained evidence
partagés externes non instrumentés `libopencv_features.so`,
`libopencv_core.so` et `libtbb.so`. Elle ne masque aucune frame Lardon3D. Les
warnings GCC `-Wmaybe-uninitialized` des contrôles OpenCV Feature/SIFT sont
classés non matériels : le callback fournit une Task non nulle et le helper
initialise la structure avant toute autre sortie d'échec. Les warnings OpenCV
du build GV appartiennent aux headers externes.
Cette preuve TSan ne vaut pas validation de concurrence Vulkan. Le backend The canonical global-maintenance record is:
ORB Vulkan réel est couvert séparément par le build Clang Vulkan-on 939/939,
la suite 65/65 et ses tests de backend/handle/publication ; cette séparation
doit rester explicite dans tout rapport.
## Checklist de concurrence ```text
docs/architecture/global_maintenance_audit.md
GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN
```
Avant de livrer un ticket touchant la concurrence : The current A6000 checkpoint is later:
- [ ] Toutes les variables partagées sont protégées par un mutex ```text
- [ ] Les mutex sont toujours libérés (même en cas d'erreur) real-a6000-pre-sfm-2026-09-02
- [ ] Les variables de condition sont vérifiées dans une boucle `while` REAL_A6000_PRE_SFM=PASS/FROZEN
- [ ] Aucun appel ncurses depuis un worker ```
- [ ] L'annulation des Tasks est coopérative (pas de `pthread_cancel`)
- [ ] TSan ne signale aucune erreur The later A6000 proof exercised current bounded parallel paths through selected
- [ ] Le build ASan ne signale aucune fuite mémoire liée aux threads Feature batch, Candidate, Matcher, Geometric Verifier v3 and Tracks without
changing the historical TSan qualification.
Historical evidence remains historical; new changes require validation scoped
to their actual concurrency surface.
## Summary
```text
NCURSES_OWNER=MAIN_THREAD_ONLY
ACTIVE_HEAVY_QUEUE_CALLBACKS=1
TASK_CANCELLATION=COOPERATIVE
INTERNAL_PARALLELISM=BOUNDED
PER_ITEM_ATOMICITY_REQUIRES_CROSS_ITEM_SERIALISM=NO
OWNER_ONLY_PUBLICATION_REQUIRES_SERIAL_PREPARATION=NO
TSAN_PORTABLE_PROJECT_MATRIX=QUALIFIED_PASS
TSAN_EXTERNAL_OPENCV_TBB=QUALIFIED
VULKAN_CONCURRENCY_VALIDATION=SEPARATE
NO_FIXED_GLOBAL_J8=YES
NO_REPEATED_UNCHANGED_WIPE=YES
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```

View file

@ -1,136 +1,298 @@
# Procédures de test # Testing
## Vue d'ensemble ## Status
Lardon3D utilise le framework de test intégré à Meson. Chaque module possède ```text
un fichier de test dans `tests/` correspondant au module testé. DOCUMENTATION_LANGUAGE=ENGLISH
TEST_POLICY=HOST_AWARE
REPEATED_UNCHANGED_EXPENSIVE_VALIDATION=AVOID
TSAN_OPEN_CV_TBB_QUALIFICATION=REQUIRED
VULKAN_CONCURRENCY_VALIDATION=SEPARATE
```
## Lancer les tests Lardon3D uses Meson's test runner. Tests live under `tests/` and combine unit,
integration, persistence, restart, resource and real-path validation.
## Normal commands
Run the configured suite:
```sh ```sh
# Tous les tests
meson test -C build --print-errorlogs meson test -C build --print-errorlogs
```
# Un test spécifique Run one named test:
meson test -C build test_task_queue --print-errorlogs
# Tests avec verbose ```sh
meson test -C build <test-name> --print-errorlogs
```
Verbose execution:
```sh
meson test -C build -v --print-errorlogs meson test -C build -v --print-errorlogs
```
# Réexécuter uniquement les tests échoués Re-run failures only:
```sh
meson test -C build --reprint=failed meson test -C build --reprint=failed
``` ```
## Structure des tests Use the names registered by the current `meson.build`; this document does not
maintain a second authoritative list of every test target.
## Validation policy
Validation must match the change.
A documentation-only change normally requires:
```text ```text
tests/ git diff --check
├── test_task_queue.c # tests de la file de tâches targeted content checks
├── test_task.c # tests du module task targeted link/authority review
├── test_resource_governor.c # tests du gouverneur
├── test_hardware_profile.c # tests du profil matériel
├── test_import.c # tests de l'import
├── test_project.c # tests des projets
└── test_*.c # autres modules
``` ```
## Écrire un test It does not justify wiping and rebuilding unchanged code.
```c A code change normally requires, in increasing scope:
#include <glib.h>
#include "lardon3d/task.h"
void test_task_create(void) { ```text
task_estimate_t est = { targeted build
.ram_bytes = 1024 * 1024, targeted tests
.gpu_bytes = 0, broader affected suite
.cpu_weight = 1, sanitizer or concurrency validation when relevant
.io_weight = 0, full suite when the change or release boundary justifies it
.batch_size = 10,
.batch_max = 100
};
task_t *t = task_create("test", &est, NULL, NULL);
g_assert_nonnull(t);
g_assert_cmpint(task_get_state(t), ==, TASK_STATE_IDLE);
task_destroy(t);
}
int main(int argc, char **argv) {
g_test_init(&argc, &argv, NULL);
g_test_add_func("/task/create", test_task_create);
return g_test_run();
}
``` ```
## Conventions Do not repeatedly rerun an unchanged expensive suite between documentation
edits merely to create activity.
1. **Préfixe `test_`** : chaque fonction de test porte le préfixe `test_`. ## Host-aware parallelism
2. **Chemin hiérarchique** : le nom du test suit le pattern `/module/action`.
3. **Asserts GLib** : utiliser `g_assert_*` pour les vérifications.
4. **Nettoyage** : chaque test libère toutes ses ressources.
5. **Isolation** : un test ne dépend pas de l'état d'un autre test.
6. **Déterminisme** : les tests ne dépendent pas de l'heure, du filesystem
ou de l'état réseau (sauf test d'import).
## Commentaires source Build and test parallelism are host-aware.
Les commentaires documentent le pourquoi et les contrats non évidents : Do not encode a project-wide fixed `-j8`, `--num-processes 1`, or equivalent
invariants, propriété et durée de vie, persistance, ainsi que limites et constant as canonical policy.
frontières de ressources. Les API publiques documentent leurs contrats non
évidents. Ils ne paraphrasent pas le code ligne par ligne et sont mis à jour
avec tout changement de comportement.
## Tests unitaires vs tests d'intégration The correct width depends on the current machine, interactive reserve, memory,
toolchain and workload. Use all safe useful host capacity while preserving the
defined interactive reserve.
`test-visual-index` couvre les descriptors synthétiques, le retrieval ORB réel, ```text
les filtres inter-ScanSets, quatre queries concurrentes, la corruption/absence/ RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
troncature d'un segment et 4 000 Feature Sets synthétiques. Le scénario de SERIALISM_REQUIRES_PROOF=CANONICAL
reprise `visual_index.update` est exercé dans `test-feature-task`. ```
| Type | Portée | Fichier | If a temporary validation must be serialized for determinism, diagnosis or a
|---|---|---| known tool limitation, label that serialization as test-specific evidence
| Unitaire | Un module isolé | `tests/test_<module>.c` | rather than a global default.
| Intégration | Interaction entre modules | `tests/test_<module>.c` avec dépendances réelles |
## Validation par ticket ## Fresh build policy
Avant de livrer un ticket, exécuter la séquence complète : Do not use `meson setup --wipe` by default.
Prefer:
```sh ```sh
# 1. Build clean meson setup --reconfigure build
CC=clang meson setup build --wipe meson compile -C build
meson compile -C build -j8
# 2. Tests
meson test -C build --print-errorlogs
# 3. Style
git diff --check
# 4. Si mémoire/concurrence touchés
CC=clang meson setup build-asan --wipe -Db_sanitize=address,undefined
meson compile -C build-asan -j8
meson test -C build-asan --print-errorlogs
# 5. Si concurrence touchée
CC=clang meson setup build-tsan --wipe -Db_sanitize=thread -Db_lundef=false
meson compile -C build-tsan -j8
meson test -C build-tsan --print-errorlogs
``` ```
## Dépannage Create or wipe a build directory when the configuration genuinely needs a
fresh environment, for example:
### Test qui échoue en ASan ```text
different sanitizer set
portable Vulkan-off proof
Vulkan-on proof
compiler-family change
known stale/corrupt build directory
release-grade clean proof
```
Vérifier les durées de vie des allocations. Ne jamais libérer un objet puis Repeated wipes of the same unchanged configuration waste time and invalidate
y accéder. Vérifier que chaque `task_destroy()` est appelée. incremental-build advantages.
### Test qui échoue en TSan ## Sanitizers
Vérifier que toutes les variables partagées sont protégées par un mutex. ### ASan / UBSan
Vérifier que ncurses est utilisé uniquement depuis le thread principal.
### Test qui échoue uniquement en release For memory, lifetime, ownership or undefined-behavior changes, use a dedicated
sanitizer build.
Vérifier les assertions et les overflow arithmétiques. Compiler avec Example configuration:
`-fsanitize=undefined` pour détecter les comportements indéfinis.
```sh
CC=clang CXX=clang++ meson setup build-asan -Db_sanitize=address,undefined
meson compile -C build-asan
meson test -C build-asan --print-errorlogs
```
Reconfigure or wipe only when the existing sanitizer directory does not match
the requested configuration.
### LeakSanitizer qualification
The retained global maintenance evidence must not be summarized as
`LSan 64/64`.
The full first leak-enabled run exposed an externally attributed OpenCL loader
leak and two timeout anomalies. The retained qualified result is:
```text
ASan/UBSan full suite: PASS with detect_leaks=0
proved subset without the external loader: LSan PASS
full leak-enabled suite: not an unqualified PASS
```
Preserve that distinction in future reports unless new evidence supersedes it.
## ThreadSanitizer
Concurrency changes require TSan where the instrumented boundary is meaningful.
The retained global maintenance TSan proof used GCC/G++ with Vulkan disabled
and covered the selected concurrent targets plus deterministic repetitions.
The only retained suppression file is:
```text
tests/tsan-opencv.supp
```
Its purpose is limited to external OpenCV/TBB objects. It must not suppress
Lardon3D frames.
Therefore never report a blanket statement such as:
```text
TSan proves the entire Vulkan build race-free
```
The valid qualification is:
```text
project concurrent paths covered by the retained portable TSan matrix
external OpenCV/TBB reports qualified by the narrow suppression boundary
Vulkan concurrency validated separately
```
## Vulkan validation
ORB Vulkan uses a separate validation boundary.
The retained global maintenance evidence includes a Vulkan-on build and suite
on the real Radeon 780M/RADV host, plus dedicated backend/handle/publication
tests.
That evidence is not interchangeable with TSan.
SIFT/RootSIFT feasibility results did not establish a production GPU backend;
do not turn feasibility checks into production validation claims.
## Determinism and repetition
Repeat tests when repetition proves something specific:
```text
deterministic restart
race sensitivity
resource adaptation
ordering stability
flaky regression reproduction
```
Do not repeat unchanged tests without a stated purpose.
When repetition is the evidence, record:
```text
exact test/corpus
run count
relevant configuration
success/failure count
digest or invariant when applicable
```
## Test isolation
Tests should:
- own and clean up their temporary resources;
- avoid depending on another test's execution order;
- avoid network state unless the test explicitly owns that dependency;
- use synthetic/private Resource snapshots where the test is about deterministic
policy rather than live host telemetry;
- avoid changing global process state without restoring it.
OpenCV thread configuration is process-wide and must be restored on every exit
path in tests that change it.
## Public-header validation
When a public C header changes, run a standalone C17 syntax probe in addition to
normal build coverage.
Conceptually:
```sh
cc -x c -std=c17 -fsyntax-only -Iinclude -include lardon3d/<header>.h /dev/null
```
Use the current supported compiler matrix when the change affects ABI or
C/C++ interoperability.
## Source comments
Source comments explain non-obvious contracts:
```text
invariants
ownership and lifetime
persistence ordering
resource boundaries
recovery behavior
scientific constraints
```
They should not paraphrase obvious code line by line.
Repository source comments are English.
## Current retained global maintenance evidence
The canonical detailed record is:
```text
docs/architecture/global_maintenance_audit.md
GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN
```
That historical checkpoint includes fresh portable/Vulkan builds, full suites,
sanitizer work, TSan work, public-header probes, ABI/application-link checks and
independent review.
It remains historical evidence. New changes require only the validation
appropriate to the changed surface unless a new global checkpoint is being
created.
## Ticket closure checklist
Before closing a code ticket:
- confirm the requested scope only was changed;
- run `git diff --check`;
- run targeted tests for changed behavior;
- run the affected broader suite when justified;
- run ASan/UBSan for memory/lifetime-sensitive changes;
- run TSan for concurrency-sensitive project code when applicable;
- keep Vulkan validation separate from portable TSan claims;
- preserve exact external-library qualifications;
- avoid fixed host-parallelism constants;
- avoid repeated unchanged clean builds or suites;
- report what actually ran, not a stronger claim.
For documentation-only remediation, use documentation checks rather than
rebuilding unchanged production code.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
#ifndef LARDON3D_CALIBRATION_AF_STUDY_H
#define LARDON3D_CALIBRATION_AF_STUDY_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_CALIBRATION_AF_STUDY_VERSION = 1,
LARDON3D_CALIBRATION_AF_STUDY_PROBE_MODEL_VERSION = 1,
LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE = 32,
LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY = 128,
LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES = 64,
LARDON3D_CALIBRATION_AF_STUDY_MAX_PAIRS = 2016,
LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES = 131072,
};
typedef enum {
LARDON3D_CALIBRATION_AF_STUDY_OK = 0,
LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_AF_STUDY_CAPACITY,
LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE,
LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR,
} Lardon3DCalibrationAfStudyResult;
typedef enum {
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT = 1,
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT = 2,
} Lardon3DCalibrationAfStudySampleRole;
typedef struct {
Lardon3DCalibrationAfStudySampleRole role;
char focus_token[LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY];
unsigned char calibration_evidence_sha256[LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE];
double fx;
double fy;
double cx;
double cy;
double k1;
double k2;
double p1;
double p2;
} Lardon3DCalibrationAfStudySample;
typedef struct {
unsigned char study_context_sha256[LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE];
uint32_t width;
uint32_t height;
const Lardon3DCalibrationAfStudySample *samples;
size_t sample_count;
} Lardon3DCalibrationAfStudyInput;
typedef struct {
uint32_t sample_count;
uint32_t fit_count;
uint32_t holdout_count;
uint32_t pair_count;
uint32_t same_focus_pair_count;
uint32_t cross_focus_pair_count;
uint32_t fit_holdout_pair_count;
double all_center_max_px;
double all_edge_probe_max_px;
double all_corner_probe_max_px;
double all_global_probe_max_px;
double same_focus_global_probe_max_px;
double cross_focus_global_probe_max_px;
double fit_holdout_global_probe_max_px;
} Lardon3DCalibrationAfStudySummary;
/* Produce deterministic AF-study evidence from already acquired calibration
* results. This API performs no calibration solve, no Project DB access and no
* scientific PASS/FAIL decision. `study_context_sha256` is the caller-retained
* identity of the exact body/lens/focal/non-focus geometric study context.
* Focus tokens are opaque exact observations and may repeat across independent
* calibration samples. A repeated exact (focus token, calibration evidence
* SHA-256) pair is rejected because it is not independent evidence.
*
* Projection deltas use the frozen pinhole + k1/k2/p1/p2 forward model on nine
* canonical normalized probes: centre, four edge probes and four corner probes
* at +/-0.7. Metrics are measurements only; this v1 API freezes no acceptance
* threshold. The binary L3DAFST1 artifact canonicalizes samples independent of
* caller order, includes every pairwise metric, and is suitable for hashing as
* retained evidence. */
Lardon3DCalibrationAfStudyResult lardon3d_calibration_af_study_produce(
const Lardon3DCalibrationAfStudyInput *input,
unsigned char *artifact, size_t artifact_capacity, size_t *written,
unsigned char artifact_sha256[LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE],
Lardon3DCalibrationAfStudySummary *summary);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,61 @@
#ifndef LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_H
#define LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_H
#include <lardon3d/calibration_af_study.h>
#include <lardon3d/calibration_af_study_workflow.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_OK = 0,
LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE,
LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CAPACITY,
LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CRYPTO_ERROR,
LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_ENCODING_ERROR,
} Lardon3DCalibrationAfStudyAssemblyResult;
typedef struct {
const Lardon3DCalibrationWorkflowExternalEvidence *external;
Lardon3DCalibrationAfStudySampleRole role;
const char *focus_token;
} Lardon3DCalibrationAfStudyAssemblyEntry;
typedef struct {
/* Exact caller-retained identity of the common body/lens/focal/non-focus
* geometric study context. The assembly does not derive or reinterpret it. */
unsigned char study_context_sha256[
LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE];
const Lardon3DCalibrationAfStudyAssemblyEntry *entries;
size_t entry_count;
} Lardon3DCalibrationAfStudyAssemblyInput;
/* Assemble 2..MAX_SAMPLES already-materialized Calibration Workflow results
* into one deterministic L3DAFST1 artifact.
*
* Each entry is converted through the frozen Workflow bridge; no solver file
* is parsed here. All samples must share exact oriented dimensions because one
* L3DAFST1 study has one image geometry. The common optical/non-focus identity
* remains the explicit study_context_sha256 supplied by the caller.
*
* Duplicate calibration_evidence_sha256 values are rejected regardless of
* focus token or FIT/HOLDOUT role. A single physical calibration result cannot
* therefore be relabelled to masquerade as independent AF evidence.
*
* This boundary performs no Project DB access, no metadata interpretation, no
* physical applicability decision and no acceptance thresholding. */
Lardon3DCalibrationAfStudyAssemblyResult
lardon3d_calibration_af_study_assemble_materialized(
const Lardon3DCalibrationAfStudyAssemblyInput *input,
unsigned char *artifact, size_t artifact_capacity, size_t *written,
unsigned char artifact_sha256[
LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE],
Lardon3DCalibrationAfStudySummary *summary);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,51 @@
#ifndef LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_H
#define LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_H
#include <lardon3d/calibration_af_study.h>
#include <lardon3d/calibration_workflow.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK = 0,
LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE,
LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR,
} Lardon3DCalibrationAfStudyWorkflowResult;
/* Convert one already validated/materialized Calibration Workflow result into
* one AF-study sample without re-parsing any solver file.
*
* `focus_token` and FIT/HOLDOUT role are study annotations. They are
* deliberately excluded from calibration_evidence_sha256, so relabelling the
* same calibration result cannot manufacture a new calibration-evidence
* identity.
*
* calibration_evidence_sha256 is domain-separated and binds:
* - target identity;
* - optical-state identity;
* - solver executable/configuration identity;
* - initialization and validation evidence;
* - exact oriented dimensions;
* - exact published binary64 fx,fy,cx,cy,k1,k2,p1,p2.
*
* The bridge checks the materialization invariants it consumes, including
* deterministic validation-evidence binding and equality of all three repeated
* full solves. It publishes repeated_parameters[0].
*
* No Project DB access, metadata interpretation, physical AF applicability
* decision, interpolation, extrapolation, solver execution or thresholding
* occurs here. */
Lardon3DCalibrationAfStudyWorkflowResult
lardon3d_calibration_af_study_sample_from_materialized_evidence(
const Lardon3DCalibrationWorkflowExternalEvidence *external,
Lardon3DCalibrationAfStudySampleRole role, const char *focus_token,
Lardon3DCalibrationAfStudySample *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,91 @@
#ifndef LARDON3D_CALIBRATION_BOOTSTRAP_V2_H
#define LARDON3D_CALIBRATION_BOOTSTRAP_V2_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/project_db.h>
#include <lardon3d/sparse_sfm_model.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_CALIBRATION_BOOTSTRAP_V2_ARTIFACT_VERSION = 2,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_BYTES = 600000,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_GROUPS = 4096,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES = 4096,
};
typedef enum {
LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK = 0,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_PROVENANCE_MISMATCH,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR,
LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY,
} Lardon3DCalibrationBootstrapV2Result;
typedef struct {
unsigned char artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE];
Lardon3DSparseCalibrationScope scope;
uint32_t calibration_count;
uint32_t group_count;
} Lardon3DCalibrationBootstrapV2Output;
typedef struct {
uint32_t selected_item_index;
uint64_t image_id;
uint64_t calibration_id;
} Lardon3DCalibrationBootstrapV2Member;
/* Validate and publish one complete artifact without attaching its scope.
* `members` is caller-owned storage of at least V2_MAX_ENTRIES only when that
* many entries are accepted; `member_capacity` may be smaller and yields
* INVALID_ARGUMENT before database mutation when the artifact will not fit.
* Successful members are ordered by selected_item_index and expose the exact
* immutable calibration created/reused for each selected image.
*
* This primitive exists so Workflow v2 can establish exact Capture-owned v26
* applicability and selection before the single READY attachment. It never
* infers Capture identity from image, path, digest, or group identity, and it
* never attaches the returned complete scope. Exact retry is deterministic;
* immutable calibrations and the unattached scope may survive later workflow
* failure under the existing publication contract. */
Lardon3DCalibrationBootstrapV2Result
lardon3d_calibration_bootstrap_v2_publish_unattached(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DCalibrationBootstrapV2Member *members, size_t member_capacity,
Lardon3DCalibrationBootstrapV2Output *output);
/* Import one complete L3DCALB2 artifact for an immutable selected execution.
* All pointers are required and borrowed only for the call; artifact_size is
* bounded by V2_MAX_BYTES and must match expected_artifact_sha256 before any
* parsing or database access. The fixed-width little-endian format accepts
* only the existing eight-parameter PINHOLE model and complete, nonoverlapping
* coverage of selected item indexes, image IDs, and representation SHA-256s.
*
* Each serialized group is hashed independently and that digest becomes every
* member calibration's IMPORTED_TRUSTED provenance fingerprint. Thus target,
* optical-state, solver, initialization, validation, and exact membership
* evidence remain group-local even when one scope contains heterogeneous
* groups. Validation completes before publication. Existing immutable
* calibrations and one exact all-image scope are reused on retry; the scope is
* attached only after complete publication, so a failure cannot make the
* execution READY. No Capture identity, solver result beyond intrinsics, or
* compatibility relation is inferred by this C ABI. */
Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DCalibrationBootstrapV2Output *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -103,6 +103,12 @@ typedef struct {
double target_white_border_mm; double target_white_border_mm;
double target_measurements_mm[LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS]; double target_measurements_mm[LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS];
double measurement_resolution_mm; double measurement_resolution_mm;
/* Calibration Science v1 defines planarity as a categorical physical
* attestation, not a numeric flatness tolerance. This legacy ABI field is
* therefore a required NAN sentinel and MUST NOT carry an invented physical
* measurement. The future workflow coordinator binds the canonical session
* manifest containing `planarity PASS <sha256>` through
* initialization_evidence_sha256. */
double target_flatness_mm; double target_flatness_mm;
double holdout_rmse_px; double holdout_rmse_px;
double holdout_maximum_residual_px; double holdout_maximum_residual_px;

View file

@ -0,0 +1,103 @@
#ifndef LARDON3D_CALIBRATION_TOOLING_V2_H
#define LARDON3D_CALIBRATION_TOOLING_V2_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/calibration_bootstrap_v2.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_CALIBRATION_TOOLING_V2_VERSION = 2,
LARDON3D_CALIBRATION_TOOLING_V2_VALIDATION_FLAGS = 15,
};
typedef enum {
LARDON3D_CALIBRATION_TOOLING_V2_OK = 0,
LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED,
LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY,
LARDON3D_CALIBRATION_TOOLING_V2_ENCODING_ERROR,
LARDON3D_CALIBRATION_TOOLING_V2_IMPORT_ERROR,
} Lardon3DCalibrationToolingV2Result;
/* One selected-image member of a caller-declared scientific group. The item
* index is the immutable selected-execution order, not an operational group
* index. Parameters use the exact v1/OpenCV pinhole meaning. All storage is
* caller-owned and borrowed only while the API call is active. */
typedef struct {
uint32_t selected_item_index;
uint64_t image_id;
unsigned char representation_sha256[32];
uint32_t width;
uint32_t height;
double fx, fy, cx, cy, k1, k2, p1, p2;
uint32_t support_images;
uint32_t support_observations;
double reprojection_rmse_px;
double maximum_parameter_delta;
uint32_t validation_flags;
} Lardon3DCalibrationToolingV2Entry;
/* Group identity is the exact bounded state identifier plus its explicit
* version. The six nonzero digests independently bind optical applicability,
* physical target, solver executable/configuration, initialization, and
* validation evidence. Entries must be strictly increasing by selected item
* index; groups must be strictly increasing by (group_identity_sha256,
* group_version). These canonical orders make identical evidence byte-stable
* without hiding caller mistakes by silently reordering it. */
typedef struct {
unsigned char group_identity_sha256[32];
uint32_t group_version;
unsigned char optical_state_sha256[32];
unsigned char target_sha256[32];
unsigned char solver_executable_sha256[32];
unsigned char solver_configuration_sha256[32];
unsigned char initialization_evidence_sha256[32];
unsigned char validation_evidence_sha256[32];
const Lardon3DCalibrationToolingV2Entry *entries;
size_t entry_count;
} Lardon3DCalibrationToolingV2Group;
/* A bounded heterogeneous publication manifest. Every item index in
* [0, entry_count) must occur exactly once across all groups, and every image
* ID must be globally unique. Tooling validates only explicit evidence and
* model-domain safety; it does not run a solver, invent acceptance thresholds,
* or decide optical compatibility. */
typedef struct {
const Lardon3DCalibrationToolingV2Group *groups;
size_t group_count;
size_t entry_count;
} Lardon3DCalibrationToolingV2Evidence;
/* Validate borrowed evidence and its borrowed nested group/entry arrays for
* bounded counts, canonical ordering, and exact selected-index/image coverage.
* Validation does not mutate evidence, artifact bytes, or Project DB state.
* INVALID_ARGUMENT reports a missing top-level pointer/groups array or
* out-of-range top-level counts; EVIDENCE_REJECTED reports invalid nested or
* scientific evidence within an otherwise bounded top-level manifest. */
Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_validate(
const Lardon3DCalibrationToolingV2Evidence *evidence);
/* Encode deterministic L3DCALB2 into caller storage and return its SHA-256.
* `written` may be NULL; no partial artifact is reported as written. */
Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_produce(
const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact,
size_t artifact_capacity, size_t *written, unsigned char artifact_sha256[32]);
/* Validate, encode, then call the production v2 bootstrap importer. The DB is
* untouched when validation or encoding fails. */
Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_import(
Lardon3DProjectDb *database, uint64_t execution_id,
const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact,
size_t artifact_capacity, size_t *written,
Lardon3DCalibrationBootstrapV2Output *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,179 @@
#ifndef LARDON3D_CALIBRATION_WORKFLOW_H
#define LARDON3D_CALIBRATION_WORKFLOW_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/calibration_tooling.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_CALIBRATION_WORKFLOW_VERSION = 1,
LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES = 128 * 1024 * 1024,
LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS = 4096,
LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY = 1025,
};
typedef enum {
LARDON3D_CALIBRATION_WORKFLOW_OK = 0,
LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR,
LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE,
LARDON3D_CALIBRATION_WORKFLOW_CAPACITY,
LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE,
LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH,
LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR,
} Lardon3DCalibrationWorkflowResult;
/* `campaign_state_path` is a canonical external acquisition manifest:
*
* L3DCAL_CAMPAIGN_STATE_V1
* execution <selected-execution-id>
* optical_configuration <explicit-v23-configuration-id>
* optical_state <sha256> <complete-Science-v1-state-token>
* capture <zero-based-selected-item-index> <capture-id>
*
* Capture rows are contiguous and ordered. The later DB-binding stage proves
* that these IDs are exactly the selected execution and that each Capture owns
* the declared explicit optical configuration; this input boundary performs no
* DB access and therefore never treats the manifest alone as that proof. */
typedef struct {
const char *session_path;
const char *detection_path;
const char *solve_path;
const char *evidence_path;
const char *producer_path;
const char *campaign_state_path;
} Lardon3DCalibrationWorkflowInputFiles;
typedef struct {
unsigned char session_sha256[32];
unsigned char detection_sha256[32];
unsigned char solve_sha256[32];
unsigned char evidence_sha256[32];
unsigned char producer_sha256[32];
unsigned char campaign_state_sha256[32];
unsigned char optical_state_sha256[32];
unsigned char solver_executable_sha256[32];
unsigned char solver_configuration_sha256[32];
uint64_t selected_execution_id;
uint64_t optical_configuration_id;
uint32_t capture_count;
uint64_t capture_ids[LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS];
char optical_state_token[LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY];
} Lardon3DCalibrationWorkflowInputBoundary;
/* Validate only the immutable external input boundary. This function performs
* no Project DB access, no calibration solve, no Tooling import and no
* selected-execution mutation. Every path is opened O_NONBLOCK/O_NOFOLLOW,
* must resolve to a bounded regular file, and is SHA-256 checked before its
* syntax/provenance is consumed. */
Lardon3DCalibrationWorkflowResult lardon3d_calibration_workflow_validate_input_boundary(
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationWorkflowInputBoundary *boundary);
typedef enum {
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_SOURCE_SIZE = 1,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_DECODE = 2,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_DECODED_DIMENSIONS = 3,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_INSUFFICIENT_CHARUCO = 4,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_INVALID_CHARUCO_ID = 5,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_OCCUPANCY = 6,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_TARGET_PHYSICAL_QUADRANTS = 7,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_CLIPPING = 8,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_PRE_SOLVE_CORNER_RMS = 9,
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_COORDINATE_EQUIVALENCE = 10,
} Lardon3DCalibrationWorkflowRejectionReason;
typedef struct {
Lardon3DCalibrationWorkflowInputBoundary boundary;
unsigned char target_sha256[32];
unsigned char optical_state_sha256[32];
unsigned char solver_executable_sha256[32];
unsigned char solver_configuration_sha256[32];
unsigned char initialization_evidence_sha256[32];
unsigned char validation_evidence_sha256[32];
uint32_t target_family;
uint32_t target_squares_x;
uint32_t target_squares_y;
double target_square_length_mm;
double target_marker_length_mm;
double target_active_width_mm;
double target_active_height_mm;
double target_white_border_mm;
double target_measurements_mm[LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS];
double measurement_resolution_mm;
double target_flatness_mm;
double holdout_rmse_px;
double holdout_maximum_residual_px;
uint32_t extra_distortion_coefficient_count;
/* Exact decoded/oriented geometry shared by every accepted calibration view.
* It is retained from detection.json + coordinate evidence and is never
* inferred from camera parameters or campaign images. */
uint32_t oriented_width;
uint32_t oriented_height;
const Lardon3DCalibrationToolingView *views;
size_t view_count;
const Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks;
size_t coordinate_check_count;
double repeated_parameters[3][8];
double fit_parameters[8];
uint32_t support_images;
uint32_t support_observations;
double reprojection_rmse_px;
double maximum_residual_px;
double high_residual_fraction;
double maximum_parameter_delta;
uint32_t validation_flags;
} Lardon3DCalibrationWorkflowExternalEvidence;
/* Materialize the already validated external session and solver bundle into
* bounded Science-v1 evidence. Caller owns `views` and `coordinate_checks`;
* output borrows those arrays on success. This stage performs no Project DB
* access, no Tooling import and no selected-execution mutation. */
Lardon3DCalibrationWorkflowResult
lardon3d_calibration_workflow_materialize_external_evidence(
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationToolingView *views, size_t view_capacity,
Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks,
size_t coordinate_check_capacity,
Lardon3DCalibrationWorkflowExternalEvidence *output);
/* Bind already materialized external evidence to the exact durable selected
* execution. This stage is read-only: it verifies selected item order/Capture
* identity, explicit v23 optical assignment, managed representation
* size/SHA-256, safe project-relative regular-file access and the exact
* OpenCV-decoded oriented dimensions. Caller owns `entries`; `output` borrows
* them on success. No Tooling import or selected-execution mutation occurs. */
Lardon3DCalibrationWorkflowResult
lardon3d_calibration_workflow_bind_selected_execution(
Lardon3DProjectDb *database, const char *project_path,
const Lardon3DCalibrationWorkflowExternalEvidence *external,
Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity,
Lardon3DCalibrationToolingEvidence *output);
/* Complete the bounded calibration workflow through the frozen Tooling and
* Bootstrap importer. All supplied arrays and artifact storage remain caller
* owned; `output` is cleared unless import reaches the exact READY transition.
* Input validation, materialization and selected-execution binding finish
* before this function invokes the only mutating operation. Exact retries use
* Bootstrap's immutable import semantics. */
Lardon3DCalibrationWorkflowResult
lardon3d_calibration_workflow_complete(
Lardon3DProjectDb *database, const char *project_path,
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationToolingView *views, size_t view_capacity,
Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks,
size_t coordinate_check_capacity, Lardon3DCalibrationToolingEntry *entries,
size_t entry_capacity, unsigned char *artifact, size_t artifact_capacity,
size_t *artifact_size, Lardon3DCalibrationBootstrapOutput *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,80 @@
#ifndef LARDON3D_CALIBRATION_WORKFLOW_V2_H
#define LARDON3D_CALIBRATION_WORKFLOW_V2_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/calibration_bootstrap_v2.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LARDON3D_CALIBRATION_WORKFLOW_V2_AUTOMATIC = 1,
LARDON3D_CALIBRATION_WORKFLOW_V2_EXISTING_EXPLICIT = 2,
LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT = 3,
} Lardon3DCalibrationWorkflowV2BindingKind;
typedef struct {
uint32_t selected_item_index;
/* Capture identity must be copied from the selected execution's durable item
* mapping. Workflow rejects any mismatch and never derives it from image_id,
* artifact group number, path, filename, or SHA-256. */
uint64_t capture_id;
Lardon3DCalibrationWorkflowV2BindingKind kind;
/* EXISTING_EXPLICIT requires an existing exact v26 applicability. */
uint64_t applicability_id;
/* PUBLISH_EXPLICIT uses this Capture's complete observed tuple as the exact
* applicability exemplar. It may equal capture_id. Other modes require 0. */
uint64_t exemplar_capture_id;
} Lardon3DCalibrationWorkflowV2Binding;
typedef enum {
LARDON3D_CALIBRATION_WORKFLOW_V2_READY = 0,
LARDON3D_CALIBRATION_WORKFLOW_V2_CALIBRATION_REQUIRED,
LARDON3D_CALIBRATION_WORKFLOW_V2_SELECTION_REQUIRED,
LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_EVIDENCE,
LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT,
LARDON3D_CALIBRATION_WORKFLOW_V2_DB_ERROR,
LARDON3D_CALIBRATION_WORKFLOW_V2_OUT_OF_MEMORY,
} Lardon3DCalibrationWorkflowV2Result;
typedef struct {
Lardon3DCalibrationBootstrapV2Output publication;
uint32_t selected_item_count;
} Lardon3DCalibrationWorkflowV2Output;
/* Compose heterogeneous L3DCALB2 publication with exact Project DB v26
* applicability and the final selected-execution READY transition.
*
* All pointers are borrowed for the call. `bindings` must cover every selected
* item exactly once and must repeat the durable selected item -> Capture
* mapping. AUTOMATIC accepts exactly one compatible applicability and reports
* CALIBRATION_REQUIRED/SELECTION_REQUIRED truthfully for zero/multiple choices.
* EXISTING_EXPLICIT verifies and durably selects the requested exact
* applicability. PUBLISH_EXPLICIT creates/reuses deterministic optical profile
* metadata for the artifact calibration, creates/reuses exact exemplar
* applicability, and selects it explicitly.
*
* READY is returned only when every durable Capture resolves to the same
* calibration_id as its complete scope member and that exact scope is attached.
* Invalid evidence or an assignment mismatch is a distinct non-ready error.
* Pre-attachment failure never attaches a scope; immutable publication and
* optical evidence created by an earlier phase may remain and exact retry
* converges. No schema change, solver execution, or Capture inference occurs.
*/
Lardon3DCalibrationWorkflowV2Result lardon3d_calibration_workflow_v2_complete(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char
expected_artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE],
const Lardon3DCalibrationWorkflowV2Binding *bindings, size_t binding_count,
Lardon3DCalibrationWorkflowV2Output *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -9,9 +9,9 @@
#include <lardon3d/visual_index.h> #include <lardon3d/visual_index.h>
typedef struct { typedef struct {
uint32_t generated_count; // paires nouvellement créées uint32_t generated_count; // newly created pairs
uint32_t skipped_count; // paires déjà existantes (idempotence) uint32_t skipped_count; // already existing pairs (idempotent reuse)
uint32_t queried_count; // candidats retournés par le Visual Index uint32_t queried_count; // candidates returned by the Visual Index
} Lardon3DCandidatePairGenStats; } Lardon3DCandidatePairGenStats;
Lardon3DVisualIndexResult lardon3d_candidate_pair_generate( Lardon3DVisualIndexResult lardon3d_candidate_pair_generate(

View file

@ -42,7 +42,7 @@ bool lardon3d_project_enqueue_image_import(Lardon3DAppState *state, uint64_t sca
bool lardon3d_image_import_reconstruct(const Lardon3DTaskDurableSnapshot *snapshot, void *context, bool lardon3d_image_import_reconstruct(const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding); Lardon3DTaskKindBinding *binding);
/* Compatibilité TUI : handle léger sur une tâche de la queue, sans thread. */ /* TUI compatibility: lightweight handle over a Queue Task, with no private thread. */
Lardon3DImportTask *lardon3d_import_task_create(void); Lardon3DImportTask *lardon3d_import_task_create(void);
bool lardon3d_import_task_start(Lardon3DImportTask *task, Lardon3DAppState *state, bool lardon3d_import_task_start(Lardon3DImportTask *task, Lardon3DAppState *state,
const char *source_directory); const char *source_directory);

View file

@ -15,6 +15,10 @@ enum {
LARDON3D_OPTICAL_TEXT_CAPACITY = 128, LARDON3D_OPTICAL_TEXT_CAPACITY = 128,
LARDON3D_OPTICAL_PROVENANCE_CAPACITY = 256, LARDON3D_OPTICAL_PROVENANCE_CAPACITY = 256,
LARDON3D_OPTICAL_PAGE_MAX = 128, LARDON3D_OPTICAL_PAGE_MAX = 128,
LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE = 32,
/* This is an operational persistence/API bound, not a scientific focus
* range. Every member remains one opaque exact observation token. */
LARDON3D_OPTICAL_FOCUS_DOMAIN_TOKEN_MAX = 64,
}; };
typedef enum { typedef enum {
@ -119,6 +123,86 @@ typedef struct {
uint64_t sparse_calibration_id; uint64_t sparse_calibration_id;
} Lardon3DOpticalCaptureCalibrationSelection; } Lardon3DOpticalCaptureCalibrationSelection;
typedef enum {
LARDON3D_OPTICAL_OBSERVATION_UNKNOWN = 1,
LARDON3D_OPTICAL_OBSERVATION_OBSERVED = 2,
} Lardon3DOpticalObservationState;
typedef enum {
LARDON3D_OPTICAL_STABILIZATION_UNKNOWN = 1,
LARDON3D_OPTICAL_STABILIZATION_OFF = 2,
LARDON3D_OPTICAL_STABILIZATION_ON = 3,
} Lardon3DOpticalStabilizationState;
typedef enum {
LARDON3D_OPTICAL_GEOMETRIC_STATE_METADATA = 1,
LARDON3D_OPTICAL_GEOMETRIC_STATE_CALLER_EXPLICIT = 2,
} Lardon3DOpticalGeometricStateProvenance;
typedef enum {
LARDON3D_OPTICAL_CALIBRATION_REQUIRED = 1,
LARDON3D_OPTICAL_CALIBRATION_RESOLVED = 2,
LARDON3D_OPTICAL_CALIBRATION_SELECTION_REQUIRED = 3,
} Lardon3DOpticalCalibrationResolutionKind;
typedef struct {
uint64_t capture_id;
uint64_t optical_configuration_id;
uint32_t state_version;
Lardon3DOpticalGeometricStateProvenance provenance;
Lardon3DOpticalObservationState focus_state;
/* Focus is an exact, bounded observation token, not a physical-distance or
* autofocus domain. Empty is required when focus is explicitly unknown. */
char focus_observation[LARDON3D_OPTICAL_TEXT_CAPACITY];
Lardon3DOpticalObservationState aperture_state;
uint32_t aperture_x1000;
Lardon3DOpticalStabilizationState stabilization;
Lardon3DOpticalObservationState crop_state;
char crop_observation[LARDON3D_OPTICAL_TEXT_CAPACITY];
Lardon3DOpticalObservationState pipeline_state;
char pipeline_observation[LARDON3D_OPTICAL_TEXT_CAPACITY];
Lardon3DOpticalObservationState representation_state;
char representation_observation[LARDON3D_OPTICAL_TEXT_CAPACITY];
Lardon3DOpticalObservationState decoded_geometry_state;
uint32_t decoded_width;
uint32_t decoded_height;
} Lardon3DOpticalCaptureGeometricState;
typedef struct {
uint64_t applicability_id;
uint64_t calibration_profile_id;
uint64_t optical_configuration_id;
uint64_t exemplar_capture_id;
} Lardon3DOpticalCalibrationApplicabilityV2;
typedef struct {
uint64_t focus_domain_id;
/* The v26 applicability retains calibration/configuration identity and its
* exemplar retains the exact complete non-focus geometric tuple. */
uint64_t applicability_id;
uint64_t calibration_profile_id;
uint64_t optical_configuration_id;
uint64_t exemplar_capture_id;
uint32_t domain_version;
unsigned char evidence_sha256[LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE];
uint32_t token_count;
} Lardon3DOpticalFocusDomainV2;
typedef struct {
Lardon3DOpticalCalibrationResolutionKind kind;
uint64_t applicability_id;
uint64_t calibration_profile_id;
uint64_t sparse_calibration_id;
} Lardon3DOpticalCalibrationResolutionV2;
typedef struct {
uint64_t capture_id;
uint64_t applicability_id;
uint64_t calibration_profile_id;
uint64_t optical_configuration_id;
uint64_t sparse_calibration_id;
} Lardon3DOpticalCaptureCalibrationSelectionV2;
/* All profile/config creation calls borrow input only for the call and return a /* All profile/config creation calls borrow input only for the call and return a
* caller-owned, NUL-terminated copy; input and output storage must not overlap. * caller-owned, NUL-terminated copy; input and output storage must not overlap.
* The generated row-ID field in a create input must be zero; referenced IDs * The generated row-ID field in a create input must be zero; referenced IDs
@ -247,6 +331,64 @@ Lardon3DProjectDbResult lardon3d_optical_capture_calibration_selection_load(
Lardon3DProjectDb *database, uint64_t capture_id, Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureCalibrationSelection *output); Lardon3DOpticalCaptureCalibrationSelection *output);
/* v26 state is Capture-owned and separate from both Capture identity and the
* v23 body/lens/focal configuration. Unknown values remain explicit. Create is
* immutable: an exact retry is idempotent and any differing tuple conflicts. */
Lardon3DProjectDbResult lardon3d_optical_capture_geometric_state_create(
Lardon3DProjectDb *database,
const Lardon3DOpticalCaptureGeometricState *input,
Lardon3DOpticalCaptureGeometricState *output);
Lardon3DProjectDbResult lardon3d_optical_capture_geometric_state_load(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureGeometricState *output);
/* Applicability binds an existing v1 calibration profile to the exemplar's
* exact configuration and complete observed-state tuple. It authorizes no
* body/lens/focal substitution, unknown default, interpolation or extrapolation. */
Lardon3DProjectDbResult lardon3d_optical_calibration_applicability_v2_create(
Lardon3DProjectDb *database, uint64_t calibration_profile_id,
uint64_t exemplar_capture_id,
Lardon3DOpticalCalibrationApplicabilityV2 *output);
/* Attach one physically validated discrete focus domain to an existing exact
* v26 applicability. evidence_sha256 must be a nonzero retained-evidence or
* provenance digest. focus_tokens contains 1..FOCUS_DOMAIN_TOKEN_MAX distinct,
* nonempty, NUL-terminated opaque tokens; the call borrows the array and
* strings only for its duration. Members are stored as a deterministic exact
* set: order has no meaning, exact retry is idempotent, and conflicting reuse
* of the applicability is CONSTRAINT. The API performs no numeric conversion,
* EXIF interpretation, interpolation, extrapolation, or physical validation.
* output is mandatory. On OK, it receives the durable focus domain and its
* attached applicability identity; for valid calls it is zeroed on non-OK. */
Lardon3DProjectDbResult lardon3d_optical_focus_domain_v2_create(
Lardon3DProjectDb *database, uint64_t applicability_id,
uint32_t domain_version,
const unsigned char evidence_sha256[LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE],
const char *const *focus_tokens, size_t token_count,
Lardon3DOpticalFocusDomainV2 *output);
/* Resolution counts distinct eligible applicability rows: exact v26 state
* matches plus v27 domains whose non-focus geometry is exact and whose focus
* token is an exact member. NONE is CALIBRATION_REQUIRED, ONE is RESOLVED, and
* MANY is SELECTION_REQUIRED. All are successful outcomes. */
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_resolve_v2(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCalibrationResolutionV2 *output);
/* Selection requires a complete observed geometric-state tuple and an
* applicability currently eligible by exact v26 match or exact v27 domain
* membership; UNKNOWN in any geometry-relevant field is a CONSTRAINT. The
* first selection is immutable, exact retry is idempotent, and a conflicting
* applicability is rejected without changing the selection. */
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_select_v2(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t applicability_id);
/* Loads the Capture's durable v2 selection without creating or changing it.
* NOT_FOUND means no selection exists. OK returns the complete stored tuple
* only when its applicability remains exactly compatible with the Capture's
* complete observed geometric state; broken dependencies are CORRUPT. */
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_selection_load_v2(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureCalibrationSelectionV2 *output);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -11,10 +11,10 @@
#include <lardon3d/sparse_sfm_incremental.h> #include <lardon3d/sparse_sfm_incremental.h>
enum { enum {
/* v25 adds only durable features.extract.batch/1 operational state. The /* v27 additively stores validated focus domains as bounded sets of exact
* selected execution remains the immutable ordered image domain; migration * observed tokens attached to v26 applicability. Migration creates no
* creates no batch association, cursor, Feature Set, or inferred identity. */ * domain or token from historical geometric state. */
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 25, LARDON3D_PROJECT_DB_SCHEMA_VERSION = 27,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,

View file

@ -325,11 +325,11 @@ const char *lardon3d_resource_decision_name(
Lardon3DResourcePressure lardon3d_resource_governor_pressure( Lardon3DResourcePressure lardon3d_resource_governor_pressure(
Lardon3DResourceGovernor *governor Lardon3DResourceGovernor *governor
); );
/* Enregistre les métriques d'un lot terminé pour l'adaptation dynamique /* Records metrics from one completed batch for dynamic adaptation of future
* de la taille des lots futurs. batch_size est le nombre d'éléments dont le * batch sizes. batch_size is the number of items whose processing was validated
* traitement a é validé dans ce lot. peak_memory_bytes == 0 signifie que * in this batch. peak_memory_bytes == 0 means the measurement is unknown and
* la mesure est inconnue et n'alimente jamais l'adaptation mémoire. Le buffer * never feeds memory adaptation. The buffer is bounded to 8 entries per Task
* est borné (8 entrées par classe de tâche). Thread-safe. */ * class. Thread-safe. */
bool lardon3d_resource_governor_record_batch( bool lardon3d_resource_governor_record_batch(
Lardon3DResourceGovernor *governor, Lardon3DResourceGovernor *governor,
Lardon3DResourceTaskClass task_class, Lardon3DResourceTaskClass task_class,

View file

@ -108,8 +108,8 @@ Lardon3DTask *lardon3d_task_create_typed(
Lardon3DTaskUserdataDestroy userdata_destroy Lardon3DTaskUserdataDestroy userdata_destroy
); );
void lardon3d_task_destroy(Lardon3DTask *task); void lardon3d_task_destroy(Lardon3DTask *task);
/* Exécute le callback dans le thread appelant. Le callback est invoqué hors /* Executes the callback on the calling thread. The callback runs outside
* mutex de tâche; le contract d'exécution et l'état appartiennent à la tâche. * the Task mutex; the execution contract and state remain owned by the Task.
*/ */
bool lardon3d_task_start( bool lardon3d_task_start(
Lardon3DTask *task, Lardon3DTask *task,
@ -169,16 +169,16 @@ Lardon3DTask *lardon3d_task_restore_typed(
void *userdata, void *userdata,
Lardon3DTaskUserdataDestroy userdata_destroy Lardon3DTaskUserdataDestroy userdata_destroy
); );
/* Une restauration typée réussie transfère userdata/userdata_destroy à la /* A successful typed restore transfers userdata/userdata_destroy ownership
* tâche. En cas d'échec, l'appelant en reste propriétaire. */ * to the Task. On failure, the caller retains ownership. */
bool lardon3d_task_kind_is_valid(const char *task_kind); bool lardon3d_task_kind_is_valid(const char *task_kind);
bool lardon3d_task_kind( bool lardon3d_task_kind(
const Lardon3DTask *task, const Lardon3DTask *task,
char task_kind[LARDON3D_TASK_KIND_CAPACITY], char task_kind[LARDON3D_TASK_KIND_CAPACITY],
uint32_t *task_kind_version uint32_t *task_kind_version
); );
/* Appelé au plus une fois, hors mutex de tâche et après libération de la /* Invoked at most once, outside the Task mutex and after the terminal
* réservation terminale. Le userdata de tâche reste vivant jusqu'au retour. */ * reservation is released. Task userdata remains alive until the callback returns. */
bool lardon3d_task_set_finished_callback( bool lardon3d_task_set_finished_callback(
Lardon3DTask *task, Lardon3DTask *task,
Lardon3DTaskFinishedCallback callback, Lardon3DTaskFinishedCallback callback,
@ -190,21 +190,20 @@ bool lardon3d_task_resource_estimate(
const Lardon3DTask *task, const Lardon3DTask *task,
Lardon3DResourceEstimate *estimate Lardon3DResourceEstimate *estimate
); );
/* L'exécution ne reçoit pas de politique d'admission : c'est au gouverneur de /* Execution does not receive admission policy: the Governor must confirm
* confirmer la réservation avant l'exécution. * the reservation before execution.
*/ */
bool lardon3d_task_execution_contract( bool lardon3d_task_execution_contract(
const Lardon3DTask *task, const Lardon3DTask *task,
Lardon3DTaskExecutionContract *contract Lardon3DTaskExecutionContract *contract
); );
/* Libère la réservation courante, en obtient une nouvelle auprès du gouverneur /* Releases the current reservation, obtains a new one from the Governor,
* et met à jour le contrat. À appeler uniquement depuis le callback en cours * and updates the execution contract. Call only from the currently executing
* d'exécution. Une réponse WAIT du gouverneur est une indisponibilité * callback. A Governor WAIT is temporary unavailability: this function waits
* temporaire : la fonction attend un changement de ressources puis retente * for a resource change and retries admission without failing the Task. Batch
* l'admission sans échouer la tâche. Les bornes de lot se poursuivent après * bounds continue under the new admission. Returns false if the Task is
* cette nouvelle admission. Retourne false si la tâche est annulée * cancelled (TASK_CANCELLED), the Governor returns REJECT, or an internal
* (TASK_CANCELLED), si le gouverneur répond REJECT ou en cas d'erreur interne * error fails the Task (TASK_FAILED). */
* (TASK_FAILED). */
bool lardon3d_task_sequence_break( bool lardon3d_task_sequence_break(
Lardon3DTask *task, Lardon3DTask *task,
Lardon3DResourceGovernor *governor, Lardon3DResourceGovernor *governor,

View file

@ -42,10 +42,6 @@ typedef struct {
* serialized worker. capacity bounds pending Tasks, not terminal history; the * serialized worker. capacity bounds pending Tasks, not terminal history; the
* Governor retains resource-admission ownership and must outlive the Queue. * Governor retains resource-admission ownership and must outlive the Queue.
* *
* File d'exécution bornée, à ordre d'attente FIFO avec sélection adaptative du
* premier travail admissible, et un seul worker: ownership d'ordonnancement et
* de backpressure seulement. L'admission des demandes reste au Governneur.
*
* CONTRACT: after a successful add, the Queue owns Task and its userdata. Once * CONTRACT: after a successful add, the Queue owns Task and its userdata. Once
* the Task is terminal and its finished callback has returned, the Queue keeps * the Task is terminal and its finished callback has returned, the Queue keeps
* only a snapshot and promptly destroys the real Task outside the Queue lock. * only a snapshot and promptly destroys the real Task outside the Queue lock.
@ -73,8 +69,8 @@ Lardon3DTaskQueue *lardon3d_task_queue_create(
* starts after destruction safe. NULL is accepted. Never call destroy * starts after destruction safe. NULL is accepted. Never call destroy
* synchronously from a Task finished callback running on this Queue. */ * synchronously from a Task finished callback running on this Queue. */
void lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue); void lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue);
/* La file devient propriétaire de task uniquement en cas de succès. /* The Queue takes ownership of task only on success.
* Bloquante : attend une place libre si la file est pleine. A zero Task ID is * Blocking: waits for a free slot when the Queue is full. A zero Task ID is
* assigned from a nonzero monotonic sequence and is never generated twice * assigned from a nonzero monotonic sequence and is never generated twice
* during this Queue lifetime, including after terminal-history eviction or * during this Queue lifetime, including after terminal-history eviction or
* removal. Once UINT64_MAX has been generated (or consumed by a restored * removal. Once UINT64_MAX has been generated (or consumed by a restored
@ -85,8 +81,8 @@ bool lardon3d_task_queue_add(
Lardon3DTask *task, Lardon3DTask *task,
uint64_t *task_id uint64_t *task_id
); );
/* Non-bloquante : retourne false si la file est pleine ou en arrêt. /* Non-blocking: returns false if the Queue is full or stopping.
La file devient propriétaire de task uniquement en cas de succès. */ The Queue takes ownership of task only on success. */
bool lardon3d_task_queue_try_add( bool lardon3d_task_queue_try_add(
Lardon3DTaskQueue *queue, Lardon3DTaskQueue *queue,
Lardon3DTask *task, Lardon3DTask *task,

View file

@ -179,7 +179,17 @@ lardon3d_app = executable(
'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/optical_profiles.c', 'src/optical_profiles.c',
'src/calibration_bootstrap.c', 'src/calibration_bootstrap.c',
'src/calibration_bootstrap_v2.c',
'src/calibration_workflow_v2.c',
'src/calibration_tooling.c', 'src/calibration_tooling.c',
'src/calibration_tooling_v2.c',
'src/calibration_af_study.c',
'src/calibration_af_study_workflow.c',
'src/calibration_af_study_assembly.c',
'src/calibration_workflow.cpp',
'src/calibration_workflow_materialize.cpp',
'src/calibration_workflow_bind.cpp',
'src/calibration_workflow_complete.cpp',
'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_geometry.cpp',
'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_incremental.cpp',
'src/sparse_sfm_bundle_adjustment.cpp', 'src/sparse_sfm_bundle_adjustment.cpp',
@ -895,6 +905,159 @@ calibration_tooling_test = executable(
test('calibration-tooling', calibration_tooling_test, timeout: 30) test('calibration-tooling', calibration_tooling_test, timeout: 30)
calibration_publication_v2_test = executable(
'test-calibration-publication-v2',
sources: [
'tests/test_calibration_publication_v2.c',
'src/calibration_tooling_v2.c', 'src/calibration_bootstrap_v2.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, cc.find_library('m')],
)
test('calibration-publication-v2', calibration_publication_v2_test, timeout: 30)
calibration_af_study_test = executable(
'test-calibration-af-study',
sources: [
'tests/test_calibration_af_study.c',
'src/calibration_af_study.c',
],
include_directories: include_directories('include'),
dependencies: [openssl, cc.find_library('m')],
)
test('calibration-af-study', calibration_af_study_test, timeout: 30)
calibration_af_study_workflow_test = executable(
'test-calibration-af-study-workflow',
sources: [
'tests/test_calibration_af_study_workflow.c',
'src/calibration_af_study_workflow.c',
],
include_directories: include_directories('include'),
dependencies: [openssl, cc.find_library('m')],
)
test(
'calibration-af-study-workflow',
calibration_af_study_workflow_test,
timeout: 30,
)
calibration_af_study_assembly_test = executable(
'test-calibration-af-study-assembly',
sources: [
'tests/test_calibration_af_study_assembly.c',
'src/calibration_af_study_assembly.c',
'src/calibration_af_study_workflow.c',
'src/calibration_af_study.c',
],
include_directories: include_directories('include'),
dependencies: [openssl, cc.find_library('m')],
)
test(
'calibration-af-study-assembly',
calibration_af_study_assembly_test,
timeout: 30,
)
calibration_workflow_v2_test = executable(
'test-calibration-workflow-v2',
sources: [
'tests/test_calibration_workflow_v2.c',
'src/calibration_workflow_v2.c',
'src/calibration_tooling_v2.c', 'src/calibration_bootstrap_v2.c',
'src/optical_profiles.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, cc.find_library('m')],
)
test('calibration-workflow-v2', calibration_workflow_v2_test, timeout: 30)
calibration_workflow_test = executable(
'test-calibration-workflow',
sources: [
'tests/test_calibration_workflow.cpp',
'src/calibration_workflow.cpp',
],
include_directories: include_directories('include'),
dependencies: [openssl],
)
test('calibration-workflow', calibration_workflow_test, timeout: 30)
calibration_workflow_materialization_test = executable(
'test-calibration-workflow-materialization',
sources: [
'tests/test_calibration_workflow_materialize.cpp',
'src/calibration_workflow.cpp',
'src/calibration_workflow_materialize.cpp',
],
include_directories: include_directories('include'),
dependencies: [openssl],
)
test(
'calibration-workflow-materialization',
calibration_workflow_materialization_test,
timeout: 30,
)
calibration_workflow_binding_test = executable(
'test-calibration-workflow-binding',
sources: [
'tests/test_calibration_workflow_bind.cpp',
'src/calibration_workflow_bind.cpp',
'src/optical_profiles.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv],
)
test(
'calibration-workflow-binding',
calibration_workflow_binding_test,
timeout: 30,
)
calibration_workflow_ready_test = executable(
'test-calibration-workflow-ready',
sources: [
'tests/test_calibration_workflow_ready.cpp',
'src/calibration_workflow.cpp',
'src/calibration_workflow_materialize.cpp',
'src/calibration_workflow_bind.cpp',
'src/calibration_workflow_complete.cpp',
'src/calibration_tooling.c', 'src/calibration_bootstrap.c',
'src/optical_profiles.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, cc.find_library('m')],
)
test(
'calibration-workflow-ready',
calibration_workflow_ready_test,
timeout: 30,
)
optical_profiles_test = executable( optical_profiles_test = executable(
'test-optical-profiles', 'test-optical-profiles',
sources: [ sources: [

90
prompt.md Normal file
View file

@ -0,0 +1,90 @@
# Lardon3D — Final Execution Contract
This file is the entry point for the collective Lardon3D execution contract.
```text
CONTRACT_SCOPE=ENTIRE_PROMPT_TREE
CONTRACT_LANGUAGE=ENGLISH
WORK_FIRST_RETURN_LAST=REQUIRED
CANONICAL_WORKING_BRANCH=main
CURRENT_STATE_AUTHORITY=main
GIT_CLOSURE_OWNER=HUMAN
IMPLEMENTATION_AUTHORIZATION=NO
```
## Mandatory reading order
Before any major implementation tranche, read:
1. `AGENTS.md`
2. `README.md`
3. `docs/README.md`
4. `docs/product/product_definition.md`
5. `docs/roadmap/roadmap.md`
6. every file under `prompt/` in numeric order from `00_AUTHORITY.md` through `33_DEFINITION_OF_DONE.md`
7. the specialized canonical `docs/architecture/**` documents cited by the tranche
The files under `prompt/` form one collective contract. No child file may be interpreted in isolation when another child file defines a complementary constraint.
## Absolute rules
- Preserve all applicable PASS/FROZEN scientific and architectural boundaries.
- Never invent calibration, lens identity, metric scale, campaign equivalence, provenance, or scientific identity.
- Never create a Project DB version or production Task Kind without explicit human authorization.
- Never create a second Task Runtime, Queue, scheduler, Resource Governor, generic executor, or backend framework merely for convenience.
- Never modify `scan3d/` without explicit human authorization.
- Never use retained real scientific projects as destructive scratch/test workspaces.
- Never reinterpret historical checkpoints as current state.
- Never treat GitHub and Forgejo as different project authorities; they are mirrors of the same canonical `main`.
- Never stage with `git add -A`.
- Never commit or push unless the human explicitly authorizes Git closure.
- Never force-push unless the human explicitly authorizes that exact operation.
## STOP
STOP before any change that would:
- reopen a PASS/FROZEN boundary;
- change a FROZEN scientific threshold or identity;
- introduce pseudo-calibration or silent calibration substitution;
- introduce an unauthorized Project DB schema version;
- introduce an unauthorized production Task Kind;
- create an unnecessary parallel runtime/scheduler/backend framework;
- modify `scan3d/`;
- perform a destructive operation;
- create/switch/merge/rebase branches without authorization;
- resolve an unexplained mirror divergence by overwriting one remote;
- contradict executable code/schema and a canonical contract;
- resolve a material product ambiguity by assumption.
A STOP report contains only the relevant authority, the exact contradiction, and the minimum human decision required.
## Execution discipline
For an authorized tranche:
1. identify the governing authority;
2. inspect only the context needed to execute correctly;
3. define the exact file and subsystem scope;
4. implement the complete tranche;
5. run proportional delta-based validation;
6. fix defects within scope;
7. run `git diff --check`;
8. report only after the tranche is complete or a real STOP condition is reached.
Do not return after each file. Do not repeat global A-to-Z audits when unchanged FROZEN systems already have retained evidence.
## Current head assumptions
This contract was prepared against the repository state that declares:
```text
CURRENT_PROJECT_DB_SCHEMA=v27
PRODUCTION_TASK_KINDS=16
PRODUCT_DEFINITION_V1=PASS/FROZEN
PROMPT_TREE=NEXT
REAL_S21_TRACKS=PASS/FROZEN
REAL_A6000_PRE_SFM=PASS/FROZEN
```
Before implementation, verify these assumptions against current `main`. If current `main` has legitimately advanced, update only the CURRENT statements that are stale; never rewrite historical or FROZEN meaning silently.

63
prompt/00_AUTHORITY.md Normal file
View file

@ -0,0 +1,63 @@
# 00 — Authority
## Status
```text
AUTHORITY_CONTRACT=REQUIRED
```
## Authority
Primary authority is the repository on canonical branch `main`.
Read and apply, in order of role rather than by blindly overriding specialized contracts:
- `AGENTS.md`: global agent obligations and repository limits.
- `docs/product/product_definition.md`: final product destination.
- specialized `docs/architecture/**`: acquired scientific, persistence, runtime, identity and resource contracts.
- `docs/roadmap/roadmap.md`: lifecycle and current sequencing.
- `README.md` and `docs/README.md`: current navigation and summary.
- historical audits/tags/checkpoints: evidence for their own checkpoint only.
- executable code/schema: authority when exact APIs, constants, DDL or runtime behavior are being quoted.
## CURRENT
The canonical working branch is `main`.
```text
CANONICAL_WORKING_BRANCH=main
CURRENT_STATE_AUTHORITY=main
OTHER_BRANCHES_ARE_NOT_AUTHORITY
```
## FROZEN
A specialized PASS/FROZEN contract is not superseded by this prompt tree. This tree references and constrains execution around those contracts; it does not duplicate them.
## Git authority
The local working tree is the current working authority. GitHub (`github`) and Forgejo (`origin`) are mirrors of the same project, not independent variants.
After an explicitly authorized Git closure:
```text
local main
-> github/main
-> origin/main
```
should normally identify the same commit.
## Required Git inspection before Git-sensitive work
```sh
git branch --show-current
git status --porcelain
git remote -v
git rev-parse HEAD
git rev-parse github/main
git rev-parse origin/main
```
If a remote is missing, inaccessible, or unexpectedly divergent: STOP. Do not choose one mirror as the winner by assumption.

View file

@ -0,0 +1,49 @@
# 01 — Product Vision
## Status
```text
PRODUCT_VISION=FROZEN_TARGET
```
## Authority
`docs/product/product_definition.md` is the product authority.
## REQUIRED_PRODUCT_TARGET
Lardon3D is a persistent, incremental, resource-aware Linux photogrammetry system controlled primarily by an ncurses TUI, with a required companion graphical viewer.
The product must let a user:
- create and reopen durable projects;
- ingest traceable still, future live-device, and video-derived captures;
- manage camera/lens/optical configurations without code changes for ordinary equipment;
- reach explicit calibration readiness;
- execute the existing scientific pre-SfM and Sparse SfM pipeline without invented science;
- produce durable Dense/MVS, mesh, refinement, texturing and exports;
- inspect sparse/dense/mesh/texture and camera evidence;
- analyze coverage;
- receive actionable supplementary-capture guidance;
- use live A6000 HDMI and S21 acquisition adapters without forking the scientific pipeline;
- restart after crashes/reboots without hidden transient knowledge.
## Principles
```text
SCIENTIFIC_TRACEABILITY=REQUIRED
DETERMINISM=REQUIRED
PERSISTENT_PROGRESS=REQUIRED
ATOMIC_PUBLICATION=REQUIRED
BOUNDED_EXECUTION=REQUIRED
MAXIMUM_SAFE_USEFUL_THROUGHPUT=REQUIRED
SERIALISM_REQUIRES_PROOF=REQUIRED
NO_SILENT_SCIENTIFIC_SUBSTITUTION=REQUIRED
NO_DESTRUCTIVE_AUTOMATION=REQUIRED
```
The system must expose uncertainty rather than replace it with guesses.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,154 @@
# 02 — Current and Frozen State
## Status
```text
CURRENT_PROJECT_DB_SCHEMA=v27
PRODUCTION_TASK_KINDS=16
USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS
CURRENT_IMPLEMENTATION_CURSOR=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION
```
## Authority
`README.md`, `AGENTS.md`, `docs/roadmap/roadmap.md`, specialized architecture documents, and retained checkpoint evidence.
## CURRENT
```text
Project DB head v27
Production Task kinds 16
Feature Store IMPLEMENTED
Visual Index IMPLEMENTED
Candidate Pair IMPLEMENTED
Matcher IMPLEMENTED
TUI operational observatory VALIDATED
User-facing TUI/control language PASS
External SSD controller VALIDATED
Calibration Solver Preflight v1 PASS
Calibration Evidence Solver v1 IMPLEMENTED/VALIDATED
Calibration solver white-border evidence PASS/FROZEN
Calibration solver producer identity PASS/FROZEN
Calibration solver per-view evidence PASS/FROZEN
Calibration solver bundle repair PASS/FROZEN
Calibration workflow input boundary PASS/FROZEN
Calibration workflow evidence materialization PASS/FROZEN
Calibration workflow selected-execution binding PASS/FROZEN
Calibration workflow Tooling/Bootstrap READY PASS/FROZEN
Calibration Science v2 heterogeneous optics PASS/FROZEN
Calibration v2 optical-state foundation PASS/FROZEN
Calibration v2 heterogeneous publication PASS/FROZEN
Calibration Workflow v2 PASS/FROZEN
Adaptive capture settings semantics PASS/FROZEN
Autofocus v2 foundation PASS/FROZEN
Calibration AF study evidence v1 PASS/FROZEN
Calibration AF study Workflow bridge v1 PASS/FROZEN
Calibration AF study assembly v1 PASS/FROZEN
Calibration Tooling planarity alignment PASS/FROZEN
```
The additive schema lineage is:
```text
v22 selected scientific execution foundation
v23 generic optical-context overlay
v24 raw.develop.batch/1
v25 features.extract.batch/1
v26 Capture geometric state/applicability
```
## FROZEN
Preserve at their documented boundaries:
- Capture / Asset Provenance;
- acquisition ingestion and durable campaign execution;
- Photo Quality Triage;
- Selected Scientific Execution;
- Geometric Verifier v3;
- Track Model / Track Builder;
- Sparse SfM capability through Gate G (detailed lifecycle: Gate A decision/historical; Gates B-G PASS/FROZEN);
- Phase H v1;
- MVS-M1 external OpenMVS 2.4.0 boundary;
- Calibration Science v1;
- Calibration Tooling v1;
- Calibration Bootstrap v1;
- Internal Parallelism / Compute Resources;
- Resource / Compute Governor;
- ORB Vulkan backend;
- Global Maintenance Audit;
- Real S21 Tracks;
- Real A6000 pre-SfM.
`FROZEN` protects meaning and boundary, not necessarily every file byte.
## Current calibration-v2 design intent
Calibration v1 remains a valid frozen compatibility path. It is not weakened or
retroactively reinterpreted.
The current product dependency has changed before the physical campaign because
real A6000 engine-bay acquisition evidence showed that normal operation requires
heterogeneous optics and autofocus. Calibration Science v2 must therefore define
an additive path in which:
```text
one project may mix camera bodies
one project may mix lenses
one project may mix focal configurations
one selected execution may use multiple calibrations
each selected image must resolve to exactly one compatible calibration
autofocus is a normal supported acquisition mode
exposure settings are adaptive rather than fixed scientific constants
```
Capture/quality acceptance and calibration readiness are distinct. A sharp,
usable image with unresolved optics is retained and reported as
`CALIBRATION_REQUIRED`; it is not discarded merely because calibration evidence
is not yet available.
No v2 rule may silently substitute another body, lens, focal state, focus state
or calibration.
## Retained real S21 checkpoint
```text
REAL_S21_TRACKS=PASS/FROZEN
project=/home/fy59/Documents/Lardon/.real-pre-sfm-2026-08-31/s21-gv-v3
Tracks=912447
Track observations=2495768
Track length min/max=2/42
digest=c30eba192627bf73eaf21ff30d81038d8cc6bbf36a69226f88cdc8c37f7d74a1
Sparse SfM=NOT EXECUTED
Dense/MVS=NOT EXECUTED
```
## Retained real A6000 checkpoint
```text
REAL_A6000_PRE_SFM=PASS/FROZEN
tag=real-a6000-pre-sfm-2026-09-02
project=/home/fy59/Documents/Lardon/.real-pre-sfm-2026-09-01/a6000-pre-sfm-v23-final
Project DB=v25
Selected images=689
Feature Sets=689
Candidate Pairs=38420
Match Results=38420
Applicable GVRs=37805
Verified GVRs=10952
Rejected GVRs=26853
Track Sets=1
Tracks=130714
Track observations=318944
Sparse SfM Tasks=0
Sparse Reconstructions=0
Dense/MVS=0
GV v3 fingerprint=6944a471d611d8ffc59dac7cf15a5b79b97e2371d4c51785c477d68c1577f74c
```
The project-directory name is historical and must not be mistaken for schema head.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

57
prompt/03_END_PRODUCT.md Normal file
View file

@ -0,0 +1,57 @@
# 03 — End Product
## Status
```text
END_PRODUCT=REQUIRED_PRODUCT_TARGET
```
## Authority
`docs/product/product_definition.md`.
## REQUIRED_PRODUCT_TARGET
The finished product is not complete at Sparse SfM. It is complete only when the durable workflow reaches usable reconstruction, inspection, improvement and export.
Required top-level capabilities:
- data-driven optics onboarding;
- calibration assistant and accepted calibration import;
- real known-calibration Sparse SfM;
- durable restartable Dense/OpenMVS execution;
- mesh, refinement, texturing and consolidation;
- interoperable traceable export;
- passive graphical viewer;
- offline coverage analysis;
- actionable capture guidance;
- live localization and truthful loss/confidence states;
- stock A6000 HDMI integration;
- S21 integration without root;
- deterministic video keyframes;
- safe optional SSD scratch;
- recovery, diagnostics and resource-aware performance;
- local-first operation with no cloud dependency for core workflows;
- explicit preview-before-delete cleanup/storage management.
## Local-first and cleanup requirements
Core project operation, reconstruction, calibration processing, viewer, coverage analysis, capture guidance and export must not require a cloud service. Future optional network/device adapters must make network use explicit; user project imagery is not uploaded merely to operate the product.
Cleanup is explicit and reviewable. The product may identify orphan temporary assets, superseded immutable generations, export caches and stale scratch, but it must show what will be removed before destructive cleanup. FROZEN/historical evidence is never deleted automatically because a newer generation exists.
## REJECTED
The final product does not require:
- a GUI replacement for the TUI;
- camera firmware or hardware modification;
- cloud reconstruction;
- generic backend frameworks without demonstrated need;
- a second scheduler/runtime;
- invented metric scale.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,61 @@
# 04 — End-to-End Pipeline
## Status
```text
PIPELINE_TARGET=REQUIRED
```
## Authority
`docs/product/product_definition.md` plus specialized architecture contracts for each existing stage.
## REQUIRED_PRODUCT_TARGET
```text
project
-> acquisition / Capture / provenance
-> optics assignment
-> calibration readiness
-> quality selection
-> deterministic selected scientific representation
-> Features
-> Visual Index
-> Candidate Pairs
-> Matcher
-> Geometric Verification
-> Tracks
-> Sparse SfM
-> incremental enrichment when lineage permits
-> explicit multi-campaign registration when required
-> Dense / MVS
-> mesh
-> refinement
-> texturing
-> consolidation
-> coverage analysis
-> viewer / capture guidance
-> export
```
Video and live sources enter through acquisition boundaries and converge into the same normal scientific pipeline after durable Capture promotion.
## Invariants
Every scientific stage must consume explicit durable identities and publish atomically. A downstream failure must not invalidate a valid upstream immutable generation.
## Quality boundary
Existing quality recommendations remain explainable and non-destructive:
```text
GOOD
SUSPECT
REJECT
```
Where the FROZEN selection contract permits a human override, that override is explicit, durable and visible and does not rewrite measured quality evidence. Live guidance should reuse the same quality concepts where practical without turning preview evidence into a scientific Capture.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,35 @@
# 05 — Scientific Invariants
## Status
```text
SCIENTIFIC_INVARIANTS=MANDATORY
```
## Authority
Specialized PASS/FROZEN architecture documents remain authoritative.
## FROZEN
- No pseudo-calibration.
- No EXIF-as-calibration fallback.
- No silent lens identity inference.
- No silent calibration substitution or interpolation.
- No metric-scale claim for arbitrary monocular gauge.
- No silent merging of independent campaigns.
- No mutation of immutable upstream scientific generations.
- No treating an external process success code as scientific acceptance.
- No anonymous temporary representation may become a durable scientific result without provenance.
- Deterministic scientific identities and parameter fingerprints remain explicit.
- Scientific identity must not depend on CPU thread count, worker scheduling, host model, temporary path or wall-clock duration.
- GPU/CPU execution choice is operational unless a scientific contract explicitly makes a backend scientifically non-transparent.
- Where exact bit identity cannot honestly be guaranteed across dependency/hardware versions, the owning scientific contract must define the reproducibility boundary explicitly.
## STOP
If a planned feature requires a new threshold, weighting function, estimator, classification policy, frame-selection science, coverage score or other scientific rule that is not already FROZEN, first define and validate a versioned scientific contract. Do not hide new science inside implementation code.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,42 @@
# 06 — Identity Model
## Status
```text
IDENTITY_DISCIPLINE=REQUIRED
```
## Authority
`AGENTS.md`, persistence/project database documents, Capture/provenance and scientific model documents.
## FROZEN
Never silently equate:
```text
Capture != file
Capture != asset
Capture != image_id
Capture != SHA-256
Capture != path
Capture != filename/basename
Capture != Task ID
Capture != campaign group ID
Task ID != scientific acquisition identity
campaign group ID != Capture identity
```
Current meanings:
- SHA-256: immutable asset bytes.
- `capture_id`: physical acquisition representation in Project DB.
- `image_id`: scientific image representation.
- Task ID: durable operational work.
- campaign group ID: stable operational grouping within an acquisition request.
Future Dense, mesh, texture, coverage, registration, live-promotion and video-keyframe identities must be explicit, versioned, durable and provenance-linked. They must not be inferred from paths or timestamps.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,63 @@
# 07 — Persistence and Recovery
## Status
```text
PERSISTENCE_RECOVERY=REQUIRED
```
## Authority
`docs/architecture/persistence.md`, `docs/architecture/project_database.md`, `docs/architecture/runtime.md`, `docs/architecture/task_system.md`.
## FROZEN
Existing persistence semantics, task recovery and schema lineage are consumed as-is.
## REQUIRED_PRODUCT_TARGET
New long-running product stages must be:
- durable;
- restartable;
- failure-atomic;
- bounded;
- explicit about immutable inputs and published generations;
- recoverable without private solver/process state.
Do not persist transient library internals merely to resume work.
External-process orchestration must durably preserve enough exact input/output identity and checkpoint state to restart safely.
### Project portability
The durable project must not depend on:
- absolute temporary paths;
- current CPU topology;
- current GPU device;
- current external-scratch mount path;
- a live camera connection;
- a previous process ID;
- a specific Task worker instance.
Hardware-specific operational state is rediscovered; scientific identities remain stable.
### Recovery cases
Final-product long-running stages must define recovery behavior for application close, system reboot, Task cancellation, process crash, external-process failure, safe-drained scratch disconnect and partial physical-asset publication where existing orphan semantics permit it.
Restart must never infer the scientific input from filename, timestamp or another heuristic.
## Schema rule
```text
NEW_PROJECT_DB_VERSION_REQUIRES_HUMAN_AUTHORIZATION
NEW_PRODUCTION_TASK_KIND_REQUIRES_HUMAN_AUTHORIZATION
```
A planned capability does not itself grant that authorization.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,48 @@
# 08 — Resource Policy
## Status
```text
RESOURCE_UTILIZATION_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
SERIALISM_REQUIRES_PROOF=CANONICAL
```
## Authority
`AGENTS.md`, `docs/architecture/resource_governor.md`, `docs/architecture/internal_parallelism.md`, `docs/architecture/resource_aware_pipeline.md`.
## FROZEN
First preserve normal interactive workstation use. Then use all remaining resources that are both safe and useful.
Reference-host evidence only:
```text
16 logical CPUs
~4 logical CPUs interactive reserve
~12 logical CPUs compute
~3 GiB MemAvailable hard reserve
Radeon 780M UMA
```
These are not product constants.
## Rules
- per-item atomicity does not imply cross-item serialization;
- owner-only publication does not imply serial preparation;
- bounded internal fan-out is expected when independent useful work exists;
- all participants are bounded, accounted, cancellable and joined;
- UMA is charged once against host RAM;
- swap and zram are pressure mechanisms, not admitted RAM;
- scratch is storage, not RAM;
- pressure may reduce admission;
- safe useful capacity must be readmitted when pressure clears;
- I/O or measured scaling knees may justify under-utilization;
- fixed historical `CPU1`, `-j8`, batch sizes or worker counts never become universal policy;
- normal users observe but do not need to select authoritative CPU width, batch/window, inflight depth, participant count, GPU backend, scratch mode or RAM budget;
- no stage may introduce an unbounded whole-project load merely for implementation convenience.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,33 @@
# 09 — Task, Queue and Governor
## Status
```text
TASK_RUNTIME_AUTHORITY=SINGLE
QUEUE_AUTHORITY=SINGLE
RESOURCE_GOVERNOR_AUTHORITY=SINGLE
```
## Authority
`docs/architecture/task_system.md`, `task_queue.md`, `task_kind_registry.md`, `resource_governor.md`, `scheduler_resource_integration.md`.
## CURRENT/FROZEN
The current production Queue executes one active callback at a time. Reuse the existing Task -> Queue -> Resource Governor ownership model.
Do not introduce:
- a second scheduler;
- a second production queue;
- a generic executor;
- an uncontrolled worker pool;
- a parallel resource authority.
General inter-Task parallelism remains deferred unless a future dependency and correctness proof requires it.
Bounded internal parallelism within one admitted Task is allowed and expected when scientifically independent work exists and publication semantics remain deterministic.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

41
prompt/10_CAMERA_MODEL.md Normal file
View file

@ -0,0 +1,41 @@
# 10 — Camera Model
## Status
```text
CAMERA_MODEL_V1=FROZEN
```
## Authority
`docs/architecture/sparse_sfm.md` and calibration contracts.
## FROZEN
Sparse SfM v1 is known-calibration only.
The v1 model is binary64 pinhole with zero skew and OpenCV-compatible distortion:
```text
fx, fy, cx, cy
k1, k2, p1, p2
```
or explicit zero distortion.
Image coordinates: top-left origin, +x right, +y down.
Camera frame: x right, y down, z forward.
Pose is world-to-camera:
```text
Xc = R_cw * Xw + t_cw
Cw = -transpose(R_cw) * t_cw
```
Higher-order lens models are outside this FROZEN v1 boundary unless separately authorized and scientifically contracted.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,66 @@
# 11 — Optics Onboarding
## Status
```text
OPTICS_ONBOARDING=PLANNED
```
## Authority
`docs/product/product_definition.md`; current v23 optical-context architecture and persistence remain authoritative.
## REQUIRED_PRODUCT_TARGET
```text
NEW_CAMERA_REQUIRES_CODE_CHANGE=NO
NEW_LENS_REQUIRES_CODE_CHANGE=NO
ELECTRONIC_LENS_WITH_METADATA=SUPPORTED
MANUAL_LENS_WITHOUT_EXIF=SUPPORTED
MULTIPLE_LENSES_PER_CAMERA=SUPPORTED
ZOOM_MULTIPLE_FOCALS=SUPPORTED
MULTIPLE_OPTICAL_CONFIGURATIONS_PER_PROJECT=SUPPORTED
SILENT_CALIBRATION_SUBSTITUTION=FORBIDDEN
SILENT_LENS_IDENTITY_INFERENCE=FORBIDDEN
OPTICS_TUI_WORKFLOW=REQUIRED
PROFILE_IMPORT_EXPORT=REQUIRED
OPTICS_PROFILE_ONBOARDING_TARGET<=5_MINUTES
```
The time target excludes physical calibration acquisition.
Aliases must be exact and reviewable. Fuzzy metadata matching may assist discovery but may never silently create scientific identity.
Profile import/export must be bounded and versioned, support preview/dry-run, reject incompatible required semantics, preserve identities, and never be implemented as a raw SQLite dump.
## Portable profile encoding decision
The v1 portable equipment-profile encoding is a single deterministic bounded binary container:
```text
OPTICS_PROFILE_PORTABLE_FORMAT=L3DOPRF1
OPTICS_PROFILE_PORTABLE_VERSION=1
BYTE_ORDER=LITTLE_ENDIAN
NATIVE_STRUCT_SERIALIZATION=FORBIDDEN
RAW_SQLITE_EXPORT=FORBIDDEN
UNBOUNDED_JSON=FORBIDDEN
```
`L3DOPRF1` carries camera-body profiles, lens profiles, explicit aliases and optical configurations. All integers/floating-point fields use explicit fixed widths; variable UTF-8 strings and record arrays are length/count-prefixed and must have explicit hard maxima in the public format contract before parser implementation. Unknown required semantics or unsupported versions are rejected before database mutation.
Calibration scientific payloads are not re-encoded as profile data. Portable calibrations remain exact `L3DCALB1` artifacts and continue through Calibration Bootstrap v1. The optics-profile import preview may report compatible/missing calibration requirements but may not synthesize or merge calibration.
Import is two-phase:
```text
bounded parse + validation + conflict preview
-> explicit human acceptance
-> one failure-atomic database mutation
```
Conflicting profile identities are never silently merged.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

152
prompt/12_CALIBRATION.md Normal file
View file

@ -0,0 +1,152 @@
# 12 — Calibration
## Status
```text
CALIBRATION_SCIENCE_V1=PASS/FROZEN
CALIBRATION_TOOLING_V1=PASS/FROZEN
CALIBRATION_TOOLING_PLANARITY_ALIGNMENT=PASS/FROZEN
CALIBRATION_BOOTSTRAP_V1=PASS/FROZEN
CALIBRATION_EVIDENCE_SOLVER_V1=IMPLEMENTED/VALIDATED
CALIBRATION_SOLVER_WHITE_BORDER_V1=PASS/FROZEN
CALIBRATION_SOLVER_PRODUCER_IDENTITY_V1=PASS/FROZEN
CALIBRATION_SOLVER_PER_VIEW_EVIDENCE_V1=PASS/FROZEN
CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN
CALIBRATION_WORKFLOW=PASS/FROZEN
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN
CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY_V1=PASS/FROZEN
CALIBRATION_SCIENCE_V2=PASS/FROZEN
CALIBRATION_V2_HETEROGENEOUS_OPTICS=PASS/FROZEN
CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PASS/FROZEN
CALIBRATION_V2_WORKFLOW_READY=PASS/FROZEN
ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PASS/FROZEN
AUTOFOCUS_V2_FOUNDATION=PASS/FROZEN
CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN
CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1=PASS/FROZEN
CALIBRATION_AF_STUDY_ASSEMBLY_V1=PASS/FROZEN
CURRENT_CALIBRATION_NEXT=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION
```
## Authority
`docs/architecture/calibration_science_v1.md`, `docs/architecture/calibration_science_v2.md`, `docs/architecture/calibration_bootstrap.md` and `docs/architecture/calibration_solver_preflight_v1.md` are the current specialized calibration documents. Science v1 remains FROZEN; the v2 document is the current additive design authority until its contract is validated and frozen.
`docs/architecture/calibration_tooling.md` is the specialized Tooling authority.
`docs/architecture/calibration_workflow.md` is the specialized workflow authority. Its public API is `include/lardon3d/calibration_workflow.h`; the FROZEN Tooling API remains `include/lardon3d/calibration_tooling.h`.
A bounded corrective review established that Calibration Science v1 defines target planarity as a categorical physical attestation, not a numeric flatness tolerance. Tooling preserves its public structure layout while requiring `target_flatness_mm` to be NaN, so callers cannot invent a millimetre measurement. The canonical session's `planarity PASS <sha256>` evidence is bound through immutable initialization evidence.
The external `tools/calibration_evidence_solver/` implementation is present and validated by its deterministic synthetic CPU1 self-test. It remains external to the Lardon3D runtime and Project DB.
Its session v1 now requires an explicit measured `white_border` of at least 30 mm. No default border width may be invented by the solver or the future coordinator.
## CURRENT
Historical S21 and A6000 Engine Bay campaigns:
```text
CALIBRATION_UNAVAILABLE
BLOCKED_BY_KNOWN_CALIBRATION_DATA
```
Do not retro-calibrate them by invention.
## Calibration Science v2 product requirements
Science v2 is additive. It does not weaken or rewrite Science v1.
The product requirement is:
```text
ONE_PROJECT_MAY_MIX_CAMERAS=YES
ONE_PROJECT_MAY_MIX_LENSES=YES
ONE_PROJECT_MAY_MIX_FOCAL_CONFIGURATIONS=YES
PER_IMAGE_COMPATIBLE_CALIBRATION=REQUIRED
AUTOFOCUS_NORMAL_OPERATION=SUPPORTED_TARGET
MANUAL_FOCUS_LOCK_REQUIRED_FOR_NORMAL_CAPTURE=NO
SHUTTER_SPEED_FIXED=NO
ISO_FIXED=NO
WHITE_BALANCE_FIXED=NO
EXPOSURE_SETTINGS_ADAPTIVE=YES
PHOTO_REJECTION_BASED_SOLELY_ON_EXPOSURE_METADATA=FORBIDDEN
UNKNOWN_OPTICS_REJECT_CAPTURE=NO
UNKNOWN_OPTICS_STATE=CALIBRATION_REQUIRED
SILENT_CALIBRATION_SUBSTITUTION=FORBIDDEN
```
The core separation is:
```text
CAPTURE / PHOTO QUALITY
!=
CALIBRATION READINESS
```
A Capture may be sharp, non-clipped and scientifically useful as image data
while still being `CALIBRATION_REQUIRED`. It remains durable in the project.
Purely photometric settings such as shutter speed, ISO, white balance and
exposure compensation may vary according to conditions. Their numeric values
alone are not scientific rejection criteria. Actual decoded-image evidence
remains authoritative for quality: sharpness/defocus/motion blur, clipping,
usable tonal information, contrast/texture, noise or local SNR where available,
and downstream feature usability.
Camera/lens/focal/focus/aperture/stabilization/crop/pipeline state is treated
according to whether it can alter geometry. Lardon3D must observe and classify
that state, automatically select an exactly compatible validated calibration
when one exists, expose `CALIBRATION_REQUIRED` when none exists, and expose
`SELECTION_REQUIRED` when compatibility remains ambiguous.
Autofocus is a normal v2 acquisition mode. A real autofocus applicability
envelope must be supported only by physical calibration/hold-out evidence.
Historical EXIF or MakerNote focus observations may guide experiment design but
must never fabricate such evidence.
See `docs/architecture/calibration_science_v2.md`.
## FROZEN v1 flow
```text
dedicated physical calibration acquisition
-> external OpenCV 5.x Calibration Evidence Solver v1
-> immutable session manifest + complete solver bundle
-> workflow coordinator
-> Calibration Tooling v1
-> deterministic L3DCALB1 v1
-> Calibration Bootstrap v1
-> exact selected-execution calibration scope attachment
-> READY
-> real Sparse SfM
```
The workflow coordinator now has three PASS/FROZEN non-mutating checkpoints. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality. Evidence Materialization v1 parses the retained session and solver bundle into bounded Science v1 target, per-view, coordinate, repeated-solve, fit, residual, hold-out and provenance evidence without opening Project DB. Selected Execution Binding v1 is read-only: it proves the exact selected execution and Capture mapping, explicit optical configuration, managed representation size/SHA-256, safe project-relative file containment, decoded geometry dimensions, and deterministic `Lardon3DCalibrationToolingEntry` construction. Missing or mismatched evidence is rejected; nothing is inferred. The final remaining boundary composes the validated binding with the FROZEN Tooling and Bootstrap import path to reach truthful `READY`.
## REQUIRED_PRODUCT_TARGET
The TUI calibration assistant must expose:
```text
READY
CALIBRATION_REQUIRED
SELECTION_REQUIRED
```
It must guide physical evidence acquisition, exact optical configuration, accepted/rejected evidence and remaining evidence requirements.
Solver exit success is never sufficient acceptance.
## REJECTED
Pseudo-calibration, EXIF calibration, nearby focal substitution, lens substitution, silent interpolation, silent backfill, and silent identity inference.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

48
prompt/13_SPARSE_SFM.md Normal file
View file

@ -0,0 +1,48 @@
# 13 — Sparse SfM
## Status
```text
SPARSE_SFM_V1=IMPLEMENTED
GATE_A=DECISION/HISTORICAL
GATE_B=PASS/FROZEN
GATE_C=PASS/FROZEN
GATE_D=PASS/FROZEN
GATE_E=PASS/FROZEN
GATE_F=PASS/FROZEN
GATE_G=PASS/FROZEN
```
## Authority
`docs/architecture/sparse_sfm.md` is the detailed authority.
## FROZEN
Sparse SfM consumes one exact immutable Track Set and one exact compatible known-calibration scope.
It does not recover unknown intrinsics, infer metric scale, mutate Tracks, or merge unrelated campaigns.
The production Task remains:
```text
sparse_sfm.run/1
```
Its FROZEN CPU1/BATCH1 execution is a specific validated Sparse v1 choice and must not be generalized to unrelated stages.
## CURRENT
```text
REAL_S21_SPARSE_SFM=NOT_EXECUTED
REAL_A6000_SPARSE_SFM=NOT_EXECUTED
REAL_SPARSE_SFM_BLOCKER=KNOWN_CALIBRATION_DATA
```
## PLANNED
Run a dedicated physically calibrated real campaign, reach `READY`, then execute the existing Sparse SfM path as a retained real proof. Do not use historical uncalibrated campaigns to bypass the calibration contract.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,46 @@
# 14 — Multi-Campaign Reconstruction
## Status
```text
MULTI_CAMPAIGN_REGISTRATION=PLANNED
MULTI_CAMPAIGN_FUSION=PLANNED
RAW_PROJECT_MERGE_WITHOUT_REGISTRATION=REJECTED
```
## Authority
`docs/product/product_definition.md`, Phase H documentation and reconstruction architecture.
## FROZEN
Phase H v1 is used only when its FROZEN lineage prerequisites are actually satisfied. It is not a generic merger of independent campaigns.
## REQUIRED_PRODUCT_TARGET
Independent campaigns such as separate S21 and A6000 reconstructions require:
```text
independent reconstruction A
+
independent reconstruction B
-> explicit registration
-> durable transform
-> registration quality/provenance
-> explicit accepted alignment
-> fusion/consolidation
```
Use rigid or similarity transform according to explicit scale knowledge.
Campaign contribution and provenance must remain inspectable after alignment and fusion.
Registration may use automatic overlap evidence and may offer explicit manual control-point assistance as a fallback. The registration algorithm, acceptance thresholds and quality policy are future science and must be versioned/validated before implementation.
## REJECTED
Naively combining independent Feature Sets, Tracks or reconstructions merely because they depict the same object.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

39
prompt/15_DENSE_MVS.md Normal file
View file

@ -0,0 +1,39 @@
# 15 — Dense / MVS
## Status
```text
MVS_M1=PASS/FROZEN
DURABLE_DENSE_EXECUTION=PLANNED
```
## Authority
`docs/architecture/reconstruction_pipeline.md` and the canonical MVS-M1 boundary documented there.
## FROZEN
OpenMVS is the first dense backend target. MVS-M1 defines the existing external OpenMVS v2.4.0 boundary.
Do not begin by inventing a generic backend framework.
## REQUIRED_PRODUCT_TARGET
Dense execution must become:
- durable;
- restartable;
- Resource-Governor controlled;
- scratch-aware;
- bounded;
- cancellable;
- failure-atomic;
- inspectable in the TUI.
Validate produced assets before publication. A zero external-process exit code is not sufficient proof.
Dense failure must never invalidate the source Sparse Reconstruction.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,46 @@
# 16 — Mesh, Refinement, Texture and Export
## Status
```text
MESH_TEXTURE_EXPORT=PLANNED
```
## Authority
`docs/product/product_definition.md` and future scoped architecture contracts created before implementation.
## REQUIRED_PRODUCT_TARGET
```text
Dense
-> mesh
-> refinement
-> texturing
-> consolidation
-> export
```
Every stage publishes an explicit immutable generation with provenance.
Minimum target formats:
```text
point cloud: PLY
mesh: PLY, OBJ
textured mesh: OBJ + MTL + texture assets
portable viewer/export: GLB/GLTF
STL: optional
```
Export must preserve scale truth. Never label arbitrary monocular gauge units as millimetres or metres.
Exports must carry a manifest or sidecar sufficient to identify the source reconstruction, mesh/texture generation and scale status.
Consolidation selects an explicit user-facing result from immutable upstream generations. It does not delete those generations; selection remains explicit and reversible until deliberate cleanup.
Intermediate external-tool artifacts must not become authoritative until validated and atomically published.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

70
prompt/17_TUI.md Normal file
View file

@ -0,0 +1,70 @@
# 17 — TUI
## Status
```text
TUI=PRIMARY_CONTROL_SURFACE
USER_INTERFACE_LANGUAGE=ENGLISH
USER_FACING_TUI_LANGUAGE_NORMALIZATION=PASS
```
## Authority
`AGENTS.md`, `README.md`, current TUI architecture and `docs/product/product_definition.md`.
## CURRENT/FROZEN
ncurses remains on its designated/main thread. Reuse the current business-logic / TUI / layout separation and current Queue/Project lifetime boundary.
Validated sizing includes:
```text
full >=100x30
reference compact 72x20
minimum supported 60x15
```
Below minimum, render only the bounded terminal-too-small fallback.
## REQUIRED_PRODUCT_TARGET
The TUI progressively controls:
- project;
- acquisition;
- optics;
- calibration;
- quality;
- pipeline;
- Tasks;
- Governor;
- SSD/scratch;
- reconstructions;
- viewer;
- coverage;
- capture guidance;
- export;
- diagnostics/help.
Displayed actions and actual handlers must remain consistent.
Diagnostics must distinguish at least:
- scientific rejection;
- invalid input;
- missing prerequisite;
- resource wait/throttle;
- runtime failure;
- corruption;
- unsupported version/backend;
- user cancellation.
A generic unowned `failed` state is insufficient for final-product workflows.
Direct TUI/control labels and the user-facing project/import/catalog/runtime-session surface have been normalized to English and validated.
Historical persisted labels, path names and deliberate UTF-8 test fixtures are not renamed merely to satisfy a text scan. Internal subsystem diagnostics are not to be mechanically rewritten across FROZEN or persistence-sensitive boundaries; any diagnostic intentionally exposed to the final user-facing product must converge to English when its owning subsystem is explicitly scoped.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

50
prompt/18_VIEWER.md Normal file
View file

@ -0,0 +1,50 @@
# 18 — Viewer
## Status
```text
VIEWER=PLANNED
VIEWER_ROLE=PASSIVE_SNAPSHOT_CONSUMER
VIEWER_CAN_BE_DISABLED=REQUIRED
VIEWER_BLOCKS_ENGINE=NO
```
## Authority
`docs/architecture/viewer.md` plus `docs/product/product_definition.md`.
## REQUIRED_PRODUCT_TARGET
The graphical viewer is required but does not replace the TUI.
It never owns mutable scientific truth, Task scheduling or Resource Governor policy.
It must be able to slow down, drop frames, close or crash without corrupting or blocking the engine.
Target visualization includes:
- sparse landmarks;
- dense cloud;
- mesh;
- textured mesh;
- registered cameras/frustums;
- reconstruction components;
- Track support;
- reprojection diagnostics;
- campaign/ScanSet contribution;
- coverage heatmap;
- weak/unseen areas and holes;
- live camera pose;
- suggested capture targets.
Required interaction includes orbit/pan/zoom, reset/focus, element/region selection, camera selection, visibility toggles, diagnostic inspection, valid-scale measurement and region-of-interest selection for coverage/guidance.
Visual snapshot buffering must remain bounded. When the viewer falls behind, obsolete snapshots are dropped rather than accumulated without bound.
Viewer annotations do not silently become scientific input. They may enter science only through an explicit supported operation such as a future control-point registration workflow.
Functional isolation is mandatory. A separate Unix process is allowed if it improves isolation without duplicating authority.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

35
prompt/19_LIVE_CAPTURE.md Normal file
View file

@ -0,0 +1,35 @@
# 19 — Live Capture Foundation
## Status
```text
LIVE_CAMERA_SOURCE=PLANNED
```
## Authority
`docs/product/product_definition.md`; Capture/provenance contracts remain authoritative downstream.
## REQUIRED_PRODUCT_TARGET
Create one generic live-acquisition adapter boundary before device-specific integrations.
An adapter may own:
- discovery;
- connection;
- preview transport;
- optional shutter/control;
- metadata retrieval;
- full-resolution file transfer;
- disconnect/reconnect.
It may not redefine scientific identities, calibration, Features, Tracks, reconstruction or resource accounting.
Preview frames are ephemeral observations until explicitly promoted through the supported Capture/provenance path.
Heavy reconstruction compute remains PC-side.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

43
prompt/20_A6000.md Normal file
View file

@ -0,0 +1,43 @@
# 20 — Sony A6000
## Status
```text
A6000_LIVE_INTEGRATION=PLANNED
A6000_FIRMWARE_MODIFICATION=REJECTED
A6000_HARDWARE_MODIFICATION=REJECTED
```
## Authority
`docs/product/product_definition.md`.
## REQUIRED_PRODUCT_TARGET
The Sony A6000 remains stock.
Primary live path:
```text
A6000 native HDMI
-> external capture device
-> Linux capture device (V4L2/UVC-class where available)
-> Lardon3D live adapter
-> viewer/localization/guidance
```
Discover actual capture-device capabilities; do not hard-code one USB capture card.
USB may be used only for capabilities genuinely supported and verified: control, shutter, metadata and file transfer.
Live preview correctness must not depend on USB control availability.
If remote shutter is unavailable, on-camera capture followed by normal full-resolution ingestion remains valid.
```text
A6000_HEAVY_COMPUTE=PC_SIDE_ONLY
```
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

31
prompt/21_S21.md Normal file
View file

@ -0,0 +1,31 @@
# 21 — Samsung S21
## Status
```text
S21_LIVE_INTEGRATION=PLANNED
S21_ROOT_REQUIRED=NO
```
## Authority
`docs/product/product_definition.md`.
## REQUIRED_PRODUCT_TARGET
The S21 uses a device-specific acquisition adapter only at the device/acquisition boundary.
Required properties:
- no root;
- no scientific-core fork;
- graceful disconnect/reconnect;
- explicit full-resolution Capture ingestion;
- preview frames remain ephemeral until promoted;
- exact optical/calibration assignment for scientific use.
The exact Android transport is deliberately deferred until implementation research proves the best supported mechanism.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,39 @@
# 22 — Coverage Analysis
## Status
```text
COVERAGE_ANALYSIS=PLANNED
COVERAGE_SCIENCE=NOT_YET_FROZEN
```
## Authority
`docs/product/product_definition.md` defines the product need; a separate versioned scientific contract is required before implementation.
## REQUIRED_PRODUCT_TARGET
Minimum classification:
```text
UNKNOWN
UNSEEN
WEAK
ADEQUATE
```
Candidate evidence may include observation/view count, distinct camera count, angular diversity, parallax, incidence angle, camera distance, projected resolution, image quality, Feature/Track support, reprojection quality, triangulation quality, visibility/occlusion, dense/mesh evidence, hole/boundary evidence and campaign provenance.
Coverage may operate on the complete meaningful reconstructed target surface or an explicit user-selected region of interest. A global `complete` claim is invalid when no meaningful target surface/ROI exists.
Sparse-only coverage analysis is allowed only with an explicit lower-confidence/support boundary. Dense/mesh evidence may strengthen visibility and hole reasoning but must not retroactively falsify sparse uncertainty.
## STOP
Thresholds, weights, confidence computation and classification rules are science. Freeze and validate them before implementation.
Never fabricate a closed surface from sparse points merely to claim that an area is covered.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,65 @@
# 23 — Capture Guidance
## Status
```text
CAPTURE_GUIDANCE=PLANNED
AUTO_CAPTURE=IDEA
```
## Authority
`docs/product/product_definition.md` and the future FROZEN coverage/localization contracts.
## REQUIRED_PRODUCT_TARGET
Guidance must transform a weakness into an actionable suggestion containing, where available:
- target region;
- suggested direction;
- viewpoint/position zone;
- angle;
- distance;
- baseline;
- expected evidence improvement;
- confidence;
- reason.
Target loop:
```text
existing reconstruction
-> coverage analysis
-> weak/unseen target
-> live camera localization
-> project target into live view
-> guide operator
-> operator acquires full-resolution capture
-> normal ingestion
-> normal incremental/re-registration scientific update as allowed by existing lineage contracts
-> coverage refresh
```
Minimum live localization states:
```text
UNAVAILABLE
SEARCHING
LOCALIZED
LOW_CONFIDENCE
LOST
```
Never display a stale pose as current after tracking loss without an explicit stale/lost indication.
Guidance must be actionable in operator terms where applicable: left/right/up/down, closer/farther, rotate toward/away from target, change incidence angle and change baseline.
Weak/unseen semantics must not rely on color alone.
When localization or coverage confidence is insufficient, do not assert a precise overlay or a certain `capture here` instruction. Fall back to a truthful search/diagnostic state.
For v1, capture remains user-triggered.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,41 @@
# 24 — Video and Deterministic Keyframes
## Status
```text
VIDEO_INGESTION=PLANNED
KEYFRAME_SCIENCE=NOT_YET_FROZEN
```
## Authority
`docs/product/product_definition.md`.
## REQUIRED_PRODUCT_TARGET
Video is an acquisition source, not a second SfM pipeline.
```text
SOURCE video asset
-> deterministic timeline/frame identity
-> bounded deterministic keyframe extraction
-> quality/blur/redundancy analysis
-> explicit selected keyframes
-> normal Capture/provenance
-> existing scientific pipeline
```
Every retained keyframe must preserve:
- source video asset identity;
- exact frame/timestamp identity;
- extraction algorithm/version;
- parameter fingerprint.
## STOP
Define and validate the versioned keyframe scoring/selection science before implementation. Do not hide heuristic thresholds in unversioned code.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

44
prompt/25_SSD_SCRATCH.md Normal file
View file

@ -0,0 +1,44 @@
# 25 — External SSD and Scratch
## Status
```text
EXTERNAL_SSD_CONTROLLER=VALIDATED
TASK_SCRATCH_CONSUMERS=PLANNED
PROJECT_SCRATCH_OPT_IN=REQUIRED
SWAP_OPT_IN=REQUIRED
```
## Authority
Current SSD controller/resource documents and `docs/product/product_definition.md`.
## REQUIRED_PRODUCT_TARGET
Expose:
- physical device identity;
- mount state;
- scratch state;
- swap state;
- capacity/usage;
- active leases;
- drain state;
- safe-to-unplug state.
Project scratch use is explicit opt-in. Swap enable/disable is also explicit user choice when supported by the validated controller. A newly connected device is never adopted automatically.
Dense/mesh/refinement/texturing scratch consumers acquire storage only through the existing Governor-owned scratch lease boundary.
```text
SCRATCH!=RAM
SWAP!=RAM
```
## REJECTED
Automatic destructive repartitioning, formatting, destructive fsck, overwriting unknown filesystems, or force-unmounting an active lease.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

37
prompt/26_PERFORMANCE.md Normal file
View file

@ -0,0 +1,37 @@
# 26 — Performance
## Status
```text
PERFORMANCE_POLICY=MAXIMUM_SAFE_USEFUL_THROUGHPUT
```
## Authority
`AGENTS.md`, resource-governor and internal-parallelism documents, plus measured tranche-specific evidence.
## REQUIRED
Performance work is evidence-driven and must preserve science, identity, publication order and boundedness.
Use host-aware build/test parallelism and Governor-aware production execution.
Do not freeze reference-host values such as `12` CPUs, `-j8`, a historic batch size, or `--num-processes 1` as universal policy.
A long serial path with independent work and safe idle resources is a defect unless there is concrete proof of:
- true dependency/scientific serialism;
- useful-scaling knee;
- memory bound/pressure;
- I/O saturation;
- validated GPU execution;
- unavoidable deterministic publication constraint;
- another measured bottleneck.
Validated useful GPU backends are preferred automatically when eligible. Backend failure/fallback behavior must be explicit where the scientific stage supports fallback; do not silently change scientific identity.
Optimize useful throughput, not utilization graphs.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,37 @@
# 27 — Tests and Validation
## Status
```text
VALIDATION_STRATEGY=DELTA_BASED
```
## Authority
`docs/development/testing.md`, `AGENTS.md`, global-maintenance evidence and specialized architecture documents.
## REQUIRED
Unchanged PASS/FROZEN boundaries inherit retained evidence unless the delta touches their assumptions.
For unchanged globally reviewed foundations, the retained maintenance baseline is tag `global-maintenance-2026-09-01`; the later `real-a6000-pre-sfm-2026-09-02` checkpoint adds real-data evidence without replacing the maintenance authority. Use the relevant delta rather than replaying a global audit.
For each tranche:
1. identify changed contracts;
2. build with safe host-aware parallelism;
3. run focused unit/integration tests;
4. run relevant persistence/restart/resource/concurrency checks;
5. use ASan/UBSan and TSan only where applicable and preserve documented third-party qualifications;
6. validate real-data only when the tranche explicitly requires it and use non-destructive copies/workspaces;
7. run `git diff --check`.
Do not claim blanket TSan validity for Vulkan unless explicitly proven.
Historical schema downgrade fixtures must be structurally truthful. A fixture targeting schema version N must remove every schema object introduced after N before rewriting `schema_version`; changing metadata alone is not a valid historical fixture. In particular, v25-aware downgrade fixtures must account for the additive v24 `raw_development_batch_tasks` and v25 `feature_extract_batch_tasks` overlays when targeting earlier versions.
Do not rerun full historical A-to-Z qualification merely for confidence theater.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,40 @@
# 28 — Documentation and Comments
## Status
```text
DOCUMENTATION_LANGUAGE=ENGLISH
SOURCE_COMMENT_LANGUAGE=ENGLISH
```
## Authority
`AGENTS.md` repository language policy.
## REQUIRED
Current repository technical prose and production source comments converge on English.
Comments should document mainly:
- why;
- contract;
- invariant;
- ownership/lifetime;
- identity;
- persistence ordering;
- restart;
- cancellation;
- concurrency;
- resource accounting;
- FROZEN boundaries.
Do not comment obvious statements line by line.
Historical documents may be translated, but translation must not silently modernize their lifecycle, schema version, identities, numbers, evidence or conclusions.
This prompt tree must not become a duplicate shadow architecture. Reference canonical documents and restate only execution-critical invariants.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,32 @@
# 29 — Agent Routing
## Status
```text
AGENT_ARCHITECTURE=IMPLEMENTATION_NEUTRAL
```
## Authority
`AGENTS.md` and this collective prompt contract.
## REQUIRED
This contract must work with:
- one coding agent;
- Codex;
- ChatGPT plus a coding agent;
- future compatible coding agents.
The primary executing agent owns tranche coherence.
Sub-agents are optional and may be used only when they reduce cost/time without losing scope control, authority awareness or validation quality.
Do not create agent hierarchies for appearance.
For Codex specifically: read the entire prompt tree before major implementation, use `main`, do not choose historical branches, do not treat the two remotes as separate projects, and return only after complete scoped work or a real STOP.
## Canonical-reference rule
When implementation enters this area, read the cited canonical document in full before editing. If this prompt summary and the canonical document appear to conflict, do not silently choose the shorter text: determine whether this file is stale or whether a real contract contradiction exists, then apply the STOP rules where necessary.

View file

@ -0,0 +1,58 @@
# 30 — Git, Scope and STOP
## Status
```text
GIT_CLOSURE_OWNER=HUMAN
PROTECTED_SCOPE=scan3d/
```
## Authority
`AGENTS.md` and explicit human ticket scope.
## Git rules
Without explicit human authorization, do not:
- stage;
- commit;
- push;
- create/switch branches or continue development from a detached historical tag/checkpoint;
- merge;
- rebase;
- reset;
- restore;
- stash;
- clean;
- amend;
- force a Git operation.
Never use `git add -A`.
If Git closure is explicitly authorized, stage only exact tranche paths and synchronize both mirrors as a pair unless the human explicitly says otherwise.
## Protected scope
- do not modify `scan3d/`;
- do not modify other repositories;
- do not use retained real scientific projects destructively.
## STOP conditions
STOP before:
- reopening PASS/FROZEN;
- changing FROZEN science/identity;
- pseudo-calibration;
- unauthorized DB schema or Task Kind;
- unnecessary generic runtime/backend subsystem;
- destructive operations;
- protected-scope edits;
- unauthorized branch operations;
- force push;
- unilateral mirror overwrite;
- real contradiction between executable code/schema and canonical contract;
- material unresolved product ambiguity.
Difficulty alone is not a STOP condition.

View file

@ -0,0 +1,59 @@
# 31 — Implementation Order
## Status
```text
IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN
IMPLEMENTATION_AUTHORIZATION=NO
STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS
CURRENT_NEXT=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION
```
## Authority
`docs/product/product_definition.md`, current roadmap and dependencies confirmed on current `main`.
## PLANNED ORDER
Implementation remains unauthorized until the human explicitly authorizes a tranche.
Default dependency order:
0. user-facing repository/UI language normalization where appropriate — PASS;
1. final usable Calibration Science/Tooling/Bootstrap/Workflow v1 compatibility path — PASS/FROZEN;
2. Calibration Science v2 design for heterogeneous cameras/lenses/focals, adaptive capture settings and autofocus — PASS/FROZEN;
3. Calibration v2 heterogeneous-optics persistence foundation — PASS/FROZEN;
4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — PASS/FROZEN;
5. Heterogeneous Workflow v2 truthful READY proof — PASS/FROZEN;
6. Adaptive capture settings / generic autofocus foundation — PASS/FROZEN;
7. physical autofocus/optical applicability validation and dedicated calibrated real campaign — CURRENT; `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` supplies deterministic measurement evidence, `CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1=PASS/FROZEN` converts already-materialized Calibration Workflow evidence into AF-study samples without reparsing solver artifacts, and `CALIBRATION_AF_STUDY_ASSEMBLY_V1=PASS/FROZEN` assembles multiple independent materialized samples into one bounded L3DAFST1 artifact; none makes a physical applicability decision;
8. real Sparse SfM proof;
9. durable Dense/OpenMVS orchestration;
10. mesh / refinement / texturing / export;
11. viewer foundation;
12. offline Coverage Analysis scientific contract and implementation;
13. multi-campaign registration / fusion;
14. generic live acquisition adapter foundation;
15. A6000 HDMI integration;
16. S21 integration;
17. live camera localization;
18. live coverage overlay;
19. actionable Capture Guidance;
20. video ingestion / deterministic keyframes;
21. final integration, UX, restart and performance proof;
22. Product Definition v1 Definition-of-Done closure.
## Adjustment rule
Change this order only when current repository inspection proves a real dependency difference. Preserve product intent and document the dependency.
Step 0 closure does not authorize mechanical rewriting of historical persisted labels, path names, deliberate UTF-8 fixtures or persistence-sensitive internal strings. Such text is changed only when its owning scope requires it.
Do not jump to A6000 live work merely because it is visually interesting. Reconstruction/viewer/coverage foundations required for truthful guidance come first.
The calibration-v2 insertion before the physical campaign is dependency-driven,
not feature creep: real acquisition evidence established that the v1 locked-focus
single-optical-state path would make normal engine-bay capture impractical and
would contradict the already-FROZEN product requirement for multiple optical
configurations per project. Science v2 must be defined before collecting the
physical evidence intended to validate it.

View file

@ -0,0 +1,57 @@
# 32 — Feature Backlog
## Status
```text
BACKLOG_CLASSIFICATION=REQUIRED
```
## Authority
`docs/product/product_definition.md` and this execution contract.
## REQUIRED_V1
- data-driven camera/lens onboarding;
- manual lens support;
- zoom/multi-focal configurations;
- profile import/export;
- usable calibration workflow;
- real calibrated Sparse SfM;
- durable Dense/OpenMVS;
- mesh/refinement/texturing/export;
- graphical viewer;
- offline coverage;
- multi-campaign registration/fusion where required;
- generic live adapter;
- stock A6000 HDMI integration;
- S21 integration without root;
- live localization;
- live coverage overlay;
- actionable capture guidance;
- deterministic video keyframes;
- restart/recovery;
- safe useful resource utilization;
- optional SSD scratch lifecycle.
## OPTIONAL_FUTURE / DEFERRED
```text
AUTO_CAPTURE
MULTI_GPU
DISTRIBUTED_COMPUTE
GENERAL_INTER_TASK_DAG
GENERIC_BACKEND_FRAMEWORK
CLOUD_RECONSTRUCTION
```
## REJECTED
```text
CAMERA_FIRMWARE_MODIFICATION
CAMERA_HARDWARE_MODIFICATION
SILENT_CALIBRATION_SUBSTITUTION
SILENT_LENS_IDENTITY_INFERENCE
```
An IDEA or OPTIONAL item never becomes v1 scope because an agent prefers it.

View file

@ -0,0 +1,61 @@
# 33 — Definition of Done
## Status
```text
PRODUCT_DONE=OBJECTIVE_EVIDENCE_REQUIRED
```
## Authority
`docs/product/product_definition.md` and all specialized FROZEN contracts.
## Final objective evidence
Lardon3D v1 is complete only when retained evidence demonstrates all of the following:
- ordinary new camera-body onboarding requires no code change;
- ordinary new-lens onboarding requires no code change;
- manual lens without EXIF is supported;
- zoom/multiple focal optical configurations are supported;
- profile import/export is bounded, versioned and conflict-safe;
- calibration workflow reaches truthful `READY`;
- a dedicated calibrated real campaign completes real Sparse SfM;
- Dense/OpenMVS execution is durable, restartable and failure-atomic;
- mesh generation works;
- refinement works;
- texturing works;
- interoperable exports are traceable;
- arbitrary monocular scale is never mislabeled metric;
- viewer displays sparse evidence;
- viewer displays dense evidence;
- viewer displays mesh;
- viewer displays textured mesh;
- registered cameras/frustums and components are inspectable;
- viewer can be disabled, lag or close/crash without corrupting or blocking the engine;
- offline Coverage Analysis classifies `UNKNOWN/UNSEEN/WEAK/ADEQUATE` with versioned validated science and confidence;
- the system produces an actionable supplementary viewpoint;
- live localization exposes truthful confidence and loss;
- stock Sony A6000 works through native HDMI capture without camera modification;
- a supplementary full-resolution capture returns through the normal provenance/scientific pipeline;
- S21 integration requires no root and does not fork the scientific core;
- independent multi-campaign reconstruction can produce an explicit durable registration transform with quality/provenance and accepted fusion without raw Feature/Track merging;
- video ingestion produces deterministic bounded keyframes traceable to exact source timeline identity, algorithm/version and parameter fingerprint before normal Capture ingestion;
- restart/recovery works across long-running stages;
- safe useful CPU/GPU execution is demonstrated on representative hardware;
- optional SSD scratch has complete lease/drain/safe-unplug lifecycle when used and swap remains explicit opt-in;
- project portability does not depend on temporary paths, current hardware topology/device IDs, scratch mount path, live-device connection, process ID or worker instance;
- core workflows remain local-first with no required cloud upload;
- cleanup previews destructive removals and never automatically deletes FROZEN/historical evidence;
- user-visible diagnostics distinguish scientific rejection, invalid input, missing prerequisite, resource wait/throttle, runtime failure, corruption, unsupported version/backend and cancellation;
- no calibration, lens, scale, campaign relationship or provenance is invented;
- historical identities and PASS/FROZEN checkpoints remain interpretable;
- documentation and TUI behavior match the acquired product state.
## Not sufficient
The product is not done merely because it compiles, unit tests pass, Sparse SfM exists, OpenMVS can be launched manually, or a mesh can be produced by hand.
## Closure
Final closure requires a delta-based validation report showing how every REQUIRED_V1 item above is satisfied or explicitly identifying any remaining blocker. Only then may `PRODUCT_DEFINITION_V1` product implementation be considered fulfilled.

View file

@ -397,9 +397,9 @@ bool run_impl(Lardon3DTask *t, void *p) {
c->db, lardon3d_task_id(t), group_id, materialized_capture_id, c->db, lardon3d_task_id(t), group_id, materialized_capture_id,
group_id) != LARDON3D_PROJECT_DB_OK) group_id) != LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(t, "Rétention de Capture impossible."); return lardon3d_task_fail(t, "Rétention de Capture impossible.");
/* Limite de reprise acceptée: entre le retour de S3-E et cette rétention /* Accepted recovery boundary: between S3-E returning and this durable
* durable, une identité de capture ne peut pas être déduite à posteriori * retention, Capture identity cannot be reconstructed retroactively from
* depuis les chemins/métadonnées/ID d'image. * paths, metadata, or image IDs.
*/ */
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING #ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
const char *after_retention = const char *after_retention =

275
src/calibration_af_study.c Normal file
View file

@ -0,0 +1,275 @@
#include <lardon3d/calibration_af_study.h>
#include <math.h>
#include <openssl/evp.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
enum {
kHeaderSize = 72,
kSampleSize = 232,
kPairSize = 44,
kPairFlagSameFocus = 1,
kPairFlagFitHoldout = 2,
};
static const unsigned char kMagic[8] = {'L','3','D','A','F','S','T','1'};
static const double kProbes[9][2] = {
{0.0, 0.0},
{-0.7, 0.0}, {0.7, 0.0}, {0.0, -0.7}, {0.0, 0.7},
{-0.7, -0.7}, {0.7, -0.7}, {-0.7, 0.7}, {0.7, 0.7},
};
typedef struct {
size_t original_index;
size_t token_length;
} CanonicalSample;
typedef struct {
double center;
double edge;
double corner;
double global;
} PairMetric;
static bool nonzero_digest(const unsigned char value[32]) {
unsigned char any = 0;
for (size_t i = 0; i < 32; ++i) any |= value[i];
return any != 0;
}
static bool bounded_token_length(const char token[128], size_t *length) {
if (!token || !length) return false;
for (size_t i = 0; i < 128; ++i) {
if (token[i] == '\0') {
if (i == 0) return false;
*length = i;
return true;
}
}
return false;
}
static bool finite_parameters(const Lardon3DCalibrationAfStudySample *s,
uint32_t width, uint32_t height) {
const double p[8] = {s->fx,s->fy,s->cx,s->cy,s->k1,s->k2,s->p1,s->p2};
for (size_t i = 0; i < 8; ++i)
if (!isfinite(p[i])) return false;
return s->fx > 0.0 && s->fy > 0.0 && s->cx >= 0.0 && s->cy >= 0.0 &&
s->cx < (double)width && s->cy < (double)height;
}
static int byte_compare(const unsigned char *a, size_t an,
const unsigned char *b, size_t bn) {
const size_t n = an < bn ? an : bn;
const int cmp = memcmp(a, b, n);
if (cmp != 0) return cmp;
if (an < bn) return -1;
if (an > bn) return 1;
return 0;
}
static int canonical_compare(const Lardon3DCalibrationAfStudyInput *input,
const CanonicalSample *a,
const CanonicalSample *b) {
const Lardon3DCalibrationAfStudySample *sa = &input->samples[a->original_index];
const Lardon3DCalibrationAfStudySample *sb = &input->samples[b->original_index];
int cmp = byte_compare((const unsigned char *)sa->focus_token, a->token_length,
(const unsigned char *)sb->focus_token, b->token_length);
if (cmp != 0) return cmp;
cmp = memcmp(sa->calibration_evidence_sha256,
sb->calibration_evidence_sha256, 32);
if (cmp != 0) return cmp;
if ((uint32_t)sa->role < (uint32_t)sb->role) return -1;
if ((uint32_t)sa->role > (uint32_t)sb->role) return 1;
return 0;
}
static void canonical_sort(const Lardon3DCalibrationAfStudyInput *input,
CanonicalSample *values, size_t count) {
for (size_t i = 1; i < count; ++i) {
CanonicalSample value = values[i];
size_t j = i;
while (j > 0 && canonical_compare(input, &value, &values[j - 1]) < 0) {
values[j] = values[j - 1];
--j;
}
values[j] = value;
}
}
static void project(const Lardon3DCalibrationAfStudySample *s,
double x, double y, double *u, double *v) {
const double r2 = x*x + y*y;
const double radial = 1.0 + s->k1*r2 + s->k2*r2*r2;
const double xd = x*radial + 2.0*s->p1*x*y + s->p2*(r2 + 2.0*x*x);
const double yd = y*radial + s->p1*(r2 + 2.0*y*y) + 2.0*s->p2*x*y;
*u = s->fx*xd + s->cx;
*v = s->fy*yd + s->cy;
}
static bool pair_metric(const Lardon3DCalibrationAfStudySample *a,
const Lardon3DCalibrationAfStudySample *b,
PairMetric *out) {
memset(out, 0, sizeof(*out));
for (size_t i = 0; i < 9; ++i) {
double au, av, bu, bv;
project(a, kProbes[i][0], kProbes[i][1], &au, &av);
project(b, kProbes[i][0], kProbes[i][1], &bu, &bv);
const double delta = hypot(au - bu, av - bv);
if (!isfinite(delta)) return false;
if (i == 0) out->center = delta;
else if (i <= 4 && delta > out->edge) out->edge = delta;
else if (i >= 5 && delta > out->corner) out->corner = delta;
if (delta > out->global) out->global = delta;
}
return true;
}
static void put_u32(unsigned char **p, uint32_t value) {
for (size_t i = 0; i < 4; ++i) (*p)[i] = (unsigned char)(value >> (8u*i));
*p += 4;
}
static void put_u64(unsigned char **p, uint64_t value) {
for (size_t i = 0; i < 8; ++i) (*p)[i] = (unsigned char)(value >> (8u*i));
*p += 8;
}
static void put_f64(unsigned char **p, double value) {
uint64_t bits = 0;
if (value == 0.0) value = 0.0;
memcpy(&bits, &value, sizeof(bits));
put_u64(p, bits);
}
static bool sha256(const unsigned char *bytes, size_t size, unsigned char out[32]) {
unsigned int length = 0;
return EVP_Digest(bytes, size, out, &length, EVP_sha256(), NULL) == 1 && length == 32;
}
Lardon3DCalibrationAfStudyResult lardon3d_calibration_af_study_produce(
const Lardon3DCalibrationAfStudyInput *input,
unsigned char *artifact, size_t artifact_capacity, size_t *written,
unsigned char artifact_sha256[32],
Lardon3DCalibrationAfStudySummary *summary) {
if (written) *written = 0;
if (artifact_sha256) memset(artifact_sha256, 0, 32);
if (summary) memset(summary, 0, sizeof(*summary));
if (!input || !artifact || !written || !artifact_sha256 || !summary ||
!input->samples || input->sample_count < 2 ||
input->sample_count > LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES ||
input->width == 0 || input->height == 0 ||
!nonzero_digest(input->study_context_sha256))
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT;
CanonicalSample canonical[LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES];
uint32_t fit_count = 0, holdout_count = 0;
for (size_t i = 0; i < input->sample_count; ++i) {
const Lardon3DCalibrationAfStudySample *s = &input->samples[i];
size_t token_length = 0;
if ((s->role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT &&
s->role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT) ||
!bounded_token_length(s->focus_token, &token_length) ||
!nonzero_digest(s->calibration_evidence_sha256) ||
!finite_parameters(s, input->width, input->height))
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE;
canonical[i].original_index = i;
canonical[i].token_length = token_length;
if (s->role == LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT) ++fit_count;
else ++holdout_count;
}
canonical_sort(input, canonical, input->sample_count);
for (size_t i = 1; i < input->sample_count; ++i) {
const Lardon3DCalibrationAfStudySample *a = &input->samples[canonical[i-1].original_index];
const Lardon3DCalibrationAfStudySample *b = &input->samples[canonical[i].original_index];
if (canonical[i-1].token_length == canonical[i].token_length &&
memcmp(a->focus_token, b->focus_token, canonical[i].token_length) == 0 &&
memcmp(a->calibration_evidence_sha256, b->calibration_evidence_sha256, 32) == 0)
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE;
}
const size_t pair_count = input->sample_count * (input->sample_count - 1) / 2;
if (pair_count > LARDON3D_CALIBRATION_AF_STUDY_MAX_PAIRS)
return LARDON3D_CALIBRATION_AF_STUDY_CAPACITY;
const size_t required = (size_t)kHeaderSize + input->sample_count*(size_t)kSampleSize + pair_count*(size_t)kPairSize;
if (required > LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES || artifact_capacity < required)
return LARDON3D_CALIBRATION_AF_STUDY_CAPACITY;
unsigned char *p = artifact;
memcpy(p, kMagic, 8); p += 8;
put_u32(&p, LARDON3D_CALIBRATION_AF_STUDY_VERSION);
put_u32(&p, LARDON3D_CALIBRATION_AF_STUDY_PROBE_MODEL_VERSION);
memcpy(p, input->study_context_sha256, 32); p += 32;
put_u32(&p, input->width);
put_u32(&p, input->height);
put_u32(&p, (uint32_t)input->sample_count);
put_u32(&p, (uint32_t)pair_count);
put_u32(&p, fit_count);
put_u32(&p, holdout_count);
for (size_t rank = 0; rank < input->sample_count; ++rank) {
const CanonicalSample *c = &canonical[rank];
const Lardon3DCalibrationAfStudySample *s = &input->samples[c->original_index];
put_u32(&p, (uint32_t)s->role);
put_u32(&p, (uint32_t)c->token_length);
memset(p, 0, 128);
memcpy(p, s->focus_token, c->token_length); p += 128;
memcpy(p, s->calibration_evidence_sha256, 32); p += 32;
put_f64(&p, s->fx); put_f64(&p, s->fy); put_f64(&p, s->cx); put_f64(&p, s->cy);
put_f64(&p, s->k1); put_f64(&p, s->k2); put_f64(&p, s->p1); put_f64(&p, s->p2);
}
Lardon3DCalibrationAfStudySummary local_summary = {0};
local_summary.sample_count = (uint32_t)input->sample_count;
local_summary.fit_count = fit_count;
local_summary.holdout_count = holdout_count;
local_summary.pair_count = (uint32_t)pair_count;
for (size_t ai = 0; ai < input->sample_count; ++ai) {
for (size_t bi = ai + 1; bi < input->sample_count; ++bi) {
const CanonicalSample *ca = &canonical[ai];
const CanonicalSample *cb = &canonical[bi];
const Lardon3DCalibrationAfStudySample *a = &input->samples[ca->original_index];
const Lardon3DCalibrationAfStudySample *b = &input->samples[cb->original_index];
PairMetric metric;
if (!pair_metric(a, b, &metric))
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE;
const bool same_focus = ca->token_length == cb->token_length &&
memcmp(a->focus_token, b->focus_token, ca->token_length) == 0;
const bool fit_holdout = a->role != b->role;
uint32_t flags = 0;
if (same_focus) flags |= kPairFlagSameFocus;
if (fit_holdout) flags |= kPairFlagFitHoldout;
put_u32(&p, (uint32_t)ai);
put_u32(&p, (uint32_t)bi);
put_u32(&p, flags);
put_f64(&p, metric.center);
put_f64(&p, metric.edge);
put_f64(&p, metric.corner);
put_f64(&p, metric.global);
if (same_focus) ++local_summary.same_focus_pair_count;
else ++local_summary.cross_focus_pair_count;
if (fit_holdout) ++local_summary.fit_holdout_pair_count;
if (metric.center > local_summary.all_center_max_px) local_summary.all_center_max_px = metric.center;
if (metric.edge > local_summary.all_edge_probe_max_px) local_summary.all_edge_probe_max_px = metric.edge;
if (metric.corner > local_summary.all_corner_probe_max_px) local_summary.all_corner_probe_max_px = metric.corner;
if (metric.global > local_summary.all_global_probe_max_px) local_summary.all_global_probe_max_px = metric.global;
if (same_focus && metric.global > local_summary.same_focus_global_probe_max_px)
local_summary.same_focus_global_probe_max_px = metric.global;
if (!same_focus && metric.global > local_summary.cross_focus_global_probe_max_px)
local_summary.cross_focus_global_probe_max_px = metric.global;
if (fit_holdout && metric.global > local_summary.fit_holdout_global_probe_max_px)
local_summary.fit_holdout_global_probe_max_px = metric.global;
}
}
if ((size_t)(p - artifact) != required)
return LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR;
if (!sha256(artifact, required, artifact_sha256))
return LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR;
*summary = local_summary;
*written = required;
return LARDON3D_CALIBRATION_AF_STUDY_OK;
}

View file

@ -0,0 +1,112 @@
#include <lardon3d/calibration_af_study_assembly.h>
#include <stdbool.h>
#include <string.h>
static bool nonzero_digest(const unsigned char value[32]) {
unsigned char any = 0;
for (size_t index = 0; index < 32; ++index) any |= value[index];
return any != 0;
}
static Lardon3DCalibrationAfStudyAssemblyResult map_bridge_result(
Lardon3DCalibrationAfStudyWorkflowResult result) {
switch (result) {
case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT;
case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE;
case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CRYPTO_ERROR;
case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK:
break;
}
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE;
}
static Lardon3DCalibrationAfStudyAssemblyResult map_study_result(
Lardon3DCalibrationAfStudyResult result) {
switch (result) {
case LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT;
case LARDON3D_CALIBRATION_AF_STUDY_CAPACITY:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CAPACITY;
case LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE;
case LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR:
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_ENCODING_ERROR;
case LARDON3D_CALIBRATION_AF_STUDY_OK:
break;
}
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE;
}
Lardon3DCalibrationAfStudyAssemblyResult
lardon3d_calibration_af_study_assemble_materialized(
const Lardon3DCalibrationAfStudyAssemblyInput *input,
unsigned char *artifact, size_t artifact_capacity, size_t *written,
unsigned char artifact_sha256[32],
Lardon3DCalibrationAfStudySummary *summary) {
if (written) *written = 0;
if (artifact_sha256) memset(artifact_sha256, 0, 32);
if (summary) memset(summary, 0, sizeof(*summary));
if (!input || !artifact || !written || !artifact_sha256 || !summary ||
!input->entries || input->entry_count < 2 ||
input->entry_count > LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES ||
!nonzero_digest(input->study_context_sha256))
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT;
Lardon3DCalibrationAfStudySample
samples[LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES];
memset(samples, 0, sizeof(samples));
uint32_t width = 0;
uint32_t height = 0;
for (size_t index = 0; index < input->entry_count; ++index) {
const Lardon3DCalibrationAfStudyAssemblyEntry *entry =
&input->entries[index];
if (!entry->external || !entry->focus_token)
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT;
Lardon3DCalibrationAfStudyWorkflowResult bridge =
lardon3d_calibration_af_study_sample_from_materialized_evidence(
entry->external, entry->role, entry->focus_token, &samples[index]);
if (bridge != LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK)
return map_bridge_result(bridge);
if (index == 0) {
width = entry->external->oriented_width;
height = entry->external->oriented_height;
} else if (entry->external->oriented_width != width ||
entry->external->oriented_height != height) {
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE;
}
/* Stronger than the raw L3DAFST1 producer's (token,digest) duplicate rule:
* an already-materialized calibration result is independent evidence only
* once, regardless of how the caller labels focus or study role. */
for (size_t previous = 0; previous < index; ++previous) {
if (memcmp(samples[previous].calibration_evidence_sha256,
samples[index].calibration_evidence_sha256, 32) == 0)
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE;
}
}
Lardon3DCalibrationAfStudyInput study = {0};
memcpy(study.study_context_sha256, input->study_context_sha256, 32);
study.width = width;
study.height = height;
study.samples = samples;
study.sample_count = input->entry_count;
Lardon3DCalibrationAfStudyResult result =
lardon3d_calibration_af_study_produce(
&study, artifact, artifact_capacity, written, artifact_sha256,
summary);
if (result != LARDON3D_CALIBRATION_AF_STUDY_OK)
return map_study_result(result);
return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_OK;
}

View file

@ -0,0 +1,211 @@
#include <lardon3d/calibration_af_study_workflow.h>
#include <math.h>
#include <openssl/evp.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
static bool nonzero_digest(const unsigned char value[32]) {
unsigned char any = 0;
for (size_t index = 0; index < 32; ++index) any |= value[index];
return any != 0;
}
static bool token_copy(
const char *input,
char output[LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY]) {
if (!input) return false;
size_t length = 0;
while (length < LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY &&
input[length] != '\0')
++length;
if (length == 0 ||
length >= LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY)
return false;
memset(output, 0, LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY);
memcpy(output, input, length);
return true;
}
static bool digest_begin(EVP_MD_CTX **ctx, const char *domain) {
*ctx = EVP_MD_CTX_new();
return *ctx &&
EVP_DigestInit_ex(*ctx, EVP_sha256(), NULL) == 1 &&
EVP_DigestUpdate(*ctx, domain, strlen(domain)) == 1;
}
static bool digest_add(EVP_MD_CTX *ctx, const void *bytes, size_t size) {
return EVP_DigestUpdate(ctx, bytes, size) == 1;
}
static bool digest_finish(EVP_MD_CTX *ctx, unsigned char output[32]) {
unsigned int output_size = 0;
const bool ok =
EVP_DigestFinal_ex(ctx, output, &output_size) == 1 && output_size == 32;
EVP_MD_CTX_free(ctx);
return ok;
}
static void encode_u32_le(uint32_t value, unsigned char output[4]) {
for (size_t index = 0; index < 4; ++index)
output[index] = (unsigned char)(value >> (8u * index));
}
static void encode_f64_le(double value, unsigned char output[8]) {
uint64_t bits = 0;
if (value == 0.0) value = 0.0;
memcpy(&bits, &value, sizeof(bits));
for (size_t index = 0; index < 8; ++index)
output[index] = (unsigned char)(bits >> (8u * index));
}
static bool validation_binding(
const Lardon3DCalibrationWorkflowInputBoundary *boundary,
unsigned char output[32]) {
static const char domain[] = "L3DCAL_WORKFLOW_VALIDATION_V1\n";
EVP_MD_CTX *ctx = NULL;
if (!digest_begin(&ctx, domain)) {
if (ctx) EVP_MD_CTX_free(ctx);
return false;
}
const bool ok =
digest_add(ctx, boundary->detection_sha256, 32) &&
digest_add(ctx, boundary->solve_sha256, 32) &&
digest_add(ctx, boundary->evidence_sha256, 32) &&
digest_add(ctx, boundary->producer_sha256, 32);
if (!ok) {
EVP_MD_CTX_free(ctx);
return false;
}
return digest_finish(ctx, output);
}
static bool sample_identity(
const Lardon3DCalibrationWorkflowExternalEvidence *external,
unsigned char output[32]) {
static const char domain[] = "L3DAF_CALIBRATION_SAMPLE_V1\n";
EVP_MD_CTX *ctx = NULL;
if (!digest_begin(&ctx, domain)) {
if (ctx) EVP_MD_CTX_free(ctx);
return false;
}
bool ok =
digest_add(ctx, external->target_sha256, 32) &&
digest_add(ctx, external->optical_state_sha256, 32) &&
digest_add(ctx, external->solver_executable_sha256, 32) &&
digest_add(ctx, external->solver_configuration_sha256, 32) &&
digest_add(ctx, external->initialization_evidence_sha256, 32) &&
digest_add(ctx, external->validation_evidence_sha256, 32);
unsigned char encoded_u32[4];
encode_u32_le(external->oriented_width, encoded_u32);
ok = ok && digest_add(ctx, encoded_u32, sizeof(encoded_u32));
encode_u32_le(external->oriented_height, encoded_u32);
ok = ok && digest_add(ctx, encoded_u32, sizeof(encoded_u32));
for (size_t parameter = 0; parameter < 8 && ok; ++parameter) {
unsigned char encoded_f64[8];
encode_f64_le(external->repeated_parameters[0][parameter], encoded_f64);
ok = digest_add(ctx, encoded_f64, sizeof(encoded_f64));
}
if (!ok) {
EVP_MD_CTX_free(ctx);
return false;
}
return digest_finish(ctx, output);
}
static bool boundary_provenance_valid(
const Lardon3DCalibrationWorkflowExternalEvidence *external) {
const Lardon3DCalibrationWorkflowInputBoundary *boundary =
&external->boundary;
if (!nonzero_digest(boundary->session_sha256) ||
!nonzero_digest(boundary->detection_sha256) ||
!nonzero_digest(boundary->solve_sha256) ||
!nonzero_digest(boundary->evidence_sha256) ||
!nonzero_digest(boundary->producer_sha256) ||
!nonzero_digest(boundary->optical_state_sha256) ||
!nonzero_digest(boundary->solver_executable_sha256) ||
!nonzero_digest(boundary->solver_configuration_sha256))
return false;
return memcmp(external->optical_state_sha256,
boundary->optical_state_sha256, 32) == 0 &&
memcmp(external->solver_executable_sha256,
boundary->solver_executable_sha256, 32) == 0 &&
memcmp(external->solver_configuration_sha256,
boundary->solver_configuration_sha256, 32) == 0 &&
memcmp(external->initialization_evidence_sha256,
boundary->session_sha256, 32) == 0;
}
static bool published_parameters_valid(
const Lardon3DCalibrationWorkflowExternalEvidence *external) {
const double *published = external->repeated_parameters[0];
for (size_t parameter = 0; parameter < 8; ++parameter) {
if (!isfinite(published[parameter])) return false;
if (external->repeated_parameters[1][parameter] != published[parameter] ||
external->repeated_parameters[2][parameter] != published[parameter])
return false;
}
return published[0] > 0.0 && published[1] > 0.0 &&
published[2] >= 0.0 && published[3] >= 0.0 &&
published[2] < (double)external->oriented_width &&
published[3] < (double)external->oriented_height;
}
Lardon3DCalibrationAfStudyWorkflowResult
lardon3d_calibration_af_study_sample_from_materialized_evidence(
const Lardon3DCalibrationWorkflowExternalEvidence *external,
Lardon3DCalibrationAfStudySampleRole role, const char *focus_token,
Lardon3DCalibrationAfStudySample *output) {
if (output) memset(output, 0, sizeof(*output));
if (!external || !focus_token || !output ||
(role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT &&
role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT))
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT;
Lardon3DCalibrationAfStudySample sample = {0};
if (!token_copy(focus_token, sample.focus_token))
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT;
if (external->oriented_width == 0 || external->oriented_height == 0 ||
!nonzero_digest(external->target_sha256) ||
!nonzero_digest(external->optical_state_sha256) ||
!nonzero_digest(external->solver_executable_sha256) ||
!nonzero_digest(external->solver_configuration_sha256) ||
!nonzero_digest(external->initialization_evidence_sha256) ||
!nonzero_digest(external->validation_evidence_sha256) ||
!boundary_provenance_valid(external) ||
!published_parameters_valid(external))
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE;
unsigned char expected_validation[32] = {0};
if (!validation_binding(&external->boundary, expected_validation))
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR;
if (memcmp(external->validation_evidence_sha256,
expected_validation, 32) != 0)
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE;
sample.role = role;
if (!sample_identity(external, sample.calibration_evidence_sha256))
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR;
sample.fx = external->repeated_parameters[0][0];
sample.fy = external->repeated_parameters[0][1];
sample.cx = external->repeated_parameters[0][2];
sample.cy = external->repeated_parameters[0][3];
sample.k1 = external->repeated_parameters[0][4];
sample.k2 = external->repeated_parameters[0][5];
sample.p1 = external->repeated_parameters[0][6];
sample.p2 = external->repeated_parameters[0][7];
*output = sample;
return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK;
}

View file

@ -0,0 +1,350 @@
#include <lardon3d/calibration_bootstrap_v2.h>
#include <math.h>
#include <openssl/evp.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
enum {
BOOTSTRAP_V2_HEADER_SIZE = 28,
BOOTSTRAP_V2_GROUP_SIZE = 232,
BOOTSTRAP_V2_ENTRY_SIZE = 144,
BOOTSTRAP_V2_VALIDATION_FLAGS = 15,
};
static const unsigned char bootstrap_v2_magic[8] = {'L', '3', 'D', 'C', 'A', 'L', 'B', '2'};
typedef struct {
const unsigned char *bytes;
size_t remaining;
} BootstrapV2Reader;
typedef struct {
uint32_t selected_item_index;
unsigned char representation_sha256[32];
Lardon3DSparseCalibration calibration;
Lardon3DSparseCalibrationMember member;
} BootstrapV2Entry;
static bool take(BootstrapV2Reader *reader, void *output, size_t count) {
if (count > reader->remaining) return false;
if (output) memcpy(output, reader->bytes, count);
reader->bytes += count;
reader->remaining -= count;
return true;
}
static bool read_u32(BootstrapV2Reader *reader, uint32_t *output) {
unsigned char bytes[4];
if (!take(reader, bytes, sizeof(bytes))) return false;
*output = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) |
((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
return true;
}
static bool read_u64(BootstrapV2Reader *reader, uint64_t *output) {
unsigned char bytes[8];
if (!take(reader, bytes, sizeof(bytes))) return false;
*output = 0;
for (size_t index = 0; index < sizeof(bytes); ++index)
*output |= (uint64_t)bytes[index] << (8u * index);
return true;
}
static bool read_f64(BootstrapV2Reader *reader, double *output) {
uint64_t bits = 0;
if (!read_u64(reader, &bits)) return false;
memcpy(output, &bits, sizeof(bits));
if (!isfinite(*output)) return false;
if (*output == 0.0) *output = 0.0;
return true;
}
static bool sha256(const unsigned char *bytes, size_t size, unsigned char output[32]) {
unsigned int output_size = 0;
return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 &&
output_size == 32;
}
static bool nonzero_digest(const unsigned char value[32]) {
unsigned char any = 0;
for (size_t index = 0; index < 32; ++index) any |= value[index];
return any != 0;
}
static Lardon3DCalibrationBootstrapV2Result db_result(Lardon3DProjectDbResult result) {
if (result == LARDON3D_PROJECT_DB_CONSTRAINT || result == LARDON3D_PROJECT_DB_NOT_FOUND)
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT;
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR;
}
static Lardon3DCalibrationBootstrapV2Result bootstrap_v2_publish(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[32],
Lardon3DCalibrationBootstrapV2Member *published_members,
size_t member_capacity, bool attach_scope,
Lardon3DCalibrationBootstrapV2Output *output) {
if (!database || execution_id == 0 || !artifact || !expected_artifact_sha256 || !output ||
artifact_size < BOOTSTRAP_V2_HEADER_SIZE ||
artifact_size > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_BYTES)
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_INVALID_ARGUMENT;
memset(output, 0, sizeof(*output));
unsigned char artifact_sha256[32];
if (!sha256(artifact, artifact_size, artifact_sha256))
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR;
if (memcmp(artifact_sha256, expected_artifact_sha256, 32) != 0)
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_PROVENANCE_MISMATCH;
BootstrapV2Reader reader = {artifact, artifact_size};
unsigned char magic[8];
uint32_t version, model_kind, model_version, group_count, entry_count;
if (!take(&reader, magic, sizeof(magic)) || !read_u32(&reader, &version) ||
!read_u32(&reader, &model_kind) || !read_u32(&reader, &model_version) ||
!read_u32(&reader, &group_count) || !read_u32(&reader, &entry_count) ||
memcmp(magic, bootstrap_v2_magic, sizeof(magic)) != 0 ||
version != LARDON3D_CALIBRATION_BOOTSTRAP_V2_ARTIFACT_VERSION ||
model_kind != LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE ||
model_version != LARDON3D_SPARSE_SFM_CALIBRATION_VERSION || group_count == 0 ||
group_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_GROUPS || entry_count == 0 ||
entry_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES)
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
if (!attach_scope && (!published_members || member_capacity < entry_count))
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_INVALID_ARGUMENT;
size_t expected_size = BOOTSTRAP_V2_HEADER_SIZE + (size_t)group_count * BOOTSTRAP_V2_GROUP_SIZE;
if (entry_count > (SIZE_MAX - expected_size) / BOOTSTRAP_V2_ENTRY_SIZE)
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
expected_size += (size_t)entry_count * BOOTSTRAP_V2_ENTRY_SIZE;
if (artifact_size != expected_size)
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
BootstrapV2Entry *entries = calloc(entry_count, sizeof(*entries));
bool *covered = calloc(entry_count, sizeof(*covered));
if (!entries || !covered) {
free(entries);
free(covered);
return LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY;
}
Lardon3DCalibrationBootstrapV2Result result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK;
size_t parsed_entries = 0;
unsigned char previous_group_identity[32] = {0};
uint32_t previous_group_version = 0;
for (uint32_t group_index = 0;
group_index < group_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK;
++group_index) {
const unsigned char *group_start = reader.bytes;
unsigned char group_identity[32], evidence[6][32];
uint32_t group_version, member_count;
if (!take(&reader, group_identity, 32) || !read_u32(&reader, &group_version)) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
for (size_t index = 0; index < 6; ++index)
if (!take(&reader, evidence[index], 32)) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
if (result != LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK ||
!read_u32(&reader, &member_count)) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
int group_order = group_index == 0 ? -1 :
memcmp(previous_group_identity, group_identity, 32);
if (!nonzero_digest(group_identity) || group_version == 0 || member_count == 0 ||
member_count > entry_count - parsed_entries || group_order > 0 ||
(group_order == 0 && previous_group_version >= group_version)) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
for (size_t index = 0; index < 6; ++index)
if (!nonzero_digest(evidence[index])) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
memcpy(previous_group_identity, group_identity, 32);
previous_group_version = group_version;
size_t group_first_entry = parsed_entries;
uint32_t previous_item_index = 0;
for (uint32_t member_index = 0;
member_index < member_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK;
++member_index) {
BootstrapV2Entry *entry = &entries[parsed_entries];
uint32_t width, height, support_images, support_observations, validation_flags;
double reprojection_rmse_px, maximum_parameter_delta;
if (!read_u32(&reader, &entry->selected_item_index) ||
!read_u64(&reader, &entry->member.image_id) ||
!take(&reader, entry->representation_sha256, 32) || !read_u32(&reader, &width) ||
!read_u32(&reader, &height) || !read_f64(&reader, &entry->calibration.fx) ||
!read_f64(&reader, &entry->calibration.fy) ||
!read_f64(&reader, &entry->calibration.cx) ||
!read_f64(&reader, &entry->calibration.cy) ||
!read_f64(&reader, &entry->calibration.k1) ||
!read_f64(&reader, &entry->calibration.k2) ||
!read_f64(&reader, &entry->calibration.p1) ||
!read_f64(&reader, &entry->calibration.p2) ||
!read_u32(&reader, &support_images) ||
!read_u32(&reader, &support_observations) ||
!read_f64(&reader, &reprojection_rmse_px) ||
!read_f64(&reader, &maximum_parameter_delta) ||
!read_u32(&reader, &validation_flags) ||
entry->selected_item_index >= entry_count || covered[entry->selected_item_index] ||
(member_index > 0 && previous_item_index >= entry->selected_item_index) ||
entry->member.image_id == 0 || !nonzero_digest(entry->representation_sha256) ||
width == 0 || height == 0 || entry->calibration.fx <= 0.0 ||
entry->calibration.fy <= 0.0 || entry->calibration.cx < 0.0 ||
entry->calibration.cy < 0.0 || entry->calibration.cx >= (double)width ||
entry->calibration.cy >= (double)height || support_images == 0 ||
support_observations == 0 || reprojection_rmse_px < 0.0 ||
maximum_parameter_delta < 0.0 || validation_flags != BOOTSTRAP_V2_VALIDATION_FLAGS) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
for (size_t prior = 0; prior < parsed_entries; ++prior)
if (entries[prior].member.image_id == entry->member.image_id) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
break;
}
covered[entry->selected_item_index] = true;
previous_item_index = entry->selected_item_index;
entry->calibration.model_kind = model_kind;
entry->calibration.model_version = model_version;
entry->calibration.width = width;
entry->calibration.height = height;
entry->calibration.provenance_kind =
LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED;
++parsed_entries;
}
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) {
unsigned char group_fingerprint[32];
size_t group_size = (size_t)(reader.bytes - group_start);
if (!sha256(group_start, group_size, group_fingerprint)) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR;
break;
}
/* Group-local hashing preserves independently acquired provenance: an
* unrelated group's addition cannot redefine these calibrations. */
for (size_t index = group_first_entry; index < parsed_entries; ++index)
memcpy(entries[index].calibration.provenance_fingerprint, group_fingerprint, 32);
}
}
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK &&
(parsed_entries != entry_count || reader.remaining != 0))
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
for (uint32_t index = 0;
index < entry_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; ++index)
if (!covered[index]) result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT;
Lardon3DProjectDbSelectedExecution execution;
Lardon3DProjectDbResult db_status = LARDON3D_PROJECT_DB_OK;
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) {
db_status = lardon3d_project_db_load_selected_execution(database, execution_id, &execution);
if (db_status != LARDON3D_PROJECT_DB_OK)
result = db_result(db_status);
else if (execution.item_count != entry_count ||
(execution.stage != LARDON3D_SELECTED_EXECUTION_CALIBRATION &&
execution.stage != LARDON3D_SELECTED_EXECUTION_READY))
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT;
}
for (size_t index = 0;
index < parsed_entries && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; ++index) {
BootstrapV2Entry *entry = &entries[index];
Lardon3DProjectDbSelectedExecutionItem selected;
Lardon3DProjectDbImage image;
Lardon3DProjectDbImageAsset asset;
db_status = lardon3d_project_db_load_selected_execution_item(
database, execution_id, entry->selected_item_index, &selected);
if (db_status != LARDON3D_PROJECT_DB_OK || !selected.has_image ||
selected.image_id != entry->member.image_id) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT;
break;
}
db_status = lardon3d_project_db_load_image(database, entry->member.image_id, &image, &asset);
if (db_status != LARDON3D_PROJECT_DB_OK ||
memcmp(asset.sha256, entry->representation_sha256, 32) != 0) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT;
break;
}
}
/* All bytes and immutable selected bindings are verified before this first
* write. Publication uses the existing short, content-addressed DB APIs; a
* crash may retain reusable calibrations but READY remains guarded by the
* single complete-scope attachment below. */
for (size_t index = 0;
index < parsed_entries && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; ++index) {
Lardon3DSparseCalibration stored;
db_status = lardon3d_sparse_calibration_create(database, &entries[index].calibration,
&stored);
if (db_status != LARDON3D_PROJECT_DB_OK) {
result = db_result(db_status);
break;
}
entries[index].member.calibration_id = stored.calibration_id;
memcpy(entries[index].member.calibration_hash, stored.scientific_hash, 32);
}
Lardon3DSparseCalibrationScope scope;
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) {
Lardon3DSparseCalibrationMember *members = calloc(entry_count, sizeof(*members));
if (!members)
result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY;
else {
for (size_t index = 0; index < entry_count; ++index) members[index] = entries[index].member;
db_status = lardon3d_sparse_calibration_scope_create(database, members, entry_count, &scope);
free(members);
if (db_status != LARDON3D_PROJECT_DB_OK) result = db_result(db_status);
}
}
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK && attach_scope) {
db_status = lardon3d_project_db_assign_selected_calibration_scope(database, execution_id,
scope.scope_id);
if (db_status != LARDON3D_PROJECT_DB_OK) result = db_result(db_status);
}
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) {
memcpy(output->artifact_sha256, artifact_sha256, 32);
output->scope = scope;
output->calibration_count = entry_count;
output->group_count = group_count;
if (!attach_scope) {
/* Item order, not artifact group order, is the durable composition key.
* Workflow must bind it back to selected_execution_items.capture_id and
* must never reverse-map Capture identity from image_id. */
for (size_t index = 0; index < entry_count; ++index) {
BootstrapV2Entry *entry = &entries[index];
Lardon3DCalibrationBootstrapV2Member *member =
&published_members[entry->selected_item_index];
member->selected_item_index = entry->selected_item_index;
member->image_id = entry->member.image_id;
member->calibration_id = entry->member.calibration_id;
}
}
}
free(entries);
free(covered);
return result;
}
Lardon3DCalibrationBootstrapV2Result
lardon3d_calibration_bootstrap_v2_publish_unattached(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[32],
Lardon3DCalibrationBootstrapV2Member *members, size_t member_capacity,
Lardon3DCalibrationBootstrapV2Output *output) {
return bootstrap_v2_publish(database, execution_id, artifact, artifact_size,
expected_artifact_sha256, members,
member_capacity, false, output);
}
Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[32],
Lardon3DCalibrationBootstrapV2Output *output) {
return bootstrap_v2_publish(database, execution_id, artifact, artifact_size,
expected_artifact_sha256, NULL, 0, true, output);
}

View file

@ -79,7 +79,7 @@ Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_validate(
!finite_value(e->target_white_border_mm) || e->target_white_border_mm < 30.0 || !finite_value(e->target_white_border_mm) || e->target_white_border_mm < 30.0 ||
e->extra_distortion_coefficient_count != 0 || !finite_value(e->measurement_resolution_mm) || e->extra_distortion_coefficient_count != 0 || !finite_value(e->measurement_resolution_mm) ||
e->measurement_resolution_mm <= 0 || e->measurement_resolution_mm > 0.1 || e->measurement_resolution_mm <= 0 || e->measurement_resolution_mm > 0.1 ||
!finite_value(e->target_flatness_mm) || e->target_flatness_mm < 0 || e->target_flatness_mm > 0.20) !isnan(e->target_flatness_mm))
return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED;
double lo = 30.0, hi = 30.0; double lo = 30.0, hi = 30.0;
for (size_t i = 0; i < LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS; ++i) { for (size_t i = 0; i < LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS; ++i) {

View file

@ -0,0 +1,198 @@
#include <lardon3d/calibration_tooling_v2.h>
#include <math.h>
#include <openssl/evp.h>
#include <stdbool.h>
#include <string.h>
enum {
TOOLING_V2_HEADER_SIZE = 28,
TOOLING_V2_GROUP_SIZE = 232,
TOOLING_V2_ENTRY_SIZE = 144,
};
static bool nonzero_digest(const unsigned char value[32]) {
unsigned char any = 0;
for (size_t index = 0; index < 32; ++index) any |= value[index];
return any != 0;
}
static void put_u32(unsigned char *output, uint32_t value) {
for (size_t index = 0; index < 4; ++index)
output[index] = (unsigned char)(value >> (8u * index));
}
static void put_u64(unsigned char *output, uint64_t value) {
for (size_t index = 0; index < 8; ++index)
output[index] = (unsigned char)(value >> (8u * index));
}
static void put_f64(unsigned char *output, double value) {
uint64_t bits = 0;
/* The format has one representation of zero so equal scientific values do
* not acquire different artifact identities through a negative sign bit. */
if (value == 0.0) value = 0.0;
memcpy(&bits, &value, sizeof(bits));
put_u64(output, bits);
}
static bool sha256(const unsigned char *bytes, size_t size, unsigned char output[32]) {
unsigned int output_size = 0;
return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 &&
output_size == 32;
}
static bool entry_valid(const Lardon3DCalibrationToolingV2Entry *entry) {
const double parameters[] = {entry->fx, entry->fy, entry->cx, entry->cy,
entry->k1, entry->k2, entry->p1, entry->p2};
if (entry->image_id == 0 || !nonzero_digest(entry->representation_sha256) ||
entry->width == 0 || entry->height == 0 || entry->support_images == 0 ||
entry->support_observations == 0 ||
entry->validation_flags != LARDON3D_CALIBRATION_TOOLING_V2_VALIDATION_FLAGS)
return false;
for (size_t index = 0; index < sizeof(parameters) / sizeof(parameters[0]); ++index)
if (!isfinite(parameters[index])) return false;
return entry->fx > 0.0 && entry->fy > 0.0 && entry->cx >= 0.0 && entry->cy >= 0.0 &&
entry->cx < (double)entry->width && entry->cy < (double)entry->height &&
isfinite(entry->reprojection_rmse_px) && entry->reprojection_rmse_px >= 0.0 &&
isfinite(entry->maximum_parameter_delta) && entry->maximum_parameter_delta >= 0.0;
}
Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_validate(
const Lardon3DCalibrationToolingV2Evidence *evidence) {
if (!evidence || !evidence->groups || evidence->group_count == 0 ||
evidence->group_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_GROUPS ||
evidence->entry_count == 0 ||
evidence->entry_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES)
return LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT;
bool covered[LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES] = {false};
size_t total_entries = 0;
for (size_t group_index = 0; group_index < evidence->group_count; ++group_index) {
const Lardon3DCalibrationToolingV2Group *group = &evidence->groups[group_index];
if (!group->entries || group->entry_count == 0 || group->group_version == 0 ||
!nonzero_digest(group->group_identity_sha256) ||
!nonzero_digest(group->optical_state_sha256) || !nonzero_digest(group->target_sha256) ||
!nonzero_digest(group->solver_executable_sha256) ||
!nonzero_digest(group->solver_configuration_sha256) ||
!nonzero_digest(group->initialization_evidence_sha256) ||
!nonzero_digest(group->validation_evidence_sha256))
return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
if (group_index > 0) {
const Lardon3DCalibrationToolingV2Group *previous = &evidence->groups[group_index - 1];
int order = memcmp(previous->group_identity_sha256, group->group_identity_sha256, 32);
if (order > 0 || (order == 0 && previous->group_version >= group->group_version))
return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
}
if (group->entry_count > evidence->entry_count - total_entries)
return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
total_entries += group->entry_count;
for (size_t entry_index = 0; entry_index < group->entry_count; ++entry_index) {
const Lardon3DCalibrationToolingV2Entry *entry = &group->entries[entry_index];
if (!entry_valid(entry) || entry->selected_item_index >= evidence->entry_count ||
covered[entry->selected_item_index] ||
(entry_index > 0 &&
group->entries[entry_index - 1].selected_item_index >= entry->selected_item_index))
return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
covered[entry->selected_item_index] = true;
/* Scientific images cannot belong to two groups under different item
* indexes; item coverage alone would not detect that identity conflict. */
for (size_t prior_group = 0; prior_group <= group_index; ++prior_group) {
const Lardon3DCalibrationToolingV2Group *candidate_group =
&evidence->groups[prior_group];
size_t limit = prior_group == group_index ? entry_index : candidate_group->entry_count;
for (size_t prior_entry = 0; prior_entry < limit; ++prior_entry)
if (candidate_group->entries[prior_entry].image_id == entry->image_id)
return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
}
}
}
if (total_entries != evidence->entry_count)
return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
for (size_t index = 0; index < evidence->entry_count; ++index)
if (!covered[index]) return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED;
return LARDON3D_CALIBRATION_TOOLING_V2_OK;
}
Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_produce(
const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact,
size_t artifact_capacity, size_t *written, unsigned char artifact_sha256[32]) {
if (written) *written = 0;
if (!artifact || !artifact_sha256) return LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT;
Lardon3DCalibrationToolingV2Result result =
lardon3d_calibration_tooling_v2_validate(evidence);
if (result != LARDON3D_CALIBRATION_TOOLING_V2_OK) return result;
if (evidence->group_count > (SIZE_MAX - TOOLING_V2_HEADER_SIZE) / TOOLING_V2_GROUP_SIZE)
return LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY;
size_t size = TOOLING_V2_HEADER_SIZE + evidence->group_count * TOOLING_V2_GROUP_SIZE;
if (evidence->entry_count > (SIZE_MAX - size) / TOOLING_V2_ENTRY_SIZE)
return LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY;
size += evidence->entry_count * TOOLING_V2_ENTRY_SIZE;
if (size > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_BYTES || artifact_capacity < size)
return LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY;
size_t at = 0;
memcpy(artifact + at, "L3DCALB2", 8);
at += 8;
#define PUT32(value) do { put_u32(artifact + at, (uint32_t)(value)); at += 4; } while (0)
PUT32(LARDON3D_CALIBRATION_TOOLING_V2_VERSION);
PUT32(LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE);
PUT32(LARDON3D_SPARSE_SFM_CALIBRATION_VERSION);
PUT32(evidence->group_count);
PUT32(evidence->entry_count);
for (size_t group_index = 0; group_index < evidence->group_count; ++group_index) {
const Lardon3DCalibrationToolingV2Group *group = &evidence->groups[group_index];
const unsigned char *digests[] = {
group->group_identity_sha256, group->optical_state_sha256, group->target_sha256,
group->solver_executable_sha256, group->solver_configuration_sha256,
group->initialization_evidence_sha256, group->validation_evidence_sha256};
memcpy(artifact + at, digests[0], 32); at += 32;
PUT32(group->group_version);
for (size_t digest_index = 1; digest_index < 7; ++digest_index) {
memcpy(artifact + at, digests[digest_index], 32);
at += 32;
}
PUT32(group->entry_count);
for (size_t entry_index = 0; entry_index < group->entry_count; ++entry_index) {
const Lardon3DCalibrationToolingV2Entry *entry = &group->entries[entry_index];
PUT32(entry->selected_item_index);
put_u64(artifact + at, entry->image_id); at += 8;
memcpy(artifact + at, entry->representation_sha256, 32); at += 32;
PUT32(entry->width); PUT32(entry->height);
const double parameters[] = {entry->fx, entry->fy, entry->cx, entry->cy,
entry->k1, entry->k2, entry->p1, entry->p2};
for (size_t index = 0; index < 8; ++index) {
put_f64(artifact + at, parameters[index]); at += 8;
}
PUT32(entry->support_images); PUT32(entry->support_observations);
put_f64(artifact + at, entry->reprojection_rmse_px); at += 8;
put_f64(artifact + at, entry->maximum_parameter_delta); at += 8;
PUT32(entry->validation_flags);
}
}
#undef PUT32
if (at != size || !sha256(artifact, size, artifact_sha256))
return LARDON3D_CALIBRATION_TOOLING_V2_ENCODING_ERROR;
if (written) *written = size;
return LARDON3D_CALIBRATION_TOOLING_V2_OK;
}
Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_import(
Lardon3DProjectDb *database, uint64_t execution_id,
const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact,
size_t artifact_capacity, size_t *written,
Lardon3DCalibrationBootstrapV2Output *output) {
unsigned char artifact_sha256[32];
size_t local_written = 0;
size_t *encoded_size = written ? written : &local_written;
Lardon3DCalibrationToolingV2Result result = lardon3d_calibration_tooling_v2_produce(
evidence, artifact, artifact_capacity, encoded_size, artifact_sha256);
if (result != LARDON3D_CALIBRATION_TOOLING_V2_OK) return result;
if (!database || execution_id == 0 || !output)
return LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT;
return lardon3d_calibration_bootstrap_v2_import(
database, execution_id, artifact, *encoded_size, artifact_sha256, output) ==
LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK
? LARDON3D_CALIBRATION_TOOLING_V2_OK
: LARDON3D_CALIBRATION_TOOLING_V2_IMPORT_ERROR;
}

View file

@ -0,0 +1,483 @@
#include <lardon3d/calibration_workflow.h>
#include <cerrno>
#include <charconv>
#include <cctype>
#include <cstdint>
#include <cmath>
#include <cstring>
#include <fcntl.h>
#include <locale>
#include <map>
#include <openssl/evp.h>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
namespace {
constexpr size_t kSha256Bytes = 32;
constexpr size_t kJsonDepthMax = 64;
constexpr size_t kJsonKeyMax = 128;
struct MappedFile {
int fd = -1;
const unsigned char *data = nullptr;
size_t size = 0;
unsigned char sha256[kSha256Bytes]{};
~MappedFile() {
if (data && size) munmap(const_cast<unsigned char *>(data), size);
if (fd >= 0) close(fd);
}
MappedFile() = default;
MappedFile(const MappedFile &) = delete;
MappedFile &operator=(const MappedFile &) = delete;
};
bool digest_bytes(const unsigned char *data, size_t size, unsigned char output[32]) {
unsigned int length = 0;
return EVP_Digest(data, size, output, &length, EVP_sha256(), nullptr) == 1 && length == 32;
}
Lardon3DCalibrationWorkflowResult map_regular_file(const char *path, MappedFile *out) {
if (!path || !*path || !out) return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) {
if (errno == ELOOP) return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
struct stat st{};
if (fstat(fd, &st) != 0) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
if (!S_ISREG(st.st_mode)) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
}
if (st.st_size <= 0) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
}
if (static_cast<uint64_t>(st.st_size) > LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
}
const size_t size = static_cast<size_t>(st.st_size);
void *mapped = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
out->fd = fd;
out->data = static_cast<const unsigned char *>(mapped);
out->size = size;
if (!digest_bytes(out->data, out->size, out->sha256))
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}
bool hex_digit(char c, unsigned *value) {
if (c >= '0' && c <= '9') *value = static_cast<unsigned>(c - '0');
else if (c >= 'a' && c <= 'f') *value = static_cast<unsigned>(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') *value = static_cast<unsigned>(c - 'A' + 10);
else return false;
return true;
}
bool parse_sha256(std::string_view text, unsigned char output[32]) {
if (text.size() != 64) return false;
for (size_t i = 0; i < 32; ++i) {
unsigned hi = 0, lo = 0;
if (!hex_digit(text[2 * i], &hi) || !hex_digit(text[2 * i + 1], &lo)) return false;
output[i] = static_cast<unsigned char>((hi << 4u) | lo);
}
return true;
}
bool nonzero_sha(const unsigned char value[32]) {
unsigned char any = 0;
for (size_t i = 0; i < 32; ++i) any |= value[i];
return any != 0;
}
bool token_ok(std::string_view value) {
if (value.empty() || value.size() >= LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY)
return false;
for (unsigned char c : value) {
if (!(std::isalnum(c) || c == '_' || c == '-' || c == '.' || c == ':')) return false;
}
return true;
}
bool parse_u64_any(std::string_view text, uint64_t *value) {
if (!value || text.empty()) return false;
uint64_t parsed = 0;
auto result = std::from_chars(text.data(), text.data() + text.size(), parsed, 10);
if (result.ec != std::errc() || result.ptr != text.data() + text.size()) return false;
*value = parsed;
return true;
}
bool parse_u64_positive(std::string_view text, uint64_t *value) {
return parse_u64_any(text, value) && *value != 0;
}
bool parse_u32_any(std::string_view text, uint32_t *value) {
uint64_t parsed = 0;
if (!parse_u64_any(text, &parsed) || parsed > UINT32_MAX) return false;
*value = static_cast<uint32_t>(parsed);
return true;
}
bool parse_finite_double(std::string_view text, double *value) {
if (!value || text.empty() || text.size() > 128) return false;
std::istringstream stream{std::string(text)};
stream.imbue(std::locale::classic());
double parsed = 0.0;
stream >> parsed;
if (!stream || !stream.eof() || !std::isfinite(parsed)) return false;
*value = parsed;
return true;
}
std::vector<std::string_view> words(std::string_view line) {
std::vector<std::string_view> out;
size_t at = 0;
while (at < line.size()) {
while (at < line.size() && (line[at] == ' ' || line[at] == '\t')) ++at;
if (at == line.size()) break;
size_t end = at;
while (end < line.size() && line[end] != ' ' && line[end] != '\t') ++end;
out.push_back(line.substr(at, end - at));
at = end;
}
return out;
}
struct SessionIdentity {
std::string decoder;
std::string decoder_version;
std::string optical_state_token;
unsigned char optical_state_sha256[32]{};
};
bool parse_session(std::string_view text, SessionIdentity *identity) {
if (!identity || text.empty() || text.back() != '\n' || text.find('\0') != std::string_view::npos ||
text.find('\r') != std::string_view::npos)
return false;
size_t at = 0;
size_t line_number = 0;
bool target = false, measurement = false, white_border = false, planarity = false;
bool decoder = false, optical_state = false, image = false;
while (at < text.size()) {
size_t end = text.find('\n', at);
if (end == std::string_view::npos) return false;
std::string_view line = text.substr(at, end - at);
at = end + 1;
++line_number;
if (line_number == 1) {
if (line != "L3DCAL_SESSION_V1") return false;
continue;
}
if (line.empty()) continue;
auto w = words(line);
if (w.empty()) return false;
const auto tag = w[0];
if (tag == "target") {
uint32_t squares_x = 0, squares_y = 0; double square = 0, marker = 0;
if (target || w.size() != 8 || !token_ok(w[1]) || w[3] != "DICT_5X5_100" ||
!parse_u32_any(w[4], &squares_x) || !parse_u32_any(w[5], &squares_y) ||
!parse_finite_double(w[6], &square) || !parse_finite_double(w[7], &marker) ||
squares_x != 9 || squares_y != 7 || square != 30.0 || marker != 21.0) return false;
unsigned char hash[32]; if (!parse_sha256(w[2], hash)) return false; target = true;
} else if (tag == "measurement") {
if (measurement || w.size() != 13 || !token_ok(w[1])) return false;
double v = 0; if (!parse_finite_double(w[2], &v) || v <= 0 || v > .1) return false;
for (size_t i = 3; i < w.size(); ++i) if (!parse_finite_double(w[i], &v)) return false;
measurement = true;
} else if (tag == "white_border") {
double v = 0; if (white_border || w.size() != 2 || !parse_finite_double(w[1], &v) || v < 30.0) return false;
white_border = true;
} else if (tag == "planarity") {
unsigned char hash[32];
if (planarity || w.size() != 3 || w[1] != "PASS" || !parse_sha256(w[2], hash)) return false;
planarity = true;
} else if (tag == "decoder") {
if (decoder || w.size() != 3 || !token_ok(w[1]) || !token_ok(w[2])) return false;
identity->decoder.assign(w[1]); identity->decoder_version.assign(w[2]); decoder = true;
} else if (tag == "optical_state") {
if (optical_state || w.size() != 3 || !parse_sha256(w[1], identity->optical_state_sha256) ||
!token_ok(w[2]) || w[2] == "UNKNOWN") return false;
identity->optical_state_token.assign(w[2]); optical_state = true;
} else if (tag == "image") {
unsigned char hash[32]; uint32_t orientation = 0;
if (w.size() != 4 || w[1].empty() || w[1].size() > 4096 || !parse_sha256(w[2], hash) ||
!parse_u32_any(w[3], &orientation) ||
(orientation != 90 && orientation != 180 && orientation != 270 && w[3] != "0")) return false;
image = true;
} else if (tag == "pre_solve" || tag == "clipping") {
unsigned char hash[32]; double v = 0;
if (w.size() != 3 || !parse_sha256(w[1], hash) || !parse_finite_double(w[2], &v)) return false;
} else if (tag == "coordinate") {
unsigned char hash[32]; uint32_t orientation = 0, width = 0, height = 0, count = 0; double dx = 0, dy = 0;
if (w.size() != 10 || !parse_sha256(w[1], hash) || !token_ok(w[2]) || !token_ok(w[3]) ||
!parse_u32_any(w[4], &orientation) || !parse_u32_any(w[5], &width) || !parse_u32_any(w[6], &height) ||
!parse_u32_any(w[7], &count) || !parse_finite_double(w[8], &dx) || !parse_finite_double(w[9], &dy) ||
width == 0 || height == 0 || count < 20 || count > 48 ||
(orientation != 90 && orientation != 180 && orientation != 270 && w[4] != "0")) return false;
} else if (tag == "coordinate_point") {
unsigned char hash[32]; double a = 0, b = 0, c = 0, d = 0;
if (w.size() != 7 || !parse_sha256(w[1], hash) || !token_ok(w[2]) ||
!parse_finite_double(w[3], &a) || !parse_finite_double(w[4], &b) ||
!parse_finite_double(w[5], &c) || !parse_finite_double(w[6], &d)) return false;
} else if (tag == "distance") {
unsigned char hash[32]; double meters = 0; uint32_t band = 0;
if (w.size() != 4 || !parse_sha256(w[1], hash) || !parse_finite_double(w[2], &meters) ||
!parse_u32_any(w[3], &band) || meters <= 0 || band > 2) return false;
} else {
return false;
}
}
return target && measurement && white_border && planarity && decoder && optical_state && image;
}
struct CampaignState {
uint64_t execution_id = 0;
uint64_t optical_configuration_id = 0;
std::string optical_state_token;
unsigned char optical_state_sha256[32]{};
std::vector<uint64_t> captures;
};
bool parse_campaign_state(std::string_view text, CampaignState *state) {
if (!state || text.empty() || text.back() != '\n' || text.find('\0') != std::string_view::npos ||
text.find('\r') != std::string_view::npos) return false;
std::vector<std::string_view> lines;
size_t at = 0;
while (at < text.size()) {
size_t end = text.find('\n', at); if (end == std::string_view::npos) return false;
if (end > at) lines.push_back(text.substr(at, end - at));
at = end + 1;
}
if (lines.size() < 5 || lines[0] != "L3DCAL_CAMPAIGN_STATE_V1") return false;
auto execution = words(lines[1]);
auto configuration = words(lines[2]);
auto optical = words(lines[3]);
if (execution.size() != 2 || execution[0] != "execution" || !parse_u64_positive(execution[1], &state->execution_id) ||
configuration.size() != 2 || configuration[0] != "optical_configuration" ||
!parse_u64_positive(configuration[1], &state->optical_configuration_id) || optical.size() != 3 ||
optical[0] != "optical_state" || !parse_sha256(optical[1], state->optical_state_sha256) ||
!token_ok(optical[2]) || optical[2] == "UNKNOWN") return false;
state->optical_state_token.assign(optical[2]);
for (size_t i = 4; i < lines.size(); ++i) {
auto capture = words(lines[i]);
uint64_t index = 0, capture_id = 0;
if (capture.size() != 3 || capture[0] != "capture" || !parse_u64_any(capture[1], &index) ||
!parse_u64_positive(capture[2], &capture_id) || index != state->captures.size()) return false;
if (state->captures.size() >= LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS) return false;
state->captures.push_back(capture_id);
}
return !state->captures.empty();
}
class JsonParser {
public:
explicit JsonParser(std::string_view text) : text_(text) {}
bool parse(std::map<std::string, std::string> *top_scalars) {
top_scalars_ = top_scalars;
skip_ws();
if (!parse_object(0, true)) return false;
skip_ws();
return at_ == text_.size();
}
private:
bool parse_object(size_t depth, bool top) {
if (depth > kJsonDepthMax || !take('{')) return false;
skip_ws(); if (take('}')) return true;
std::set<std::string> keys;
while (true) {
std::string key;
if (!parse_string(&key, true) || key.size() > kJsonKeyMax || !keys.insert(key).second) return false;
skip_ws(); if (!take(':')) return false; skip_ws();
if (!parse_value(depth + 1, top ? &key : nullptr)) return false;
skip_ws(); if (take('}')) return true; if (!take(',')) return false; skip_ws();
}
}
bool parse_array(size_t depth) {
if (depth > kJsonDepthMax || !take('[')) return false;
skip_ws(); if (take(']')) return true;
while (true) {
if (!parse_value(depth + 1, nullptr)) return false;
skip_ws(); if (take(']')) return true; if (!take(',')) return false; skip_ws();
}
}
bool parse_value(size_t depth, const std::string *top_key) {
if (depth > kJsonDepthMax || at_ >= text_.size()) return false;
if (text_[at_] == '{') return parse_object(depth, false);
if (text_[at_] == '[') return parse_array(depth);
if (text_[at_] == '"') {
std::string value;
if (!parse_string(top_key ? &value : nullptr, top_key != nullptr)) return false;
if (top_key) (*top_scalars_)[*top_key] = value;
return true;
}
size_t start = at_;
if (parse_literal("true") || parse_literal("false") || parse_literal("null") || parse_number()) {
if (top_key) (*top_scalars_)[*top_key] = std::string(text_.substr(start, at_ - start));
return true;
}
return false;
}
bool parse_string(std::string *out, bool capture) {
if (!take('"')) return false;
if (capture && out) out->clear();
while (at_ < text_.size()) {
unsigned char c = static_cast<unsigned char>(text_[at_++]);
if (c == '"') return true;
if (c < 0x20) return false;
if (c == '\\') {
if (at_ >= text_.size()) return false;
char e = text_[at_++];
if (e == 'u') {
if (at_ + 4 > text_.size()) return false;
for (size_t i = 0; i < 4; ++i) { unsigned v = 0; if (!hex_digit(text_[at_ + i], &v)) return false; }
at_ += 4;
} else if (std::string_view("\"\\/bfnrt").find(e) == std::string_view::npos) return false;
if (capture) return false;
} else if (capture && out) {
out->push_back(static_cast<char>(c));
}
}
return false;
}
bool parse_number() {
size_t p = at_;
if (p < text_.size() && text_[p] == '-') ++p;
if (p >= text_.size()) return false;
if (text_[p] == '0') ++p;
else {
if (text_[p] < '1' || text_[p] > '9') return false;
while (p < text_.size() && std::isdigit(static_cast<unsigned char>(text_[p]))) ++p;
}
if (p < text_.size() && text_[p] == '.') {
++p; size_t digits = p; while (p < text_.size() && std::isdigit(static_cast<unsigned char>(text_[p]))) ++p;
if (p == digits) return false;
}
if (p < text_.size() && (text_[p] == 'e' || text_[p] == 'E')) {
++p; if (p < text_.size() && (text_[p] == '+' || text_[p] == '-')) ++p;
size_t digits = p; while (p < text_.size() && std::isdigit(static_cast<unsigned char>(text_[p]))) ++p;
if (p == digits) return false;
}
at_ = p; return true;
}
bool parse_literal(std::string_view literal) {
if (text_.substr(at_, literal.size()) != literal) return false;
at_ += literal.size(); return true;
}
void skip_ws() { while (at_ < text_.size() && (text_[at_] == ' ' || text_[at_] == '\n' || text_[at_] == '\r' || text_[at_] == '\t')) ++at_; }
bool take(char c) { if (at_ >= text_.size() || text_[at_] != c) return false; ++at_; return true; }
std::string_view text_;
size_t at_ = 0;
std::map<std::string, std::string> *top_scalars_ = nullptr;
};
bool scalar(const std::map<std::string, std::string>& values, const char *key, std::string *out) {
auto it = values.find(key); if (it == values.end()) return false; *out = it->second; return true;
}
bool parse_json_top(std::string_view text, std::map<std::string, std::string> *top) {
if (text.empty() || text.find('\0') != std::string_view::npos) return false;
JsonParser parser(text); return parser.parse(top);
}
bool require_format(const std::map<std::string, std::string>& top, const char *expected) {
auto it = top.find("format"); return it != top.end() && it->second == expected;
}
} // namespace
extern "C" Lardon3DCalibrationWorkflowResult lardon3d_calibration_workflow_validate_input_boundary(
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationWorkflowInputBoundary *boundary) {
if (!files || !boundary || !files->session_path || !files->detection_path || !files->solve_path ||
!files->evidence_path || !files->producer_path || !files->campaign_state_path)
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
std::memset(boundary, 0, sizeof(*boundary));
MappedFile session, detection, solve, evidence, producer, campaign;
MappedFile *mapped[] = {&session, &detection, &solve, &evidence, &producer, &campaign};
const char *paths[] = {files->session_path, files->detection_path, files->solve_path,
files->evidence_path, files->producer_path, files->campaign_state_path};
for (size_t i = 0; i < 6; ++i) {
Lardon3DCalibrationWorkflowResult r = map_regular_file(paths[i], mapped[i]);
if (r != LARDON3D_CALIBRATION_WORKFLOW_OK) return r;
}
SessionIdentity session_identity;
if (!parse_session(std::string_view(reinterpret_cast<const char *>(session.data), session.size), &session_identity))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
CampaignState campaign_state;
if (!parse_campaign_state(std::string_view(reinterpret_cast<const char *>(campaign.data), campaign.size), &campaign_state))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
std::map<std::string, std::string> detection_top, solve_top, evidence_top, producer_top;
if (!parse_json_top(std::string_view(reinterpret_cast<const char *>(detection.data), detection.size), &detection_top) ||
!parse_json_top(std::string_view(reinterpret_cast<const char *>(solve.data), solve.size), &solve_top) ||
!parse_json_top(std::string_view(reinterpret_cast<const char *>(evidence.data), evidence.size), &evidence_top) ||
!parse_json_top(std::string_view(reinterpret_cast<const char *>(producer.data), producer.size), &producer_top))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
if (!require_format(detection_top, "L3DCAL_DETECTION_V1") || !require_format(solve_top, "L3DCAL_SOLVE_V1") ||
!require_format(evidence_top, "L3DCAL_EVIDENCE_BUNDLE_V1") || !require_format(producer_top, "L3DCAL_PRODUCER_V1"))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
std::string detection_decoder, detection_version, evidence_optical, evidence_state;
std::string producer_executable, producer_configuration, producer_session, producer_optical, producer_threads;
if (!scalar(detection_top, "decoder", &detection_decoder) || !scalar(detection_top, "decoder_version", &detection_version) ||
!scalar(evidence_top, "optical_sha256", &evidence_optical) || !scalar(evidence_top, "optical_state", &evidence_state) ||
!scalar(producer_top, "solver_executable_sha256", &producer_executable) ||
!scalar(producer_top, "solver_configuration_sha256", &producer_configuration) ||
!scalar(producer_top, "session_sha256", &producer_session) || !scalar(producer_top, "optical_sha256", &producer_optical) ||
!scalar(producer_top, "threads", &producer_threads))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
unsigned char evidence_optical_sha[32], producer_optical_sha[32], producer_session_sha[32];
unsigned char executable_sha[32], configuration_sha[32];
if (!parse_sha256(evidence_optical, evidence_optical_sha) || !parse_sha256(producer_optical, producer_optical_sha) ||
!parse_sha256(producer_session, producer_session_sha) || !parse_sha256(producer_executable, executable_sha) ||
!parse_sha256(producer_configuration, configuration_sha) || producer_threads != "1" ||
!nonzero_sha(executable_sha) || !nonzero_sha(configuration_sha))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
if (detection_decoder != session_identity.decoder || detection_version != session_identity.decoder_version ||
std::memcmp(evidence_optical_sha, session_identity.optical_state_sha256, 32) != 0 ||
evidence_state != session_identity.optical_state_token ||
std::memcmp(producer_optical_sha, session_identity.optical_state_sha256, 32) != 0 ||
std::memcmp(producer_session_sha, session.sha256, 32) != 0 ||
std::memcmp(campaign_state.optical_state_sha256, session_identity.optical_state_sha256, 32) != 0 ||
campaign_state.optical_state_token != session_identity.optical_state_token)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
std::memcpy(boundary->session_sha256, session.sha256, 32);
std::memcpy(boundary->detection_sha256, detection.sha256, 32);
std::memcpy(boundary->solve_sha256, solve.sha256, 32);
std::memcpy(boundary->evidence_sha256, evidence.sha256, 32);
std::memcpy(boundary->producer_sha256, producer.sha256, 32);
std::memcpy(boundary->campaign_state_sha256, campaign.sha256, 32);
std::memcpy(boundary->optical_state_sha256, session_identity.optical_state_sha256, 32);
std::memcpy(boundary->solver_executable_sha256, executable_sha, 32);
std::memcpy(boundary->solver_configuration_sha256, configuration_sha, 32);
boundary->selected_execution_id = campaign_state.execution_id;
boundary->optical_configuration_id = campaign_state.optical_configuration_id;
boundary->capture_count = static_cast<uint32_t>(campaign_state.captures.size());
for (size_t i = 0; i < campaign_state.captures.size(); ++i) boundary->capture_ids[i] = campaign_state.captures[i];
std::memcpy(boundary->optical_state_token, session_identity.optical_state_token.data(), session_identity.optical_state_token.size());
boundary->optical_state_token[session_identity.optical_state_token.size()] = '\0';
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}

View file

@ -0,0 +1,365 @@
// Calibration Workflow reaches Project DB through frozen Tooling/Bootstrap
// headers. Establish the C ABI before that transitive include path is guarded;
// project_db.c provides these symbols with C linkage.
extern "C" {
#include <lardon3d/project_db.h>
}
#include <lardon3d/calibration_workflow.h>
#include <lardon3d/optical_profiles.h>
#include <opencv2/imgcodecs.hpp>
#include <openssl/evp.h>
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <new>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <unistd.h>
#include <utility>
#include <vector>
namespace {
class OwnedFd {
public:
OwnedFd() = default;
explicit OwnedFd(int fd) : fd_(fd) {}
~OwnedFd() {
if (fd_ >= 0) (void)close(fd_);
}
OwnedFd(const OwnedFd&) = delete;
OwnedFd& operator=(const OwnedFd&) = delete;
OwnedFd(OwnedFd&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; }
OwnedFd& operator=(OwnedFd&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) (void)close(fd_);
fd_ = other.fd_;
other.fd_ = -1;
}
return *this;
}
int get() const { return fd_; }
bool valid() const { return fd_ >= 0; }
private:
int fd_ = -1;
};
Lardon3DCalibrationWorkflowResult db_result(Lardon3DProjectDbResult result) {
if (result == LARDON3D_PROJECT_DB_OK)
return LARDON3D_CALIBRATION_WORKFLOW_OK;
if (result == LARDON3D_PROJECT_DB_NOT_FOUND ||
result == LARDON3D_PROJECT_DB_CONSTRAINT)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
return LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR;
}
bool path_component_valid(std::string_view component) {
return !component.empty() && component != "." && component != "..";
}
Lardon3DCalibrationWorkflowResult open_project_asset(
const char *project_path, const char *relative_path, uint64_t expected_size,
const unsigned char expected_sha256[32], std::vector<unsigned char> *bytes) {
if (!project_path || !*project_path || !relative_path || !*relative_path ||
!expected_sha256 || !bytes)
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
std::string_view path(relative_path);
if (path.front() == '/' || path.back() == '/')
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
int root = open(project_path,
O_RDONLY | O_DIRECTORY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
if (root < 0) {
if (errno == ELOOP || errno == ENOTDIR)
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
OwnedFd current(root);
size_t at = 0;
while (at < path.size()) {
const size_t slash = path.find('/', at);
const bool last = slash == std::string_view::npos;
const size_t end = last ? path.size() : slash;
const std::string_view component = path.substr(at, end - at);
if (!path_component_valid(component))
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
const std::string name(component);
int flags = O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW;
if (!last) flags |= O_DIRECTORY;
int next = openat(current.get(), name.c_str(), flags);
if (next < 0) {
if (errno == ELOOP || errno == ENOTDIR)
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
OwnedFd opened(next);
struct stat st{};
if (fstat(opened.get(), &st) != 0)
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
if (!last) {
if (!S_ISDIR(st.st_mode))
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
current = std::move(opened);
at = slash + 1;
continue;
}
if (!S_ISREG(st.st_mode))
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
if (st.st_size <= 0 || expected_size == 0 ||
static_cast<uint64_t>(st.st_size) != expected_size)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
if (expected_size > LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES)
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
bytes->assign(static_cast<size_t>(expected_size), 0);
size_t used = 0;
while (used < bytes->size()) {
ssize_t count = pread(opened.get(), bytes->data() + used,
bytes->size() - used, static_cast<off_t>(used));
if (count < 0 && errno == EINTR) continue;
if (count <= 0) return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
used += static_cast<size_t>(count);
}
unsigned char actual[32]{};
unsigned int digest_size = 0;
if (EVP_Digest(bytes->data(), bytes->size(), actual, &digest_size,
EVP_sha256(), nullptr) != 1 ||
digest_size != 32)
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
if (std::memcmp(actual, expected_sha256, 32) != 0)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
}
bool execution_stage_valid(const Lardon3DProjectDbSelectedExecution& execution) {
if (execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION)
return !execution.has_calibration_scope;
if (execution.stage == LARDON3D_SELECTED_EXECUTION_READY)
return execution.has_calibration_scope && execution.calibration_scope_id != 0;
return false;
}
void fill_entry(const Lardon3DCalibrationWorkflowExternalEvidence& external,
const Lardon3DProjectDbSelectedExecutionItem& item,
const Lardon3DProjectDbImageAsset& asset,
Lardon3DCalibrationToolingEntry *entry) {
std::memset(entry, 0, sizeof(*entry));
entry->image_id = item.image_id;
std::memcpy(entry->representation_sha256, asset.sha256, 32);
std::memcpy(entry->optical_state_sha256, external.optical_state_sha256, 32);
entry->width = external.oriented_width;
entry->height = external.oriented_height;
const double *p = external.repeated_parameters[0];
entry->fx = p[0];
entry->fy = p[1];
entry->cx = p[2];
entry->cy = p[3];
entry->k1 = p[4];
entry->k2 = p[5];
entry->p1 = p[6];
entry->p2 = p[7];
const double *fit = external.fit_parameters;
entry->fit_fx = fit[0];
entry->fit_fy = fit[1];
entry->fit_cx = fit[2];
entry->fit_cy = fit[3];
entry->fit_k1 = fit[4];
entry->fit_k2 = fit[5];
entry->fit_p1 = fit[6];
entry->fit_p2 = fit[7];
std::memcpy(entry->repeated_parameters, external.repeated_parameters,
sizeof(entry->repeated_parameters));
entry->support_images = external.support_images;
entry->support_observations = external.support_observations;
entry->reprojection_rmse_px = external.reprojection_rmse_px;
entry->maximum_parameter_delta = external.maximum_parameter_delta;
entry->validation_flags = external.validation_flags;
}
void fill_tooling_evidence(
const Lardon3DCalibrationWorkflowExternalEvidence& external,
const Lardon3DCalibrationToolingEntry *entries, size_t entry_count,
Lardon3DCalibrationToolingEvidence *output) {
std::memset(output, 0, sizeof(*output));
std::memcpy(output->target_sha256, external.target_sha256, 32);
std::memcpy(output->optical_state_sha256, external.optical_state_sha256, 32);
std::memcpy(output->solver_executable_sha256,
external.solver_executable_sha256, 32);
std::memcpy(output->solver_configuration_sha256,
external.solver_configuration_sha256, 32);
std::memcpy(output->initialization_evidence_sha256,
external.initialization_evidence_sha256, 32);
std::memcpy(output->validation_evidence_sha256,
external.validation_evidence_sha256, 32);
output->target_family = external.target_family;
output->target_squares_x = external.target_squares_x;
output->target_squares_y = external.target_squares_y;
output->target_square_length_mm = external.target_square_length_mm;
output->target_marker_length_mm = external.target_marker_length_mm;
output->target_active_width_mm = external.target_active_width_mm;
output->target_active_height_mm = external.target_active_height_mm;
output->target_white_border_mm = external.target_white_border_mm;
std::memcpy(output->target_measurements_mm, external.target_measurements_mm,
sizeof(output->target_measurements_mm));
output->measurement_resolution_mm = external.measurement_resolution_mm;
output->target_flatness_mm = external.target_flatness_mm;
output->holdout_rmse_px = external.holdout_rmse_px;
output->holdout_maximum_residual_px =
external.holdout_maximum_residual_px;
output->extra_distortion_coefficient_count =
external.extra_distortion_coefficient_count;
output->views = external.views;
output->view_count = external.view_count;
output->entries = entries;
output->entry_count = entry_count;
output->coordinate_checks = external.coordinate_checks;
output->coordinate_check_count = external.coordinate_check_count;
}
Lardon3DCalibrationWorkflowResult bind_selected_execution_impl(
Lardon3DProjectDb *database, const char *project_path,
const Lardon3DCalibrationWorkflowExternalEvidence *external,
Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity,
Lardon3DCalibrationToolingEvidence *output) {
if (output) std::memset(output, 0, sizeof(*output));
if (!database || !project_path || !*project_path || !external || !entries ||
!output)
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
const uint32_t item_count = external->boundary.capture_count;
if (item_count == 0 ||
item_count > LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS ||
item_count > entry_capacity)
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
if (!external->views || external->view_count == 0 ||
external->view_count > LARDON3D_CALIBRATION_TOOLING_MAX_VIEWS ||
!external->coordinate_checks || external->coordinate_check_count == 0 ||
external->coordinate_check_count >
LARDON3D_CALIBRATION_TOOLING_MAX_COORDINATE_CHECKS ||
external->oriented_width == 0 || external->oriented_height == 0)
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
if (std::memcmp(external->optical_state_sha256,
external->boundary.optical_state_sha256, 32) != 0)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
std::vector<Lardon3DCalibrationToolingEntry> staged(item_count);
Lardon3DProjectDbSelectedExecution execution{};
Lardon3DProjectDbResult dbr = lardon3d_project_db_load_selected_execution(
database, external->boundary.selected_execution_id, &execution);
if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr);
if (!execution_stage_valid(execution) ||
execution.next_item_index != execution.item_count ||
execution.item_count != item_count)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
Lardon3DOpticalConfiguration configuration{};
dbr = lardon3d_optical_configuration_load(
database, external->boundary.optical_configuration_id, &configuration);
if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr);
if (configuration.optical_configuration_id !=
external->boundary.optical_configuration_id)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
for (uint32_t index = 0; index < item_count; ++index) {
Lardon3DProjectDbSelectedExecutionItem item{};
dbr = lardon3d_project_db_load_selected_execution_item(
database, execution.execution_id, index, &item);
if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr);
if (item.item_index != index || !item.has_image || item.image_id == 0 ||
item.capture_id != external->boundary.capture_ids[index])
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
Lardon3DOpticalCaptureAssignment assignment{};
dbr = lardon3d_optical_capture_assignment_load(
database, item.capture_id, &assignment);
if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr);
if (assignment.capture_id != item.capture_id ||
assignment.optical_configuration_id !=
external->boundary.optical_configuration_id)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
if (assignment.has_campaign_origin &&
(assignment.campaign_task_id != execution.campaign_task_id ||
assignment.campaign_group_id != item.campaign_group_id))
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
Lardon3DProjectDbCapture image_capture{};
dbr = lardon3d_project_db_find_capture_for_image(
database, item.image_id, &image_capture);
if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr);
if (image_capture.capture_id != item.capture_id)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
Lardon3DProjectDbImage image{};
Lardon3DProjectDbImageAsset asset{};
dbr = lardon3d_project_db_load_image(database, item.image_id, &image, &asset);
if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr);
if (image.image_id != item.image_id || image.asset_id != asset.asset_id ||
asset.state != LARDON3D_DB_IMAGE_ASSET_READY)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
std::vector<unsigned char> bytes;
Lardon3DCalibrationWorkflowResult result =
open_project_asset(project_path, asset.path, asset.size_bytes,
asset.sha256, &bytes);
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
try {
cv::Mat decoded = cv::imdecode(bytes, cv::IMREAD_GRAYSCALE);
if (decoded.empty() || decoded.cols <= 0 || decoded.rows <= 0 ||
static_cast<uint32_t>(decoded.cols) != external->oriented_width ||
static_cast<uint32_t>(decoded.rows) != external->oriented_height)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
} catch (const cv::Exception&) {
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
} catch (...) {
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
fill_entry(*external, item, asset, &staged[index]);
}
std::memcpy(entries, staged.data(),
static_cast<size_t>(item_count) * sizeof(*entries));
fill_tooling_evidence(*external, entries, item_count, output);
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}
} // namespace
extern "C" Lardon3DCalibrationWorkflowResult
lardon3d_calibration_workflow_bind_selected_execution(
Lardon3DProjectDb *database, const char *project_path,
const Lardon3DCalibrationWorkflowExternalEvidence *external,
Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity,
Lardon3DCalibrationToolingEvidence *output) {
try {
return bind_selected_execution_impl(database, project_path, external,
entries, entry_capacity, output);
} catch (const std::bad_alloc&) {
if (output) std::memset(output, 0, sizeof(*output));
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
} catch (...) {
if (output) std::memset(output, 0, sizeof(*output));
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
}

View file

@ -0,0 +1,95 @@
// This translation unit calls the C Project DB and frozen Tooling APIs from
// C++. Their declarations must have C linkage before the workflow include path.
extern "C" {
#include <lardon3d/project_db.h>
}
#include <lardon3d/calibration_workflow.h>
#include <cstring>
#include <new>
namespace {
Lardon3DCalibrationWorkflowResult tooling_result(
Lardon3DCalibrationToolingResult result) {
if (result == LARDON3D_CALIBRATION_TOOLING_OK)
return LARDON3D_CALIBRATION_WORKFLOW_OK;
if (result == LARDON3D_CALIBRATION_TOOLING_CAPACITY)
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
if (result == LARDON3D_CALIBRATION_TOOLING_IMPORT_ERROR)
return LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR;
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
}
Lardon3DCalibrationWorkflowResult complete_impl(
Lardon3DProjectDb *database, const char *project_path,
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationToolingView *views, size_t view_capacity,
Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks,
size_t coordinate_check_capacity, Lardon3DCalibrationToolingEntry *entries,
size_t entry_capacity, unsigned char *artifact, size_t artifact_capacity,
size_t *artifact_size, Lardon3DCalibrationBootstrapOutput *output) {
if (artifact_size) *artifact_size = 0;
if (output) std::memset(output, 0, sizeof(*output));
if (!database || !project_path || !*project_path || !files || !views ||
!coordinate_checks || !entries || !artifact || !artifact_size || !output)
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
Lardon3DCalibrationWorkflowExternalEvidence external{};
Lardon3DCalibrationWorkflowResult result =
lardon3d_calibration_workflow_materialize_external_evidence(
files, views, view_capacity, coordinate_checks, coordinate_check_capacity,
&external);
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
Lardon3DCalibrationToolingEvidence tooling{};
result = lardon3d_calibration_workflow_bind_selected_execution(
database, project_path, &external, entries, entry_capacity, &tooling);
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
result = tooling_result(lardon3d_calibration_tooling_import(
database, external.boundary.selected_execution_id, &tooling, artifact,
artifact_capacity, artifact_size, output));
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
// Tooling owns immutable import publication. The workflow's success contract
// is stricter: only the exact returned scope attached to READY is success.
Lardon3DProjectDbSelectedExecution execution{};
const Lardon3DProjectDbResult loaded =
lardon3d_project_db_load_selected_execution(
database, external.boundary.selected_execution_id, &execution);
if (loaded != LARDON3D_PROJECT_DB_OK ||
execution.stage != LARDON3D_SELECTED_EXECUTION_READY ||
!execution.has_calibration_scope || execution.calibration_scope_id == 0 ||
execution.calibration_scope_id != output->scope.scope_id)
return LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR;
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}
} // namespace
extern "C" Lardon3DCalibrationWorkflowResult
lardon3d_calibration_workflow_complete(
Lardon3DProjectDb *database, const char *project_path,
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationToolingView *views, size_t view_capacity,
Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks,
size_t coordinate_check_capacity, Lardon3DCalibrationToolingEntry *entries,
size_t entry_capacity, unsigned char *artifact, size_t artifact_capacity,
size_t *artifact_size, Lardon3DCalibrationBootstrapOutput *output) {
try {
return complete_impl(database, project_path, files, views, view_capacity,
coordinate_checks, coordinate_check_capacity, entries,
entry_capacity, artifact, artifact_capacity,
artifact_size, output);
} catch (const std::bad_alloc&) {
if (artifact_size) *artifact_size = 0;
if (output) std::memset(output, 0, sizeof(*output));
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
} catch (...) {
if (artifact_size) *artifact_size = 0;
if (output) std::memset(output, 0, sizeof(*output));
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
}

View file

@ -0,0 +1,940 @@
#include <lardon3d/calibration_workflow.h>
#include <openssl/evp.h>
#include <algorithm>
#include <array>
#include <cerrno>
#include <charconv>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <locale.h>
#include <map>
#include <set>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <unistd.h>
#include <utility>
#include <vector>
namespace {
constexpr size_t kShaBytes = 32;
constexpr size_t kMaxObservations =
LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS * 48u;
struct ShaLess {
bool operator()(const std::array<unsigned char, 32>& a,
const std::array<unsigned char, 32>& b) const {
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
}
};
bool hex_digit(char c, unsigned *value) {
if (c >= '0' && c <= '9') *value = static_cast<unsigned>(c - '0');
else if (c >= 'a' && c <= 'f') *value = static_cast<unsigned>(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') *value = static_cast<unsigned>(c - 'A' + 10);
else return false;
return true;
}
bool parse_sha(std::string_view text, unsigned char output[32]) {
if (text.size() != 64) return false;
for (size_t i = 0; i < 32; ++i) {
unsigned hi = 0, lo = 0;
if (!hex_digit(text[2 * i], &hi) || !hex_digit(text[2 * i + 1], &lo))
return false;
output[i] = static_cast<unsigned char>((hi << 4u) | lo);
}
return true;
}
std::array<unsigned char, 32> sha_array(const unsigned char value[32]) {
std::array<unsigned char, 32> out{};
std::memcpy(out.data(), value, 32);
return out;
}
bool digest(const unsigned char *data, size_t size, unsigned char output[32]) {
unsigned int length = 0;
return EVP_Digest(data, size, output, &length, EVP_sha256(), nullptr) == 1 &&
length == 32;
}
Lardon3DCalibrationWorkflowResult read_verified(
const char *path, const unsigned char expected[32], std::string *output) {
if (!path || !expected || !output)
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) {
if (errno == ELOOP) return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
struct stat st{};
if (fstat(fd, &st) != 0) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
if (!S_ISREG(st.st_mode)) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
}
if (st.st_size <= 0) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
}
if (static_cast<uint64_t>(st.st_size) >
LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
}
const size_t size = static_cast<size_t>(st.st_size);
output->assign(size, '\0');
size_t used = 0;
while (used < size) {
ssize_t n = pread(fd, output->data() + used, size - used,
static_cast<off_t>(used));
if (n < 0 && errno == EINTR) continue;
if (n <= 0) {
close(fd);
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
}
used += static_cast<size_t>(n);
}
if (close(fd) != 0) return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
unsigned char actual[32]{};
if (!digest(reinterpret_cast<const unsigned char *>(output->data()),
output->size(), actual))
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
if (std::memcmp(actual, expected, 32) != 0)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}
bool parse_u64(std::string_view text, uint64_t *value) {
if (!value || text.empty()) return false;
uint64_t v = 0;
auto result = std::from_chars(text.data(), text.data() + text.size(), v, 10);
if (result.ec != std::errc() || result.ptr != text.data() + text.size())
return false;
*value = v;
return true;
}
bool parse_u32(std::string_view text, uint32_t *value) {
uint64_t v = 0;
if (!parse_u64(text, &v) || v > UINT32_MAX) return false;
*value = static_cast<uint32_t>(v);
return true;
}
bool parse_double_c(std::string_view text, double *value) {
if (!value || text.empty() || text.size() > 128) return false;
char buffer[129];
std::memcpy(buffer, text.data(), text.size());
buffer[text.size()] = '\0';
locale_t locale = newlocale(LC_NUMERIC_MASK, "C", static_cast<locale_t>(0));
if (!locale) return false;
char *end = nullptr;
errno = 0;
double v = strtod_l(buffer, &end, locale);
freelocale(locale);
if (errno == ERANGE || end != buffer + text.size() || !std::isfinite(v))
return false;
*value = v;
return true;
}
std::vector<std::string_view> words(std::string_view line) {
std::vector<std::string_view> out;
size_t at = 0;
while (at < line.size()) {
while (at < line.size() && (line[at] == ' ' || line[at] == '\t')) ++at;
if (at == line.size()) break;
size_t end = at;
while (end < line.size() && line[end] != ' ' && line[end] != '\t') ++end;
out.push_back(line.substr(at, end - at));
at = end;
}
return out;
}
bool token_ok(std::string_view token) {
if (token.empty() ||
token.size() >= LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY)
return false;
for (unsigned char c : token) {
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' ||
c == ':'))
return false;
}
return true;
}
struct SessionView {
std::array<unsigned char, 32> sha{};
uint32_t orientation = 0;
double pre_solve = -1.0;
double clipping = -1.0;
double distance = -1.0;
uint32_t distance_band = UINT32_MAX;
uint32_t coordinate_orientation = UINT32_MAX;
uint32_t coordinate_width = 0;
uint32_t coordinate_height = 0;
uint32_t coordinate_count = 0;
unsigned coordinate_coverage = 0;
std::vector<Lardon3DCalibrationToolingCoordinateCheck> checks;
};
struct SessionData {
std::string target_id;
std::string instrument;
std::string decoder;
std::string decoder_version;
std::string optical_token;
unsigned char generator_sha[32]{};
unsigned char planarity_sha[32]{};
unsigned char optical_sha[32]{};
double measurements[10]{};
double measurement_resolution = 0.0;
double white_border = 0.0;
std::vector<SessionView> views;
};
int coverage_bit(std::string_view label) {
static const std::array<std::string_view, 9> labels = {
"center", "top", "right", "bottom", "left",
"top_left", "top_right", "bottom_left", "bottom_right"};
for (size_t i = 0; i < labels.size(); ++i)
if (label == labels[i]) return static_cast<int>(i);
return -1;
}
bool parse_session_materialization(std::string_view text, SessionData *out) {
if (!out || text.empty() || text.back() != '\n' ||
text.find('\0') != std::string_view::npos ||
text.find('\r') != std::string_view::npos)
return false;
std::map<std::array<unsigned char, 32>, size_t, ShaLess> index;
bool target = false, measurement = false, border = false, planarity = false;
bool decoder = false, optical = false;
size_t at = 0, line_number = 0;
while (at < text.size()) {
size_t end = text.find('\n', at);
if (end == std::string_view::npos) return false;
std::string_view line = text.substr(at, end - at);
at = end + 1;
++line_number;
if (line_number == 1) {
if (line != "L3DCAL_SESSION_V1") return false;
continue;
}
if (line.empty()) continue;
auto w = words(line);
if (w.empty()) return false;
if (w[0] == "target") {
uint32_t sx = 0, sy = 0;
double square = 0.0, marker = 0.0;
if (target || w.size() != 8 || !token_ok(w[1]) ||
!parse_sha(w[2], out->generator_sha) ||
w[3] != "DICT_5X5_100" || !parse_u32(w[4], &sx) ||
!parse_u32(w[5], &sy) || !parse_double_c(w[6], &square) ||
!parse_double_c(w[7], &marker) || sx != 9 || sy != 7 ||
square != 30.0 || marker != 21.0)
return false;
out->target_id.assign(w[1]);
target = true;
} else if (w[0] == "measurement") {
if (measurement || w.size() != 13 || !token_ok(w[1]) ||
!parse_double_c(w[2], &out->measurement_resolution))
return false;
out->instrument.assign(w[1]);
for (size_t i = 0; i < 10; ++i)
if (!parse_double_c(w[i + 3], &out->measurements[i])) return false;
measurement = true;
} else if (w[0] == "white_border") {
if (border || w.size() != 2 || !parse_double_c(w[1], &out->white_border))
return false;
border = true;
} else if (w[0] == "planarity") {
if (planarity || w.size() != 3 || w[1] != "PASS" ||
!parse_sha(w[2], out->planarity_sha))
return false;
planarity = true;
} else if (w[0] == "decoder") {
if (decoder || w.size() != 3 || !token_ok(w[1]) || !token_ok(w[2]))
return false;
out->decoder.assign(w[1]);
out->decoder_version.assign(w[2]);
decoder = true;
} else if (w[0] == "optical_state") {
if (optical || w.size() != 3 || !parse_sha(w[1], out->optical_sha) ||
!token_ok(w[2]) || w[2] == "UNKNOWN")
return false;
out->optical_token.assign(w[2]);
optical = true;
} else if (w[0] == "image") {
if (w.size() != 4 || w[1].empty() || w[1].size() > 4096) return false;
SessionView view;
if (!parse_sha(w[2], view.sha.data()) || !parse_u32(w[3], &view.orientation) ||
(view.orientation != 0 && view.orientation != 90 &&
view.orientation != 180 && view.orientation != 270))
return false;
auto inserted = index.emplace(view.sha, out->views.size());
if (!inserted.second ||
out->views.size() >= LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS)
return false;
out->views.push_back(view);
} else if (w[0] == "pre_solve" || w[0] == "clipping" ||
w[0] == "distance" || w[0] == "coordinate" ||
w[0] == "coordinate_point") {
if (w.size() < 2) return false;
std::array<unsigned char, 32> sha{};
if (!parse_sha(w[1], sha.data())) return false;
auto found = index.find(sha);
if (found == index.end()) return false;
SessionView& view = out->views[found->second];
if (w[0] == "pre_solve") {
if (w.size() != 3 || view.pre_solve >= 0 ||
!parse_double_c(w[2], &view.pre_solve))
return false;
} else if (w[0] == "clipping") {
if (w.size() != 3 || view.clipping >= 0 ||
!parse_double_c(w[2], &view.clipping))
return false;
} else if (w[0] == "distance") {
if (w.size() != 4 || view.distance > 0 ||
!parse_double_c(w[2], &view.distance) ||
!parse_u32(w[3], &view.distance_band) ||
view.distance <= 0 || view.distance_band > 2)
return false;
} else if (w[0] == "coordinate") {
double initial_dx = 0.0, initial_dy = 0.0;
if (w.size() != 10 || view.coordinate_count != 0 ||
w[2] != out->decoder || w[3] != out->decoder_version ||
!parse_u32(w[4], &view.coordinate_orientation) ||
!parse_u32(w[5], &view.coordinate_width) ||
!parse_u32(w[6], &view.coordinate_height) ||
!parse_u32(w[7], &view.coordinate_count) ||
!parse_double_c(w[8], &initial_dx) ||
!parse_double_c(w[9], &initial_dy) ||
view.coordinate_orientation != view.orientation ||
view.coordinate_width == 0 || view.coordinate_height == 0 ||
view.coordinate_count < 20 || view.coordinate_count > 48 ||
initial_dx < 0 || initial_dy < 0)
return false;
view.checks.reserve(view.coordinate_count);
} else {
double sx = 0.0, sy = 0.0, fx = 0.0, fy = 0.0;
if (w.size() != 7 || view.coordinate_count == 0 ||
view.checks.size() >= view.coordinate_count ||
!parse_double_c(w[3], &sx) || !parse_double_c(w[4], &sy) ||
!parse_double_c(w[5], &fx) || !parse_double_c(w[6], &fy))
return false;
int bit = coverage_bit(w[2]);
if (bit < 0) return false;
view.coordinate_coverage |= 1u << static_cast<unsigned>(bit);
Lardon3DCalibrationToolingCoordinateCheck check{};
std::memcpy(check.source_sha256, view.sha.data(), 32);
check.orientation_degrees = view.orientation;
check.dx_px = sx - fx;
check.dy_px = sy - fy;
view.checks.push_back(check);
}
} else {
return false;
}
}
if (!target || !measurement || !border || !planarity || !decoder || !optical ||
out->views.empty())
return false;
for (const SessionView& view : out->views) {
if (view.pre_solve < 0 || view.clipping < 0 || view.distance <= 0 ||
view.distance_band > 2 || view.coordinate_count < 20 ||
view.checks.size() != view.coordinate_count ||
view.coordinate_coverage != 0x1ffu)
return false;
}
return true;
}
class JsonCursor {
public:
explicit JsonCursor(std::string_view text) : text_(text) {}
void ws() {
while (at_ < text_.size() &&
(text_[at_] == ' ' || text_[at_] == '\n' ||
text_[at_] == '\r' || text_[at_] == '\t'))
++at_;
}
bool ch(char expected) {
ws();
if (at_ >= text_.size() || text_[at_] != expected) return false;
++at_;
return true;
}
bool key(std::string_view expected) {
std::string value;
return string(&value) && value == expected && ch(':');
}
bool string(std::string *output) {
ws();
if (at_ >= text_.size() || text_[at_] != '"') return false;
++at_;
output->clear();
while (at_ < text_.size()) {
unsigned char c = static_cast<unsigned char>(text_[at_++]);
if (c == '"') return true;
if (c < 0x20 || c == '\\') return false;
output->push_back(static_cast<char>(c));
}
return false;
}
bool u64(uint64_t *output) {
ws();
size_t start = at_;
if (start >= text_.size() || text_[start] < '0' || text_[start] > '9')
return false;
while (at_ < text_.size() && text_[at_] >= '0' && text_[at_] <= '9') ++at_;
return parse_u64(text_.substr(start, at_ - start), output);
}
bool u32(uint32_t *output) {
uint64_t v = 0;
if (!u64(&v) || v > UINT32_MAX) return false;
*output = static_cast<uint32_t>(v);
return true;
}
bool boolean(bool *output) {
ws();
if (text_.substr(at_, 4) == "true") {
at_ += 4; *output = true; return true;
}
if (text_.substr(at_, 5) == "false") {
at_ += 5; *output = false; return true;
}
return false;
}
bool hex_double(double *output) {
std::string value;
return string(&value) && parse_double_c(value, output);
}
bool done() {
ws();
return at_ == text_.size();
}
private:
std::string_view text_;
size_t at_ = 0;
};
bool comma(JsonCursor *j) { return j->ch(','); }
uint32_t rejection_reason(std::string_view reason) {
static const std::array<std::string_view, 10> reasons = {
"source_size", "decode", "decoded_dimensions", "insufficient_charuco",
"invalid_charuco_id", "occupancy", "target_physical_quadrants",
"clipping", "pre_solve_corner_rms", "coordinate_equivalence"};
for (size_t i = 0; i < reasons.size(); ++i)
if (reason == reasons[i]) return static_cast<uint32_t>(i + 1);
return 0;
}
struct DetectionView {
Lardon3DCalibrationToolingView tooling{};
uint32_t width = 0;
uint32_t height = 0;
bool coordinate_pass = false;
std::set<uint32_t> corner_ids;
};
bool parse_detection(std::string_view text, const SessionData& session,
std::vector<DetectionView> *views) {
JsonCursor j(text);
std::string format, decoder, decoder_version;
if (!j.ch('{') || !j.key("format") || !j.string(&format) ||
format != "L3DCAL_DETECTION_V1" || !comma(&j) ||
!j.key("decoder") || !j.string(&decoder) || decoder != session.decoder ||
!comma(&j) || !j.key("decoder_version") ||
!j.string(&decoder_version) || decoder_version != session.decoder_version ||
!comma(&j) || !j.key("views") || !j.ch('['))
return false;
std::map<std::array<unsigned char, 32>, size_t, ShaLess> session_index;
for (size_t i = 0; i < session.views.size(); ++i)
session_index.emplace(session.views[i].sha, i);
std::set<std::array<unsigned char, 32>, ShaLess> seen;
std::array<unsigned char, 32> previous{};
bool have_previous = false;
j.ws();
if (!j.ch(']')) {
while (true) {
DetectionView dv;
std::string sha_text, decision, reason;
uint32_t physical_quadrants = 0, comparison_points = 0;
double coordinate_dx = 0.0, coordinate_dy = 0.0;
bool coordinate_pass = false, holdout = false;
if (!j.ch('{') || !j.key("source_sha256") || !j.string(&sha_text) ||
!parse_sha(sha_text, dv.tooling.source_sha256) || !comma(&j) ||
!j.key("orientation") || !j.u32(&dv.tooling.orientation_degrees) ||
!comma(&j) || !j.key("oriented_width") || !j.u32(&dv.width) ||
!comma(&j) || !j.key("oriented_height") || !j.u32(&dv.height) ||
!comma(&j) || !j.key("decision") || !j.string(&decision) ||
!comma(&j) || !j.key("reason") || !j.string(&reason) ||
!comma(&j) || !j.key("frame_region") || !j.u32(&dv.tooling.quadrant) ||
!comma(&j) || !j.key("distance_band") ||
!j.u32(&dv.tooling.distance_band) || !comma(&j) ||
!j.key("holdout") || !j.boolean(&holdout) || !comma(&j) ||
!j.key("target_occupancy") || !j.hex_double(&dv.tooling.target_occupancy) ||
!comma(&j) || !j.key("normal_angle_degrees") ||
!j.hex_double(&dv.tooling.normal_angle_degrees) || !comma(&j) ||
!j.key("measured_distance_metres") ||
!j.hex_double(&dv.tooling.distance_metres) || !comma(&j) ||
!j.key("pre_solve_corner_rms_px") ||
!j.hex_double(&dv.tooling.corner_rms_px) || !comma(&j) ||
!j.key("clipping_fraction") ||
!j.hex_double(&dv.tooling.clipped_fraction) || !comma(&j) ||
!j.key("physical_target_quadrants") || !j.u32(&physical_quadrants) ||
!comma(&j) || !j.key("target_corner_quadrant_mask") ||
!j.u32(&dv.tooling.target_corner_quadrant_mask) || !comma(&j) ||
!j.key("corner_count") || !j.u32(&dv.tooling.corner_count) ||
!comma(&j) || !j.key("residual_count") ||
!j.u32(&dv.tooling.residual_count) || !comma(&j) ||
!j.key("high_residual_count") ||
!j.u32(&dv.tooling.high_residual_count) || !comma(&j) ||
!j.key("reprojection_rmse_px") ||
!j.hex_double(&dv.tooling.reprojection_rmse_px) || !comma(&j) ||
!j.key("maximum_residual_px") ||
!j.hex_double(&dv.tooling.maximum_residual_px) || !comma(&j) ||
!j.key("coordinate_equivalence") || !j.ch('{') ||
!j.key("comparison_points") || !j.u32(&comparison_points) ||
!comma(&j) || !j.key("max_abs_dx_px") || !j.hex_double(&coordinate_dx) ||
!comma(&j) || !j.key("max_abs_dy_px") || !j.hex_double(&coordinate_dy) ||
!comma(&j) || !j.key("pass") || !j.boolean(&coordinate_pass) ||
!j.ch('}') || !comma(&j) || !j.key("corners") || !j.ch('['))
return false;
auto key = sha_array(dv.tooling.source_sha256);
auto sit = session_index.find(key);
if (sit == session_index.end() || !seen.insert(key).second) return false;
if (have_previous &&
!std::lexicographical_compare(previous.begin(), previous.end(),
key.begin(), key.end()))
return false;
previous = key;
have_previous = true;
const SessionView& sv = session.views[sit->second];
size_t corner_count = 0;
j.ws();
if (!j.ch(']')) {
while (true) {
uint32_t id = 0;
double x = 0.0, y = 0.0;
if (!j.ch('{') || !j.key("id") || !j.u32(&id) || !comma(&j) ||
!j.key("x") || !j.hex_double(&x) || !comma(&j) ||
!j.key("y") || !j.hex_double(&y) || !j.ch('}') ||
!dv.corner_ids.insert(id).second)
return false;
++corner_count;
j.ws();
if (j.ch(']')) break;
if (!comma(&j)) return false;
}
}
if (!j.ch('}')) return false;
dv.tooling.accepted = decision == "accepted" ? 1u : 0u;
if (decision != "accepted" && decision != "rejected") return false;
if (dv.tooling.accepted) {
if (reason != "-" || !coordinate_pass || dv.width == 0 || dv.height == 0 ||
dv.width != sv.coordinate_width ||
dv.height != sv.coordinate_height ||
dv.tooling.orientation_degrees != sv.orientation ||
dv.tooling.corner_rms_px != sv.pre_solve ||
dv.tooling.distance_metres != sv.distance ||
dv.tooling.distance_band != sv.distance_band ||
comparison_points != sv.coordinate_count ||
dv.tooling.corner_count != corner_count ||
dv.tooling.residual_count != dv.tooling.corner_count)
return false;
} else {
dv.tooling.rejection_reason = rejection_reason(reason);
if (reason == "-" || dv.tooling.rejection_reason == 0 || holdout)
return false;
}
dv.tooling.holdout = holdout ? 1u : 0u;
unsigned pop = 0;
for (unsigned bit = 0; bit < 4; ++bit)
pop += (dv.tooling.target_corner_quadrant_mask >> bit) & 1u;
if (physical_quadrants != pop ||
dv.tooling.high_residual_count > dv.tooling.residual_count ||
coordinate_dx < 0 || coordinate_dy < 0)
return false;
dv.coordinate_pass = coordinate_pass;
views->push_back(std::move(dv));
j.ws();
if (j.ch(']')) break;
if (!comma(&j)) return false;
}
}
if (!j.ch('}') || !j.done() || views->size() != session.views.size())
return false;
return true;
}
bool parse_parameter_array(JsonCursor *j, double output[8]) {
if (!j->ch('[')) return false;
for (size_t i = 0; i < 8; ++i) {
if (!j->hex_double(&output[i])) return false;
if (i + 1 != 8 && !comma(j)) return false;
}
return j->ch(']');
}
bool parse_vec3(JsonCursor *j) {
if (!j->ch('[')) return false;
for (size_t i = 0; i < 3; ++i) {
double value = 0.0;
if (!j->hex_double(&value)) return false;
if (i + 1 != 3 && !comma(j)) return false;
}
return j->ch(']');
}
struct SolveData {
double repeated[3][8]{};
double fit[8]{};
};
bool parse_solve(std::string_view text, size_t accepted_count, SolveData *out) {
JsonCursor j(text);
std::string format;
if (!j.ch('{') || !j.key("format") || !j.string(&format) ||
format != "L3DCAL_SOLVE_V1" || !comma(&j) ||
!j.key("runs") || !j.ch('['))
return false;
for (uint32_t run = 0; run < 3; ++run) {
uint32_t run_id = UINT32_MAX;
double opencv_rms = 0.0;
if (!j.ch('{') || !j.key("run") || !j.u32(&run_id) || run_id != run ||
!comma(&j) || !j.key("params") ||
!parse_parameter_array(&j, out->repeated[run]) || !comma(&j) ||
!j.key("opencv_rms_px") || !j.hex_double(&opencv_rms) ||
opencv_rms < 0 || !comma(&j) || !j.key("poses") || !j.ch('['))
return false;
size_t poses = 0;
j.ws();
if (!j.ch(']')) {
while (true) {
if (!j.ch('{') || !j.key("rvec") || !parse_vec3(&j) ||
!comma(&j) || !j.key("tvec_m") || !parse_vec3(&j) || !j.ch('}'))
return false;
++poses;
j.ws();
if (j.ch(']')) break;
if (!comma(&j)) return false;
}
}
if (poses != accepted_count || !j.ch('}')) return false;
if (run != 2 && !comma(&j)) return false;
}
if (!j.ch(']') || !comma(&j) || !j.key("fit_params") ||
!parse_parameter_array(&j, out->fit) || !j.ch('}') || !j.done())
return false;
for (size_t run = 1; run < 3; ++run)
if (std::memcmp(out->repeated[0], out->repeated[run],
sizeof(out->repeated[0])) != 0)
return false;
return true;
}
struct EvidenceData {
double global_rmse = 0.0;
double global_max = 0.0;
double high_fraction = 0.0;
double holdout_rmse = 0.0;
double holdout_max = 0.0;
double maximum_delta = 0.0;
uint32_t flags = 0;
};
bool parse_flags(std::string_view text, uint32_t *flags) {
if (text.size() != 3 || text[0] != '0' || text[1] != 'x') return false;
unsigned v = 0;
if (!hex_digit(text[2], &v) || v > UINT32_MAX) return false;
*flags = static_cast<uint32_t>(v);
return true;
}
bool parse_evidence(std::string_view text, const SessionData& session,
const std::vector<DetectionView>& views,
EvidenceData *out) {
JsonCursor j(text);
std::string format, target_id, generator, instrument, planarity, optical,
optical_state, flags_text;
double resolution = 0.0;
bool deterministic = false;
if (!j.ch('{') || !j.key("format") || !j.string(&format) ||
format != "L3DCAL_EVIDENCE_BUNDLE_V1" || !comma(&j) ||
!j.key("target") || !j.ch('{') ||
!j.key("id") || !j.string(&target_id) || !comma(&j) ||
!j.key("generator_sha256") || !j.string(&generator) || !comma(&j) ||
!j.key("instrument") || !j.string(&instrument) || !comma(&j) ||
!j.key("resolution_mm") || !j.hex_double(&resolution) || !comma(&j) ||
!j.key("planarity_evidence_sha256") || !j.string(&planarity) ||
!j.ch('}') || !comma(&j) ||
!j.key("optical_sha256") || !j.string(&optical) || !comma(&j) ||
!j.key("optical_state") || !j.string(&optical_state) || !comma(&j) ||
!j.key("validation_flags") || !j.string(&flags_text) || !comma(&j) ||
!j.key("global_rmse_px") || !j.hex_double(&out->global_rmse) ||
!comma(&j) || !j.key("maximum_residual_px") ||
!j.hex_double(&out->global_max) || !comma(&j) ||
!j.key("high_residual_fraction") || !j.hex_double(&out->high_fraction) ||
!comma(&j) || !j.key("holdout") || !j.ch('{') ||
!j.key("rmse_px") || !j.hex_double(&out->holdout_rmse) || !comma(&j) ||
!j.key("maximum_px") || !j.hex_double(&out->holdout_max) ||
!j.ch('}') || !comma(&j) ||
!j.key("maximum_parameter_delta_px") ||
!j.hex_double(&out->maximum_delta) || !comma(&j) ||
!j.key("deterministic_full_solve_equality") ||
!j.boolean(&deterministic) || !comma(&j) ||
!j.key("residuals") || !j.ch('['))
return false;
unsigned char generator_sha[32]{}, planarity_sha[32]{}, optical_sha[32]{};
if (target_id != session.target_id || instrument != session.instrument ||
resolution != session.measurement_resolution ||
!parse_sha(generator, generator_sha) ||
!parse_sha(planarity, planarity_sha) || !parse_sha(optical, optical_sha) ||
std::memcmp(generator_sha, session.generator_sha, 32) != 0 ||
std::memcmp(planarity_sha, session.planarity_sha, 32) != 0 ||
std::memcmp(optical_sha, session.optical_sha, 32) != 0 ||
optical_state != session.optical_token ||
!parse_flags(flags_text, &out->flags) || out->flags != 0x0fu ||
!deterministic || out->global_rmse < 0 || out->global_max < 0 ||
out->high_fraction < 0 || out->high_fraction > 1 ||
out->holdout_rmse < 0 || out->holdout_max < 0 ||
out->maximum_delta < 0)
return false;
std::map<std::array<unsigned char, 32>, size_t, ShaLess> view_index;
for (size_t i = 0; i < views.size(); ++i)
view_index.emplace(sha_array(views[i].tooling.source_sha256), i);
std::vector<uint32_t> counts(views.size(), 0), highs(views.size(), 0);
std::set<std::pair<size_t, uint32_t>> residual_ids;
uint64_t total = 0, total_high = 0;
j.ws();
if (!j.ch(']')) {
while (true) {
std::string sha_text;
uint32_t corner_id = 0;
double dx = 0.0, dy = 0.0, rmse = 0.0;
if (!j.ch('{') || !j.key("source_sha256") || !j.string(&sha_text) ||
!comma(&j) || !j.key("corner_id") || !j.u32(&corner_id) ||
!comma(&j) || !j.key("dx_px") || !j.hex_double(&dx) ||
!comma(&j) || !j.key("dy_px") || !j.hex_double(&dy) ||
!comma(&j) || !j.key("rmse_px") || !j.hex_double(&rmse) ||
!j.ch('}'))
return false;
unsigned char sha[32]{};
if (!parse_sha(sha_text, sha)) return false;
auto found = view_index.find(sha_array(sha));
if (found == view_index.end()) return false;
size_t vi = found->second;
const DetectionView& dv = views[vi];
if (!dv.tooling.accepted || dv.corner_ids.find(corner_id) == dv.corner_ids.end() ||
!residual_ids.emplace(vi, corner_id).second ||
rmse != dv.tooling.reprojection_rmse_px)
return false;
++counts[vi];
++total;
const double squared = dx * dx + dy * dy;
if (!std::isfinite(squared)) return false;
if (squared > 1.0) {
++highs[vi];
++total_high;
}
if (total > kMaxObservations) return false;
j.ws();
if (j.ch(']')) break;
if (!comma(&j)) return false;
}
}
if (!j.ch('}') || !j.done() || total == 0) return false;
for (size_t i = 0; i < views.size(); ++i) {
if (views[i].tooling.accepted) {
if (counts[i] != views[i].tooling.residual_count ||
highs[i] != views[i].tooling.high_residual_count)
return false;
} else if (counts[i] != 0) {
return false;
}
}
const double calculated_fraction =
static_cast<double>(total_high) / static_cast<double>(total);
if (calculated_fraction != out->high_fraction) return false;
return true;
}
bool validation_binding(const Lardon3DCalibrationWorkflowInputBoundary& boundary,
unsigned char output[32]) {
static const char domain[] = "L3DCAL_WORKFLOW_VALIDATION_V1\n";
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) return false;
bool ok = EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) == 1 &&
EVP_DigestUpdate(ctx, domain, sizeof(domain) - 1) == 1 &&
EVP_DigestUpdate(ctx, boundary.detection_sha256, 32) == 1 &&
EVP_DigestUpdate(ctx, boundary.solve_sha256, 32) == 1 &&
EVP_DigestUpdate(ctx, boundary.evidence_sha256, 32) == 1 &&
EVP_DigestUpdate(ctx, boundary.producer_sha256, 32) == 1;
unsigned int length = 0;
ok = ok && EVP_DigestFinal_ex(ctx, output, &length) == 1 && length == 32;
EVP_MD_CTX_free(ctx);
return ok;
}
} // namespace
extern "C" Lardon3DCalibrationWorkflowResult
lardon3d_calibration_workflow_materialize_external_evidence(
const Lardon3DCalibrationWorkflowInputFiles *files,
Lardon3DCalibrationToolingView *views, size_t view_capacity,
Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks,
size_t coordinate_check_capacity,
Lardon3DCalibrationWorkflowExternalEvidence *output) {
if (!files || !views || !coordinate_checks || !output)
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
std::memset(output, 0, sizeof(*output));
Lardon3DCalibrationWorkflowInputBoundary boundary{};
Lardon3DCalibrationWorkflowResult result =
lardon3d_calibration_workflow_validate_input_boundary(files, &boundary);
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
std::string session_text, detection_text, solve_text, evidence_text;
const struct {
const char *path;
const unsigned char *sha;
std::string *text;
} inputs[] = {
{files->session_path, boundary.session_sha256, &session_text},
{files->detection_path, boundary.detection_sha256, &detection_text},
{files->solve_path, boundary.solve_sha256, &solve_text},
{files->evidence_path, boundary.evidence_sha256, &evidence_text},
};
for (const auto& input : inputs) {
result = read_verified(input.path, input.sha, input.text);
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
}
SessionData session;
if (!parse_session_materialization(session_text, &session))
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
if (std::memcmp(session.optical_sha, boundary.optical_state_sha256, 32) != 0 ||
session.optical_token != boundary.optical_state_token)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
std::vector<DetectionView> parsed_views;
if (!parse_detection(detection_text, session, &parsed_views))
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
size_t accepted = 0;
uint64_t support_observations = 0;
uint32_t oriented_width = 0;
uint32_t oriented_height = 0;
for (const DetectionView& view : parsed_views) {
if (view.tooling.accepted) {
if (accepted == 0) {
oriented_width = view.width;
oriented_height = view.height;
} else if (view.width != oriented_width ||
view.height != oriented_height) {
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
}
++accepted;
support_observations += view.tooling.residual_count;
if (support_observations > UINT32_MAX)
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
}
}
if (accepted == 0 || oriented_width == 0 || oriented_height == 0)
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
SolveData solve;
if (!parse_solve(solve_text, accepted, &solve))
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
EvidenceData evidence;
if (!parse_evidence(evidence_text, session, parsed_views, &evidence))
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
size_t check_count = 0;
for (const SessionView& view : session.views) {
if (SIZE_MAX - check_count < view.checks.size())
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
check_count += view.checks.size();
}
if (parsed_views.size() > view_capacity ||
check_count > coordinate_check_capacity)
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
for (size_t i = 0; i < parsed_views.size(); ++i)
views[i] = parsed_views[i].tooling;
size_t at = 0;
for (const SessionView& view : session.views)
for (const auto& check : view.checks) coordinate_checks[at++] = check;
output->boundary = boundary;
std::memcpy(output->target_sha256, session.generator_sha, 32);
std::memcpy(output->optical_state_sha256, boundary.optical_state_sha256, 32);
std::memcpy(output->solver_executable_sha256,
boundary.solver_executable_sha256, 32);
std::memcpy(output->solver_configuration_sha256,
boundary.solver_configuration_sha256, 32);
std::memcpy(output->initialization_evidence_sha256,
boundary.session_sha256, 32);
if (!validation_binding(boundary, output->validation_evidence_sha256))
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
output->target_family =
LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100;
output->target_squares_x = 9;
output->target_squares_y = 7;
output->target_square_length_mm = 30.0;
output->target_marker_length_mm = 21.0;
output->target_active_width_mm = 270.0;
output->target_active_height_mm = 210.0;
output->target_white_border_mm = session.white_border;
for (size_t i = 0; i < 10; ++i)
output->target_measurements_mm[i] = session.measurements[i];
output->measurement_resolution_mm = session.measurement_resolution;
output->target_flatness_mm = NAN;
output->holdout_rmse_px = evidence.holdout_rmse;
output->holdout_maximum_residual_px = evidence.holdout_max;
output->extra_distortion_coefficient_count = 0;
output->oriented_width = oriented_width;
output->oriented_height = oriented_height;
output->views = views;
output->view_count = parsed_views.size();
output->coordinate_checks = coordinate_checks;
output->coordinate_check_count = check_count;
std::memcpy(output->repeated_parameters, solve.repeated,
sizeof(output->repeated_parameters));
std::memcpy(output->fit_parameters, solve.fit,
sizeof(output->fit_parameters));
output->support_images = static_cast<uint32_t>(accepted);
output->support_observations = static_cast<uint32_t>(support_observations);
output->reprojection_rmse_px = evidence.global_rmse;
output->maximum_residual_px = evidence.global_max;
output->high_residual_fraction = evidence.high_fraction;
output->maximum_parameter_delta = evidence.maximum_delta;
output->validation_flags = evidence.flags;
return LARDON3D_CALIBRATION_WORKFLOW_OK;
}

View file

@ -0,0 +1,301 @@
#include <lardon3d/calibration_workflow_v2.h>
#include <lardon3d/optical_profiles.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static bool complete_state(const Lardon3DOpticalCaptureGeometricState *state) {
return state->focus_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
state->aperture_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
state->stabilization != LARDON3D_OPTICAL_STABILIZATION_UNKNOWN &&
state->crop_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
state->pipeline_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
state->representation_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
state->decoded_geometry_state ==
LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
state->decoded_width != 0 && state->decoded_height != 0;
}
static bool same_state(const Lardon3DOpticalCaptureGeometricState *left,
const Lardon3DOpticalCaptureGeometricState *right) {
return left->optical_configuration_id == right->optical_configuration_id &&
left->state_version == right->state_version &&
left->provenance == right->provenance &&
left->focus_state == right->focus_state &&
strcmp(left->focus_observation, right->focus_observation) == 0 &&
left->aperture_state == right->aperture_state &&
left->aperture_x1000 == right->aperture_x1000 &&
left->stabilization == right->stabilization &&
left->crop_state == right->crop_state &&
strcmp(left->crop_observation, right->crop_observation) == 0 &&
left->pipeline_state == right->pipeline_state &&
strcmp(left->pipeline_observation, right->pipeline_observation) == 0 &&
left->representation_state == right->representation_state &&
strcmp(left->representation_observation,
right->representation_observation) == 0 &&
left->decoded_geometry_state == right->decoded_geometry_state &&
left->decoded_width == right->decoded_width &&
left->decoded_height == right->decoded_height;
}
static Lardon3DCalibrationWorkflowV2Result
bootstrap_result(Lardon3DCalibrationBootstrapV2Result result) {
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT ||
result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_PROVENANCE_MISMATCH)
return LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_EVIDENCE;
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT)
return LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT;
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY)
return LARDON3D_CALIBRATION_WORKFLOW_V2_OUT_OF_MEMORY;
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_INVALID_ARGUMENT)
return LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_ARGUMENT;
return LARDON3D_CALIBRATION_WORKFLOW_V2_DB_ERROR;
}
static Lardon3DCalibrationWorkflowV2Result
db_error(Lardon3DProjectDbResult result) {
return result == LARDON3D_PROJECT_DB_CONSTRAINT ||
result == LARDON3D_PROJECT_DB_NOT_FOUND
? LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT
: LARDON3D_CALIBRATION_WORKFLOW_V2_DB_ERROR;
}
static void hex_digest(const unsigned char digest[32], char output[65]) {
static const char digits[] = "0123456789abcdef";
for (size_t index = 0; index < 32; ++index) {
output[2 * index] = digits[digest[index] >> 4];
output[2 * index + 1] = digits[digest[index] & 15];
}
output[64] = '\0';
}
static Lardon3DCalibrationWorkflowV2Result
publish_applicability(Lardon3DProjectDb *database,
const Lardon3DCalibrationWorkflowV2Binding *binding,
uint64_t calibration_id, uint64_t configuration_id,
uint64_t *applicability_id) {
Lardon3DSparseCalibration calibration;
Lardon3DProjectDbResult status =
lardon3d_sparse_calibration_load(database, calibration_id, &calibration);
if (status != LARDON3D_PROJECT_DB_OK)
return db_error(status);
char scientific_hash[65];
char provenance_hash[65];
hex_digest(calibration.scientific_hash, scientific_hash);
hex_digest(calibration.provenance_fingerprint, provenance_hash);
Lardon3DOpticalCalibrationProfile requested = {0};
requested.optical_configuration_id = configuration_id;
requested.sparse_calibration_id = calibration_id;
requested.profile_version =
LARDON3D_CALIBRATION_BOOTSTRAP_V2_ARTIFACT_VERSION;
requested.applicability = LARDON3D_OPTICAL_CALIBRATION_EXACT_CONFIGURATION;
requested.created_at = 0;
(void)snprintf(requested.name, sizeof(requested.name), "L3DCALB2-%s",
scientific_hash);
(void)snprintf(requested.provenance, sizeof(requested.provenance),
"L3DCALB2 group-sha256=%s", provenance_hash);
Lardon3DOpticalCalibrationProfile profile;
status = lardon3d_optical_calibration_profile_create(database, &requested,
&profile);
if (status != LARDON3D_PROJECT_DB_OK)
return db_error(status);
Lardon3DOpticalCalibrationApplicabilityV2 applicability;
status = lardon3d_optical_calibration_applicability_v2_create(
database, profile.calibration_profile_id, binding->exemplar_capture_id,
&applicability);
if (status != LARDON3D_PROJECT_DB_OK)
return db_error(status);
*applicability_id = applicability.applicability_id;
return LARDON3D_CALIBRATION_WORKFLOW_V2_READY;
}
Lardon3DCalibrationWorkflowV2Result lardon3d_calibration_workflow_v2_complete(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[32],
const Lardon3DCalibrationWorkflowV2Binding *bindings, size_t binding_count,
Lardon3DCalibrationWorkflowV2Output *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || execution_id == 0 || !artifact ||
!expected_artifact_sha256 || !bindings || !output || binding_count == 0 ||
binding_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES)
return LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_ARGUMENT;
Lardon3DProjectDbSelectedExecution execution;
Lardon3DProjectDbResult status = lardon3d_project_db_load_selected_execution(
database, execution_id, &execution);
if (status != LARDON3D_PROJECT_DB_OK)
return db_error(status);
if (execution.item_count != binding_count ||
(execution.stage != LARDON3D_SELECTED_EXECUTION_CALIBRATION &&
execution.stage != LARDON3D_SELECTED_EXECUTION_READY))
return LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT;
const Lardon3DCalibrationWorkflowV2Binding **ordered =
calloc(binding_count, sizeof(*ordered));
Lardon3DOpticalCaptureGeometricState *states =
calloc(binding_count, sizeof(*states));
Lardon3DCalibrationBootstrapV2Member *members =
calloc(binding_count, sizeof(*members));
if (!ordered || !states || !members) {
free(ordered);
free(states);
free(members);
return LARDON3D_CALIBRATION_WORKFLOW_V2_OUT_OF_MEMORY;
}
Lardon3DCalibrationWorkflowV2Result result =
LARDON3D_CALIBRATION_WORKFLOW_V2_READY;
for (size_t index = 0; index < binding_count; ++index) {
const Lardon3DCalibrationWorkflowV2Binding *binding = &bindings[index];
if (binding->selected_item_index >= binding_count ||
ordered[binding->selected_item_index] || binding->capture_id == 0 ||
(binding->kind == LARDON3D_CALIBRATION_WORKFLOW_V2_AUTOMATIC &&
(binding->applicability_id != 0 ||
binding->exemplar_capture_id != 0)) ||
(binding->kind == LARDON3D_CALIBRATION_WORKFLOW_V2_EXISTING_EXPLICIT &&
(binding->applicability_id == 0 ||
binding->exemplar_capture_id != 0)) ||
(binding->kind == LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT &&
(binding->applicability_id != 0 ||
binding->exemplar_capture_id == 0)) ||
binding->kind < LARDON3D_CALIBRATION_WORKFLOW_V2_AUTOMATIC ||
binding->kind > LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT) {
result = LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_ARGUMENT;
break;
}
ordered[binding->selected_item_index] = binding;
}
/* Preflight all scientific Capture bindings before immutable publication.
* A missing/incomplete tuple is a truthful semantic state, while a caller
* mapping that disagrees with selected_execution_items is an error. */
for (size_t index = 0; index < binding_count &&
result == LARDON3D_CALIBRATION_WORKFLOW_V2_READY;
++index) {
const Lardon3DCalibrationWorkflowV2Binding *binding = ordered[index];
Lardon3DProjectDbSelectedExecutionItem selected;
status = lardon3d_project_db_load_selected_execution_item(
database, execution_id, (uint32_t)index, &selected);
if (status != LARDON3D_PROJECT_DB_OK || !selected.has_image ||
selected.capture_id != binding->capture_id) {
result = LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT;
break;
}
status = lardon3d_optical_capture_geometric_state_load(
database, binding->capture_id, &states[index]);
if (status == LARDON3D_PROJECT_DB_NOT_FOUND ||
(status == LARDON3D_PROJECT_DB_OK && !complete_state(&states[index]))) {
result = LARDON3D_CALIBRATION_WORKFLOW_V2_CALIBRATION_REQUIRED;
break;
}
if (status != LARDON3D_PROJECT_DB_OK) {
result = db_error(status);
break;
}
if (binding->kind == LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT) {
Lardon3DOpticalCaptureGeometricState exemplar;
status = lardon3d_optical_capture_geometric_state_load(
database, binding->exemplar_capture_id, &exemplar);
if (status == LARDON3D_PROJECT_DB_NOT_FOUND ||
(status == LARDON3D_PROJECT_DB_OK && !complete_state(&exemplar))) {
result = LARDON3D_CALIBRATION_WORKFLOW_V2_CALIBRATION_REQUIRED;
break;
}
if (status != LARDON3D_PROJECT_DB_OK) {
result = db_error(status);
break;
}
if (!same_state(&states[index], &exemplar)) {
result = LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT;
break;
}
}
if (binding->kind == LARDON3D_CALIBRATION_WORKFLOW_V2_AUTOMATIC) {
Lardon3DOpticalCalibrationResolutionV2 resolution;
status = lardon3d_optical_capture_calibration_resolve_v2(
database, binding->capture_id, &resolution);
if (status != LARDON3D_PROJECT_DB_OK) {
result = db_error(status);
break;
}
if (resolution.kind == LARDON3D_OPTICAL_CALIBRATION_REQUIRED)
result = LARDON3D_CALIBRATION_WORKFLOW_V2_CALIBRATION_REQUIRED;
else if (resolution.kind ==
LARDON3D_OPTICAL_CALIBRATION_SELECTION_REQUIRED)
result = LARDON3D_CALIBRATION_WORKFLOW_V2_SELECTION_REQUIRED;
}
}
Lardon3DCalibrationBootstrapV2Output publication;
if (result == LARDON3D_CALIBRATION_WORKFLOW_V2_READY) {
Lardon3DCalibrationBootstrapV2Result published =
lardon3d_calibration_bootstrap_v2_publish_unattached(
database, execution_id, artifact, artifact_size,
expected_artifact_sha256, members, binding_count, &publication);
if (published != LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK)
result = bootstrap_result(published);
}
for (size_t index = 0; index < binding_count &&
result == LARDON3D_CALIBRATION_WORKFLOW_V2_READY;
++index) {
const Lardon3DCalibrationWorkflowV2Binding *binding = ordered[index];
uint64_t applicability_id = binding->applicability_id;
if (binding->kind == LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT)
result = publish_applicability(
database, binding, members[index].calibration_id,
states[index].optical_configuration_id, &applicability_id);
if (result != LARDON3D_CALIBRATION_WORKFLOW_V2_READY)
break;
if (binding->kind != LARDON3D_CALIBRATION_WORKFLOW_V2_AUTOMATIC) {
status = lardon3d_optical_capture_calibration_select_v2(
database, binding->capture_id, applicability_id);
if (status != LARDON3D_PROJECT_DB_OK) {
result = db_error(status);
break;
}
}
Lardon3DOpticalCalibrationResolutionV2 resolution;
status = lardon3d_optical_capture_calibration_resolve_v2(
database, binding->capture_id, &resolution);
if (status != LARDON3D_PROJECT_DB_OK) {
result = db_error(status);
break;
}
if (resolution.kind == LARDON3D_OPTICAL_CALIBRATION_REQUIRED)
result = LARDON3D_CALIBRATION_WORKFLOW_V2_CALIBRATION_REQUIRED;
else if (resolution.kind == LARDON3D_OPTICAL_CALIBRATION_SELECTION_REQUIRED)
result = LARDON3D_CALIBRATION_WORKFLOW_V2_SELECTION_REQUIRED;
else if (resolution.sparse_calibration_id != members[index].calibration_id)
result = LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT;
}
if (result == LARDON3D_CALIBRATION_WORKFLOW_V2_READY) {
status = lardon3d_project_db_assign_selected_calibration_scope(
database, execution_id, publication.scope.scope_id);
if (status != LARDON3D_PROJECT_DB_OK)
result = db_error(status);
}
if (result == LARDON3D_CALIBRATION_WORKFLOW_V2_READY) {
status = lardon3d_project_db_load_selected_execution(database, execution_id,
&execution);
if (status != LARDON3D_PROJECT_DB_OK ||
execution.stage != LARDON3D_SELECTED_EXECUTION_READY ||
!execution.has_calibration_scope ||
execution.calibration_scope_id != publication.scope.scope_id)
result = LARDON3D_CALIBRATION_WORKFLOW_V2_DB_ERROR;
}
if (result == LARDON3D_CALIBRATION_WORKFLOW_V2_READY) {
output->publication = publication;
output->selected_item_count = (uint32_t)binding_count;
}
free(ordered);
free(states);
free(members);
return result;
}

View file

@ -162,22 +162,22 @@ validate_file(
{ {
char *path = join_path(originals_path, filename); char *path = join_path(originals_path, filename);
if (!path) { if (!path) {
set_error(error_message, error_message_size, "Erreur : chemin d'image trop long."); set_error(error_message, error_message_size, "Error: image path is too long.");
return false; return false;
} }
struct stat info; struct stat info;
if (lstat(path, &info) != 0) { if (lstat(path, &info) != 0) {
free(path); free(path);
set_error(error_message, error_message_size, "Erreur : image du manifeste absente."); set_error(error_message, error_message_size, "Error: manifest image is missing.");
return false; return false;
} }
free(path); free(path);
if (S_ISLNK(info.st_mode) || !S_ISREG(info.st_mode)) { if (S_ISLNK(info.st_mode) || !S_ISREG(info.st_mode)) {
set_error(error_message, error_message_size, "Erreur : image du manifeste non régulière."); set_error(error_message, error_message_size, "Error: manifest image is not a regular file.");
return false; return false;
} }
if (info.st_size < 0 || (uintmax_t)info.st_size != (uintmax_t)expected_size) { if (info.st_size < 0 || (uintmax_t)info.st_size != (uintmax_t)expected_size) {
set_error(error_message, error_message_size, "Erreur : taille d'image incohérente."); set_error(error_message, error_message_size, "Error: inconsistent image size.");
return false; return false;
} }
return true; return true;
@ -195,7 +195,7 @@ parse_line(
char *first_tab = strchr(line, '\t'); char *first_tab = strchr(line, '\t');
char *second_tab = first_tab ? strchr(first_tab + 1, '\t') : NULL; char *second_tab = first_tab ? strchr(first_tab + 1, '\t') : NULL;
if (!first_tab || !second_tab || strchr(second_tab + 1, '\t')) { if (!first_tab || !second_tab || strchr(second_tab + 1, '\t')) {
set_error(error_message, error_message_size, "Erreur : ligne de manifeste invalide."); set_error(error_message, error_message_size, "Error: invalid manifest line.");
return false; return false;
} }
*first_tab = '\0'; *first_tab = '\0';
@ -206,23 +206,23 @@ parse_line(
uint64_t size_bytes; uint64_t size_bytes;
if (!valid_filename(filename)) { if (!valid_filename(filename)) {
set_error(error_message, error_message_size, "Erreur : nom d'image invalide dans le manifeste."); set_error(error_message, error_message_size, "Error: invalid image name in manifest.");
return false; return false;
} }
if (!source_path[0]) { if (!source_path[0]) {
set_error(error_message, error_message_size, "Erreur : chemin source vide dans le manifeste."); set_error(error_message, error_message_size, "Error: empty source path in manifest.");
return false; return false;
} }
if (!parse_size(size_text, &size_bytes)) { if (!parse_size(size_text, &size_bytes)) {
set_error(error_message, error_message_size, "Erreur : taille invalide dans le manifeste."); set_error(error_message, error_message_size, "Error: invalid size in manifest.");
return false; return false;
} }
if (filename_exists(catalog, filename)) { if (filename_exists(catalog, filename)) {
set_error(error_message, error_message_size, "Erreur : image dupliquée dans le manifeste."); set_error(error_message, error_message_size, "Error: duplicate image in manifest.");
return false; return false;
} }
if (catalog->total_size > UINT64_MAX - size_bytes) { if (catalog->total_size > UINT64_MAX - size_bytes) {
set_error(error_message, error_message_size, "Erreur : taille totale du catalogue trop grande."); set_error(error_message, error_message_size, "Error: total catalog size is too large.");
return false; return false;
} }
if (!validate_file( if (!validate_file(
@ -235,7 +235,7 @@ parse_line(
return false; return false;
} }
if (!append_entry(catalog, filename, source_path, size_bytes)) { if (!append_entry(catalog, filename, source_path, size_bytes)) {
set_error(error_message, error_message_size, "Erreur : mémoire insuffisante pour le catalogue."); set_error(error_message, error_message_size, "Error: insufficient memory for the catalog.");
return false; return false;
} }
return true; return true;
@ -250,45 +250,45 @@ lardon3d_image_catalog_load(
{ {
set_error(error_message, error_message_size, ""); set_error(error_message, error_message_size, "");
if (!state || !state->project_loaded || !state->project_path[0]) { if (!state || !state->project_loaded || !state->project_path[0]) {
set_error(error_message, error_message_size, "Aucun projet chargé."); set_error(error_message, error_message_size, "No project loaded.");
return NULL; return NULL;
} }
Lardon3DImageCatalog *catalog = calloc(1, sizeof(*catalog)); Lardon3DImageCatalog *catalog = calloc(1, sizeof(*catalog));
if (!catalog) { if (!catalog) {
set_error(error_message, error_message_size, "Erreur : mémoire insuffisante pour le catalogue."); set_error(error_message, error_message_size, "Error: insufficient memory for the catalog.");
return NULL; return NULL;
} }
char *images_path = join_path(state->project_path, "images"); char *images_path = join_path(state->project_path, "images");
char *originals_path = images_path ? join_path(images_path, "originals") : NULL; char *originals_path = images_path ? join_path(images_path, "originals") : NULL;
char *manifest_path = images_path ? join_path(images_path, "manifest.tsv") : NULL; char *manifest_path = images_path ? join_path(images_path, "manifest.tsv") : NULL;
if (!images_path || !originals_path || !manifest_path) { if (!images_path || !originals_path || !manifest_path) {
set_error(error_message, error_message_size, "Erreur : chemin du catalogue trop long."); set_error(error_message, error_message_size, "Error: catalog path is too long.");
goto failure; goto failure;
} }
int descriptor = open(manifest_path, O_RDONLY | O_NOFOLLOW); int descriptor = open(manifest_path, O_RDONLY | O_NOFOLLOW);
if (descriptor < 0) { if (descriptor < 0) {
if (errno == ENOENT) { if (errno == ENOENT) {
set_error(error_message, error_message_size, "Aucune image importée."); set_error(error_message, error_message_size, "No imported image.");
free(images_path); free(images_path);
free(originals_path); free(originals_path);
free(manifest_path); free(manifest_path);
return catalog; return catalog;
} }
set_error(error_message, error_message_size, "Erreur : impossible d'ouvrir manifest.tsv."); set_error(error_message, error_message_size, "Error: unable to open manifest.tsv.");
goto failure; goto failure;
} }
struct stat manifest_info; struct stat manifest_info;
if (fstat(descriptor, &manifest_info) != 0 || !S_ISREG(manifest_info.st_mode)) { if (fstat(descriptor, &manifest_info) != 0 || !S_ISREG(manifest_info.st_mode)) {
(void)close(descriptor); (void)close(descriptor);
set_error(error_message, error_message_size, "Erreur : manifest.tsv n'est pas régulier."); set_error(error_message, error_message_size, "Error: manifest.tsv is not a regular file.");
goto failure; goto failure;
} }
FILE *file = fdopen(descriptor, "r"); FILE *file = fdopen(descriptor, "r");
if (!file) { if (!file) {
(void)close(descriptor); (void)close(descriptor);
set_error(error_message, error_message_size, "Erreur : impossible de lire manifest.tsv."); set_error(error_message, error_message_size, "Error: unable to read manifest.tsv.");
goto failure; goto failure;
} }
@ -298,11 +298,11 @@ lardon3d_image_catalog_load(
bool valid = length >= 0 bool valid = length >= 0
&& strcmp(line, "filename\tsize_bytes\tsource_path\n") == 0; && strcmp(line, "filename\tsize_bytes\tsource_path\n") == 0;
if (!valid) { if (!valid) {
set_error(error_message, error_message_size, "Erreur : en-tête de manifest.tsv invalide."); set_error(error_message, error_message_size, "Error: invalid manifest.tsv header.");
} }
while (valid && (length = getline(&line, &capacity, file)) >= 0) { while (valid && (length = getline(&line, &capacity, file)) >= 0) {
if (length == 0 || line[(size_t)length - 1] != '\n') { if (length == 0 || line[(size_t)length - 1] != '\n') {
set_error(error_message, error_message_size, "Erreur : ligne tronquée dans manifest.tsv."); set_error(error_message, error_message_size, "Error: truncated line in manifest.tsv.");
valid = false; valid = false;
break; break;
} }
@ -316,13 +316,13 @@ lardon3d_image_catalog_load(
error_message_size error_message_size
)) { )) {
if (!error_message || !error_message_size || !error_message[0]) { if (!error_message || !error_message_size || !error_message[0]) {
set_error(error_message, error_message_size, "Erreur : ligne de manifeste invalide."); set_error(error_message, error_message_size, "Error: invalid manifest line.");
} }
valid = false; valid = false;
} }
} }
if (ferror(file) || fclose(file) != 0) { if (ferror(file) || fclose(file) != 0) {
set_error(error_message, error_message_size, "Erreur : lecture de manifest.tsv impossible."); set_error(error_message, error_message_size, "Error: unable to read manifest.tsv.");
valid = false; valid = false;
} }
free(line); free(line);

View file

@ -279,7 +279,7 @@ lardon3d_image_view_set_filter(
(void)snprintf( (void)snprintf(
error_message, error_message,
error_message_size, error_message_size,
"Erreur : filtre invalide." "Error: invalid filter."
); );
} }
return false; return false;
@ -290,7 +290,7 @@ lardon3d_image_view_set_filter(
(void)snprintf( (void)snprintf(
error_message, error_message,
error_message_size, error_message_size,
"Erreur : filtre trop long." "Error: filter is too long."
); );
} }
return false; return false;
@ -300,7 +300,7 @@ lardon3d_image_view_set_filter(
(void)snprintf( (void)snprintf(
error_message, error_message,
error_message_size, error_message_size,
"Erreur : mémoire insuffisante pour la vue." "Error: insufficient memory for the view."
); );
} }
return false; return false;
@ -368,16 +368,16 @@ lardon3d_image_view_sort_name(Lardon3DImageSort sort)
{ {
switch (sort) { switch (sort) {
case LARDON3D_IMAGE_SORT_NAME_ASC: case LARDON3D_IMAGE_SORT_NAME_ASC:
return "Nom croissant"; return "Name ascending";
case LARDON3D_IMAGE_SORT_NAME_DESC: case LARDON3D_IMAGE_SORT_NAME_DESC:
return "Nom décroissant"; return "Name descending";
case LARDON3D_IMAGE_SORT_SIZE_ASC: case LARDON3D_IMAGE_SORT_SIZE_ASC:
return "Taille croissante"; return "Size ascending";
case LARDON3D_IMAGE_SORT_SIZE_DESC: case LARDON3D_IMAGE_SORT_SIZE_DESC:
return "Taille décroissante"; return "Size descending";
case LARDON3D_IMAGE_SORT_IMPORT_ORDER: case LARDON3D_IMAGE_SORT_IMPORT_ORDER:
default: default:
return "Ordre d'import"; return "Import order";
} }
} }

View file

@ -106,7 +106,7 @@ trim_source_path(
) )
{ {
if (!input) { if (!input) {
set_status(state, "Erreur : dossier source vide."); set_status(state, "Error: source directory is empty.");
return false; return false;
} }
@ -121,11 +121,11 @@ trim_source_path(
size_t length = (size_t)(end - start); size_t length = (size_t)(end - start);
if (length == 0) { if (length == 0) {
set_status(state, "Erreur : dossier source vide."); set_status(state, "Error: source directory is empty.");
return false; return false;
} }
if (length >= PATH_MAX) { if (length >= PATH_MAX) {
set_status(state, "Erreur : chemin source trop long."); set_status(state, "Error: source path is too long.");
return false; return false;
} }
(void)memcpy(output, start, length); (void)memcpy(output, start, length);
@ -153,7 +153,7 @@ resolve_source_path(
} }
} }
set_status(state, "Erreur : chemin source trop long ou inaccessible."); set_status(state, "Error: source path is too long or inaccessible.");
return false; return false;
} }
@ -192,7 +192,7 @@ candidate_list_append(
: candidates->capacity * 2; : candidates->capacity * 2;
if (capacity < candidates->capacity if (capacity < candidates->capacity
|| capacity > SIZE_MAX / sizeof(*candidates->items)) { || capacity > SIZE_MAX / sizeof(*candidates->items)) {
set_status(state, "Erreur : trop de fichiers à importer."); set_status(state, "Error: too many files to import.");
return false; return false;
} }
void *items = realloc( void *items = realloc(
@ -200,7 +200,7 @@ candidate_list_append(
capacity * sizeof(*candidates->items) capacity * sizeof(*candidates->items)
); );
if (!items) { if (!items) {
set_status(state, "Erreur : mémoire insuffisante pour l'import."); set_status(state, "Error: insufficient memory for import.");
return false; return false;
} }
candidates->items = items; candidates->items = items;
@ -215,7 +215,7 @@ candidate_list_append(
filename filename
); );
if (written < 0 || (size_t)written >= sizeof(candidate->filename)) { if (written < 0 || (size_t)written >= sizeof(candidate->filename)) {
set_status(state, "Erreur : nom de fichier trop long."); set_status(state, "Error: filename is too long.");
return false; return false;
} }
candidate->created = false; candidate->created = false;
@ -232,24 +232,24 @@ ensure_originals_directory(
{ {
if (!join_path(images_path, state->project_path, "images") if (!join_path(images_path, state->project_path, "images")
|| !join_path(originals_path, images_path, "originals")) { || !join_path(originals_path, images_path, "originals")) {
set_status(state, "Erreur : chemin du projet trop long."); set_status(state, "Error: project path is too long.");
return false; return false;
} }
struct stat info; struct stat info;
if (lstat(images_path, &info) != 0 || !S_ISDIR(info.st_mode)) { if (lstat(images_path, &info) != 0 || !S_ISDIR(info.st_mode)) {
set_status(state, "Erreur : dossier images absent ou invalide."); set_status(state, "Error: images directory is missing or invalid.");
return false; return false;
} }
if (lstat(originals_path, &info) == 0) { if (lstat(originals_path, &info) == 0) {
if (!S_ISDIR(info.st_mode)) { if (!S_ISDIR(info.st_mode)) {
set_status(state, "Erreur : images/originals n'est pas un dossier."); set_status(state, "Error: images/originals is not a directory.");
return false; return false;
} }
return true; return true;
} }
if (errno != ENOENT || mkdir(originals_path, 0755) != 0) { if (errno != ENOENT || mkdir(originals_path, 0755) != 0) {
set_status(state, "Erreur : impossible de créer images/originals."); set_status(state, "Error: unable to create images/originals.");
return false; return false;
} }
return true; return true;
@ -303,7 +303,7 @@ manifest_begin(
images_path, images_path,
".manifest.tsv.tmp.XXXXXX" ".manifest.tsv.tmp.XXXXXX"
)) { )) {
set_status(state, "Erreur : chemin du manifeste trop long."); set_status(state, "Error: manifest path is too long.");
return false; return false;
} }
@ -316,18 +316,18 @@ manifest_begin(
struct stat info; struct stat info;
if (fstat(previous_descriptor, &info) != 0 || !S_ISREG(info.st_mode)) { if (fstat(previous_descriptor, &info) != 0 || !S_ISREG(info.st_mode)) {
(void)close(previous_descriptor); (void)close(previous_descriptor);
set_status(state, "Erreur : manifest.tsv invalide."); set_status(state, "Error: invalid manifest.tsv.");
return false; return false;
} }
previous = fdopen(previous_descriptor, "r"); previous = fdopen(previous_descriptor, "r");
if (!previous) { if (!previous) {
(void)close(previous_descriptor); (void)close(previous_descriptor);
set_status(state, "Erreur : impossible de lire manifest.tsv."); set_status(state, "Error: unable to read manifest.tsv.");
return false; return false;
} }
writer->previous_exists = true; writer->previous_exists = true;
} else if (errno != ENOENT) { } else if (errno != ENOENT) {
set_status(state, "Erreur : impossible de lire manifest.tsv."); set_status(state, "Error: unable to read manifest.tsv.");
return false; return false;
} }
@ -336,7 +336,7 @@ manifest_begin(
if (previous) { if (previous) {
(void)fclose(previous); (void)fclose(previous);
} }
set_status(state, "Erreur : impossible de préparer manifest.tsv."); set_status(state, "Error: unable to prepare manifest.tsv.");
return false; return false;
} }
writer->file = fdopen(descriptor, "w"); writer->file = fdopen(descriptor, "w");
@ -346,7 +346,7 @@ manifest_begin(
(void)fclose(previous); (void)fclose(previous);
} }
manifest_abort(writer); manifest_abort(writer);
set_status(state, "Erreur : impossible d'écrire manifest.tsv."); set_status(state, "Error: unable to write manifest.tsv.");
return false; return false;
} }
@ -373,7 +373,7 @@ manifest_begin(
if (!success) { if (!success) {
manifest_abort(writer); manifest_abort(writer);
set_status(state, "Erreur : manifest.tsv invalide ou illisible."); set_status(state, "Error: manifest.tsv is invalid or unreadable.");
} }
return success; return success;
} }
@ -446,7 +446,7 @@ manifest_commit(Lardon3DAppState *state, ManifestWriter *writer)
} }
if (!success) { if (!success) {
(void)unlink(writer->temporary_path); (void)unlink(writer->temporary_path);
set_status(state, "Erreur : impossible de mettre à jour manifest.tsv."); set_status(state, "Error: unable to update manifest.tsv.");
} }
return success; return success;
} }
@ -602,7 +602,7 @@ analyze_source_directory(
{ {
DIR *directory = opendir(absolute_source); DIR *directory = opendir(absolute_source);
if (!directory) { if (!directory) {
set_status(state, "Erreur : impossible d'ouvrir le dossier source."); set_status(state, "Error: unable to open source directory.");
return false; return false;
} }
@ -616,7 +616,7 @@ analyze_source_directory(
struct dirent *entry = readdir(directory); struct dirent *entry = readdir(directory);
if (!entry) { if (!entry) {
if (errno != 0) { if (errno != 0) {
set_status(state, "Erreur : lecture du dossier source impossible."); set_status(state, "Error: unable to read source directory.");
success = false; success = false;
} }
break; break;
@ -628,7 +628,7 @@ analyze_source_directory(
char source_path[PATH_MAX]; char source_path[PATH_MAX];
if (!join_path(source_path, absolute_source, entry->d_name)) { if (!join_path(source_path, absolute_source, entry->d_name)) {
set_status(state, "Erreur : chemin source trop long."); set_status(state, "Error: source path is too long.");
success = false; success = false;
break; break;
} }
@ -645,7 +645,7 @@ analyze_source_directory(
if (has_forbidden_manifest_character(entry->d_name)) { if (has_forbidden_manifest_character(entry->d_name)) {
set_status( set_status(
state, state,
"Erreur : nom de fichier incompatible avec le manifeste." "Error: filename is incompatible with the manifest."
); );
success = false; success = false;
break; break;
@ -653,7 +653,7 @@ analyze_source_directory(
char destination_path[PATH_MAX]; char destination_path[PATH_MAX];
if (!join_path(destination_path, originals_path, entry->d_name)) { if (!join_path(destination_path, originals_path, entry->d_name)) {
set_status(state, "Erreur : chemin destination trop long."); set_status(state, "Error: destination path is too long.");
success = false; success = false;
break; break;
} }
@ -664,7 +664,7 @@ analyze_source_directory(
} }
if (closedir(directory) != 0) { if (closedir(directory) != 0) {
set_status(state, "Erreur : fermeture du dossier source impossible."); set_status(state, "Error: unable to close source directory.");
success = false; success = false;
} }
return success; return success;
@ -711,27 +711,25 @@ fail_import(
(void)snprintf( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Import annulé : %zu copie%s retirée%s après erreur (%s).", "Import cancelled: %zu %s removed after error (%s).",
removed, removed,
removed == 1 ? "" : "s", removed == 1 ? "copy" : "copies",
removed == 1 ? "" : "s",
reason reason
); );
} else if (result->copied > 0) { } else if (result->copied > 0) {
(void)snprintf( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Erreur critique : %zu copie%s conservée%s sans manifeste (%s).", "Critical error: %zu %s retained without manifest (%s).",
result->copied, result->copied,
result->copied == 1 ? "" : "s", result->copied == 1 ? "copy" : "copies",
result->copied == 1 ? "" : "s",
reason reason
); );
} else { } else {
(void)snprintf( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Erreur d'import : %s.", "Import error: %s.",
reason reason
); );
} }
@ -754,7 +752,7 @@ cancel_import(
(void)snprintf( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Import annulé : %zu sur %zu fichiers traités.", "Import cancelled: %zu of %zu files processed.",
processed, processed,
result->admissible_found result->admissible_found
); );
@ -788,7 +786,7 @@ lardon3d_import_directory_batch(
struct stat source_info; struct stat source_info;
if (lstat(absolute_source, &source_info) != 0 if (lstat(absolute_source, &source_info) != 0
|| !S_ISDIR(source_info.st_mode)) { || !S_ISDIR(source_info.st_mode)) {
set_status(state, "Erreur : dossier source absent ou invalide."); set_status(state, "Error: source directory is missing or invalid.");
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
char images_path[PATH_MAX], originals_path[PATH_MAX]; char images_path[PATH_MAX], originals_path[PATH_MAX];
@ -804,7 +802,7 @@ lardon3d_import_directory_batch(
DIR *directory = opendir(absolute_source); DIR *directory = opendir(absolute_source);
if (!directory) { if (!directory) {
manifest_abort(&manifest); manifest_abort(&manifest);
set_status(state, "Erreur : impossible d'ouvrir le dossier source."); set_status(state, "Error: unable to open source directory.");
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
CandidateList created = {0}; CandidateList created = {0};
@ -888,8 +886,8 @@ lardon3d_import_directory_batch(
manifest_abort(&manifest); manifest_abort(&manifest);
(void)rollback_created_files(&created, originals_path); (void)rollback_created_files(&created, originals_path);
free(created.items); free(created.items);
set_status(state, cancelled ? "Import annulé à une frontière sûre." set_status(state, cancelled ? "Import cancelled at a safe boundary."
: "Erreur pendant un lot d'import."); : "Error during an import batch.");
return cancelled ? LARDON3D_IMPORT_CANCELLED : LARDON3D_IMPORT_FAILED; return cancelled ? LARDON3D_IMPORT_CANCELLED : LARDON3D_IMPORT_FAILED;
} }
if (!manifest_commit(state, &manifest)) { if (!manifest_commit(state, &manifest)) {
@ -899,7 +897,7 @@ lardon3d_import_directory_batch(
} }
free(created.items); free(created.items);
*complete = result->processed == result->admissible_found; *complete = result->processed == result->admissible_found;
set_status(state, *complete ? "Import terminé." : "Lot d'import publié."); set_status(state, *complete ? "Import completed." : "Import batch published.");
publish_progress(control, result, result->processed, state->status_message); publish_progress(control, result, result->processed, state->status_message);
return LARDON3D_IMPORT_SUCCEEDED; return LARDON3D_IMPORT_SUCCEEDED;
} }
@ -919,7 +917,7 @@ lardon3d_import_directory_controlled(
publish_progress(control, result, 0, "Analyse du dossier source..."); publish_progress(control, result, 0, "Analyse du dossier source...");
if (!state->project_loaded) { if (!state->project_loaded) {
set_status(state, "Aucun projet chargé."); set_status(state, "No project loaded.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
@ -935,19 +933,19 @@ lardon3d_import_directory_controlled(
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
if (has_forbidden_manifest_character(absolute_source)) { if (has_forbidden_manifest_character(absolute_source)) {
set_status(state, "Erreur : chemin source incompatible avec le manifeste."); set_status(state, "Error: source path is incompatible with the manifest.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
struct stat source_directory_info; struct stat source_directory_info;
if (lstat(absolute_source, &source_directory_info) != 0) { if (lstat(absolute_source, &source_directory_info) != 0) {
set_status(state, "Erreur : dossier source inexistant."); set_status(state, "Error: source directory does not exist.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
if (!S_ISDIR(source_directory_info.st_mode)) { if (!S_ISDIR(source_directory_info.st_mode)) {
set_status(state, "Erreur : la source n'est pas un dossier."); set_status(state, "Error: source is not a directory.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
@ -956,7 +954,7 @@ lardon3d_import_directory_controlled(
char originals_path[PATH_MAX]; char originals_path[PATH_MAX];
if (!join_path(images_path, state->project_path, "images") if (!join_path(images_path, state->project_path, "images")
|| !join_path(originals_path, images_path, "originals")) { || !join_path(originals_path, images_path, "originals")) {
set_status(state, "Erreur : chemin du projet trop long."); set_status(state, "Error: project path is too long.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
@ -978,7 +976,7 @@ lardon3d_import_directory_controlled(
} }
if (analysis_cancelled) { if (analysis_cancelled) {
free(candidates.items); free(candidates.items);
set_status(state, "Import annulé : 0 fichier traité."); set_status(state, "Import cancelled: 0 files processed.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_CANCELLED; return LARDON3D_IMPORT_CANCELLED;
} }
@ -986,7 +984,7 @@ lardon3d_import_directory_controlled(
if (import_is_cancelled(control)) { if (import_is_cancelled(control)) {
free(candidates.items); free(candidates.items);
set_status(state, "Import annulé : 0 fichier traité."); set_status(state, "Import cancelled: 0 files processed.");
publish_progress(control, result, 0, state->status_message); publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_CANCELLED; return LARDON3D_IMPORT_CANCELLED;
} }
@ -1079,7 +1077,7 @@ lardon3d_import_directory_controlled(
source_path source_path
)) { )) {
success = false; success = false;
failure_reason = "écriture du manifeste impossible"; failure_reason = "unable to write manifest";
break; break;
} }
++processed; ++processed;
@ -1129,7 +1127,7 @@ lardon3d_import_directory_controlled(
state, state,
result, result,
removed, removed,
"mise à jour du manifeste impossible" "unable to update manifest"
); );
publish_progress(control, result, processed, state->status_message); publish_progress(control, result, processed, state->status_message);
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
@ -1139,13 +1137,11 @@ lardon3d_import_directory_controlled(
(void)snprintf( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Import terminé : %zu copiée%s, %zu déjà présente%s.", "Import completed: %zu copied, %zu already present.",
result->copied, result->copied,
result->copied == 1 ? "" : "s", result->already_present
result->already_present,
result->already_present == 1 ? "" : "s"
); );
publish_progress(control, result, processed, "Import terminé."); publish_progress(control, result, processed, "Import completed.");
return LARDON3D_IMPORT_SUCCEEDED; return LARDON3D_IMPORT_SUCCEEDED;
} }
@ -1187,7 +1183,7 @@ lardon3d_import_directory_batch_to_scanset(
struct stat directory_info; struct stat directory_info;
if (lstat(source, &directory_info) != 0 || !S_ISDIR(directory_info.st_mode) if (lstat(source, &directory_info) != 0 || !S_ISDIR(directory_info.st_mode)
|| S_ISLNK(directory_info.st_mode)) { || S_ISLNK(directory_info.st_mode)) {
set_status(state, "Erreur : dossier source absent ou invalide."); set_status(state, "Error: source directory is missing or invalid.");
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
DIR *directory = opendir(source); DIR *directory = opendir(source);
@ -1254,15 +1250,15 @@ lardon3d_import_directory_batch_to_scanset(
legacy_manifest_active = false; legacy_manifest_active = false;
} }
if (cancelled) { if (cancelled) {
set_status(state, "Import annulé à une frontière sûre."); set_status(state, "Import cancelled at a safe boundary.");
return LARDON3D_IMPORT_CANCELLED; return LARDON3D_IMPORT_CANCELLED;
} }
if (!success) { if (!success) {
set_status(state, "Erreur pendant un lot d'import."); set_status(state, "Error during an import batch.");
return LARDON3D_IMPORT_FAILED; return LARDON3D_IMPORT_FAILED;
} }
*complete = !remaining; *complete = !remaining;
set_status(state, *complete ? "Import terminé." : "Lot d'import publié."); set_status(state, *complete ? "Import completed." : "Import batch published.");
publish_progress(control, result, result->processed, state->status_message); publish_progress(control, result, result->processed, state->status_message);
return LARDON3D_IMPORT_SUCCEEDED; return LARDON3D_IMPORT_SUCCEEDED;
} }

View file

@ -60,7 +60,7 @@ draw_text(int row, int column, int available, const char *text)
static void static void
draw_too_small(int rows, int columns) draw_too_small(int rows, int columns)
{ {
static const char message[] = "Terminal trop petit"; static const char message[] = "Terminal too small";
int row = rows > 0 ? rows / 2 : 0; int row = rows > 0 ? rows / 2 : 0;
int column = columns > (int)(sizeof(message) - 1) int column = columns > (int)(sizeof(message) - 1)
? (columns - (int)(sizeof(message) - 1)) / 2 ? (columns - (int)(sizeof(message) - 1)) / 2
@ -86,24 +86,24 @@ screen_title(Lardon3DScreen screen)
{ {
switch (screen) { switch (screen) {
case LARDON3D_SCREEN_PROJECTS: case LARDON3D_SCREEN_PROJECTS:
return "Projets"; return "Projects";
case LARDON3D_SCREEN_IMPORT: case LARDON3D_SCREEN_IMPORT:
return "Import"; return "Import";
case LARDON3D_SCREEN_VIEWER: case LARDON3D_SCREEN_VIEWER:
return "Viewer"; return "Viewer";
case LARDON3D_SCREEN_HELP: case LARDON3D_SCREEN_HELP:
return "Aide / contrats runtime"; return "Help / runtime contracts";
case LARDON3D_SCREEN_TASKS: case LARDON3D_SCREEN_TASKS:
return "Tâches"; return "Tasks";
case LARDON3D_SCREEN_RESOURCES: case LARDON3D_SCREEN_RESOURCES:
return "Ressources / Governor"; return "Resources / Governor";
case LARDON3D_SCREEN_OPTICS: case LARDON3D_SCREEN_OPTICS:
return "Profils optiques immuables"; return "Immutable optical profiles";
case LARDON3D_SCREEN_SSD: case LARDON3D_SCREEN_SSD:
return "SSD externe"; return "External SSD";
case LARDON3D_SCREEN_HOME: case LARDON3D_SCREEN_HOME:
default: default:
return "Observatoire Lardon3D"; return "Lardon3D Observatory";
} }
} }
@ -116,26 +116,26 @@ screen_footer(
Lardon3DTuiKeyContract keys = lardon3d_tui_key_contract( Lardon3DTuiKeyContract keys = lardon3d_tui_key_contract(
interaction_mode); interaction_mode);
if (keys.enter && keys.escape && keys.f10) { if (keys.enter && keys.escape && keys.f10) {
return "F10 SSD | Enter valider | ESC annuler"; return "F10 SSD | Enter confirm | ESC cancel";
} }
if (keys.cancel_import && keys.f10) { if (keys.cancel_import && keys.f10) {
return "F10 SSD | X annuler l'import | Q/ESC désactivés"; return "F10 SSD | X cancel import | Q/ESC disabled";
} }
switch (screen) { switch (screen) {
case LARDON3D_SCREEN_PROJECTS: case LARDON3D_SCREEN_PROJECTS:
return "F10 SSD | N Nouveau O Ouvrir C Fermer | ESC Accueil F7 Optique Q"; return "F10 SSD | N New O Open C Close | ESC Home F7 Optics Q";
case LARDON3D_SCREEN_IMPORT: case LARDON3D_SCREEN_IMPORT:
return "F10 SSD | I Importer R Recharger S Tri/Filtre X Effacer | ESC Q"; return "F10 SSD | I Import R Reload S Sort/Filter X Clear | ESC Q";
case LARDON3D_SCREEN_TASKS: case LARDON3D_SCREEN_TASKS:
return "F10 SSD | ↑/↓ P pause R reprise C annuler | ESC F6 Ressources Q"; return "F10 SSD | ↑/↓ P pause R resume C cancel | ESC F6 Resources Q";
case LARDON3D_SCREEN_RESOURCES: case LARDON3D_SCREEN_RESOURCES:
return "F10 SSD | Observation seule: CPU/GPU/batch par Governor | ESC Q"; return "F10 SSD | Observation only: CPU/GPU/batch by Governor | ESC Q";
case LARDON3D_SCREEN_OPTICS: case LARDON3D_SCREEN_OPTICS:
return "F10 SSD | TAB ↑/↓ [ première ] suivante B/L/C V/A/G/K/E R retry ESC Q"; return "F10 SSD | TAB ↑/↓ [ first ] next B/L/C V/A/G/K/E R retry ESC Q";
case LARDON3D_SCREEN_SSD: case LARDON3D_SCREEN_SSD:
return "F10 SSD | activer/drainer/annuler drain (asynchrone) | ESC Q"; return "F10 SSD | enable/drain/cancel drain (asynchronous) | ESC Q";
default: default:
return "F10 SSD | F1 Aide F2 Projets F3 Import F4 Viewer F5 Tâches F6 Ressources Q"; return "F10 SSD | F1 Help F2 Projects F3 Import F4 Viewer F5 Tasks F6 Resources Q";
} }
} }
@ -233,8 +233,8 @@ static void
draw_project_line(const Lardon3DAppState *state, int row, int columns) draw_project_line(const Lardon3DAppState *state, int row, int columns)
{ {
char line[512]; char line[512];
(void)snprintf(line, sizeof(line), "Projet: %.120s%s%.370s", (void)snprintf(line, sizeof(line), "Project: %.120s%s%.370s",
state->project_loaded ? state->project_name : "aucun", state->project_loaded ? state->project_name : "none",
state->project_loaded ? " " : "", state->project_loaded ? " " : "",
state->project_loaded ? state->project_path : ""); state->project_loaded ? state->project_path : "");
draw_text(row, 2, columns - 4, line); draw_text(row, 2, columns - 4, line);
@ -267,7 +267,7 @@ draw_home(
int active_row = start + 6; int active_row = start + 6;
if (!runtime->active_task_known) { if (!runtime->active_task_known) {
draw_text_style(active_row, 2, columns - 4, draw_text_style(active_row, 2, columns - 4,
"Tâche active: aucune", LARDON3D_TUI_SEMANTIC_DIM, palette); "Active task: none", LARDON3D_TUI_SEMANTIC_DIM, palette);
return; return;
} }
const Lardon3DTaskObservation *task = const Lardon3DTaskObservation *task =
@ -302,7 +302,7 @@ draw_home(
(unsigned long long)task->id, task->name); (unsigned long long)task->id, task->name);
} else { } else {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"scientifique indéterminé | #%llu %.18s", "scientific unknown | #%llu %.18s",
(unsigned long long)task->id, task->name); (unsigned long long)task->id, task->name);
} }
} else { } else {
@ -337,15 +337,15 @@ draw_home(
if (runtime->active_progress.throughput_known) { if (runtime->active_progress.throughput_known) {
(void)snprintf(throughput, sizeof(throughput), (void)snprintf(throughput, sizeof(throughput),
runtime->active_progress.runtime_percentage runtime->active_progress.runtime_percentage
? "%.1f%%/s" : "%.1f unité/s", ? "%.1f%%/s" : "%.1f unit/s",
runtime->active_progress.units_per_second); runtime->active_progress.units_per_second);
} else { } else {
(void)snprintf(throughput, sizeof(throughput), "UNKNOWN"); (void)snprintf(throughput, sizeof(throughput), "UNKNOWN");
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Durée %s | ETA %s | débit %s%s", elapsed, eta, throughput, "Elapsed %s | ETA %s | throughput %s%s", elapsed, eta, throughput,
runtime->active_progress.resumed_prefix_excluded runtime->active_progress.resumed_prefix_excluded
? " | préfixe repris exclu" : ""); ? " | resumed prefix excluded" : "");
draw_text(active_row + 2, 2, columns - 4, line); draw_text(active_row + 2, 2, columns - 4, line);
} }
} }
@ -353,9 +353,9 @@ draw_home(
static void static void
draw_projects(const char *input_text, const char *input_label, int columns) draw_projects(const char *input_text, const char *input_label, int columns)
{ {
draw_text(5, 4, columns - 6, "N : Nouveau projet"); draw_text(5, 4, columns - 6, "N: New project");
draw_text(6, 4, columns - 6, "O : Ouvrir un projet"); draw_text(6, 4, columns - 6, "O: Open a project");
draw_text(7, 4, columns - 6, "C : Fermer le projet"); draw_text(7, 4, columns - 6, "C: Close project");
draw_input_field(input_text, input_label, 9, columns); draw_input_field(input_text, input_label, 9, columns);
} }
@ -363,19 +363,19 @@ static void
draw_catalog(const Lardon3DAppState *state, int rows, int columns) draw_catalog(const Lardon3DAppState *state, int rows, int columns)
{ {
if (!state->project_loaded || !state->image_view || !state->image_catalog) { if (!state->project_loaded || !state->image_view || !state->image_catalog) {
draw_text(5, 4, columns - 6, "Aucun projet chargé."); draw_text(5, 4, columns - 6, "No project loaded.");
return; return;
} }
size_t count = lardon3d_image_view_count(state->image_view); size_t count = lardon3d_image_view_count(state->image_view);
size_t total = lardon3d_image_catalog_count(state->image_catalog); size_t total = lardon3d_image_catalog_count(state->image_catalog);
char line[512]; char line[512];
(void)snprintf(line, sizeof(line), "Images visibles: %zu / %zu | Tri: %s", (void)snprintf(line, sizeof(line), "Visible images: %zu / %zu | Sort: %s",
count, total, lardon3d_image_view_sort_name( count, total, lardon3d_image_view_sort_name(
lardon3d_image_view_sort(state->image_view))); lardon3d_image_view_sort(state->image_view)));
draw_text(4, 2, columns - 4, line); draw_text(4, 2, columns - 4, line);
const char *filter = lardon3d_image_view_filter(state->image_view); const char *filter = lardon3d_image_view_filter(state->image_view);
(void)snprintf(line, sizeof(line), "Filtre: %s", (void)snprintf(line, sizeof(line), "Filter: %s",
filter[0] ? filter : "aucun"); filter[0] ? filter : "none");
draw_text(5, 2, columns - 4, line); draw_text(5, 2, columns - 4, line);
size_t visible = rows > 12 ? (size_t)(rows - 12) : 1; size_t visible = rows > 12 ? (size_t)(rows - 12) : 1;
size_t offset = lardon3d_image_view_offset(state->image_view); size_t offset = lardon3d_image_view_offset(state->image_view);
@ -407,7 +407,7 @@ draw_import(
if (snapshot && snapshot->status == LARDON3D_IMPORT_TASK_RUNNING) { if (snapshot && snapshot->status == LARDON3D_IMPORT_TASK_RUNNING) {
char line[256]; char line[256];
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Import: %zu/%zu | copiés %zu | présents %zu | ignorés %zu", "Import: %zu/%zu | copied %zu | present %zu | skipped %zu",
snapshot->processed, snapshot->total, snapshot->copied, snapshot->processed, snapshot->total, snapshot->copied,
snapshot->already_present, snapshot->ignored); snapshot->already_present, snapshot->ignored);
draw_text_style(5, 2, columns - 4, line, draw_text_style(5, 2, columns - 4, line,
@ -427,7 +427,7 @@ draw_import(
} }
if (percent > 100) percent = 100; if (percent > 100) percent = 100;
draw_progress_bar(7, 2, columns - 4, percent, palette); draw_progress_bar(7, 2, columns - 4, percent, palette);
draw_text(9, 2, columns - 4, "X : annuler l'import"); draw_text(9, 2, columns - 4, "X: cancel import");
} else if (input_text) { } else if (input_text) {
draw_input_field(input_text, input_label, 5, columns); draw_input_field(input_text, input_label, 5, columns);
} else { } else {
@ -446,18 +446,18 @@ draw_tasks(
{ {
char line[512]; char line[512];
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Total %zu | running %zu | pending %zu | terminal cumulées %zu", "Total %zu | running %zu | pending %zu | terminal cumulative %zu",
runtime->task_summary.total, runtime->task_summary.running, runtime->task_summary.total, runtime->task_summary.running,
runtime->task_summary.pending, runtime->task_summary.completed); runtime->task_summary.pending, runtime->task_summary.completed);
draw_text(4, 2, columns - 4, line); draw_text(4, 2, columns - 4, line);
if (runtime->task_count == 0) { if (runtime->task_count == 0) {
draw_text_style(6, 4, columns - 6, "Aucune tâche retenue.", draw_text_style(6, 4, columns - 6, "No retained task.",
LARDON3D_TUI_SEMANTIC_DIM, palette); LARDON3D_TUI_SEMANTIC_DIM, palette);
return; return;
} }
if (selected >= runtime->task_count) selected = runtime->task_count - 1; if (selected >= runtime->task_count) selected = runtime->task_count - 1;
const Lardon3DTaskObservation *chosen = &runtime->tasks[selected]; const Lardon3DTaskObservation *chosen = &runtime->tasks[selected];
(void)snprintf(line, sizeof(line), "Sélection #%llu %s | %s | %s", (void)snprintf(line, sizeof(line), "Selection #%llu %s | %s | %s",
(unsigned long long)chosen->id, chosen->name, (unsigned long long)chosen->id, chosen->name,
chosen->has_task_kind ? chosen->task_kind : "untyped", chosen->has_task_kind ? chosen->task_kind : "untyped",
lardon3d_task_state_name(chosen->state)); lardon3d_task_state_name(chosen->state));
@ -472,7 +472,7 @@ draw_tasks(
: LARDON3D_TUI_SEMANTIC_CPU), palette); : LARDON3D_TUI_SEMANTIC_CPU), palette);
if (chosen->durable_progress_known) { if (chosen->durable_progress_known) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Progression durable: %llu/%llu%s", "Durable progress: %llu/%llu%s",
(unsigned long long)chosen->durable_completed, (unsigned long long)chosen->durable_completed,
(unsigned long long)chosen->durable_total, (unsigned long long)chosen->durable_total,
chosen->state == TASK_COMPLETED chosen->state == TASK_COMPLETED
@ -485,10 +485,10 @@ draw_tasks(
: LARDON3D_TUI_SEMANTIC_NORMAL, palette); : LARDON3D_TUI_SEMANTIC_NORMAL, palette);
} else if (chosen->has_task_kind) { } else if (chosen->has_task_kind) {
draw_text_style(7, 2, columns - 4, draw_text_style(7, 2, columns - 4,
"Progression scientifique: indéterminée", "Scientific progress: unknown",
LARDON3D_TUI_SEMANTIC_WARNING, palette); LARDON3D_TUI_SEMANTIC_WARNING, palette);
} else { } else {
(void)snprintf(line, sizeof(line), "Progression runtime: %u%%", (void)snprintf(line, sizeof(line), "Runtime progress: %u%%",
chosen->progress); chosen->progress);
draw_text(7, 2, columns - 4, line); draw_text(7, 2, columns - 4, line);
} }
@ -576,7 +576,7 @@ format_external_storage_summary(
(void)snprintf(line, capacity, (void)snprintf(line, capacity,
"Governor SSD %s | alloc %s | scratch total/free %s/%s | leases %zu", "Governor SSD %s | alloc %s | scratch total/free %s/%s | leases %zu",
status, status,
resource->scratch_new_allocations_allowed ? "oui" : "non", resource->scratch_new_allocations_allowed ? "yes" : "no",
scratch_total, scratch_free, resource->scratch_leases); scratch_total, scratch_free, resource->scratch_leases);
} }
@ -590,7 +590,7 @@ draw_resources(
{ {
if (!resource->valid) { if (!resource->valid) {
draw_text_style(5, 4, columns - 6, draw_text_style(5, 4, columns - 6,
"Ressources système indisponibles (UNKNOWN).", "System resources unavailable (UNKNOWN).",
LARDON3D_TUI_SEMANTIC_WARNING, palette); LARDON3D_TUI_SEMANTIC_WARNING, palette);
char external[512]; char external[512];
format_external_storage_summary( format_external_storage_summary(
@ -615,22 +615,22 @@ draw_resources(
resource->cpu_admitted); resource->cpu_admitted);
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"CPU active/admis/disponible: %u/%s/%u (hôte %u) | utilisation %s", "CPU active/admitted/available: %u/%s/%u (host %u) | utilization %s",
resource->cpu_active, admitted, resource->cpu_available, resource->cpu_active, admitted, resource->cpu_available,
resource->cpu_logical_total, resource->cpu_logical_total,
resource->cpu_utilization_known ? "connue" : "UNKNOWN"); resource->cpu_utilization_known ? "known" : "UNKNOWN");
draw_text_style(6, 2, columns - 4, line, draw_text_style(6, 2, columns - 4, line,
LARDON3D_TUI_SEMANTIC_CPU, palette); LARDON3D_TUI_SEMANTIC_CPU, palette);
if (viewport == LARDON3D_TUI_VIEWPORT_FULL) { if (viewport == LARDON3D_TUI_VIEWPORT_FULL) {
if (resource->cpu_utilization_known) { if (resource->cpu_utilization_known) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"CPU raison: %s | utilisation %u.%02u%%", "CPU reason: %s | utilization %u.%02u%%",
resource->cpu_reason, resource->cpu_reason,
resource->cpu_utilization_basis_points / 100U, resource->cpu_utilization_basis_points / 100U,
resource->cpu_utilization_basis_points % 100U); resource->cpu_utilization_basis_points % 100U);
} else { } else {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"CPU raison: %s | utilisation UNKNOWN", "CPU reason: %s | utilization UNKNOWN",
resource->cpu_reason); resource->cpu_reason);
} }
draw_text(7, 4, columns - 6, line); draw_text(7, 4, columns - 6, line);
@ -645,14 +645,14 @@ draw_resources(
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"GPU %s | slots active/dispo %u/%u | busy %s | backend %s", "GPU %s | slots active/dispo %u/%u | busy %s | backend %s",
resource->gpu_present ? "présent" : "absent", resource->gpu_present ? "present" : "absent",
resource->gpu_slots_active, resource->gpu_slots_available, resource->gpu_slots_active, resource->gpu_slots_available,
gpu_busy, gpu_busy,
lardon3d_tui_gpu_backend_name(resource->gpu_backend)); lardon3d_tui_gpu_backend_name(resource->gpu_backend));
draw_text_style(viewport == LARDON3D_TUI_VIEWPORT_FULL ? 9 : 7, draw_text_style(viewport == LARDON3D_TUI_VIEWPORT_FULL ? 9 : 7,
2, columns - 4, line, LARDON3D_TUI_SEMANTIC_GPU, palette); 2, columns - 4, line, LARDON3D_TUI_SEMANTIC_GPU, palette);
if (viewport == LARDON3D_TUI_VIEWPORT_FULL) { if (viewport == LARDON3D_TUI_VIEWPORT_FULL) {
(void)snprintf(line, sizeof(line), "GPU raison: %s", (void)snprintf(line, sizeof(line), "GPU reason: %s",
resource->gpu_backend_reason); resource->gpu_backend_reason);
draw_text(10, 4, columns - 6, line); draw_text(10, 4, columns - 6, line);
} }
@ -662,7 +662,7 @@ draw_resources(
format_bytes(resource->ram_reserve_bytes, reserve); format_bytes(resource->ram_reserve_bytes, reserve);
format_bytes(resource->ram_reserved_bytes, reserved); format_bytes(resource->ram_reserved_bytes, reserved);
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"RAM total %s | MemAvailable %s | réserve %s | réservée Task %s", "RAM total %s | MemAvailable %s | reserve %s | Task reserved %s",
total, available, reserve, reserved); total, available, reserve, reserved);
draw_text(viewport == LARDON3D_TUI_VIEWPORT_FULL ? 11 : 8, draw_text(viewport == LARDON3D_TUI_VIEWPORT_FULL ? 11 : 8,
2, columns - 4, line); 2, columns - 4, line);
@ -672,13 +672,13 @@ draw_resources(
format_bytes(resource->swap_used_bytes, swap_used); format_bytes(resource->swap_used_bytes, swap_used);
if (resource->swap_delta_known) { if (resource->swap_delta_known) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Swap total %s | utilisé %s | delta in/out %llu/%llu pages", "Swap total %s | used %s | delta in/out %llu/%llu pages",
swap_total, swap_used, swap_total, swap_used,
(unsigned long long)resource->swap_pages_in_delta, (unsigned long long)resource->swap_pages_in_delta,
(unsigned long long)resource->swap_pages_out_delta); (unsigned long long)resource->swap_pages_out_delta);
} else { } else {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Swap total %s | utilisé %s | delta in/out UNKNOWN", "Swap total %s | used %s | delta in/out UNKNOWN",
swap_total, swap_used); swap_total, swap_used);
} }
} else { } else {
@ -702,7 +702,7 @@ draw_resources(
resource->helper_limit); resource->helper_limit);
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Contrat: batch %s | inflight %s | helpers %s | I/O active/dispo %u/%u", "Contract: batch %s | inflight %s | helpers %s | I/O active/available %u/%u",
batch, inflight, helpers, resource->io_active, batch, inflight, helpers, resource->io_active,
resource->io_available); resource->io_available);
draw_text(14, 2, columns - 4, line); draw_text(14, 2, columns - 4, line);
@ -737,17 +737,17 @@ draw_resources(
external_swap_used); external_swap_used);
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Governor SSD swap total/used %s/%s | identité %.120s", "Governor SSD swap total/used %s/%s | identity %.120s",
external_swap_total, external_swap_used, external_swap_total, external_swap_used,
resource->external_storage_registered resource->external_storage_registered
? resource->external_storage_identity : "UNKNOWN"); ? resource->external_storage_identity : "UNKNOWN");
draw_text(17, 2, columns - 4, line); draw_text(17, 2, columns - 4, line);
(void)snprintf(line, sizeof(line), "SSD raison: %.220s", (void)snprintf(line, sizeof(line), "SSD reason: %.220s",
resource->external_storage_registered resource->external_storage_registered
? resource->external_storage_reason : "UNREGISTERED"); ? resource->external_storage_reason : "UNREGISTERED");
draw_text(18, 2, columns - 4, line); draw_text(18, 2, columns - 4, line);
draw_text(20, 2, columns - 4, draw_text(20, 2, columns - 4,
"Les choix CPU/GPU/batch sont observés; aucun réglage utilisateur normal."); "CPU/GPU/batch choices are observed; no normal user tuning.");
} }
} }
@ -775,17 +775,17 @@ draw_ssd(
const Lardon3DSsdSnapshot *ssd = &runtime->ssd; const Lardon3DSsdSnapshot *ssd = &runtime->ssd;
char line[640]; char line[640];
if (!runtime->ssd_controller_available) { if (!runtime->ssd_controller_available) {
draw_text_style(4, 2, columns - 4, "Etat: UNKNOWN", draw_text_style(4, 2, columns - 4, "State: UNKNOWN",
LARDON3D_TUI_SEMANTIC_WARNING, palette); LARDON3D_TUI_SEMANTIC_WARNING, palette);
if (operation && operation->running) { if (operation && operation->running) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Opération asynchrone: %s (ncurses reste réactif)", "Asynchronous operation: %s (ncurses remains responsive)",
lardon3d_tui_ssd_action_name(operation->action)); lardon3d_tui_ssd_action_name(operation->action));
draw_text_style(5, 2, columns - 4, line, draw_text_style(5, 2, columns - 4, line,
LARDON3D_TUI_SEMANTIC_WARNING, palette); LARDON3D_TUI_SEMANTIC_WARNING, palette);
} }
draw_text(7, 2, columns - 4, draw_text(7, 2, columns - 4,
"Contrôleur/télémétrie SSD indisponible; identité, swap, scratch et usage UNKNOWN."); "SSD controller/telemetry unavailable; identity, swap, scratch and usage UNKNOWN.");
return; return;
} }
bool telemetry_actionable = !operation bool telemetry_actionable = !operation
@ -802,7 +802,7 @@ draw_ssd(
/* The async owner is exact operation state, not inferred device state. It /* The async owner is exact operation state, not inferred device state. It
* is the only way ENABLING can remain visible while the synchronous * is the only way ENABLING can remain visible while the synchronous
* controller holds its mutex through bounded side-effect verification. */ * controller holds its mutex through bounded side-effect verification. */
(void)snprintf(line, sizeof(line), "Etat: %s%s", (void)snprintf(line, sizeof(line), "State: %s%s",
lardon3d_ssd_state_name(displayed_state), lardon3d_ssd_state_name(displayed_state),
displayed_state == LARDON3D_SSD_SAFE_TO_UNPLUG displayed_state == LARDON3D_SSD_SAFE_TO_UNPLUG
? " — SAFE TO UNPLUG" : ""); ? " — SAFE TO UNPLUG" : "");
@ -810,7 +810,7 @@ draw_ssd(
ssd_semantic(displayed_state), palette); ssd_semantic(displayed_state), palette);
if (operation && operation->running) { if (operation && operation->running) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Opération asynchrone: %s (ncurses reste réactif)", "Asynchronous operation: %s (ncurses remains responsive)",
lardon3d_tui_ssd_action_name(operation->action)); lardon3d_tui_ssd_action_name(operation->action));
draw_text_style(5, 2, columns - 4, line, draw_text_style(5, 2, columns - 4, line,
LARDON3D_TUI_SEMANTIC_WARNING, palette); LARDON3D_TUI_SEMANTIC_WARNING, palette);
@ -822,10 +822,10 @@ draw_ssd(
* booleans are observations. Rendering them as inactive/unmounted * booleans are observations. Rendering them as inactive/unmounted
* would turn validation failure into guessed physical state. */ * would turn validation failure into guessed physical state. */
draw_text_style(7, 2, columns - 4, draw_text_style(7, 2, columns - 4,
"UNKNOWN — télémétrie invalide: identité, lien, swap, scratch, mount et usage.", "UNKNOWN — invalid telemetry: identity, link, swap, scratch, mount and usage.",
LARDON3D_TUI_SEMANTIC_ERROR, palette); LARDON3D_TUI_SEMANTIC_ERROR, palette);
draw_text(9, 2, columns - 4, draw_text(9, 2, columns - 4,
"Contrôle F10 désactivé jusqu'à une observation bornée valide."); "F10 control disabled until a valid bounded observation is available.");
return; return;
} }
char link_speed[64]; char link_speed[64];
@ -836,17 +836,17 @@ draw_ssd(
(void)snprintf(link_speed, sizeof(link_speed), "UNKNOWN"); (void)snprintf(link_speed, sizeof(link_speed), "UNKNOWN");
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Modèle: %s | série: %s | lien: %s", "Model: %s | serial: %s | link: %s",
ssd->model_known ? ssd->model : "UNKNOWN", ssd->model_known ? ssd->model : "UNKNOWN",
ssd->serial_known ? ssd->serial : "UNKNOWN", ssd->serial_known ? ssd->serial : "UNKNOWN",
link_speed); link_speed);
draw_text_style(7, 2, columns - 4, line, draw_text_style(7, 2, columns - 4, line,
LARDON3D_TUI_SEMANTIC_SSD, palette); LARDON3D_TUI_SEMANTIC_SSD, palette);
(void)snprintf(line, sizeof(line), "Identité stable Drive: %s", (void)snprintf(line, sizeof(line), "Stable Drive identity: %s",
ssd->drive_identity[0] ? ssd->drive_identity : "UNKNOWN"); ssd->drive_identity[0] ? ssd->drive_identity : "UNKNOWN");
draw_text(8, 2, columns - 4, line); draw_text(8, 2, columns - 4, line);
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Paire exacte: %s | swap UUID %s | scratch UUID %s", "Exact pair: %s | swap UUID %s | scratch UUID %s",
ssd->pairing_valid ? "VALID" : "INVALID/UNKNOWN", ssd->pairing_valid ? "VALID" : "INVALID/UNKNOWN",
ssd->swap_uuid[0] ? ssd->swap_uuid : "UNKNOWN", ssd->swap_uuid[0] ? ssd->swap_uuid : "UNKNOWN",
ssd->scratch_uuid[0] ? ssd->scratch_uuid : "UNKNOWN"); ssd->scratch_uuid[0] ? ssd->scratch_uuid : "UNKNOWN");
@ -861,7 +861,7 @@ draw_ssd(
format_bytes(ssd->swap_used_bytes, swap_used); format_bytes(ssd->swap_used_bytes, swap_used);
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Swap: %s | total %s | utilisé %s", "Swap: %s | total %s | used %s",
ssd->swap_active ? "ACTIVE" : "INACTIVE", ssd->swap_active ? "ACTIVE" : "INACTIVE",
swap_total, swap_used); swap_total, swap_used);
draw_text(10, 2, columns - 4, line); draw_text(10, 2, columns - 4, line);
@ -881,19 +881,19 @@ draw_ssd(
ssd->scratch_lease_count, ssd->scratch_lease_capacity); ssd->scratch_lease_count, ssd->scratch_lease_capacity);
draw_text(11, 2, columns - 4, line); draw_text(11, 2, columns - 4, line);
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Drain demandé: %s | raison: %s", "Drain requested: %s | reason: %s",
ssd->drain_requested ? "oui" : "non", ssd->drain_requested ? "yes" : "no",
ssd->reason[0] ? ssd->reason : "UNKNOWN"); ssd->reason[0] ? ssd->reason : "UNKNOWN");
draw_text_style(12, 2, columns - 4, line, draw_text_style(12, 2, columns - 4, line,
ssd->state == LARDON3D_SSD_ERROR ssd->state == LARDON3D_SSD_ERROR
? LARDON3D_TUI_SEMANTIC_ERROR ? LARDON3D_TUI_SEMANTIC_ERROR
: LARDON3D_TUI_SEMANTIC_NORMAL, palette); : LARDON3D_TUI_SEMANTIC_NORMAL, palette);
draw_text(14, 2, columns - 4, draw_text(14, 2, columns - 4,
"F10 agit uniquement sur la paire Drive/UUID validée; jamais de format/repair/poweroff."); "F10 acts only on the validated Drive/UUID pair; never format/repair/poweroff.");
draw_text(15, 2, columns - 4, draw_text(15, 2, columns - 4,
"Le swap/SSD reste une sécurité/scratch physique, jamais de la RAM scientifique."); "Swap/SSD remains safety/physical scratch, never scientific RAM.");
} else { } else {
(void)snprintf(line, sizeof(line), "Raison: %s", (void)snprintf(line, sizeof(line), "Reason: %s",
ssd->reason[0] ? ssd->reason : "UNKNOWN"); ssd->reason[0] ? ssd->reason : "UNKNOWN");
draw_text_style(6, 2, columns - 4, line, draw_text_style(6, 2, columns - 4, line,
ssd->state == LARDON3D_SSD_ERROR ssd->state == LARDON3D_SSD_ERROR
@ -904,7 +904,7 @@ draw_ssd(
ssd->swap_active ? "ACTIVE" : "INACTIVE", ssd->swap_active ? "ACTIVE" : "INACTIVE",
ssd->scratch_mounted ? "MOUNTED" : "UNMOUNTED", ssd->scratch_mounted ? "MOUNTED" : "UNMOUNTED",
ssd->scratch_lease_count, ssd->scratch_lease_count,
ssd->drain_requested ? "oui" : "non"); ssd->drain_requested ? "yes" : "no");
draw_text(10, 2, columns - 4, line); draw_text(10, 2, columns - 4, line);
} }
} }
@ -950,14 +950,14 @@ draw_optics(
if (!optics || !optics->project_bound) { if (!optics || !optics->project_bound) {
const char *message = optics && optics->message[0] const char *message = optics && optics->message[0]
? optics->message ? optics->message
: "Aucun Project DB lié; aucun profil ou assignation n'est deviné."; : "No Project DB bound; no profile or assignment is guessed.";
draw_text_style(5, 4, columns - 6, message, draw_text_style(5, 4, columns - 6, message,
optics && optics->message[0] optics && optics->message[0]
? LARDON3D_TUI_SEMANTIC_ERROR ? LARDON3D_TUI_SEMANTIC_ERROR
: LARDON3D_TUI_SEMANTIC_WARNING, palette); : LARDON3D_TUI_SEMANTIC_WARNING, palette);
if (optics && optics->message[0]) { if (optics && optics->message[0]) {
draw_text(7, 4, columns - 6, draw_text(7, 4, columns - 6,
"R : réessayer explicitement la liaison Project DB."); "R: explicitly retry Project DB binding.");
} }
return; return;
} }
@ -972,20 +972,20 @@ draw_optics(
? (optics->selected_configuration < optics->configuration_count ? (optics->selected_configuration < optics->configuration_count
? optics->selected_configuration : 0) : 0; ? optics->selected_configuration : 0) : 0;
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Body [%zu/%zu affichés%s]: %s %s — %s", "Body [%zu/%zu shown%s]: %s %s — %s",
optics->body_count ? body + 1 : 0, optics->body_count, optics->body_count ? body + 1 : 0, optics->body_count,
optics->bodies_have_next ? ", suite" : "", optics->bodies_have_next ? ", more" : "",
optics->body_count ? optics->bodies[body].manufacturer : "UNKNOWN", optics->body_count ? optics->bodies[body].manufacturer : "UNKNOWN",
optics->body_count ? optics->bodies[body].model : "", optics->body_count ? optics->bodies[body].model : "",
optics->body_count ? optics->bodies[body].name : "aucun"); optics->body_count ? optics->bodies[body].name : "none");
draw_text(4, 2, columns - 4, line); draw_text(4, 2, columns - 4, line);
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Lens [%zu/%zu affichés%s]: %s %s — %s (%s)", "Lens [%zu/%zu shown%s]: %s %s — %s (%s)",
optics->lens_count ? lens + 1 : 0, optics->lens_count, optics->lens_count ? lens + 1 : 0, optics->lens_count,
optics->lenses_have_next ? ", suite" : "", optics->lenses_have_next ? ", more" : "",
optics->lens_count ? optics->lenses[lens].manufacturer : "UNKNOWN", optics->lens_count ? optics->lenses[lens].manufacturer : "UNKNOWN",
optics->lens_count ? optics->lenses[lens].model : "", optics->lens_count ? optics->lenses[lens].model : "",
optics->lens_count ? optics->lenses[lens].name : "aucun", optics->lens_count ? optics->lenses[lens].name : "none",
optics->lens_count optics->lens_count
? lens_interface_name(optics->lenses[lens].interface_kind) ? lens_interface_name(optics->lenses[lens].interface_kind)
: "UNKNOWN"); : "UNKNOWN");
@ -999,24 +999,24 @@ draw_optics(
&optics->configurations[configuration]; &optics->configurations[configuration];
if (selected->has_focal_length) { if (selected->has_focal_length) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Config [%zu/%zu affichés%s] #%llu body #%llu lens #%llu focal %u µm", "Config [%zu/%zu shown%s] #%llu body #%llu lens #%llu focal %u µm",
configuration + 1, optics->configuration_count, configuration + 1, optics->configuration_count,
optics->configurations_have_next ? ", suite" : "", optics->configurations_have_next ? ", more" : "",
(unsigned long long)selected->optical_configuration_id, (unsigned long long)selected->optical_configuration_id,
(unsigned long long)selected->camera_body_profile_id, (unsigned long long)selected->camera_body_profile_id,
(unsigned long long)selected->lens_profile_id, (unsigned long long)selected->lens_profile_id,
selected->focal_length_um); selected->focal_length_um);
} else { } else {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Config [%zu/%zu affichés%s] #%llu body #%llu lens #%llu focal ABSENT", "Config [%zu/%zu shown%s] #%llu body #%llu lens #%llu focal ABSENT",
configuration + 1, optics->configuration_count, configuration + 1, optics->configuration_count,
optics->configurations_have_next ? ", suite" : "", optics->configurations_have_next ? ", more" : "",
(unsigned long long)selected->optical_configuration_id, (unsigned long long)selected->optical_configuration_id,
(unsigned long long)selected->camera_body_profile_id, (unsigned long long)selected->camera_body_profile_id,
(unsigned long long)selected->lens_profile_id); (unsigned long long)selected->lens_profile_id);
} }
} else { } else {
(void)snprintf(line, sizeof(line), "Config: aucune"); (void)snprintf(line, sizeof(line), "Config: none");
} }
draw_text(6, 2, columns - 4, line); draw_text(6, 2, columns - 4, line);
if (optics->calibration_count > 0) { if (optics->calibration_count > 0) {
@ -1026,15 +1026,15 @@ draw_optics(
const Lardon3DOpticalCalibrationProfile *calibration = const Lardon3DOpticalCalibrationProfile *calibration =
&optics->calibrations[selected]; &optics->calibrations[selected];
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Pane %s | calibration [%zu/%zu affichées%s] #%llu %.28s v%u", "Pane %s | calibration [%zu/%zu shown%s] #%llu %.28s v%u",
optics_pane_name(optics->active_pane), selected + 1, optics_pane_name(optics->active_pane), selected + 1,
optics->calibration_count, optics->calibration_count,
optics->calibrations_have_next ? ", suite" : "", optics->calibrations_have_next ? ", more" : "",
(unsigned long long)calibration->calibration_profile_id, (unsigned long long)calibration->calibration_profile_id,
calibration->name, calibration->profile_version); calibration->name, calibration->profile_version);
} else { } else {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Pane %s | calibration candidate: aucune", "Pane %s | calibration candidate: none",
optics_pane_name(optics->active_pane)); optics_pane_name(optics->active_pane));
} }
draw_text(7, 2, columns - 4, line); draw_text(7, 2, columns - 4, line);
@ -1056,17 +1056,17 @@ draw_optics(
draw_text_style(8, 2, columns - 4, line, semantic, palette); draw_text_style(8, 2, columns - 4, line, semantic, palette);
} else { } else {
draw_text_style(8, 2, columns - 4, draw_text_style(8, 2, columns - 4,
"Capture: non inspecté (V); unresolved reste absence d'assignation.", "Capture: not inspected (V); unresolved remains no assignment.",
LARDON3D_TUI_SEMANTIC_DIM, palette); LARDON3D_TUI_SEMANTIC_DIM, palette);
} }
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Calibrations compatibles: %zu | sélection: %s", "Compatible calibrations: %zu | selection: %s",
optics->calibration_count, optics->calibration_count,
optics->capture_selection_found ? "explicite" : "aucune/ambiguë"); optics->capture_selection_found ? "explicit" : "none/ambiguous");
draw_text(9, 2, columns - 4, line); draw_text(9, 2, columns - 4, line);
if (optics->metadata_lookup_performed) { if (optics->metadata_lookup_performed) {
(void)snprintf(line, sizeof(line), (void)snprintf(line, sizeof(line),
"Métadonnées exactes: body %s | lens %s", "Exact metadata: body %s | lens %s",
optics->metadata_body_found ? "MATCH" : "UNRESOLVED", optics->metadata_body_found ? "MATCH" : "UNRESOLVED",
optics->metadata_lens_found ? "MATCH" : "UNRESOLVED"); optics->metadata_lens_found ? "MATCH" : "UNRESOLVED");
draw_text(10, 2, columns - 4, line); draw_text(10, 2, columns - 4, line);
@ -1077,9 +1077,9 @@ draw_optics(
draw_text(13, 2, columns - 4, draw_text(13, 2, columns - 4,
"C config focal mm/? V inspect capture A assign capture G task:group"); "C config focal mm/? V inspect capture A assign capture G task:group");
draw_text(14, 2, columns - 4, draw_text(14, 2, columns - 4,
"K sélection calibration E metadata exact [ première page ] page suivante R retry"); "K select calibration E exact metadata [ first page ] next page R retry");
draw_text_style(16, 2, columns - 4, draw_text_style(16, 2, columns - 4,
"Immutable: modifier = créer une nouvelle version/configuration.", "Immutable: modify = create a new version/configuration.",
LARDON3D_TUI_SEMANTIC_WARNING, palette); LARDON3D_TUI_SEMANTIC_WARNING, palette);
} }
} }
@ -1092,24 +1092,24 @@ draw_help(
) )
{ {
draw_text(4, 2, columns - 4, draw_text(4, 2, columns - 4,
"F1 aide, F2 projets, F3 import, F4 viewer futur, F5 tâches, F6 ressources,"); "F1 help, F2 projects, F3 import, F4 future viewer, F5 tasks, F6 resources,");
draw_text(5, 2, columns - 4, draw_text(5, 2, columns - 4,
"F7 profils optiques, F10 SSD; ESC accueil; Q quitter."); "F7 optical profiles, F10 SSD; ESC home; Q quit.");
draw_text_style(7, 2, columns - 4, draw_text_style(7, 2, columns - 4,
"Vert=healthy, jaune=warning/throttled, rouge=error, cyan=GPU, bleu=CPU, magenta=SSD.", "Green=healthy, yellow=warning/throttled, red=error, cyan=GPU, blue=CPU, magenta=SSD.",
LARDON3D_TUI_SEMANTIC_HEALTHY, palette); LARDON3D_TUI_SEMANTIC_HEALTHY, palette);
draw_text(8, 2, columns - 4, draw_text(8, 2, columns - 4,
"Sans couleur/peu de paires, les libellés et bold/dim conservent le sens."); "Without color/few color pairs, labels and bold/dim preserve meaning.");
if (viewport == LARDON3D_TUI_VIEWPORT_FULL) { if (viewport == LARDON3D_TUI_VIEWPORT_FULL) {
draw_text(10, 2, columns - 4, draw_text(10, 2, columns - 4,
"La TUI observe des snapshots bornés >=1s; aucun worker ne touche ncurses."); "The TUI observes bounded snapshots >=1s; no worker touches ncurses.");
draw_text(11, 2, columns - 4, draw_text(11, 2, columns - 4,
"Le Governor choisit CPU/GPU/batch. La TUI ne modifie ni admission ni science."); "The Governor selects CPU/GPU/batch. The TUI changes neither admission nor science.");
draw_text(12, 2, columns - 4, draw_text(12, 2, columns - 4,
"Dense (future) reste NOT_APPLICABLE; aucune étape future n'est RUNNING."); "Dense (future) remains NOT_APPLICABLE; no future stage is RUNNING.");
} else { } else {
draw_text(10, 2, columns - 4, draw_text(10, 2, columns - 4,
"Governor choisit les ressources; Dense future reste NOT_APPLICABLE."); "Governor selects resources; future Dense remains NOT_APPLICABLE.");
} }
} }
@ -1170,7 +1170,7 @@ lardon3d_layout_draw_runtime(
break; break;
case LARDON3D_SCREEN_VIEWER: case LARDON3D_SCREEN_VIEWER:
draw_text_style(6, 4, columns - 8, draw_text_style(6, 4, columns - 8,
"Viewer Vulkan: PLANNED, aucun travail scientifique actif.", "Vulkan viewer: PLANNED, no active scientific work.",
LARDON3D_TUI_SEMANTIC_DIM, palette); LARDON3D_TUI_SEMANTIC_DIM, palette);
break; break;
case LARDON3D_SCREEN_HOME: case LARDON3D_SCREEN_HOME:

View file

@ -2062,3 +2062,938 @@ lardon3d_optical_capture_calibration_selection_load(
memset(output, 0, sizeof(*output)); memset(output, 0, sizeof(*output));
return result; return result;
} }
static bool
geometric_state_input_valid(const Lardon3DOpticalCaptureGeometricState *value) {
if (!value || !optical_id(value->capture_id) ||
!optical_id(value->optical_configuration_id) ||
value->state_version == 0 ||
(value->provenance != LARDON3D_OPTICAL_GEOMETRIC_STATE_METADATA &&
value->provenance != LARDON3D_OPTICAL_GEOMETRIC_STATE_CALLER_EXPLICIT) ||
value->focus_state < LARDON3D_OPTICAL_OBSERVATION_UNKNOWN ||
value->focus_state > LARDON3D_OPTICAL_OBSERVATION_OBSERVED ||
value->aperture_state < LARDON3D_OPTICAL_OBSERVATION_UNKNOWN ||
value->aperture_state > LARDON3D_OPTICAL_OBSERVATION_OBSERVED ||
value->crop_state < LARDON3D_OPTICAL_OBSERVATION_UNKNOWN ||
value->crop_state > LARDON3D_OPTICAL_OBSERVATION_OBSERVED ||
value->pipeline_state < LARDON3D_OPTICAL_OBSERVATION_UNKNOWN ||
value->pipeline_state > LARDON3D_OPTICAL_OBSERVATION_OBSERVED ||
value->representation_state < LARDON3D_OPTICAL_OBSERVATION_UNKNOWN ||
value->representation_state > LARDON3D_OPTICAL_OBSERVATION_OBSERVED ||
value->decoded_geometry_state < LARDON3D_OPTICAL_OBSERVATION_UNKNOWN ||
value->decoded_geometry_state > LARDON3D_OPTICAL_OBSERVATION_OBSERVED ||
value->stabilization < LARDON3D_OPTICAL_STABILIZATION_UNKNOWN ||
value->stabilization > LARDON3D_OPTICAL_STABILIZATION_ON)
return false;
const char *tokens[] = {value->focus_observation, value->crop_observation,
value->pipeline_observation,
value->representation_observation};
const Lardon3DOpticalObservationState states[] = {
value->focus_state, value->crop_state, value->pipeline_state,
value->representation_state};
for (size_t index = 0; index < 4; ++index)
if (!optical_text(tokens[index], LARDON3D_OPTICAL_TEXT_CAPACITY,
states[index] == LARDON3D_OPTICAL_OBSERVATION_UNKNOWN) ||
((states[index] == LARDON3D_OPTICAL_OBSERVATION_UNKNOWN) !=
(tokens[index][0] == '\0')))
return false;
return ((value->aperture_state == LARDON3D_OPTICAL_OBSERVATION_UNKNOWN &&
value->aperture_x1000 == 0) ||
(value->aperture_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->aperture_x1000 > 0)) &&
((value->decoded_geometry_state ==
LARDON3D_OPTICAL_OBSERVATION_UNKNOWN &&
value->decoded_width == 0 && value->decoded_height == 0) ||
(value->decoded_geometry_state ==
LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->decoded_width > 0 && value->decoded_height > 0));
}
static bool geometric_state_complete(
const Lardon3DOpticalCaptureGeometricState *value) {
/* UNKNOWN remains valid durable evidence, but it cannot prove geometric
compatibility or authorize publication/selection of an applicability. */
return value->focus_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->aperture_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->stabilization != LARDON3D_OPTICAL_STABILIZATION_UNKNOWN &&
value->crop_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->pipeline_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->representation_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED &&
value->decoded_geometry_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED;
}
static bool read_geometric_state(sqlite3_stmt *statement,
Lardon3DOpticalCaptureGeometricState *output) {
memset(output, 0, sizeof(*output));
for (int column = 0; column < 18; ++column) {
bool text = column == 5 || column == 10 || column == 12 || column == 14;
if (sqlite3_column_type(statement, column) !=
(text ? SQLITE_TEXT : SQLITE_INTEGER))
return false;
}
sqlite3_int64 capture_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 configuration_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 version = sqlite3_column_int64(statement, 2);
sqlite3_int64 aperture = sqlite3_column_int64(statement, 7);
sqlite3_int64 decoded_width = sqlite3_column_int64(statement, 16);
sqlite3_int64 decoded_height = sqlite3_column_int64(statement, 17);
if (capture_id <= 0 || configuration_id <= 0 || version <= 0 ||
(uint64_t)version > UINT32_MAX || aperture < 0 ||
(uint64_t)aperture > UINT32_MAX || decoded_width < 0 ||
(uint64_t)decoded_width > UINT32_MAX || decoded_height < 0 ||
(uint64_t)decoded_height > UINT32_MAX)
return false;
output->capture_id = (uint64_t)capture_id;
output->optical_configuration_id = (uint64_t)configuration_id;
output->state_version = (uint32_t)version;
output->provenance =
(Lardon3DOpticalGeometricStateProvenance)sqlite3_column_int64(statement,
3);
output->focus_state =
(Lardon3DOpticalObservationState)sqlite3_column_int64(statement, 4);
output->aperture_state =
(Lardon3DOpticalObservationState)sqlite3_column_int64(statement, 6);
output->aperture_x1000 = (uint32_t)aperture;
output->stabilization =
(Lardon3DOpticalStabilizationState)sqlite3_column_int64(statement, 8);
output->crop_state =
(Lardon3DOpticalObservationState)sqlite3_column_int64(statement, 9);
output->pipeline_state =
(Lardon3DOpticalObservationState)sqlite3_column_int64(statement, 11);
output->representation_state =
(Lardon3DOpticalObservationState)sqlite3_column_int64(statement, 13);
output->decoded_geometry_state =
(Lardon3DOpticalObservationState)sqlite3_column_int64(statement, 15);
output->decoded_width = (uint32_t)decoded_width;
output->decoded_height = (uint32_t)decoded_height;
if (!optical_copy_text(statement, 5, output->focus_observation,
sizeof(output->focus_observation), true) ||
!optical_copy_text(statement, 10, output->crop_observation,
sizeof(output->crop_observation), true) ||
!optical_copy_text(statement, 12, output->pipeline_observation,
sizeof(output->pipeline_observation), true) ||
!optical_copy_text(statement, 14, output->representation_observation,
sizeof(output->representation_observation), true))
return false;
return geometric_state_input_valid(output);
}
static const char geometric_state_columns[] =
"capture_id,optical_configuration_id,state_version,provenance,focus_state,"
"focus_observation,"
"aperture_state,aperture_x1000,stabilization,crop_state,crop_observation,"
"pipeline_state,"
"pipeline_observation,representation_state,representation_observation,"
"decoded_geometry_state,"
"decoded_width,decoded_height";
static bool
geometric_states_equal(const Lardon3DOpticalCaptureGeometricState *a,
const Lardon3DOpticalCaptureGeometricState *b) {
return a->capture_id == b->capture_id &&
a->optical_configuration_id == b->optical_configuration_id &&
a->state_version == b->state_version &&
a->provenance == b->provenance && a->focus_state == b->focus_state &&
strcmp(a->focus_observation, b->focus_observation) == 0 &&
a->aperture_state == b->aperture_state &&
a->aperture_x1000 == b->aperture_x1000 &&
a->stabilization == b->stabilization &&
a->crop_state == b->crop_state &&
strcmp(a->crop_observation, b->crop_observation) == 0 &&
a->pipeline_state == b->pipeline_state &&
strcmp(a->pipeline_observation, b->pipeline_observation) == 0 &&
a->representation_state == b->representation_state &&
strcmp(a->representation_observation, b->representation_observation) ==
0 &&
a->decoded_geometry_state == b->decoded_geometry_state &&
a->decoded_width == b->decoded_width &&
a->decoded_height == b->decoded_height;
}
static Lardon3DProjectDbResult
geometric_state_load_locked(Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureGeometricState *output) {
char query[768];
int length =
snprintf(query, sizeof(query),
"SELECT %s FROM capture_geometric_states WHERE capture_id=?1",
geometric_state_columns);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = length < 0 || (size_t)length >= sizeof(query)
? LARDON3D_PROJECT_DB_IO_ERROR
: prepare(database, query, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE)
result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW)
result = sqlite_result(database, code, "load Capture geometric state");
else if (!read_geometric_state(statement, output) ||
output->capture_id != capture_id ||
sqlite3_step(statement) != SQLITE_DONE)
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
return result;
}
Lardon3DProjectDbResult lardon3d_optical_capture_geometric_state_load(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureGeometricState *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || !optical_id(capture_id) || !output)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result =
geometric_state_load_locked(database, capture_id, output);
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK)
memset(output, 0, sizeof(*output));
return result;
}
Lardon3DProjectDbResult lardon3d_optical_capture_geometric_state_create(
Lardon3DProjectDb *database,
const Lardon3DOpticalCaptureGeometricState *input,
Lardon3DOpticalCaptureGeometricState *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || !output || input == output ||
!geometric_state_input_valid(input))
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result =
execute(database, "BEGIN IMMEDIATE", "begin Capture geometric state");
Lardon3DOpticalCaptureAssignment assignment = {0};
if (result == LARDON3D_PROJECT_DB_OK)
result = capture_assignment_load_locked(database, input->capture_id,
&assignment);
if (result == LARDON3D_PROJECT_DB_OK &&
assignment.optical_configuration_id != input->optical_configuration_id)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"INSERT OR IGNORE INTO capture_geometric_states "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?"
"15,?16,?17,?18)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)input->capture_id);
sqlite3_bind_int64(statement, 2,
(sqlite3_int64)input->optical_configuration_id);
sqlite3_bind_int64(statement, 3, input->state_version);
sqlite3_bind_int64(statement, 4, input->provenance);
sqlite3_bind_int64(statement, 5, input->focus_state);
sqlite3_bind_text(statement, 6, input->focus_observation, -1,
SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 7, input->aperture_state);
sqlite3_bind_int64(statement, 8, input->aperture_x1000);
sqlite3_bind_int64(statement, 9, input->stabilization);
sqlite3_bind_int64(statement, 10, input->crop_state);
sqlite3_bind_text(statement, 11, input->crop_observation, -1,
SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 12, input->pipeline_state);
sqlite3_bind_text(statement, 13, input->pipeline_observation, -1,
SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 14, input->representation_state);
sqlite3_bind_text(statement, 15, input->representation_observation, -1,
SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 16, input->decoded_geometry_state);
sqlite3_bind_int64(statement, 17, input->decoded_width);
sqlite3_bind_int64(statement, 18, input->decoded_height);
result = step_done(database, statement, "insert Capture geometric state");
statement = NULL;
}
(void)sqlite3_finalize(statement);
Lardon3DOpticalCaptureGeometricState stored = {0};
if (result == LARDON3D_PROJECT_DB_OK)
result = geometric_state_load_locked(database, input->capture_id, &stored);
if (result == LARDON3D_PROJECT_DB_OK &&
!geometric_states_equal(&stored, input))
result = LARDON3D_PROJECT_DB_CONSTRAINT;
result = optical_commit_or_rollback(database, result,
"commit Capture geometric state",
"rollback Capture geometric state");
(void)pthread_mutex_unlock(&database->mutex);
if (result == LARDON3D_PROJECT_DB_OK)
*output = stored;
return result;
}
static const char exact_state_predicate[] =
"t.optical_configuration_id=e.optical_configuration_id AND "
"t.state_version=e.state_version AND t.provenance=e.provenance AND "
"t.focus_state=e.focus_state AND "
"t.focus_observation=e.focus_observation AND "
"t.aperture_state=e.aperture_state AND t.aperture_x1000=e.aperture_x1000 "
"AND t.stabilization=e.stabilization AND t.crop_state=e.crop_state AND "
"t.crop_observation=e.crop_observation AND "
"t.pipeline_state=e.pipeline_state AND "
"t.pipeline_observation=e.pipeline_observation AND "
"t.representation_state=e.representation_state AND "
"t.representation_observation=e.representation_observation AND "
"t.decoded_geometry_state=e.decoded_geometry_state AND "
"t.decoded_width=e.decoded_width AND t.decoded_height=e.decoded_height";
/* A discrete focus domain varies only the exact opaque focus token. Every
other geometry and observation-provenance field remains exemplar-exact. */
static const char exact_nonfocus_state_predicate[] =
"t.optical_configuration_id=e.optical_configuration_id AND "
"t.state_version=e.state_version AND t.provenance=e.provenance AND "
"t.aperture_state=e.aperture_state AND t.aperture_x1000=e.aperture_x1000 "
"AND t.stabilization=e.stabilization AND t.crop_state=e.crop_state AND "
"t.crop_observation=e.crop_observation AND "
"t.pipeline_state=e.pipeline_state AND "
"t.pipeline_observation=e.pipeline_observation AND "
"t.representation_state=e.representation_state AND "
"t.representation_observation=e.representation_observation AND "
"t.decoded_geometry_state=e.decoded_geometry_state AND "
"t.decoded_width=e.decoded_width AND t.decoded_height=e.decoded_height";
Lardon3DProjectDbResult lardon3d_optical_calibration_applicability_v2_create(
Lardon3DProjectDb *database, uint64_t calibration_profile_id,
uint64_t exemplar_capture_id,
Lardon3DOpticalCalibrationApplicabilityV2 *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || !optical_id(calibration_profile_id) ||
!optical_id(exemplar_capture_id) || !output)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
Lardon3DOpticalCalibrationProfile profile;
Lardon3DProjectDbResult result = lardon3d_optical_calibration_profile_load(
database, calibration_profile_id, &profile);
Lardon3DOpticalCaptureGeometricState state;
if (result == LARDON3D_PROJECT_DB_OK)
result = lardon3d_optical_capture_geometric_state_load(
database, exemplar_capture_id, &state);
if (result != LARDON3D_PROJECT_DB_OK)
return result;
if (profile.optical_configuration_id != state.optical_configuration_id ||
!geometric_state_complete(&state))
return LARDON3D_PROJECT_DB_CONSTRAINT;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
result = execute(database, "BEGIN IMMEDIATE",
"begin calibration applicability v2");
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"INSERT OR IGNORE INTO "
"optical_calibration_applicabilities_v2(calibration_profile_id,optical_"
"configuration_id,exemplar_capture_id) VALUES(?1,?2,?3)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)calibration_profile_id);
sqlite3_bind_int64(statement, 2,
(sqlite3_int64)state.optical_configuration_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)exemplar_capture_id);
result =
step_done(database, statement, "insert calibration applicability v2");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"SELECT "
"applicability_id,calibration_profile_id,optical_configuration_id,"
"exemplar_capture_id FROM optical_calibration_applicabilities_v2 WHERE "
"calibration_profile_id=?1 AND exemplar_capture_id=?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)calibration_profile_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)exemplar_capture_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW)
result = code == SQLITE_DONE
? LARDON3D_PROJECT_DB_CORRUPT
: sqlite_result(database, code,
"load calibration applicability v2");
else {
sqlite3_int64 applicability_id = sqlite3_column_int64(statement, 0);
bool valid = true;
for (int column = 0; column < 4; ++column)
if (sqlite3_column_type(statement, column) != SQLITE_INTEGER)
valid = false;
if (!valid || applicability_id <= 0 ||
sqlite3_column_int64(statement, 1) !=
(sqlite3_int64)calibration_profile_id ||
sqlite3_column_int64(statement, 2) !=
(sqlite3_int64)state.optical_configuration_id ||
sqlite3_column_int64(statement, 3) !=
(sqlite3_int64)exemplar_capture_id ||
sqlite3_step(statement) != SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
output->applicability_id = (uint64_t)applicability_id;
output->calibration_profile_id = calibration_profile_id;
output->optical_configuration_id = state.optical_configuration_id;
output->exemplar_capture_id = exemplar_capture_id;
}
}
}
(void)sqlite3_finalize(statement);
result = optical_commit_or_rollback(database, result,
"commit calibration applicability v2",
"rollback calibration applicability v2");
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK)
memset(output, 0, sizeof(*output));
return result;
}
static bool focus_domain_digest_valid(
const unsigned char digest[LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE]) {
if (!digest)
return false;
for (size_t index = 0; index < LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE;
++index)
if (digest[index] != 0)
return true;
return false;
}
static bool focus_domain_tokens_valid(const char *const *tokens, size_t count) {
if (!tokens || count == 0 ||
count > LARDON3D_OPTICAL_FOCUS_DOMAIN_TOKEN_MAX)
return false;
for (size_t index = 0; index < count; ++index) {
if (!optical_text(tokens[index], LARDON3D_OPTICAL_TEXT_CAPACITY, false))
return false;
for (size_t previous = 0; previous < index; ++previous)
if (strcmp(tokens[index], tokens[previous]) == 0)
return false;
}
return true;
}
Lardon3DProjectDbResult lardon3d_optical_focus_domain_v2_create(
Lardon3DProjectDb *database, uint64_t applicability_id,
uint32_t domain_version,
const unsigned char evidence_sha256[LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE],
const char *const *focus_tokens, size_t token_count,
Lardon3DOpticalFocusDomainV2 *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || !optical_id(applicability_id) || domain_version == 0 ||
!focus_domain_digest_valid(evidence_sha256) ||
!focus_domain_tokens_valid(focus_tokens, token_count) || !output)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
/* Canonical lexical order makes a domain's durable representation
independent of caller ordering while retaining tokens byte-for-byte. */
const char *ordered[LARDON3D_OPTICAL_FOCUS_DOMAIN_TOKEN_MAX];
for (size_t index = 0; index < token_count; ++index) {
size_t position = index;
while (position > 0 && strcmp(focus_tokens[index], ordered[position - 1]) < 0) {
ordered[position] = ordered[position - 1];
--position;
}
ordered[position] = focus_tokens[index];
}
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(
database, "BEGIN IMMEDIATE", "begin optical focus domain v2");
sqlite3_stmt *statement = NULL;
uint64_t calibration_profile_id = 0;
uint64_t optical_configuration_id = 0;
uint64_t exemplar_capture_id = 0;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"SELECT calibration_profile_id,optical_configuration_id,"
"exemplar_capture_id FROM optical_calibration_applicabilities_v2 "
"WHERE applicability_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)applicability_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE)
result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW)
result = sqlite_result(database, code, "load focus domain applicability");
else if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 2) != SQLITE_INTEGER ||
sqlite3_column_int64(statement, 0) <= 0 ||
sqlite3_column_int64(statement, 1) <= 0 ||
sqlite3_column_int64(statement, 2) <= 0)
result = LARDON3D_PROJECT_DB_CORRUPT;
else {
calibration_profile_id = (uint64_t)sqlite3_column_int64(statement, 0);
optical_configuration_id = (uint64_t)sqlite3_column_int64(statement, 1);
exemplar_capture_id = (uint64_t)sqlite3_column_int64(statement, 2);
if (sqlite3_step(statement) != SQLITE_DONE)
result = LARDON3D_PROJECT_DB_CORRUPT;
}
}
(void)sqlite3_finalize(statement);
statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"INSERT OR IGNORE INTO optical_focus_domains_v2(applicability_id,"
"domain_version,evidence_sha256,token_count) VALUES(?1,?2,?3,?4)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)applicability_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)domain_version);
sqlite3_bind_blob(statement, 3, evidence_sha256,
LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE,
SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 4, (sqlite3_int64)token_count);
result = step_done(database, statement, "insert optical focus domain v2");
statement = NULL;
}
uint64_t focus_domain_id = 0;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"SELECT focus_domain_id,domain_version,evidence_sha256,token_count FROM "
"optical_focus_domains_v2 WHERE applicability_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)applicability_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW)
result = code == SQLITE_DONE
? LARDON3D_PROJECT_DB_CORRUPT
: sqlite_result(database, code, "load optical focus domain v2");
else {
sqlite3_int64 stored_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 stored_version = sqlite3_column_int64(statement, 1);
sqlite3_int64 stored_count = sqlite3_column_int64(statement, 3);
const unsigned char *stored_digest = sqlite3_column_blob(statement, 2);
if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 2) != SQLITE_BLOB ||
sqlite3_column_type(statement, 3) != SQLITE_INTEGER || stored_id <= 0 ||
stored_version <= 0 || stored_version > UINT32_MAX ||
sqlite3_column_bytes(statement, 2) !=
LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE ||
!focus_domain_digest_valid(stored_digest) || stored_count <= 0 ||
stored_count > LARDON3D_OPTICAL_FOCUS_DOMAIN_TOKEN_MAX) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else if (stored_version != (sqlite3_int64)domain_version ||
memcmp(stored_digest, evidence_sha256,
LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE) != 0 ||
stored_count != (sqlite3_int64)token_count) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else {
focus_domain_id = (uint64_t)stored_id;
if (sqlite3_step(statement) != SQLITE_DONE)
result = LARDON3D_PROJECT_DB_CORRUPT;
}
}
}
(void)sqlite3_finalize(statement);
statement = NULL;
for (size_t index = 0; index < token_count &&
result == LARDON3D_PROJECT_DB_OK;
++index) {
result = prepare(
database,
"INSERT OR IGNORE INTO optical_focus_domain_tokens_v2 "
"VALUES(?1,?2,?3)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)focus_domain_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)index);
sqlite3_bind_text(statement, 3, ordered[index], -1, SQLITE_TRANSIENT);
result = step_done(database, statement, "insert focus domain token v2");
statement = NULL;
}
}
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"SELECT token_ordinal,focus_token FROM optical_focus_domain_tokens_v2 "
"WHERE focus_domain_id=?1 ORDER BY token_ordinal",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)focus_domain_id);
for (size_t index = 0; index < token_count; ++index) {
int code = sqlite3_step(statement);
if (code != SQLITE_ROW) {
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CORRUPT
: sqlite_result(database, code,
"load focus domain token v2");
break;
}
if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_int64(statement, 0) != (sqlite3_int64)index ||
sqlite3_column_type(statement, 1) != SQLITE_TEXT ||
sqlite3_column_bytes(statement, 1) <= 0 ||
sqlite3_column_bytes(statement, 1) >= LARDON3D_OPTICAL_TEXT_CAPACITY) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
if (!optical_column_equals(statement, 1, ordered[index])) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
break;
}
}
if (result == LARDON3D_PROJECT_DB_OK &&
sqlite3_step(statement) != SQLITE_DONE)
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
result = optical_commit_or_rollback(database, result,
"commit optical focus domain v2",
"rollback optical focus domain v2");
(void)pthread_mutex_unlock(&database->mutex);
if (result == LARDON3D_PROJECT_DB_OK) {
output->focus_domain_id = focus_domain_id;
output->applicability_id = applicability_id;
output->calibration_profile_id = calibration_profile_id;
output->optical_configuration_id = optical_configuration_id;
output->exemplar_capture_id = exemplar_capture_id;
output->domain_version = domain_version;
memcpy(output->evidence_sha256, evidence_sha256,
LARDON3D_OPTICAL_FOCUS_DOMAIN_DIGEST_SIZE);
output->token_count = (uint32_t)token_count;
} else {
memset(output, 0, sizeof(*output));
}
return result;
}
static Lardon3DProjectDbResult
applicability_candidates_locked(Lardon3DProjectDb *database,
uint64_t capture_id,
uint64_t required_applicability,
Lardon3DOpticalCalibrationResolutionV2 *output,
size_t *count) {
/* A broken v2 dependency is corruption, not evidence that calibration is
required. Validate the target configuration before exact-state filtering. */
sqlite3_stmt *validation = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT a.applicability_id FROM optical_calibration_applicabilities_v2 a "
"LEFT JOIN capture_geometric_states e ON e.capture_id=a.exemplar_capture_id "
"AND e.optical_configuration_id=a.optical_configuration_id LEFT JOIN "
"optical_calibration_profiles p ON p.calibration_profile_id=a.calibration_profile_id "
"AND p.optical_configuration_id=a.optical_configuration_id LEFT JOIN "
"sparse_calibrations s ON s.calibration_id=p.sparse_calibration_id WHERE "
"a.optical_configuration_id=(SELECT optical_configuration_id FROM "
"capture_geometric_states WHERE capture_id=?1) AND (e.capture_id IS NULL OR "
"p.calibration_profile_id IS NULL OR s.calibration_id IS NULL) LIMIT 1",
&validation);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(validation, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(validation);
if (code == SQLITE_ROW)
result = LARDON3D_PROJECT_DB_CORRUPT;
else if (code != SQLITE_DONE)
result = sqlite_result(database, code,
"validate calibration applicability v2");
}
(void)sqlite3_finalize(validation);
if (result != LARDON3D_PROJECT_DB_OK)
return result;
validation = NULL;
/* Domain rows are retained scientific evidence. Broken dependencies,
incomplete exemplars or token-set corruption must fail closed before
enumeration; a domain never supplies evidence missing from its exemplar. */
result = prepare(
database,
"SELECT d.focus_domain_id FROM optical_focus_domains_v2 d LEFT JOIN "
"optical_calibration_applicabilities_v2 a ON "
"a.applicability_id=d.applicability_id LEFT JOIN capture_geometric_states e "
"ON e.capture_id=a.exemplar_capture_id AND "
"e.optical_configuration_id=a.optical_configuration_id WHERE "
"a.applicability_id IS NULL OR (a.optical_configuration_id=(SELECT "
"optical_configuration_id FROM capture_geometric_states WHERE capture_id=?1) "
"AND (e.capture_id IS NULL OR e.focus_state!=2 OR e.aperture_state!=2 OR "
"e.stabilization=0 OR e.crop_state!=2 OR e.pipeline_state!=2 OR "
"e.representation_state!=2 OR e.decoded_geometry_state!=2 OR "
"typeof(d.domain_version)!='integer' OR d.domain_version<=0 OR "
"typeof(d.evidence_sha256)!='blob' OR length(d.evidence_sha256)!=32 OR "
"d.evidence_sha256=zeroblob(32) OR typeof(d.token_count)!='integer' OR "
"d.token_count<=0 OR d.token_count>64 OR d.token_count!=(SELECT COUNT(*) "
"FROM optical_focus_domain_tokens_v2 k WHERE "
"k.focus_domain_id=d.focus_domain_id) OR (SELECT MIN(k.token_ordinal) FROM "
"optical_focus_domain_tokens_v2 k WHERE k.focus_domain_id=d.focus_domain_id)"
"!=0 OR (SELECT MAX(k.token_ordinal) FROM optical_focus_domain_tokens_v2 k "
"WHERE k.focus_domain_id=d.focus_domain_id)!=d.token_count-1 OR EXISTS("
"SELECT 1 FROM optical_focus_domain_tokens_v2 k WHERE "
"k.focus_domain_id=d.focus_domain_id AND (typeof(k.token_ordinal)!='integer' "
"OR k.token_ordinal<0 OR k.token_ordinal>=64 OR "
"typeof(k.focus_token)!='text' OR length(k.focus_token)=0 OR "
"length(k.focus_token)>=128)))) LIMIT 1",
&validation);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(validation, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(validation);
if (code == SQLITE_ROW)
result = LARDON3D_PROJECT_DB_CORRUPT;
else if (code != SQLITE_DONE)
result = sqlite_result(database, code, "validate optical focus domain v2");
}
(void)sqlite3_finalize(validation);
if (result != LARDON3D_PROJECT_DB_OK)
return result;
char query[4096];
int length = snprintf(
query, sizeof(query),
"SELECT applicability_id,calibration_profile_id,sparse_calibration_id "
"FROM (SELECT a.applicability_id AS applicability_id,"
"a.calibration_profile_id AS calibration_profile_id,"
"p.sparse_calibration_id AS sparse_calibration_id FROM "
"capture_geometric_states t JOIN "
"optical_calibration_applicabilities_v2 a ON "
"a.optical_configuration_id=t.optical_configuration_id JOIN "
"capture_geometric_states e ON e.capture_id=a.exemplar_capture_id JOIN "
"optical_calibration_profiles p ON "
"p.calibration_profile_id=a.calibration_profile_id AND "
"p.optical_configuration_id=a.optical_configuration_id JOIN "
"sparse_calibrations s ON s.calibration_id=p.sparse_calibration_id WHERE "
"t.capture_id=?1 AND %s UNION SELECT a.applicability_id,"
"a.calibration_profile_id,p.sparse_calibration_id FROM "
"capture_geometric_states t JOIN optical_focus_domains_v2 d ON "
"1=1 JOIN optical_calibration_applicabilities_v2 a ON "
"a.applicability_id=d.applicability_id AND "
"a.optical_configuration_id=t.optical_configuration_id JOIN "
"capture_geometric_states e ON e.capture_id=a.exemplar_capture_id JOIN "
"optical_focus_domain_tokens_v2 k ON "
"k.focus_domain_id=d.focus_domain_id AND "
"k.focus_token=t.focus_observation JOIN optical_calibration_profiles p ON "
"p.calibration_profile_id=a.calibration_profile_id AND "
"p.optical_configuration_id=a.optical_configuration_id JOIN "
"sparse_calibrations s ON s.calibration_id=p.sparse_calibration_id WHERE "
"t.capture_id=?1 AND t.focus_state=2 AND %s) candidates WHERE "
"(?2=0 OR applicability_id=?2) ORDER BY applicability_id LIMIT 2",
exact_state_predicate, exact_nonfocus_state_predicate);
sqlite3_stmt *statement = NULL;
result = length < 0 || (size_t)length >= sizeof(query)
? LARDON3D_PROJECT_DB_IO_ERROR
: prepare(database, query, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)required_applicability);
int code;
*count = 0;
while (*count < 2 && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 2) != SQLITE_INTEGER ||
sqlite3_column_int64(statement, 0) <= 0 ||
sqlite3_column_int64(statement, 1) <= 0 ||
sqlite3_column_int64(statement, 2) <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
if (*count == 0) {
output->applicability_id = (uint64_t)sqlite3_column_int64(statement, 0);
output->calibration_profile_id =
(uint64_t)sqlite3_column_int64(statement, 1);
output->sparse_calibration_id =
(uint64_t)sqlite3_column_int64(statement, 2);
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE && *count < 2)
result =
sqlite_result(database, code, "resolve calibration applicability v2");
}
(void)sqlite3_finalize(statement);
return result;
}
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_resolve_v2(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCalibrationResolutionV2 *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || !optical_id(capture_id) || !output)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
Lardon3DOpticalCaptureGeometricState state;
Lardon3DProjectDbResult result =
lardon3d_optical_capture_geometric_state_load(database, capture_id,
&state);
if (result == LARDON3D_PROJECT_DB_NOT_FOUND) {
/* A real Capture with no observed tuple has no valid applicability. Absence
is not permission to fabricate an unknown/default tuple. */
Lardon3DProjectDbCapture capture;
result = lardon3d_project_db_load_capture(database, capture_id, &capture);
if (result == LARDON3D_PROJECT_DB_OK) {
output->kind = LARDON3D_OPTICAL_CALIBRATION_REQUIRED;
return LARDON3D_PROJECT_DB_OK;
}
}
if (result != LARDON3D_PROJECT_DB_OK)
return result;
if (!geometric_state_complete(&state)) {
output->kind = LARDON3D_OPTICAL_CALIBRATION_REQUIRED;
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DOpticalCaptureCalibrationSelectionV2 selection;
result = lardon3d_optical_capture_calibration_selection_load_v2(
database, capture_id, &selection);
if (result == LARDON3D_PROJECT_DB_OK) {
output->kind = LARDON3D_OPTICAL_CALIBRATION_RESOLVED;
output->applicability_id = selection.applicability_id;
output->calibration_profile_id = selection.calibration_profile_id;
output->sparse_calibration_id = selection.sparse_calibration_id;
return LARDON3D_PROJECT_DB_OK;
}
if (result != LARDON3D_PROJECT_DB_NOT_FOUND)
return result;
(void)pthread_mutex_lock(&database->mutex);
size_t count = 0;
result = applicability_candidates_locked(database, capture_id, 0, output,
&count);
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK) {
memset(output, 0, sizeof(*output));
return result;
}
if (count == 0)
output->kind = LARDON3D_OPTICAL_CALIBRATION_REQUIRED;
else if (count == 1)
output->kind = LARDON3D_OPTICAL_CALIBRATION_RESOLVED;
else {
memset(output, 0, sizeof(*output));
output->kind = LARDON3D_OPTICAL_CALIBRATION_SELECTION_REQUIRED;
}
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult
lardon3d_optical_capture_calibration_select_v2(Lardon3DProjectDb *database,
uint64_t capture_id,
uint64_t applicability_id) {
if (!database || !optical_id(capture_id) || !optical_id(applicability_id))
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DOpticalCalibrationResolutionV2 candidate = {0};
size_t count = 0;
Lardon3DProjectDbResult result = execute(
database, "BEGIN IMMEDIATE", "begin Capture calibration selection v2");
if (result == LARDON3D_PROJECT_DB_OK)
result = applicability_candidates_locked(database, capture_id,
applicability_id, &candidate,
&count);
if (result == LARDON3D_PROJECT_DB_OK && count != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
Lardon3DOpticalCaptureGeometricState state = {0};
if (result == LARDON3D_PROJECT_DB_OK)
result = geometric_state_load_locked(database, capture_id, &state);
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"INSERT OR IGNORE INTO capture_calibration_selections_v2 "
"VALUES(?1,?2,?3,?4)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)applicability_id);
sqlite3_bind_int64(statement, 3,
(sqlite3_int64)candidate.calibration_profile_id);
sqlite3_bind_int64(statement, 4,
(sqlite3_int64)state.optical_configuration_id);
result = step_done(database, statement,
"insert Capture calibration selection v2");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK)
result =
prepare(database,
"SELECT "
"applicability_id,calibration_profile_id,optical_configuration_"
"id FROM capture_calibration_selections_v2 WHERE capture_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW)
result = LARDON3D_PROJECT_DB_CORRUPT;
else if (sqlite3_column_int64(statement, 0) !=
(sqlite3_int64)applicability_id ||
sqlite3_column_int64(statement, 1) !=
(sqlite3_int64)candidate.calibration_profile_id ||
sqlite3_column_int64(statement, 2) !=
(sqlite3_int64)state.optical_configuration_id)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
(void)sqlite3_finalize(statement);
result = optical_commit_or_rollback(
database, result, "commit Capture calibration selection v2",
"rollback Capture calibration selection v2");
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_selection_load_v2(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureCalibrationSelectionV2 *output) {
if (output)
memset(output, 0, sizeof(*output));
if (!database || !optical_id(capture_id) || !output)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT "
"x.capture_id,x.applicability_id,x.calibration_profile_id,x.optical_"
"configuration_id,p.sparse_calibration_id FROM "
"capture_calibration_selections_v2 x LEFT JOIN "
"optical_calibration_applicabilities_v2 a ON "
"a.applicability_id=x.applicability_id AND "
"a.calibration_profile_id=x.calibration_profile_id AND "
"a.optical_configuration_id=x.optical_configuration_id LEFT JOIN "
"optical_calibration_profiles p ON "
"p.calibration_profile_id=x.calibration_profile_id WHERE x.capture_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE)
result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW)
result = sqlite_result(database, code,
"load Capture calibration selection v2");
else {
for (int i = 0; i < 5; ++i)
if (sqlite3_column_type(statement, i) != SQLITE_INTEGER ||
sqlite3_column_int64(statement, i) <= 0)
result = LARDON3D_PROJECT_DB_CORRUPT;
if (result == LARDON3D_PROJECT_DB_OK) {
output->capture_id = (uint64_t)sqlite3_column_int64(statement, 0);
output->applicability_id = (uint64_t)sqlite3_column_int64(statement, 1);
output->calibration_profile_id =
(uint64_t)sqlite3_column_int64(statement, 2);
output->optical_configuration_id =
(uint64_t)sqlite3_column_int64(statement, 3);
output->sparse_calibration_id =
(uint64_t)sqlite3_column_int64(statement, 4);
}
}
}
(void)sqlite3_finalize(statement);
if (result == LARDON3D_PROJECT_DB_OK) {
Lardon3DOpticalCalibrationResolutionV2 candidate = {0};
size_t count = 0;
result = applicability_candidates_locked(
database, capture_id, output->applicability_id, &candidate, &count);
if (result == LARDON3D_PROJECT_DB_OK &&
(count != 1 ||
candidate.calibration_profile_id != output->calibration_profile_id ||
candidate.sparse_calibration_id != output->sparse_calibration_id))
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK)
memset(output, 0, sizeof(*output));
return result;
}

Some files were not shown because too many files have changed in this diff Show more