From 8fee7755868e3efa9b6861fedc9f50e5d4e4151c Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 3 Sep 2026 12:51:15 +0200 Subject: [PATCH] feat: add calibration v2 optical state foundation --- README.md | 5 +- docs/README.md | 7 +- docs/architecture/project_database.md | 53 ++- docs/product/product_definition.md | 2 +- docs/roadmap/roadmap.md | 14 +- include/lardon3d/optical_profiles.h | 105 +++++ include/lardon3d/project_db.h | 8 +- prompt.md | 2 +- prompt/02_CURRENT_FROZEN_STATE.md | 8 +- prompt/12_CALIBRATION.md | 5 +- prompt/31_IMPLEMENTATION_ORDER.md | 37 +- src/optical_profiles.c | 639 ++++++++++++++++++++++++++ src/project_db.c | 108 ++++- tests/test_optical_profiles.c | 281 +++++++++++ 14 files changed, 1225 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 22e9a55..70821df 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ progressive observations and constraints ### Current Project Database -The current Project DB schema is **v25**. +The current Project DB schema is **v26**. The current head is additive: @@ -39,6 +39,7 @@ 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 and exact calibration applicability ``` Earlier schema versions remain valid historical contracts where their own documentation says so. @@ -120,7 +121,7 @@ The following major foundations are implemented and validated at their documente - **Calibration Solver Preflight v1 — PASS** - **Calibration Evidence Solver v1 — IMPLEMENTED / VALIDATED** - **Calibration Tooling planarity alignment — PASS / FROZEN** -- **Project DB v24/v25 operational overlays — IMPLEMENTED / VALIDATED** +- **Project DB v24/v25/v26 operational overlays — IMPLEMENTED / VALIDATED** - raw.develop.batch/1 durable selected-execution path - features.extract.batch/1 durable selected-execution path diff --git a/docs/README.md b/docs/README.md index 966abf9..a645603 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,14 +4,14 @@ ```text DOCUMENTATION_INDEX=CURRENT -CURRENT_PROJECT_DB_SCHEMA=v25 +CURRENT_PROJECT_DB_SCHEMA=v26 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=CALIBRATION_WORKFLOW_COORDINATOR +CURRENT_NEXT=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION ``` This index separates current authority, historical evidence and future product-definition work. @@ -35,13 +35,14 @@ quote an exact API, DDL or constant. ## Current repository state ```text -Project DB head v25 +Project DB head v26 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 ``` Canonical resource objective: diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index 3b68031..8c1c45a 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -7,7 +7,7 @@ span schema versions. Historical sections remain authoritative for the version t present-tense statements in this document use the current schema head. ```text -CURRENT_PROJECT_DB_SCHEMA=v25 +CURRENT_PROJECT_DB_SCHEMA=v26 ``` The current head is additive: @@ -17,10 +17,11 @@ v22 Selected scientific execution foundation PASS / FROZEN v23 Generic optical-context overlay IMPLEMENTED / VALIDATED / REVIEWED v24 raw.develop.batch/1 persistence IMPLEMENTED / VALIDATED v25 features.extract.batch/1 persistence IMPLEMENTED / VALIDATED +v26 Capture geometric state and exact calibration applicability PASS / FROZEN ``` -Project DB v25 preserves the scientific and persistence meaning of every earlier retained row. No -migration from v22 through v25 backfills scientific identity, infers camera/lens identity, invents +Project DB v26 preserves the scientific and persistence meaning of every earlier retained row. No +migration from v22 through v26 backfills scientific identity, infers camera/lens identity, invents calibration, rewrites Capture identity, or changes historical Task payload interpretation. The current real A6000 pre-SfM proof completed the v24 RAW-batch and v25 Feature-batch paths before @@ -201,6 +202,37 @@ Match Results 38,420 No acquisition, RAW development, Candidate generation or Matcher replay was required during the final A6000 GV/Tracks continuation. +### Project DB v26 - Capture geometric state and exact calibration applicability + +**PASS / FROZEN.** + +The v25 -> v26 migration is transactional and additive. It creates empty +`capture_geometric_states`, `optical_calibration_applicabilities_v2` and +`capture_calibration_selections_v2` tables. It creates no observed state, +applicability, selection, calibration profile or scientific identity for a +historical row. + +`capture_geometric_states` is Capture-owned evidence, not Capture identity. It +references the existing exact v23 body/lens/focal configuration and stores a +bounded, versioned tuple of geometry-relevant observations: focus, aperture, +stabilization, crop, geometric pipeline, representation and decoded geometry, +with explicit metadata or caller-explicit provenance. Unknown is durable but +never a default or a compatible value. + +A v26 applicability binds an existing immutable calibration profile to one +complete exemplar state. A target Capture is compatible only when its exact +configuration and every state field match the exemplar. There is no +cross-body/lens/focal substitution, nearby-focus borrowing, interpolation or +extrapolation. An incomplete target is `CALIBRATION_REQUIRED`; zero, one and +multiple complete candidates resolve respectively to `CALIBRATION_REQUIRED`, +resolved and `SELECTION_REQUIRED`. An explicit v26 selection is immutable, +idempotent on exact retry, and rejected unless the selected applicability is +exactly compatible. + +The v1 optical-profile and calibration-selection APIs/tables remain unchanged. +The v26 tables are a separate additive foundation; they do not attach a Sparse +calibration scope or transition a selected execution to `READY`. + ## Optical TUI workflow The optical TUI uses bounded public APIs and does not write SQLite directly. It can: @@ -860,6 +892,7 @@ v21 -> v22 selected scientific execution + single RAW Task v22 -> v23 generic optical-context overlay v23 -> v24 selected RAW-batch Task persistence v24 -> v25 selected Feature-batch Task persistence +v25 -> v26 Capture geometric state + exact calibration applicability ``` Historical version-specific contracts remain valid for the rows and checkpoints they describe. @@ -867,9 +900,9 @@ An older version number is not stale when the text explicitly describes historic ## Opening and migration -An empty Project DB is created and migrated through the complete known chain to v25. +An empty Project DB is created and migrated through the complete known chain to v26. -A supported historical DB is migrated sequentially to v25. Each migration is transactional. A +A supported historical DB is migrated sequentially to v26. Each migration is transactional. A migration failure rolls back both newly created objects and the schema-version marker for that step, leaving the prior version complete and retryable. @@ -880,7 +913,7 @@ The implementation rejects: - impossible version/storage-class combinations; - malformed required durable relationships. -The migration implementation must recognize only the known sequential range through v25. It must not +The migration implementation must recognize only the known sequential range through v26. It must not skip an intermediate contract. Important rollback properties retained from historical tests include: @@ -893,6 +926,7 @@ Important rollback properties retained from historical tests include: - v23 failure leaves no partial optical overlay and a true v22; - v24 failure leaves no partial RAW-batch table/marker and a true v23; - v25 failure leaves no partial Feature-batch table/marker and a true v24. +- v26 failure leaves no partial geometric-state/applicability tables or marker and a true v25. Exact executable migration SQL is owned by `src/project_db.c`. Documentation may summarize it, but must not contradict the source or the FROZEN migration tests. @@ -1055,19 +1089,20 @@ must not be silently reinterpreted as a scientific dataset limit. ## Status summary ```text -CURRENT_PROJECT_DB_SCHEMA=v25 +CURRENT_PROJECT_DB_SCHEMA=v26 v22 selected scientific execution foundation PASS/FROZEN v23 generic optical-context overlay IMPLEMENTED/VALIDATED/REVIEWED v24 raw.develop.batch/1 persistence IMPLEMENTED/VALIDATED v25 features.extract.batch/1 persistence IMPLEMENTED/VALIDATED +v26 Capture geometric state/applicability PASS/FROZEN REAL_S21_TRACKS PASS/FROZEN REAL_A6000_PRE_SFM PASS/FROZEN ``` Current Project DB opens and migrates supported historical databases through the sequential known -chain to v25. +chain to v26. Historical contracts for Candidate Pair, Matcher, Geometric Verification, Tracks, Sparse SfM, Phase H, Capture/Asset Provenance, campaign execution, Photo Quality and selected scientific @@ -1084,4 +1119,4 @@ No current Project DB migration: - creates a generic dependency DAG; - persists active Resource Governor reservations. -Future schema changes beyond v25 require explicit human authorization. +Future schema changes beyond v26 require explicit human authorization. diff --git a/docs/product/product_definition.md b/docs/product/product_definition.md index 89e6cf5..34fe6e9 100644 --- a/docs/product/product_definition.md +++ b/docs/product/product_definition.md @@ -111,7 +111,7 @@ This document may freeze a product requirement while its implementation remains The following existing boundaries are consumed as-is: ```text -Project DB head v25 +Project DB head v26 Production Task kinds 16 Capture / Asset Provenance PASS/FROZEN Acquisition / campaign execution PASS/FROZEN diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 6d3afd7..a929613 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -11,7 +11,7 @@ It must not be used to reinterpret FROZEN scientific evidence. ## Current repository state ```text -Project DB current schema v25 +Project DB current schema v26 Production Task kinds 16 GLOBAL_MAINTENANCE_AUDIT PASS/FROZEN REAL_S21_TRACKS PASS/FROZEN @@ -24,7 +24,7 @@ SOURCE_COMMENT_AUDIT PASS PRODUCT_DEFINITION PASS/FROZEN PROMPT_TREE CURRENT USER_FACING_UI_LANGUAGE_NORMALIZATION PASS -CURRENT_NEXT CALIBRATION_V2_HETEROGENEOUS_OPTICS_FOUNDATION +CURRENT_NEXT CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION ``` The current Project DB head is additive: @@ -34,6 +34,7 @@ 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 + exact calibration applicability ``` Historical references to earlier versions remain valid when they describe the state of their own @@ -529,9 +530,12 @@ silent calibration substitution remains forbidden ``` The current Sparse calibration scope already models membership as `image_id -> calibration_id`; the -next dependency is the additive durable heterogeneous-optics/applicability foundation, which must -reuse that capability where it proves sufficient. Device-specific autofocus envelopes remain blocked -until physical evidence validates them. +v26 heterogeneous-optics foundation now durably records Capture geometric state and exact +applicability while retaining that per-image scope model. Unknown state remains retained but +`CALIBRATION_REQUIRED`, and exact compatibility resolves none/one/many candidates to +`CALIBRATION_REQUIRED`/resolved/`SELECTION_REQUIRED`. The next dependency is heterogeneous +calibration publication. Device-specific autofocus envelopes remain blocked until physical evidence +validates them. Calibration Tooling v1 consumes an already acquired Science v1 evidence bundle, validates the bounded contract and produces deterministic `L3DCALB1` v1. diff --git a/include/lardon3d/optical_profiles.h b/include/lardon3d/optical_profiles.h index c7b23f1..4bad529 100644 --- a/include/lardon3d/optical_profiles.h +++ b/include/lardon3d/optical_profiles.h @@ -119,6 +119,73 @@ typedef struct { uint64_t sparse_calibration_id; } 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 { + 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 * 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 @@ -247,6 +314,44 @@ Lardon3DProjectDbResult lardon3d_optical_capture_calibration_selection_load( Lardon3DProjectDb *database, uint64_t capture_id, 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); + +/* Resolution counts exact valid applicability rows. 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 exactly + * compatible applicability; 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 } #endif diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index 5c22b86..9b859d2 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -11,10 +11,10 @@ #include enum { - /* v25 adds only durable features.extract.batch/1 operational state. The - * selected execution remains the immutable ordered image domain; migration - * creates no batch association, cursor, Feature Set, or inferred identity. */ - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 25, + /* v26 additively stores Capture-owned observed geometric state and exact + * calibration applicability. Migration does not infer state from v23 + * configuration or historical calibration selections. */ + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 26, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, diff --git a/prompt.md b/prompt.md index 605c15e..75ae710 100644 --- a/prompt.md +++ b/prompt.md @@ -79,7 +79,7 @@ Do not return after each file. Do not repeat global A-to-Z audits when unchanged This contract was prepared against the repository state that declares: ```text -CURRENT_PROJECT_DB_SCHEMA=v25 +CURRENT_PROJECT_DB_SCHEMA=v26 PRODUCTION_TASK_KINDS=16 PRODUCT_DEFINITION_V1=PASS/FROZEN PROMPT_TREE=NEXT diff --git a/prompt/02_CURRENT_FROZEN_STATE.md b/prompt/02_CURRENT_FROZEN_STATE.md index 9416a39..968c186 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -3,10 +3,10 @@ ## Status ```text -CURRENT_PROJECT_DB_SCHEMA=v25 +CURRENT_PROJECT_DB_SCHEMA=v26 PRODUCTION_TASK_KINDS=16 USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS -CURRENT_IMPLEMENTATION_CURSOR=CALIBRATION_V2_HETEROGENEOUS_OPTICS_FOUNDATION +CURRENT_IMPLEMENTATION_CURSOR=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION ``` ## Authority @@ -16,7 +16,7 @@ CURRENT_IMPLEMENTATION_CURSOR=CALIBRATION_V2_HETEROGENEOUS_OPTICS_FOUNDATION ## CURRENT ```text -Project DB head v25 +Project DB head v26 Production Task kinds 16 Feature Store IMPLEMENTED Visual Index IMPLEMENTED @@ -36,6 +36,7 @@ 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 Adaptive capture settings semantics PLANNED Autofocus v2 foundation PLANNED Calibration Tooling planarity alignment PASS/FROZEN @@ -48,6 +49,7 @@ 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 diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index 11ab0af..0cd5c72 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -18,10 +18,11 @@ 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=PLANNED +CALIBRATION_V2_HETEROGENEOUS_OPTICS=PASS/FROZEN +CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PLANNED ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PLANNED AUTOFOCUS_V2_FOUNDATION=PLANNED -CURRENT_CALIBRATION_NEXT=CALIBRATION_V2_HETEROGENEOUS_OPTICS_FOUNDATION +CURRENT_CALIBRATION_NEXT=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION ``` ## Authority diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index 3ca2f4a..60efe3a 100644 --- a/prompt/31_IMPLEMENTATION_ORDER.md +++ b/prompt/31_IMPLEMENTATION_ORDER.md @@ -6,7 +6,7 @@ IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN IMPLEMENTATION_AUTHORIZATION=NO STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS -CURRENT_NEXT=CALIBRATION_V2_HETEROGENEOUS_OPTICS_FOUNDATION +CURRENT_NEXT=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION ``` ## Authority @@ -22,23 +22,24 @@ 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 / Tooling / Bootstrap / workflow foundation — CURRENT; -4. physical autofocus/optical applicability validation and dedicated calibrated real campaign; -5. real Sparse SfM proof; -6. durable Dense/OpenMVS orchestration; -7. mesh / refinement / texturing / export; -8. viewer foundation; -9. offline Coverage Analysis scientific contract and implementation; -10. multi-campaign registration / fusion; -11. generic live acquisition adapter foundation; -12. A6000 HDMI integration; -13. S21 integration; -14. live camera localization; -15. live coverage overlay; -16. actionable Capture Guidance; -17. video ingestion / deterministic keyframes; -18. final integration, UX, restart and performance proof; -19. Product Definition v1 Definition-of-Done closure. +3. Calibration v2 heterogeneous-optics persistence foundation — PASS/FROZEN; +4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — CURRENT; +5. physical autofocus/optical applicability validation and dedicated calibrated real campaign; +6. real Sparse SfM proof; +7. durable Dense/OpenMVS orchestration; +8. mesh / refinement / texturing / export; +9. viewer foundation; +10. offline Coverage Analysis scientific contract and implementation; +11. multi-campaign registration / fusion; +12. generic live acquisition adapter foundation; +13. A6000 HDMI integration; +14. S21 integration; +15. live camera localization; +16. live coverage overlay; +17. actionable Capture Guidance; +18. video ingestion / deterministic keyframes; +19. final integration, UX, restart and performance proof; +20. Product Definition v1 Definition-of-Done closure. ## Adjustment rule diff --git a/src/optical_profiles.c b/src/optical_profiles.c index 231b6f9..5d7881e 100644 --- a/src/optical_profiles.c +++ b/src/optical_profiles.c @@ -2062,3 +2062,642 @@ lardon3d_optical_capture_calibration_selection_load( memset(output, 0, sizeof(*output)); 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"; + +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 Lardon3DProjectDbResult +exact_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; + + char query[1800]; + int length = snprintf( + query, sizeof(query), + "SELECT " + "a.applicability_id,a.calibration_profile_id,p.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%s ORDER BY a.applicability_id LIMIT 2", + exact_state_predicate, + required_applicability ? " AND a.applicability_id=?2" : ""); + 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); + if (required_applicability) + 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 = exact_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 = exact_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 = exact_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; +} diff --git a/src/project_db.c b/src/project_db.c index ce4cc6f..b84c253 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -882,6 +882,84 @@ static const char schema_feature_extract_batch_v25[] = "parameter_fingerprint BLOB NOT NULL CHECK(typeof(parameter_fingerprint)='blob' AND " "length(parameter_fingerprint)=32));"; +/* PERSISTENCE CONTRACT v26: state is immutable Capture-owned evidence. An + applicability uses an exemplar only as the exact tuple source; the Capture + ID is not calibration identity. DDL-only migration fabricates no evidence. */ +static const char schema_optical_geometric_state_v26[] = + "CREATE TABLE capture_geometric_states(" + "capture_id INTEGER PRIMARY KEY REFERENCES captures(capture_id) ON DELETE " + "CASCADE," + "optical_configuration_id INTEGER NOT NULL,state_version INTEGER NOT NULL " + "CHECK(state_version>0 AND state_version<=4294967295)," + "provenance INTEGER NOT NULL CHECK(provenance IN(1,2)),focus_state INTEGER " + "NOT NULL CHECK(focus_state IN(1,2))," + "focus_observation TEXT NOT NULL CHECK(typeof(focus_observation)='text' " + "AND length(focus_observation)<128 AND ((focus_state=1 AND " + "length(focus_observation)=0) OR (focus_state=2 AND " + "length(focus_observation)>0)))," + "aperture_state INTEGER NOT NULL CHECK(aperture_state " + "IN(1,2)),aperture_x1000 INTEGER NOT NULL CHECK(aperture_x1000>=0 AND " + "aperture_x1000<=4294967295 AND ((aperture_state=1 AND aperture_x1000=0) " + "OR (aperture_state=2 AND aperture_x1000>0)))," + "stabilization INTEGER NOT NULL CHECK(stabilization IN(1,2,3)),crop_state " + "INTEGER NOT NULL CHECK(crop_state IN(1,2))," + "crop_observation TEXT NOT NULL CHECK(typeof(crop_observation)='text' AND " + "length(crop_observation)<128 AND ((crop_state=1 AND " + "length(crop_observation)=0) OR (crop_state=2 AND " + "length(crop_observation)>0)))," + "pipeline_state INTEGER NOT NULL CHECK(pipeline_state " + "IN(1,2)),pipeline_observation TEXT NOT NULL " + "CHECK(typeof(pipeline_observation)='text' AND " + "length(pipeline_observation)<128 AND ((pipeline_state=1 AND " + "length(pipeline_observation)=0) OR (pipeline_state=2 AND " + "length(pipeline_observation)>0)))," + "representation_state INTEGER NOT NULL CHECK(representation_state " + "IN(1,2)),representation_observation TEXT NOT NULL " + "CHECK(typeof(representation_observation)='text' AND " + "length(representation_observation)<128 AND ((representation_state=1 AND " + "length(representation_observation)=0) OR (representation_state=2 AND " + "length(representation_observation)>0)))," + "decoded_geometry_state INTEGER NOT NULL CHECK(decoded_geometry_state " + "IN(1,2)),decoded_width INTEGER NOT NULL CHECK(decoded_width>=0 AND " + "decoded_width<=4294967295),decoded_height INTEGER NOT NULL " + "CHECK(decoded_height>=0 AND decoded_height<=4294967295)," + "CHECK((decoded_geometry_state=1 AND decoded_width=0 AND decoded_height=0) " + "OR (decoded_geometry_state=2 AND decoded_width>0 AND decoded_height>0))," + "UNIQUE(capture_id,optical_configuration_id),FOREIGN " + "KEY(capture_id,optical_configuration_id) REFERENCES " + "capture_optical_configurations(capture_id,optical_configuration_id));" + "CREATE INDEX capture_geometric_states_exact_idx ON " + "capture_geometric_states(optical_configuration_id,state_version,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);" + "CREATE TABLE optical_calibration_applicabilities_v2(applicability_id " + "INTEGER PRIMARY KEY AUTOINCREMENT " + "CHECK(applicability_id>0),calibration_profile_id INTEGER NOT " + "NULL,optical_configuration_id INTEGER NOT NULL,exemplar_capture_id " + "INTEGER NOT NULL," + "UNIQUE(calibration_profile_id,exemplar_capture_id),UNIQUE(applicability_" + "id,calibration_profile_id,optical_configuration_id)," + "FOREIGN KEY(calibration_profile_id,optical_configuration_id) REFERENCES " + "optical_calibration_profiles(calibration_profile_id,optical_configuration_" + "id)," + "FOREIGN KEY(exemplar_capture_id,optical_configuration_id) REFERENCES " + "capture_geometric_states(capture_id,optical_configuration_id));" + "CREATE INDEX optical_calibration_applicabilities_v2_config_idx ON " + "optical_calibration_applicabilities_v2(optical_configuration_id," + "applicability_id);" + "CREATE TABLE capture_calibration_selections_v2(capture_id INTEGER PRIMARY " + "KEY,applicability_id INTEGER NOT NULL,calibration_profile_id INTEGER NOT " + "NULL,optical_configuration_id INTEGER NOT NULL," + "FOREIGN KEY(capture_id,optical_configuration_id) REFERENCES " + "capture_geometric_states(capture_id,optical_configuration_id)," + "FOREIGN " + "KEY(applicability_id,calibration_profile_id,optical_configuration_id) " + "REFERENCES " + "optical_calibration_applicabilities_v2(applicability_id,calibration_" + "profile_id,optical_configuration_id));"; + static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { if (destination) { (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); @@ -975,7 +1053,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int from_version != 12 && from_version != 13 && from_version != 14 && from_version != 15 && from_version != 16 && from_version != 17 && from_version != 18 && from_version != 19 && from_version != 20 && from_version != 21 && from_version != 22 && - from_version != 23 && from_version != 24) { + from_version != 23 && from_version != 24 && from_version != 25) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -1486,6 +1564,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int result = LARDON3D_PROJECT_DB_CORRUPT; } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 26) { + /* Historical rows do not contain focus/pipeline/decode observations, so + migration is strictly additive DDL and leaves every v2 table empty. */ + result = execute(database, schema_optical_geometric_state_v26, + "migrate optical geometric state v25 to v26"); + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, + "UPDATE metadata SET value=26 WHERE " + "key='schema_version' AND value=25", + "finish schema v26 migration"); + if (result == LARDON3D_PROJECT_DB_OK && + sqlite3_changes(database->connection) != 1) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -1641,6 +1734,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "capture_optical_configurations", "optical_calibration_profiles", "capture_calibration_selections"}; + const char *required_v26[] = {"capture_geometric_states", + "optical_calibration_applicabilities_v2", + "capture_calibration_selections_v2"}; for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && result == LARDON3D_PROJECT_DB_OK; ++index) { @@ -1649,6 +1745,16 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje result = LARDON3D_PROJECT_DB_CORRUPT; } } + for (size_t index = 0; + index < sizeof(required_v26) / sizeof(required_v26[0]) && + result == LARDON3D_PROJECT_DB_OK; + ++index) { + if (!table_exists(database->connection, required_v26[index])) { + copy_error(database->error, + "Current Project DB v26 schema is incomplete."); + result = LARDON3D_PROJECT_DB_CORRUPT; + } + } } if (result != LARDON3D_PROJECT_DB_OK) { copy_error(error, database->error); diff --git a/tests/test_optical_profiles.c b/tests/test_optical_profiles.c index d8b03f0..6c06fb2 100644 --- a/tests/test_optical_profiles.c +++ b/tests/test_optical_profiles.c @@ -1147,6 +1147,9 @@ static bool downgrade_to_v22_fixture(const char *path) { return raw_sql( path, "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS capture_calibration_selections_v2;" + "DROP TABLE IF EXISTS optical_calibration_applicabilities_v2;" + "DROP TABLE IF EXISTS capture_geometric_states;" "DROP TABLE IF EXISTS feature_extract_batch_tasks;" "DROP TABLE raw_development_batch_tasks;" "DROP TABLE capture_calibration_selections;" @@ -1160,6 +1163,275 @@ static bool downgrade_to_v22_fixture(const char *path) { "UPDATE metadata SET value=22 WHERE key='schema_version';COMMIT;"); } +static bool test_v26_exact_geometric_applicability(void) { + char directory[64]; + char path[256]; + CHECK(make_database_path(directory, path)); + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, &database, error) == + LARDON3D_PROJECT_DB_OK); + RepairOpticalFixture fixture; + CHECK(seed_repair_optical_fixture(database, &fixture)); + CHECK(lardon3d_optical_capture_assign_explicit(database, fixture.capture_id, + fixture.configuration_id) == + LARDON3D_PROJECT_DB_OK); + + Lardon3DProjectDbCapture peer; + Lardon3DProjectDbCapture alternate; + CHECK(lardon3d_project_db_create_capture(database, fixture.scanset_id, 2, + &peer) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_capture(database, fixture.scanset_id, 3, + &alternate) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_assign_explicit(database, peer.capture_id, + fixture.configuration_id) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_assign_explicit( + database, alternate.capture_id, + fixture.alternate_configuration_id) == LARDON3D_PROJECT_DB_OK); + + Lardon3DOpticalCalibrationResolutionV2 resolution; + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, fixture.capture_id, &resolution) == + LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_REQUIRED); + + /* Positive applicability fixtures must fully observe every field capable of + changing geometry; UNKNOWN is exercised separately below. */ + Lardon3DOpticalCaptureGeometricState state = { + .capture_id = fixture.capture_id, + .optical_configuration_id = fixture.configuration_id, + .state_version = 1, + .provenance = LARDON3D_OPTICAL_GEOMETRIC_STATE_METADATA, + .focus_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .aperture_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .aperture_x1000 = 5600, + .stabilization = LARDON3D_OPTICAL_STABILIZATION_OFF, + .crop_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .pipeline_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .representation_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .decoded_geometry_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .decoded_width = 6000, + .decoded_height = 4000, + }; + memcpy(state.focus_observation, "af-s", sizeof("af-s")); + memcpy(state.crop_observation, "full-sensor", sizeof("full-sensor")); + memcpy(state.pipeline_observation, "raw-policy-v1", sizeof("raw-policy-v1")); + memcpy(state.representation_observation, "L3DRAWD1-png", + sizeof("L3DRAWD1-png")); + Lardon3DOpticalCaptureGeometricState stored; + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &state, &stored) == LARDON3D_PROJECT_DB_OK); + CHECK(stored.focus_state == LARDON3D_OPTICAL_OBSERVATION_OBSERVED && + strcmp(stored.focus_observation, "af-s") == 0); + Lardon3DOpticalCaptureGeometricState retry; + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &state, &retry) == LARDON3D_PROJECT_DB_OK && + retry.capture_id == state.capture_id); + state.decoded_width = 5999; + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &state, &retry) == LARDON3D_PROJECT_DB_CONSTRAINT); + state.decoded_width = 6000; + + state.capture_id = peer.capture_id; + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &state, &stored) == LARDON3D_PROJECT_DB_OK); + state.capture_id = alternate.capture_id; + state.optical_configuration_id = fixture.alternate_configuration_id; + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &state, &stored) == LARDON3D_PROJECT_DB_OK); + state.optical_configuration_id = fixture.configuration_id; + + Lardon3DSparseCalibration calibration; + CHECK(create_sparse_calibration(database, 0xa6, 6000, 4000, &calibration)); + Lardon3DOpticalCalibrationProfile profile_input = { + .optical_configuration_id = fixture.configuration_id, + .sparse_calibration_id = calibration.calibration_id, + .profile_version = 2, + .applicability = LARDON3D_OPTICAL_CALIBRATION_EXACT_CONFIGURATION, + .created_at = 100, + }; + memcpy(profile_input.name, "v26 exact A", sizeof("v26 exact A")); + memcpy(profile_input.provenance, "v26 test", sizeof("v26 test")); + Lardon3DOpticalCalibrationProfile profile_a; + CHECK(lardon3d_optical_calibration_profile_create( + database, &profile_input, &profile_a) == LARDON3D_PROJECT_DB_OK); + memcpy(profile_input.name, "v26 exact B", sizeof("v26 exact B")); + Lardon3DOpticalCalibrationProfile profile_b; + CHECK(lardon3d_optical_calibration_profile_create( + database, &profile_input, &profile_b) == LARDON3D_PROJECT_DB_OK); + + Lardon3DProjectDbCapture incomplete_exemplar; + CHECK(lardon3d_project_db_create_capture(database, fixture.scanset_id, 11, + &incomplete_exemplar) == + LARDON3D_PROJECT_DB_OK && + lardon3d_optical_capture_assign_explicit( + database, incomplete_exemplar.capture_id, + fixture.configuration_id) == LARDON3D_PROJECT_DB_OK); + Lardon3DOpticalCaptureGeometricState incomplete_state = state; + incomplete_state.capture_id = incomplete_exemplar.capture_id; + incomplete_state.focus_state = LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + incomplete_state.focus_observation[0] = '\0'; + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &incomplete_state, &stored) == LARDON3D_PROJECT_DB_OK); + Lardon3DOpticalCalibrationApplicabilityV2 rejected_applicability; + CHECK(lardon3d_optical_calibration_applicability_v2_create( + database, profile_a.calibration_profile_id, + incomplete_exemplar.capture_id, &rejected_applicability) == + LARDON3D_PROJECT_DB_CONSTRAINT && + rejected_applicability.applicability_id == 0); + + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, peer.capture_id, &resolution) == LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_REQUIRED); + Lardon3DOpticalCalibrationApplicabilityV2 applicability_a; + CHECK(lardon3d_optical_calibration_applicability_v2_create( + database, profile_a.calibration_profile_id, fixture.capture_id, + &applicability_a) == LARDON3D_PROJECT_DB_OK); + Lardon3DOpticalCalibrationApplicabilityV2 applicability_retry; + CHECK(lardon3d_optical_calibration_applicability_v2_create( + database, profile_a.calibration_profile_id, fixture.capture_id, + &applicability_retry) == LARDON3D_PROJECT_DB_OK && + applicability_retry.applicability_id == + applicability_a.applicability_id); + + for (uint32_t unknown_field = 0; unknown_field < 7; ++unknown_field) { + Lardon3DProjectDbCapture unresolved; + CHECK(lardon3d_project_db_create_capture( + database, fixture.scanset_id, 4 + unknown_field, &unresolved) == + LARDON3D_PROJECT_DB_OK && + lardon3d_optical_capture_assign_explicit( + database, unresolved.capture_id, fixture.configuration_id) == + LARDON3D_PROJECT_DB_OK); + Lardon3DOpticalCaptureGeometricState unresolved_state = state; + unresolved_state.capture_id = unresolved.capture_id; + if (unknown_field == 0) { + unresolved_state.focus_state = LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + unresolved_state.focus_observation[0] = '\0'; + } else if (unknown_field == 1) { + unresolved_state.aperture_state = LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + unresolved_state.aperture_x1000 = 0; + } else if (unknown_field == 2) { + unresolved_state.stabilization = LARDON3D_OPTICAL_STABILIZATION_UNKNOWN; + } else if (unknown_field == 3) { + unresolved_state.crop_state = LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + unresolved_state.crop_observation[0] = '\0'; + } else if (unknown_field == 4) { + unresolved_state.pipeline_state = LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + unresolved_state.pipeline_observation[0] = '\0'; + } else if (unknown_field == 5) { + unresolved_state.representation_state = + LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + unresolved_state.representation_observation[0] = '\0'; + } else { + unresolved_state.decoded_geometry_state = + LARDON3D_OPTICAL_OBSERVATION_UNKNOWN; + unresolved_state.decoded_width = 0; + unresolved_state.decoded_height = 0; + } + CHECK(lardon3d_optical_capture_geometric_state_create( + database, &unresolved_state, &stored) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, unresolved.capture_id, &resolution) == + LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_REQUIRED); + CHECK(lardon3d_optical_capture_calibration_select_v2( + database, unresolved.capture_id, + applicability_a.applicability_id) == + LARDON3D_PROJECT_DB_CONSTRAINT); + Lardon3DOpticalCaptureCalibrationSelectionV2 unresolved_selection; + CHECK(lardon3d_optical_capture_calibration_selection_load_v2( + database, unresolved.capture_id, &unresolved_selection) == + LARDON3D_PROJECT_DB_NOT_FOUND && + unresolved_selection.capture_id == 0); + } + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, peer.capture_id, &resolution) == LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_RESOLVED && + resolution.calibration_profile_id == profile_a.calibration_profile_id); + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, alternate.capture_id, &resolution) == + LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_REQUIRED); + + Lardon3DOpticalCalibrationApplicabilityV2 applicability_b; + CHECK(lardon3d_optical_calibration_applicability_v2_create( + database, profile_b.calibration_profile_id, fixture.capture_id, + &applicability_b) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, peer.capture_id, &resolution) == LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_SELECTION_REQUIRED); + CHECK(lardon3d_optical_capture_calibration_select_v2( + database, peer.capture_id, applicability_a.applicability_id) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_calibration_select_v2( + database, peer.capture_id, applicability_a.applicability_id) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_calibration_select_v2( + database, peer.capture_id, applicability_b.applicability_id) == + LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_optical_capture_calibration_select_v2( + database, alternate.capture_id, applicability_a.applicability_id) == + LARDON3D_PROJECT_DB_CONSTRAINT); + Lardon3DOpticalCaptureCalibrationSelectionV2 selection; + CHECK(lardon3d_optical_capture_calibration_selection_load_v2( + database, peer.capture_id, &selection) == LARDON3D_PROJECT_DB_OK && + selection.calibration_profile_id == profile_a.calibration_profile_id && + selection.sparse_calibration_id == calibration.calibration_id); + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, peer.capture_id, &resolution) == LARDON3D_PROJECT_DB_OK && + resolution.kind == LARDON3D_OPTICAL_CALIBRATION_RESOLVED && + resolution.applicability_id == applicability_a.applicability_id); + + lardon3d_project_db_close(database); + char corruption[256]; + int corruption_bytes = snprintf( + corruption, sizeof(corruption), + "PRAGMA foreign_keys=OFF;DELETE FROM capture_geometric_states WHERE " + "capture_id=%llu;", + (unsigned long long)fixture.capture_id); + CHECK(corruption_bytes > 0 && + (size_t)corruption_bytes < sizeof(corruption) && + raw_sql(path, corruption)); + database = NULL; + CHECK(lardon3d_project_db_open(path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_capture_calibration_resolve_v2( + database, peer.capture_id, &resolution) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(database); + /* A v25 project contains no durable observation from which v26 state could + be inferred. Re-migration therefore recreates only empty additive tables. + */ + CHECK(raw_sql( + path, "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE capture_calibration_selections_v2;" + "DROP TABLE optical_calibration_applicabilities_v2;" + "DROP TABLE capture_geometric_states;" + "UPDATE metadata SET value=25 WHERE key='schema_version';COMMIT;")); + database = NULL; + CHECK(lardon3d_project_db_open(path, &database, error) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + sqlite3_int64 migrated_count = -1; + CHECK(raw_integer(path, "SELECT COUNT(*) FROM capture_geometric_states", + &migrated_count) && + migrated_count == 0); + CHECK(raw_integer( + path, "SELECT COUNT(*) FROM optical_calibration_applicabilities_v2", + &migrated_count) && + migrated_count == 0); + CHECK(raw_integer(path, + "SELECT COUNT(*) FROM capture_calibration_selections_v2", + &migrated_count) && + migrated_count == 0); + CHECK(unlink(path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + static bool test_migration_rollback_retry_and_equivalence(void) { char migrated_directory[64]; char migrated_path[256]; @@ -1232,6 +1504,9 @@ static bool test_migration_rollback_retry_and_equivalence(void) { "capture_optical_configurations", "optical_calibration_profiles", "capture_calibration_selections", + "capture_geometric_states", + "optical_calibration_applicabilities_v2", + "capture_calibration_selections_v2", }; char query[256]; for (size_t index = 0; index < sizeof(empty_tables) / sizeof(empty_tables[0]); @@ -1263,6 +1538,11 @@ static bool test_migration_rollback_retry_and_equivalence(void) { "optical_calibration_profiles_config_idx", "optical_calibration_profiles_sparse_idx", "capture_calibration_selections", + "capture_geometric_states", + "capture_geometric_states_exact_idx", + "optical_calibration_applicabilities_v2", + "optical_calibration_applicabilities_v2_config_idx", + "capture_calibration_selections_v2", }; char migrated_sql[8192]; char fresh_sql[8192]; @@ -1283,6 +1563,7 @@ static bool test_migration_rollback_retry_and_equivalence(void) { int main(void) { return test_profiles_assignments_and_calibrations() && + test_v26_exact_geometric_applicability() && test_calibration_conflict_result_contract() && test_calibration_dependency_corruption_precedence() && test_campaign_request_corruption_prevents_optics_mutation() &&