diff --git a/docs/README.md b/docs/README.md index 79ea6eb..7689472 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ REAL_S21_TRACKS=PASS/FROZEN REAL_A6000_PRE_SFM=PASS/FROZEN PRODUCT_DEFINITION_V1=PASS/FROZEN PROMPT_TREE=CURRENT -CURRENT_NEXT=CALIBRATION_V2_WORKFLOW_READY +CURRENT_NEXT=AUTOFOCUS_V2_FOUNDATION ``` This index separates current authority, historical evidence and future product-definition work. diff --git a/docs/architecture/calibration_publication_v2.md b/docs/architecture/calibration_publication_v2.md index baf1289..1cd239e 100644 --- a/docs/architecture/calibration_publication_v2.md +++ b/docs/architecture/calibration_publication_v2.md @@ -82,8 +82,36 @@ 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, and attaches only that -complete scope. 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. +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. diff --git a/docs/architecture/calibration_workflow.md b/docs/architecture/calibration_workflow.md index 470ace4..4bd07b5 100644 --- a/docs/architecture/calibration_workflow.md +++ b/docs/architecture/calibration_workflow.md @@ -8,7 +8,8 @@ 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 -CURRENT_WORKFLOW_NEXT=DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN +CALIBRATION_WORKFLOW_V2=PASS/FROZEN +CURRENT_WORKFLOW_NEXT=ADAPTIVE_CAPTURE_SETTINGS_AND_AUTOFOCUS_V2_FOUNDATION ``` ## Authority @@ -201,6 +202,32 @@ 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 diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index f84bf2a..4ae0241 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -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_WORKFLOW_READY +CURRENT_NEXT AUTOFOCUS_V2_FOUNDATION ``` The current Project DB head is additive: @@ -537,8 +537,10 @@ applicability while retaining that per-image scope model. Unknown state remains calibration publication. `CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PASS/FROZEN`: additive L3DCALB2/Tooling/Bootstrap v2 now retains independent group-local provenance, validates complete exact selected-image coverage, and attaches one existing per-image scope only after complete -publication. The next dependency is heterogeneous Workflow v2 truthful READY. Device-specific -autofocus envelopes remain blocked until physical evidence validates them. +publication. `CALIBRATION_WORKFLOW_V2=PASS/FROZEN`: the workflow now proves exact Capture/state +applicability and matching per-image calibration IDs before its sole final scope attachment. The next +dependency is the generic adaptive-settings/autofocus foundation. 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/calibration_bootstrap_v2.h b/include/lardon3d/calibration_bootstrap_v2.h index 9ad9300..77fdec5 100644 --- a/include/lardon3d/calibration_bootstrap_v2.h +++ b/include/lardon3d/calibration_bootstrap_v2.h @@ -35,6 +35,33 @@ typedef struct { 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 diff --git a/include/lardon3d/calibration_workflow_v2.h b/include/lardon3d/calibration_workflow_v2.h new file mode 100644 index 0000000..46d88b6 --- /dev/null +++ b/include/lardon3d/calibration_workflow_v2.h @@ -0,0 +1,80 @@ +#ifndef LARDON3D_CALIBRATION_WORKFLOW_V2_H +#define LARDON3D_CALIBRATION_WORKFLOW_V2_H + +#include +#include + +#include + +#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 diff --git a/meson.build b/meson.build index bdbe033..454f789 100644 --- a/meson.build +++ b/meson.build @@ -180,6 +180,7 @@ lardon3d_app = executable( 'src/optical_profiles.c', 'src/calibration_bootstrap.c', 'src/calibration_bootstrap_v2.c', + 'src/calibration_workflow_v2.c', 'src/calibration_tooling.c', 'src/calibration_tooling_v2.c', 'src/calibration_workflow.cpp', @@ -916,6 +917,23 @@ calibration_publication_v2_test = executable( test('calibration-publication-v2', calibration_publication_v2_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: [ diff --git a/prompt/02_CURRENT_FROZEN_STATE.md b/prompt/02_CURRENT_FROZEN_STATE.md index f3f1f4c..9b6be58 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -6,7 +6,7 @@ CURRENT_PROJECT_DB_SCHEMA=v26 PRODUCTION_TASK_KINDS=16 USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS -CURRENT_IMPLEMENTATION_CURSOR=CALIBRATION_V2_WORKFLOW_READY +CURRENT_IMPLEMENTATION_CURSOR=ADAPTIVE_CAPTURE_SETTINGS_AND_AUTOFOCUS_V2_FOUNDATION ``` ## Authority @@ -38,6 +38,7 @@ 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 PLANNED Autofocus v2 foundation PLANNED Calibration Tooling planarity alignment PASS/FROZEN diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index 383d6f6..1c40105 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -20,10 +20,10 @@ 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=PLANNED +CALIBRATION_V2_WORKFLOW_READY=PASS/FROZEN ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PLANNED AUTOFOCUS_V2_FOUNDATION=PLANNED -CURRENT_CALIBRATION_NEXT=CALIBRATION_V2_WORKFLOW_READY +CURRENT_CALIBRATION_NEXT=AUTOFOCUS_V2_FOUNDATION ``` ## Authority diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index 169a38d..7f672c0 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_WORKFLOW_READY +CURRENT_NEXT=AUTOFOCUS_V2_FOUNDATION ``` ## Authority @@ -24,23 +24,24 @@ Default dependency order: 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 — CURRENT; -6. physical autofocus/optical applicability validation and dedicated calibrated real campaign; -7. real Sparse SfM proof; -8. durable Dense/OpenMVS orchestration; -9. mesh / refinement / texturing / export; -10. viewer foundation; -11. offline Coverage Analysis scientific contract and implementation; -12. multi-campaign registration / fusion; -13. generic live acquisition adapter foundation; -14. A6000 HDMI integration; -15. S21 integration; -16. live camera localization; -17. live coverage overlay; -18. actionable Capture Guidance; -19. video ingestion / deterministic keyframes; -20. final integration, UX, restart and performance proof; -21. Product Definition v1 Definition-of-Done closure. +5. Heterogeneous Workflow v2 truthful READY proof — PASS/FROZEN; +6. Adaptive capture settings / generic autofocus foundation — CURRENT; +7. physical autofocus/optical applicability validation and dedicated calibrated real campaign; +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 diff --git a/src/calibration_bootstrap_v2.c b/src/calibration_bootstrap_v2.c index 79481d7..f44f2ca 100644 --- a/src/calibration_bootstrap_v2.c +++ b/src/calibration_bootstrap_v2.c @@ -79,10 +79,12 @@ static Lardon3DCalibrationBootstrapV2Result db_result(Lardon3DProjectDbResult re return LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR; } -Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import( +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 || @@ -109,6 +111,8 @@ Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import( 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; @@ -295,7 +299,7 @@ Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import( if (db_status != LARDON3D_PROJECT_DB_OK) result = db_result(db_status); } } - if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) { + 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); @@ -305,8 +309,42 @@ Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import( 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); +} diff --git a/src/calibration_workflow_v2.c b/src/calibration_workflow_v2.c new file mode 100644 index 0000000..158e9a8 --- /dev/null +++ b/src/calibration_workflow_v2.c @@ -0,0 +1,301 @@ +#include + +#include + +#include +#include +#include +#include + +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; +} diff --git a/tests/test_calibration_workflow_v2.c b/tests/test_calibration_workflow_v2.c new file mode 100644 index 0000000..568f04e --- /dev/null +++ b/tests/test_calibration_workflow_v2.c @@ -0,0 +1,342 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, \ + #condition); \ + return false; \ + } \ + } while (0) + +static bool sql(const char *path, const char *text) { + sqlite3 *database = NULL; + char *error = NULL; + if (sqlite3_open(path, &database) != SQLITE_OK) + return false; + int code = sqlite3_exec(database, text, NULL, NULL, &error); + if (code != SQLITE_OK) + fprintf(stderr, "SQL: %s\n", error ? error : "error"); + sqlite3_free(error); + sqlite3_close(database); + return code == SQLITE_OK; +} + +static void digest(unsigned char output[32], unsigned char seed) { + for (size_t index = 0; index < 32; ++index) + output[index] = (unsigned char)(seed + index); +} + +static void evidence(Lardon3DCalibrationToolingV2Evidence *value, + Lardon3DCalibrationToolingV2Group groups[2], + Lardon3DCalibrationToolingV2Entry entries[2]) { + memset(value, 0, sizeof(*value)); + memset(groups, 0, 2 * sizeof(*groups)); + memset(entries, 0, 2 * sizeof(*entries)); + for (size_t index = 0; index < 2; ++index) { + digest(groups[index].group_identity_sha256, (unsigned char)(1 + index)); + groups[index].group_version = 1; + digest(groups[index].optical_state_sha256, (unsigned char)(11 + index)); + digest(groups[index].target_sha256, (unsigned char)(21 + index)); + digest(groups[index].solver_executable_sha256, (unsigned char)(31 + index)); + digest(groups[index].solver_configuration_sha256, + (unsigned char)(41 + index)); + digest(groups[index].initialization_evidence_sha256, + (unsigned char)(51 + index)); + digest(groups[index].validation_evidence_sha256, + (unsigned char)(61 + index)); + groups[index].entries = &entries[index]; + groups[index].entry_count = 1; + entries[index].selected_item_index = (uint32_t)index; + entries[index].image_id = index + 1; + memset(entries[index].representation_sha256, (int)(0x11 * (index + 1)), 32); + entries[index].width = index == 0 ? 4000 : 6000; + entries[index].height = index == 0 ? 3000 : 4000; + entries[index].fx = index == 0 ? 3000.0 : 4500.0; + entries[index].fy = entries[index].fx + 1.0; + entries[index].cx = entries[index].width / 2.0; + entries[index].cy = entries[index].height / 2.0; + entries[index].support_images = 20; + entries[index].support_observations = 1000; + entries[index].reprojection_rmse_px = 0.4; + entries[index].maximum_parameter_delta = 0.01; + entries[index].validation_flags = + LARDON3D_CALIBRATION_TOOLING_V2_VALIDATION_FLAGS; + } + value->groups = groups; + value->group_count = 2; + value->entry_count = 2; +} + +static bool create_project(const char *path, Lardon3DProjectDb **database, + uint64_t *execution_id) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, database, error) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(*database); + *database = NULL; + CHECK(sql( + path, + "INSERT INTO scansets VALUES(1,'workflow-v2',1,1);" + "INSERT INTO tasks " + "VALUES(1,'quality','photo_quality.triage',1,5,5,1,1,0,0,0,0,1);" + "INSERT INTO tasks " + "VALUES(2,'campaign','acquisition_campaign.run',1,5,5,1,1,0,0,0,0,1);" + "INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01');" + "INSERT INTO photo_quality_triage_results VALUES" + "(1,1,0,1,0,1,0,1,1,1,1,1,1,0,0,1,1,0,'GOOD')," + "(1,2,1,1,0,1,0,1,1,1,1,1,1,0,0,1,1,0,'GOOD');" + "INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');" + "INSERT INTO captures VALUES(1,1,1),(2,1,2);" + "INSERT INTO acquisition_campaign_captures VALUES(2,11,1),(2,12,2);" + "INSERT INTO image_assets VALUES" + "(1,X'1111111111111111111111111111111111111111111111111111111111111111'," + "'assets/images/11/" + "1111111111111111111111111111111111111111111111111111111111111111',1,1,1)" + "," + "(2,X'2222222222222222222222222222222222222222222222222222222222222222'," + "'assets/images/22/" + "2222222222222222222222222222222222222222222222222222222222222222',1,1,2)" + ";" + "INSERT INTO images VALUES(1,1,1,'a','a',NULL,1),(2,1,2,'b','b',NULL,2);" + "INSERT INTO capture_images VALUES(1,1),(2,2);" + "INSERT INTO camera_body_profiles VALUES(1,'Maker','Body','Body');" + "INSERT INTO lens_profiles VALUES(1,'Maker','L1','L1',2,2,24000,24000);" + "INSERT INTO lens_profiles VALUES(2,'Maker','L2','L2',2,2,50000,50000);" + "INSERT INTO optical_configurations VALUES(1,1,1,24000),(2,1,2,50000);" + "INSERT INTO capture_optical_configurations " + "VALUES(1,1,2,NULL,NULL),(2,2,2,NULL,NULL);")); + CHECK(lardon3d_project_db_open(path, database, error) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbSelectedExecutionItem items[2] = { + {.item_index = 0, + .quality_group_id = 1, + .campaign_group_id = 11, + .capture_id = 1, + .representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE}, + {.item_index = 1, + .quality_group_id = 2, + .campaign_group_id = 12, + .capture_id = 2, + .representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE}}; + Lardon3DProjectDbSelectedExecution execution; + CHECK(lardon3d_project_db_create_selected_execution(*database, 1, 2, items, 2, + 1, &execution) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_selected_representation( + *database, execution.execution_id, 0, 1, 1) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_selected_representation( + *database, execution.execution_id, 1, 2, 2) == + LARDON3D_PROJECT_DB_OK); + *execution_id = execution.execution_id; + return true; +} + +static bool add_state(Lardon3DProjectDb *database, uint64_t capture_id, + uint64_t configuration_id, uint32_t width, + uint32_t height) { + Lardon3DOpticalCaptureGeometricState input = { + .capture_id = capture_id, + .optical_configuration_id = configuration_id, + .state_version = 1, + .provenance = LARDON3D_OPTICAL_GEOMETRIC_STATE_CALLER_EXPLICIT, + .focus_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .aperture_state = LARDON3D_OPTICAL_OBSERVATION_OBSERVED, + .aperture_x1000 = 8000, + .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 = width, + .decoded_height = height}; + strcpy(input.focus_observation, "fixed"); + strcpy(input.crop_observation, "full"); + strcpy(input.pipeline_observation, "raw-v1"); + strcpy(input.representation_observation, "png-v1"); + Lardon3DOpticalCaptureGeometricState output; + return lardon3d_optical_capture_geometric_state_create( + database, &input, &output) == LARDON3D_PROJECT_DB_OK; +} + +static bool is_unattached(Lardon3DProjectDb *database, uint64_t execution_id) { + Lardon3DProjectDbSelectedExecution execution; + return lardon3d_project_db_load_selected_execution( + database, execution_id, &execution) == LARDON3D_PROJECT_DB_OK && + execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && + !execution.has_calibration_scope; +} + +static bool run_cases(void) { + Lardon3DCalibrationToolingV2Evidence source; + Lardon3DCalibrationToolingV2Group groups[2]; + Lardon3DCalibrationToolingV2Entry entries[2]; + evidence(&source, groups, entries); + unsigned char artifact[1024], artifact_hash[32]; + size_t artifact_size = 0; + CHECK(lardon3d_calibration_tooling_v2_produce( + &source, artifact, sizeof(artifact), &artifact_size, + artifact_hash) == LARDON3D_CALIBRATION_TOOLING_V2_OK); + + char directory[] = "/tmp/lardon3d-workflow-v2-XXXXXX"; + CHECK(mkdtemp(directory) != NULL); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/project.db", directory) > 0); + Lardon3DProjectDb *database = NULL; + uint64_t execution_id = 0; + CHECK(create_project(path, &database, &execution_id)); + Lardon3DCalibrationWorkflowV2Binding bindings[2] = { + {.selected_item_index = 0, + .capture_id = 1, + .kind = LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT, + .exemplar_capture_id = 1}, + {.selected_item_index = 1, + .capture_id = 2, + .kind = LARDON3D_CALIBRATION_WORKFLOW_V2_PUBLISH_EXPLICIT, + .exemplar_capture_id = 2}}; + Lardon3DCalibrationWorkflowV2Output output; + + /* No complete observed tuple is a semantic non-ready state and must not + * trigger the unattached publication primitive. */ + CHECK(lardon3d_calibration_workflow_v2_complete( + database, execution_id, artifact, artifact_size, artifact_hash, + bindings, 2, + &output) == LARDON3D_CALIBRATION_WORKFLOW_V2_CALIBRATION_REQUIRED); + CHECK(is_unattached(database, execution_id)); + CHECK(add_state(database, 1, 1, 4000, 3000)); + CHECK(add_state(database, 2, 2, 6000, 4000)); + + unsigned char wrong_hash[32] = {0}; + memset(&output, 0xa5, sizeof(output)); + CHECK(lardon3d_calibration_workflow_v2_complete( + database, execution_id, artifact, artifact_size, wrong_hash, + bindings, 2, + &output) == LARDON3D_CALIBRATION_WORKFLOW_V2_INVALID_EVIDENCE); + const Lardon3DCalibrationWorkflowV2Output empty_output = {0}; + CHECK(memcmp(&output, &empty_output, sizeof(output)) == 0); + CHECK(is_unattached(database, execution_id)); + + Lardon3DCalibrationBootstrapV2Member probe_members[2]; + Lardon3DCalibrationBootstrapV2Output probe_publication; + Lardon3DCalibrationBootstrapV2Result probe = + lardon3d_calibration_bootstrap_v2_publish_unattached( + database, execution_id, artifact, artifact_size, artifact_hash, + probe_members, 2, &probe_publication); + if (probe != LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) + fprintf(stderr, "unattached probe result=%d\n", (int)probe); + CHECK(probe == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK); + + Lardon3DCalibrationWorkflowV2Result completed = + lardon3d_calibration_workflow_v2_complete( + database, execution_id, artifact, artifact_size, artifact_hash, + bindings, 2, &output); + if (completed != LARDON3D_CALIBRATION_WORKFLOW_V2_READY) + fprintf(stderr, "happy workflow result=%d\n", (int)completed); + CHECK(completed == LARDON3D_CALIBRATION_WORKFLOW_V2_READY); + uint64_t scope_id = output.publication.scope.scope_id; + Lardon3DSparseCalibrationMember members[2]; + size_t count = 0; + uint64_t next = 0; + CHECK(lardon3d_sparse_calibration_scope_list_members( + database, scope_id, 0, members, 2, &count, &next) == + LARDON3D_PROJECT_DB_OK); + CHECK(count == 2 && members[0].calibration_id != members[1].calibration_id); + CHECK(lardon3d_calibration_workflow_v2_complete( + database, execution_id, artifact, artifact_size, artifact_hash, + bindings, 2, &output) == LARDON3D_CALIBRATION_WORKFLOW_V2_READY && + output.publication.scope.scope_id == scope_id); + lardon3d_project_db_close(database); + + /* A requested applicability for the other heterogeneous member is exact- + * state incompatible and cannot attach the otherwise complete scope. */ + char wrong_path[512]; + CHECK(snprintf(wrong_path, sizeof(wrong_path), "%s/wrong.db", directory) > 0); + CHECK(create_project(wrong_path, &database, &execution_id)); + CHECK(add_state(database, 1, 1, 4000, 3000)); + CHECK(add_state(database, 2, 2, 6000, 4000)); + Lardon3DCalibrationBootstrapV2Member published[2]; + Lardon3DCalibrationBootstrapV2Output publication; + CHECK(lardon3d_calibration_bootstrap_v2_publish_unattached( + database, execution_id, artifact, artifact_size, artifact_hash, + published, 2, + &publication) == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK); + Lardon3DOpticalCalibrationProfile profile = { + .optical_configuration_id = 2, + .sparse_calibration_id = published[1].calibration_id, + .profile_version = 1, + .applicability = LARDON3D_OPTICAL_CALIBRATION_EXACT_CONFIGURATION}; + strcpy(profile.name, "wrong-member"); + strcpy(profile.provenance, "test"); + Lardon3DOpticalCalibrationProfile stored_profile; + CHECK(lardon3d_optical_calibration_profile_create( + database, &profile, &stored_profile) == LARDON3D_PROJECT_DB_OK); + Lardon3DOpticalCalibrationApplicabilityV2 applicability; + CHECK(lardon3d_optical_calibration_applicability_v2_create( + database, stored_profile.calibration_profile_id, 2, + &applicability) == LARDON3D_PROJECT_DB_OK); + bindings[0].kind = LARDON3D_CALIBRATION_WORKFLOW_V2_EXISTING_EXPLICIT; + bindings[0].applicability_id = applicability.applicability_id; + bindings[0].exemplar_capture_id = 0; + CHECK(lardon3d_calibration_workflow_v2_complete( + database, execution_id, artifact, artifact_size, artifact_hash, + bindings, 2, + &output) == LARDON3D_CALIBRATION_WORKFLOW_V2_ASSIGNMENT_CONFLICT); + CHECK(is_unattached(database, execution_id)); + lardon3d_project_db_close(database); + + /* Two exact candidates remain visibly ambiguous in AUTOMATIC mode even + * when both happen to reference the artifact's immutable calibration. */ + char ambiguous_path[512]; + CHECK(snprintf(ambiguous_path, sizeof(ambiguous_path), "%s/ambiguous.db", + directory) > 0); + CHECK(create_project(ambiguous_path, &database, &execution_id)); + CHECK(add_state(database, 1, 1, 4000, 3000)); + CHECK(add_state(database, 2, 2, 6000, 4000)); + CHECK(lardon3d_calibration_bootstrap_v2_publish_unattached( + database, execution_id, artifact, artifact_size, artifact_hash, + published, 2, + &publication) == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK); + for (size_t index = 0; index < 2; ++index) { + memset(&profile, 0, sizeof(profile)); + profile.optical_configuration_id = 1; + profile.sparse_calibration_id = published[0].calibration_id; + profile.profile_version = 1; + profile.applicability = LARDON3D_OPTICAL_CALIBRATION_EXACT_CONFIGURATION; + profile.created_at = 0; + CHECK(snprintf(profile.name, sizeof(profile.name), "ambiguous-%zu", index) > + 0); + strcpy(profile.provenance, "test ambiguity"); + CHECK(lardon3d_optical_calibration_profile_create( + database, &profile, &stored_profile) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_optical_calibration_applicability_v2_create( + database, stored_profile.calibration_profile_id, 1, + &applicability) == LARDON3D_PROJECT_DB_OK); + } + bindings[0].kind = LARDON3D_CALIBRATION_WORKFLOW_V2_AUTOMATIC; + bindings[0].applicability_id = 0; + bindings[0].exemplar_capture_id = 0; + CHECK(lardon3d_calibration_workflow_v2_complete( + database, execution_id, artifact, artifact_size, artifact_hash, + bindings, 2, + &output) == LARDON3D_CALIBRATION_WORKFLOW_V2_SELECTION_REQUIRED); + CHECK(is_unattached(database, execution_id)); + lardon3d_project_db_close(database); + + CHECK(unlink(path) == 0); + CHECK(unlink(wrong_path) == 0); + CHECK(unlink(ambiguous_path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + +int main(void) { return run_cases() ? EXIT_SUCCESS : EXIT_FAILURE; }