diff --git a/docs/architecture/calibration_workflow.md b/docs/architecture/calibration_workflow.md index 5ff1caf..470ace4 100644 --- a/docs/architecture/calibration_workflow.md +++ b/docs/architecture/calibration_workflow.md @@ -3,11 +3,12 @@ ## Status ```text -CALIBRATION_WORKFLOW=IN_PROGRESS +CALIBRATION_WORKFLOW=PASS/FROZEN CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN -CURRENT_WORKFLOW_NEXT=TOOLING_BOOTSTRAP_READY +CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY_V1=PASS/FROZEN +CURRENT_WORKFLOW_NEXT=DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN ``` ## Authority @@ -188,9 +189,19 @@ Bootstrap, or transition the selected execution to `READY`. It proves: Entries are staged internally and published to caller storage only after every selected item passes. Exact retries are read-only and deterministic. -## Current next boundary +## Tooling / Bootstrap READY v1 -The final calibration workflow boundary is: +The final public composition validates and materializes input, performs the +read-only selected-execution binding, then invokes only FROZEN Calibration +Tooling. Tooling produces L3DCALB1 v1 and invokes FROZEN Bootstrap. Success +requires the returned scope to be the exact scope attached to READY; exact +retries converge through immutable importer semantics. + +The software workflow is PASS/FROZEN. It does not establish physical evidence: +historical S21/A6000 campaigns remain CALIBRATION_UNAVAILABLE and +BLOCKED_BY_KNOWN_CALIBRATION_DATA. + +## Current next boundary ```text validated input -> materialized evidence -> selected-execution binding diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 6651dbb..d482aea 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_WORKFLOW_TOOLING_BOOTSTRAP_READY +CURRENT_NEXT DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN ``` The current Project DB head is additive: @@ -533,6 +533,8 @@ The solver session contract now also requires explicit measured `white_border >= `CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN`: a read-only coordinator boundary now proves the exact selected execution and Capture mapping, explicit optical configuration, managed representation size/SHA-256, safe project-relative file containment, decoded geometry dimensions, and deterministic ToolingEntry construction. It never invokes Tooling/Bootstrap or changes selected-execution state. The next boundary is the FROZEN Tooling -> Bootstrap -> truthful READY composition. +`CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY_V1=PASS/FROZEN`: valid retained external evidence now composes through the three workflow checkpoints and only the FROZEN Tooling/Bootstrap importer to truthful READY. This software proof does not acquire a physical calibration; historical S21/A6000 campaigns remain CALIBRATION_UNAVAILABLE. + A bounded Tooling correction aligned planarity handling with Calibration Science v1: Science v1 defines a categorical physical planarity attestation, not a numeric flatness threshold. Tooling therefore rejects invented finite `target_flatness_mm` values. `L3DCALB1` v1 and Calibration @@ -902,7 +904,7 @@ PRODUCT_DEFINITION_V1 PASS/FROZEN PROMPT_TREE CURRENT CALIBRATION_EVIDENCE_SOLVER_V1 IMPLEMENTED/VALIDATED CALIBRATION_TOOLING_ALIGNMENT PASS/FROZEN -CURRENT_NEXT CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY +CURRENT_NEXT DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN ``` Implementation proceeds only through explicitly human-authorized tranches under `prompt.md` and the diff --git a/include/lardon3d/calibration_workflow.h b/include/lardon3d/calibration_workflow.h index 07ab68a..5c23bac 100644 --- a/include/lardon3d/calibration_workflow.h +++ b/include/lardon3d/calibration_workflow.h @@ -156,6 +156,22 @@ lardon3d_calibration_workflow_bind_selected_execution( Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity, Lardon3DCalibrationToolingEvidence *output); +/* Complete the bounded calibration workflow through the frozen Tooling and + * Bootstrap importer. All supplied arrays and artifact storage remain caller + * owned; `output` is cleared unless import reaches the exact READY transition. + * Input validation, materialization and selected-execution binding finish + * before this function invokes the only mutating operation. Exact retries use + * Bootstrap's immutable import semantics. */ +Lardon3DCalibrationWorkflowResult +lardon3d_calibration_workflow_complete( + Lardon3DProjectDb *database, const char *project_path, + const Lardon3DCalibrationWorkflowInputFiles *files, + Lardon3DCalibrationToolingView *views, size_t view_capacity, + Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks, + size_t coordinate_check_capacity, Lardon3DCalibrationToolingEntry *entries, + size_t entry_capacity, unsigned char *artifact, size_t artifact_capacity, + size_t *artifact_size, Lardon3DCalibrationBootstrapOutput *output); + #ifdef __cplusplus } #endif diff --git a/meson.build b/meson.build index a4fb0d5..98024e4 100644 --- a/meson.build +++ b/meson.build @@ -183,6 +183,7 @@ lardon3d_app = executable( 'src/calibration_workflow.cpp', 'src/calibration_workflow_materialize.cpp', 'src/calibration_workflow_bind.cpp', + 'src/calibration_workflow_complete.cpp', 'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_bundle_adjustment.cpp', @@ -949,6 +950,30 @@ test( timeout: 30, ) +calibration_workflow_ready_test = executable( + 'test-calibration-workflow-ready', + sources: [ + 'tests/test_calibration_workflow_ready.cpp', + 'src/calibration_workflow.cpp', + 'src/calibration_workflow_materialize.cpp', + 'src/calibration_workflow_bind.cpp', + 'src/calibration_workflow_complete.cpp', + 'src/calibration_tooling.c', 'src/calibration_bootstrap.c', + 'src/optical_profiles.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, opencv, cc.find_library('m')], +) + +test( + 'calibration-workflow-ready', + calibration_workflow_ready_test, + timeout: 30, +) + optical_profiles_test = executable( 'test-optical-profiles', sources: [ diff --git a/prompt/02_CURRENT_FROZEN_STATE.md b/prompt/02_CURRENT_FROZEN_STATE.md index 7b336a4..218f695 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -6,7 +6,7 @@ CURRENT_PROJECT_DB_SCHEMA=v25 PRODUCTION_TASK_KINDS=16 USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS -CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY +CURRENT_IMPLEMENTATION_CURSOR=2_DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN ``` ## Authority @@ -34,6 +34,7 @@ Calibration solver bundle repair PASS/FROZEN Calibration workflow input boundary PASS/FROZEN Calibration workflow evidence materialization PASS/FROZEN Calibration workflow selected-execution binding PASS/FROZEN +Calibration workflow Tooling/Bootstrap READY PASS/FROZEN Calibration Tooling planarity alignment PASS/FROZEN ``` diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index c188e85..a0aa0b6 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -12,11 +12,13 @@ CALIBRATION_SOLVER_WHITE_BORDER_V1=PASS/FROZEN CALIBRATION_SOLVER_PRODUCER_IDENTITY_V1=PASS/FROZEN CALIBRATION_SOLVER_PER_VIEW_EVIDENCE_V1=PASS/FROZEN CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN -CALIBRATION_WORKFLOW=IN_PROGRESS +CALIBRATION_WORKFLOW=PASS/FROZEN CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN CURRENT_CALIBRATION_NEXT=WORKFLOW_TOOLING_BOOTSTRAP_READY +CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY_V1=PASS/FROZEN +CURRENT_CALIBRATION_NEXT=DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN ``` ## Authority diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index 1abd468..5efdf8c 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=1_CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY +CURRENT_NEXT=2_DEDICATED_PHYSICAL_CALIBRATED_REAL_CAMPAIGN ``` ## Authority @@ -20,7 +20,7 @@ Implementation remains unauthorized until the human explicitly authorizes a tran Default dependency order: 0. user-facing repository/UI language normalization where appropriate — PASS; -1. final usable calibration workflow — IN PROGRESS; Input Boundary v1, Evidence Materialization v1 and Selected Execution Binding v1 PASS/FROZEN; current next sub-boundary: FROZEN Tooling -> Bootstrap -> truthful READY; +1. final usable calibration workflow — PASS/FROZEN; valid external evidence now composes through FROZEN Tooling and Bootstrap to truthful READY; 2. dedicated physical calibrated real campaign; 3. real Sparse SfM proof; 4. durable Dense/OpenMVS orchestration; diff --git a/src/calibration_workflow_complete.cpp b/src/calibration_workflow_complete.cpp new file mode 100644 index 0000000..0da302e --- /dev/null +++ b/src/calibration_workflow_complete.cpp @@ -0,0 +1,95 @@ +// This translation unit calls the C Project DB and frozen Tooling APIs from +// C++. Their declarations must have C linkage before the workflow include path. +extern "C" { +#include +} + +#include + +#include +#include + +namespace { + +Lardon3DCalibrationWorkflowResult tooling_result( + Lardon3DCalibrationToolingResult result) { + if (result == LARDON3D_CALIBRATION_TOOLING_OK) + return LARDON3D_CALIBRATION_WORKFLOW_OK; + if (result == LARDON3D_CALIBRATION_TOOLING_CAPACITY) + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + if (result == LARDON3D_CALIBRATION_TOOLING_IMPORT_ERROR) + return LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR; + return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE; +} + +Lardon3DCalibrationWorkflowResult complete_impl( + Lardon3DProjectDb *database, const char *project_path, + const Lardon3DCalibrationWorkflowInputFiles *files, + Lardon3DCalibrationToolingView *views, size_t view_capacity, + Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks, + size_t coordinate_check_capacity, Lardon3DCalibrationToolingEntry *entries, + size_t entry_capacity, unsigned char *artifact, size_t artifact_capacity, + size_t *artifact_size, Lardon3DCalibrationBootstrapOutput *output) { + if (artifact_size) *artifact_size = 0; + if (output) std::memset(output, 0, sizeof(*output)); + if (!database || !project_path || !*project_path || !files || !views || + !coordinate_checks || !entries || !artifact || !artifact_size || !output) + return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT; + + Lardon3DCalibrationWorkflowExternalEvidence external{}; + Lardon3DCalibrationWorkflowResult result = + lardon3d_calibration_workflow_materialize_external_evidence( + files, views, view_capacity, coordinate_checks, coordinate_check_capacity, + &external); + if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result; + + Lardon3DCalibrationToolingEvidence tooling{}; + result = lardon3d_calibration_workflow_bind_selected_execution( + database, project_path, &external, entries, entry_capacity, &tooling); + if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result; + + result = tooling_result(lardon3d_calibration_tooling_import( + database, external.boundary.selected_execution_id, &tooling, artifact, + artifact_capacity, artifact_size, output)); + if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result; + + // Tooling owns immutable import publication. The workflow's success contract + // is stricter: only the exact returned scope attached to READY is success. + Lardon3DProjectDbSelectedExecution execution{}; + const Lardon3DProjectDbResult loaded = + lardon3d_project_db_load_selected_execution( + database, external.boundary.selected_execution_id, &execution); + if (loaded != LARDON3D_PROJECT_DB_OK || + execution.stage != LARDON3D_SELECTED_EXECUTION_READY || + !execution.has_calibration_scope || execution.calibration_scope_id == 0 || + execution.calibration_scope_id != output->scope.scope_id) + return LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR; + return LARDON3D_CALIBRATION_WORKFLOW_OK; +} + +} // namespace + +extern "C" Lardon3DCalibrationWorkflowResult +lardon3d_calibration_workflow_complete( + Lardon3DProjectDb *database, const char *project_path, + const Lardon3DCalibrationWorkflowInputFiles *files, + Lardon3DCalibrationToolingView *views, size_t view_capacity, + Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks, + size_t coordinate_check_capacity, Lardon3DCalibrationToolingEntry *entries, + size_t entry_capacity, unsigned char *artifact, size_t artifact_capacity, + size_t *artifact_size, Lardon3DCalibrationBootstrapOutput *output) { + try { + return complete_impl(database, project_path, files, views, view_capacity, + coordinate_checks, coordinate_check_capacity, entries, + entry_capacity, artifact, artifact_capacity, + artifact_size, output); + } catch (const std::bad_alloc&) { + if (artifact_size) *artifact_size = 0; + if (output) std::memset(output, 0, sizeof(*output)); + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + } catch (...) { + if (artifact_size) *artifact_size = 0; + if (output) std::memset(output, 0, sizeof(*output)); + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } +} diff --git a/tests/test_calibration_workflow_ready.cpp b/tests/test_calibration_workflow_ready.cpp new file mode 100644 index 0000000..283c213 --- /dev/null +++ b/tests/test_calibration_workflow_ready.cpp @@ -0,0 +1,566 @@ +// The fixture calls the C Project DB API directly. Establish C linkage before +// Calibration Workflow's frozen Tooling/Bootstrap dependency chain. +extern "C" { +#include +} + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { \ + std::fprintf(stderr, "workflow ready failure %d: %s\n", __LINE__, #x); \ + return false; \ +} } while (0) + +namespace { + +constexpr size_t kViewCount = 60; +constexpr size_t kChecksPerView = 20; +constexpr size_t kCoordinateCount = kViewCount * kChecksPerView; + +std::string hex_repeat(unsigned char value) { + static const char digits[] = "0123456789abcdef"; + std::string out(64, '0'); + for (size_t i = 0; i < 32; ++i) { + out[2 * i] = digits[value >> 4]; + out[2 * i + 1] = digits[value & 15u]; + } + return out; +} + +std::string hex(const unsigned char value[32]) { + static const char digits[] = "0123456789abcdef"; + std::string out(64, '0'); + for (size_t i = 0; i < 32; ++i) { + out[2 * i] = digits[value[i] >> 4]; + out[2 * i + 1] = digits[value[i] & 15u]; + } + return out; +} + +std::string hf(double value) { + std::ostringstream out; + out.imbue(std::locale::classic()); + out << std::hexfloat << value; + return out.str(); +} + +bool write_text(const std::filesystem::path& path, const std::string& text) { + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out << text; + return static_cast(out); +} + +bool write_bytes(const std::filesystem::path& path, + const std::vector& bytes) { + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + return static_cast(out); +} + +bool digest(const unsigned char *bytes, size_t size, unsigned char output[32]) { + unsigned int written = 0; + return EVP_Digest(bytes, size, output, &written, EVP_sha256(), nullptr) == 1 && + written == 32; +} + +std::string sha_file_hex(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + std::ostringstream bytes; + bytes << in.rdbuf(); + const std::string data = bytes.str(); + unsigned char value[32]{}; + if (!in || !digest(reinterpret_cast(data.data()), + data.size(), value)) + return {}; + return hex(value); +} + +bool raw_sql(const std::filesystem::path& path, const std::string& sql) { + sqlite3 *database = nullptr; + if (sqlite3_open(path.c_str(), &database) != SQLITE_OK) return false; + const int code = sqlite3_exec(database, sql.c_str(), nullptr, nullptr, nullptr); + return sqlite3_close(database) == SQLITE_OK && code == SQLITE_OK; +} + +struct Fixture { + std::filesystem::path root; + std::filesystem::path db_path; + std::filesystem::path session; + std::filesystem::path detection; + std::filesystem::path solve; + std::filesystem::path evidence; + std::filesystem::path producer; + std::filesystem::path campaign; + std::filesystem::path image_path; + std::string image_asset_path; + std::vector image_bytes; + Lardon3DProjectDb *database = nullptr; + uint64_t execution_id = 0; + uint64_t configuration_id = 0; + Lardon3DCalibrationWorkflowInputFiles files{}; +}; + +void close_fixture(Fixture *fixture) { + if (fixture->database) { + lardon3d_project_db_close(fixture->database); + fixture->database = nullptr; + } + if (!fixture->root.empty()) { + std::error_code error; + std::filesystem::remove_all(fixture->root, error); + } +} + +bool seed_database_rows(const Fixture& fixture, + const unsigned char image_sha[32]) { + std::ostringstream sql; + sql << "INSERT INTO scansets(scanset_id,name,created_at,updated_at) " + "VALUES(1,'scope',1,1);" + << "INSERT INTO tasks VALUES(1,'quality','photo_quality.triage',1," + "5,5,100,1,0,0,0,0,1);" + << "INSERT INTO tasks VALUES(2,'campaign','acquisition_campaign.run',1," + "5,5,100,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,100,100,100,100,1.0,1.0,0.0,0.0,1.0,1.0,0.0,'GOOD');" + << "INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');" + << "INSERT INTO captures(capture_id,scanset_id,created_at) VALUES(1,1,1);" + << "INSERT INTO acquisition_campaign_captures VALUES(2,2,1);" + << "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) " + "VALUES(1,X'" << hex(image_sha) << "','" << fixture.image_asset_path + << "'," << fixture.image_bytes.size() << ",1,1);" + << "INSERT INTO images(image_id,scanset_id,asset_id,original_name," + "source_path,imported_at) VALUES(" + "1,1,1,'selected.png','/source/selected.png',1);" + << "INSERT INTO capture_images VALUES(1,1);"; + return raw_sql(fixture.db_path, sql.str()); +} + +bool create_optical_configuration(Fixture *fixture) { + Lardon3DOpticalCameraBodyProfile body_input{}; + std::memcpy(body_input.manufacturer, "Generic", sizeof("Generic")); + std::memcpy(body_input.model, "Workflow body", sizeof("Workflow body")); + std::memcpy(body_input.name, "Workflow body", sizeof("Workflow body")); + Lardon3DOpticalCameraBodyProfile body{}; + if (lardon3d_optical_camera_body_create(fixture->database, &body_input, &body) != + LARDON3D_PROJECT_DB_OK) + return false; + + Lardon3DOpticalLensProfile lens_input{}; + lens_input.interface_kind = LARDON3D_OPTICAL_LENS_MANUAL; + lens_input.focal_range_kind = LARDON3D_OPTICAL_FOCAL_RANGE_PRIME; + lens_input.minimum_focal_um = 35000; + lens_input.maximum_focal_um = 35000; + std::memcpy(lens_input.manufacturer, "Generic", sizeof("Generic")); + std::memcpy(lens_input.model, "Workflow 35", sizeof("Workflow 35")); + std::memcpy(lens_input.name, "Workflow 35 mm", sizeof("Workflow 35 mm")); + Lardon3DOpticalLensProfile lens{}; + if (lardon3d_optical_lens_create(fixture->database, &lens_input, &lens) != + LARDON3D_PROJECT_DB_OK) + return false; + + Lardon3DOpticalConfiguration input{}; + input.camera_body_profile_id = body.camera_body_profile_id; + input.lens_profile_id = lens.lens_profile_id; + input.has_focal_length = true; + input.focal_length_um = 35000; + Lardon3DOpticalConfiguration configuration{}; + if (lardon3d_optical_configuration_create( + fixture->database, &input, &configuration) != LARDON3D_PROJECT_DB_OK) + return false; + if (lardon3d_optical_capture_assign_explicit( + fixture->database, 1, configuration.optical_configuration_id) != + LARDON3D_PROJECT_DB_OK) + return false; + fixture->configuration_id = configuration.optical_configuration_id; + return true; +} + +std::string parameters() { + const double values[8] = {1000.0, 1001.0, 500.0, 400.0, + 0.01, -0.01, 0.001, -0.001}; + std::ostringstream out; + out << '['; + for (size_t i = 0; i < 8; ++i) { + if (i) out << ','; + out << '"' << hf(values[i]) << '"'; + } + out << ']'; + return out.str(); +} + +std::string pose() { + return "{\"rvec\":[\"" + hf(0.0) + "\",\"" + hf(0.0) + "\",\"" + + hf(0.0) + "\"],\"tvec_m\":[\"" + hf(0.0) + "\",\"" + hf(0.0) + + "\",\"" + hf(1.0) + "\"]}"; +} + +bool write_external_inputs(Fixture *fixture, double holdout_rmse) { + const std::string optical = hex_repeat(0x11); + const std::string generator = hex_repeat(0x12); + const std::string planarity = hex_repeat(0x13); + static const std::array labels = { + "center", "top", "right", "bottom", "left", "top_left", + "top_right", "bottom_left", "bottom_right"}; + static const uint32_t quadrants[10] = {0, 0, 1, 1, 2, 2, 3, 3, 4, 4}; + static const uint32_t distances[10] = {0, 1, 2, 0, 1, 2, 0, 1, 2, 0}; + static const double angles[10] = {25, 40, 55, 25, 40, 55, 25, 40, 55, 25}; + + std::ostringstream session; + session << "L3DCAL_SESSION_V1\n" + << "target board " << generator << " DICT_5X5_100 9 7 30 21\n" + << "measurement caliper 0.1 30 30 30 30 30 30 30 30 30 30\n" + << "white_border 30\n" + << "planarity PASS " << planarity << "\n" + << "decoder qualified_decoder 1\n" + << "optical_state " << optical + << " body_objective_zoom_focus_stabilization_format_pipeline\n"; + for (size_t i = 0; i < kViewCount; ++i) { + const std::string sha = hex_repeat(static_cast(i + 0x20)); + const uint32_t group = static_cast(i / 6); + const double distance = 1.0 + static_cast(distances[group]) * 0.3; + session << "image /retained/view-" << i << ".png " << sha << " 0\n" + << "pre_solve " << sha << " 0.1\n" + << "clipping " << sha << " 0\n" + << "coordinate " << sha + << " qualified_decoder 1 0 1000 800 20 0 0\n"; + for (size_t point = 0; point < kChecksPerView; ++point) { + session << "coordinate_point " << sha << ' ' + << labels[point % labels.size()] << ' ' << point << ' ' << point + 1 + << ' ' << point << ' ' << point + 1 << "\n"; + } + session << "distance " << sha << ' ' << std::setprecision(17) + << std::defaultfloat << distance << ' ' << distances[group] << "\n"; + } + if (!write_text(fixture->session, session.str())) return false; + + std::ostringstream detection; + detection << "{\n\"format\":\"L3DCAL_DETECTION_V1\",\n" + << "\"decoder\":\"qualified_decoder\",\n" + << "\"decoder_version\":\"1\",\n\"views\":[\n"; + for (size_t i = 0; i < kViewCount; ++i) { + const std::string sha = hex_repeat(static_cast(i + 0x20)); + const uint32_t group = static_cast(i / 6); + const double distance = 1.0 + static_cast(distances[group]) * 0.3; + detection << "{\"source_sha256\":\"" << sha + << "\",\"orientation\":0,\"oriented_width\":1000," + "\"oriented_height\":800,\"decision\":\"accepted\"," + "\"reason\":\"-\",\"frame_region\":" << quadrants[group] + << ",\"distance_band\":" << distances[group] + << ",\"holdout\":" << ((i % 6) == 4 ? "true" : "false") + << ",\"target_occupancy\":\"" << hf(0.4) + << "\",\"normal_angle_degrees\":\"" << hf(angles[group]) + << "\",\"measured_distance_metres\":\"" << hf(distance) + << "\",\"pre_solve_corner_rms_px\":\"" << hf(0.1) + << "\",\"clipping_fraction\":\"" << hf(0.0) + << "\",\"physical_target_quadrants\":3," + "\"target_corner_quadrant_mask\":7,\"corner_count\":40," + "\"residual_count\":40,\"high_residual_count\":0," + "\"reprojection_rmse_px\":\"" << hf(0.4) + << "\",\"maximum_residual_px\":\"" << hf(0.8) + << "\",\"coordinate_equivalence\":{\"comparison_points\":20," + "\"max_abs_dx_px\":\"" << hf(0.0) + << "\",\"max_abs_dy_px\":\"" << hf(0.0) + << "\",\"pass\":true},\"corners\":["; + for (size_t corner = 0; corner < 40; ++corner) { + if (corner) detection << ','; + detection << "{\"id\":" << corner << ",\"x\":\"" + << hf(100.0 + static_cast(corner)) << "\",\"y\":\"" + << hf(200.0 + static_cast(corner)) << "\"}"; + } + detection << "]}" << (i + 1 == kViewCount ? "\n" : ",\n"); + } + detection << "]\n}\n"; + if (!write_text(fixture->detection, detection.str())) return false; + + std::ostringstream solve; + solve << "{\n\"format\":\"L3DCAL_SOLVE_V1\",\n\"runs\":[\n"; + for (size_t run = 0; run < 3; ++run) { + solve << "{\"run\":" << run << ",\"params\":" << parameters() + << ",\"opencv_rms_px\":\"" << hf(0.4) << "\",\"poses\":["; + for (size_t i = 0; i < kViewCount; ++i) { + if (i) solve << ','; + solve << pose(); + } + solve << "]}" << (run == 2 ? "\n" : ",\n"); + } + solve << "],\n\"fit_params\":" << parameters() << "\n}\n"; + if (!write_text(fixture->solve, solve.str())) return false; + + std::ostringstream evidence; + evidence << "{\n\"format\":\"L3DCAL_EVIDENCE_BUNDLE_V1\",\n" + << "\"target\":{\"id\":\"board\",\"generator_sha256\":\"" + << generator + << "\",\"instrument\":\"caliper\",\"resolution_mm\":\"" + << hf(0.1) << "\",\"planarity_evidence_sha256\":\"" << planarity + << "\"},\n\"optical_sha256\":\"" << optical + << "\",\n\"optical_state\":" + "\"body_objective_zoom_focus_stabilization_format_pipeline\",\n" + << "\"validation_flags\":\"0xf\",\n\"global_rmse_px\":\"" + << hf(0.4) << "\",\"maximum_residual_px\":\"" << hf(0.8) + << "\",\"high_residual_fraction\":\"" << hf(0.0) + << "\",\n\"holdout\":{\"rmse_px\":\"" << hf(holdout_rmse) + << "\",\"maximum_px\":\"" << hf(0.8) + << "\"},\n\"maximum_parameter_delta_px\":\"" << hf(0.0) + << "\",\n\"deterministic_full_solve_equality\":true,\n" + "\"residuals\":["; + bool first = true; + for (size_t i = 0; i < kViewCount; ++i) { + const std::string sha = hex_repeat(static_cast(i + 0x20)); + for (size_t corner = 0; corner < 40; ++corner) { + if (!first) evidence << ','; + first = false; + evidence << "{\"source_sha256\":\"" << sha << "\",\"corner_id\":" + << corner << ",\"dx_px\":\"" << hf(0.4) + << "\",\"dy_px\":\"" << hf(0.0) << "\",\"rmse_px\":\"" + << hf(0.4) << "\"}"; + } + } + evidence << "]\n}\n"; + if (!write_text(fixture->evidence, evidence.str())) return false; + + const std::string session_sha = sha_file_hex(fixture->session); + if (session_sha.empty()) return false; + if (!write_text( + fixture->producer, + "{\n\"format\":\"L3DCAL_PRODUCER_V1\",\n" + "\"solver_executable_sha256\":\"" + hex_repeat(0xa1) + "\",\n" + "\"solver_configuration_sha256\":\"" + hex_repeat(0xb1) + "\",\n" + "\"session_sha256\":\"" + session_sha + "\",\n" + "\"opencv_version\":\"5.0.0\",\n" + "\"opencv_build_sha256\":\"" + hex_repeat(0xc1) + "\",\n" + "\"threads\":1,\n\"rng_seed\":1278432342,\n" + "\"optical_sha256\":\"" + optical + "\"\n}\n")) + return false; + + if (!write_text( + fixture->campaign, + "L3DCAL_CAMPAIGN_STATE_V1\nexecution " + + std::to_string(fixture->execution_id) + + "\noptical_configuration " + std::to_string(fixture->configuration_id) + + "\noptical_state " + optical + + " body_objective_zoom_focus_stabilization_format_pipeline\n" + "capture 0 1\n")) + return false; + + fixture->files = {fixture->session.c_str(), fixture->detection.c_str(), + fixture->solve.c_str(), fixture->evidence.c_str(), + fixture->producer.c_str(), fixture->campaign.c_str()}; + return true; +} + +bool make_fixture(Fixture *fixture, double holdout_rmse = 0.4) { + char temporary[] = "/tmp/lardon3d-workflow-ready-XXXXXX"; + char *root = mkdtemp(temporary); + if (!root) return false; + fixture->root = root; + fixture->db_path = fixture->root / "project.db"; + fixture->session = fixture->root / "session"; + fixture->detection = fixture->root / "detection"; + fixture->solve = fixture->root / "solve"; + fixture->evidence = fixture->root / "evidence"; + fixture->producer = fixture->root / "producer"; + fixture->campaign = fixture->root / "campaign"; + + cv::Mat image(800, 1000, CV_8UC1, cv::Scalar(127)); + if (!cv::imencode(".png", image, fixture->image_bytes)) return false; + unsigned char image_sha[32]{}; + if (!digest(fixture->image_bytes.data(), fixture->image_bytes.size(), image_sha)) + return false; + const std::string image_hex = hex(image_sha); + // Binding accepts only the canonical content-addressed managed-asset layout; + // the session view hashes are external evidence and are not Capture identity. + fixture->image_asset_path = + "assets/images/" + image_hex.substr(0, 2) + "/" + image_hex; + fixture->image_path = fixture->root / fixture->image_asset_path; + if (!std::filesystem::create_directories(fixture->image_path.parent_path()) || + !write_bytes(fixture->image_path, fixture->image_bytes)) + return false; + + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; + if (lardon3d_project_db_open(fixture->db_path.c_str(), &fixture->database, + error) != LARDON3D_PROJECT_DB_OK) + return false; + lardon3d_project_db_close(fixture->database); + fixture->database = nullptr; + if (!seed_database_rows(*fixture, image_sha)) return false; + if (lardon3d_project_db_open(fixture->db_path.c_str(), &fixture->database, + error) != LARDON3D_PROJECT_DB_OK) + return false; + if (!create_optical_configuration(fixture)) return false; + if (lardon3d_project_db_set_selected_capture_image(fixture->database, 1, 1) != + LARDON3D_PROJECT_DB_OK) + return false; + + Lardon3DProjectDbSelectedExecutionItem item{}; + item.item_index = 0; + item.quality_group_id = 1; + item.campaign_group_id = 2; + item.capture_id = 1; + item.representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE; + Lardon3DProjectDbSelectedExecution execution{}; + if (lardon3d_project_db_create_selected_execution( + fixture->database, 1, 2, &item, 1, 10, &execution) != + LARDON3D_PROJECT_DB_OK) + return false; + fixture->execution_id = execution.execution_id; + if (lardon3d_project_db_record_selected_representation( + fixture->database, fixture->execution_id, 0, 1, 1) != + LARDON3D_PROJECT_DB_OK) + return false; + return write_external_inputs(fixture, holdout_rmse); +} + +bool is_calibration_without_scope(const Fixture& fixture) { + Lardon3DProjectDbSelectedExecution execution{}; + return lardon3d_project_db_load_selected_execution( + fixture.database, fixture.execution_id, &execution) == + LARDON3D_PROJECT_DB_OK && + execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && + !execution.has_calibration_scope && execution.calibration_scope_id == 0; +} + +Lardon3DCalibrationWorkflowResult complete( + Fixture *fixture, Lardon3DCalibrationToolingView *views, size_t view_capacity, + Lardon3DCalibrationToolingCoordinateCheck *checks, size_t check_capacity, + Lardon3DCalibrationToolingEntry *entries, unsigned char *artifact, + size_t artifact_capacity, size_t *artifact_size, + Lardon3DCalibrationBootstrapOutput *output) { + return lardon3d_calibration_workflow_complete( + fixture->database, fixture->root.c_str(), &fixture->files, views, + view_capacity, checks, check_capacity, entries, 1, artifact, + artifact_capacity, artifact_size, output); +} + +bool happy_path_and_exact_retry() { + Fixture fixture; + CHECK(make_fixture(&fixture)); + CHECK(is_calibration_without_scope(fixture)); + + Lardon3DCalibrationToolingView views[kViewCount]{}; + Lardon3DCalibrationToolingCoordinateCheck checks[kCoordinateCount]{}; + Lardon3DCalibrationToolingEntry entries[1]{}; + unsigned char artifact[LARDON3D_CALIBRATION_BOOTSTRAP_MAX_BYTES]{}; + size_t artifact_size = 0; + Lardon3DCalibrationBootstrapOutput output{}; + CHECK(complete(&fixture, views, kViewCount, checks, kCoordinateCount, entries, + artifact, sizeof(artifact), &artifact_size, &output) == + LARDON3D_CALIBRATION_WORKFLOW_OK); + CHECK(artifact_size == 292 && output.calibration_count == 1 && + output.scope.scope_id != 0 && output.scope.member_count == 1); + + Lardon3DProjectDbSelectedExecution ready{}; + CHECK(lardon3d_project_db_load_selected_execution( + fixture.database, fixture.execution_id, &ready) == + LARDON3D_PROJECT_DB_OK); + CHECK(ready.stage == LARDON3D_SELECTED_EXECUTION_READY && + ready.has_calibration_scope && + ready.calibration_scope_id == output.scope.scope_id); + + const std::vector first_artifact( + artifact, artifact + artifact_size); + const Lardon3DCalibrationBootstrapOutput first_output = output; + std::memset(artifact, 0, sizeof(artifact)); + artifact_size = 0; + std::memset(&output, 0, sizeof(output)); + CHECK(complete(&fixture, views, kViewCount, checks, kCoordinateCount, entries, + artifact, sizeof(artifact), &artifact_size, &output) == + LARDON3D_CALIBRATION_WORKFLOW_OK); + CHECK(artifact_size == first_artifact.size() && + std::memcmp(artifact, first_artifact.data(), artifact_size) == 0); + CHECK(output.scope.scope_id == first_output.scope.scope_id && + output.scope.member_count == first_output.scope.member_count && + std::memcmp(output.scope.scientific_hash, + first_output.scope.scientific_hash, 32) == 0 && + std::memcmp(output.artifact_sha256, + first_output.artifact_sha256, 32) == 0); + + close_fixture(&fixture); + return true; +} + +bool pre_import_failures_preserve_calibration() { + Fixture fixture; + CHECK(make_fixture(&fixture)); + const std::string optical = hex_repeat(0x11); + CHECK(write_text( + fixture.campaign, + "L3DCAL_CAMPAIGN_STATE_V1\nexecution " + + std::to_string(fixture.execution_id + 1) + "\noptical_configuration " + + std::to_string(fixture.configuration_id) + "\noptical_state " + optical + + " body_objective_zoom_focus_stabilization_format_pipeline\n" + "capture 0 1\n")); + + Lardon3DCalibrationToolingView views[kViewCount]{}; + Lardon3DCalibrationToolingCoordinateCheck checks[kCoordinateCount]{}; + Lardon3DCalibrationToolingEntry entries[1]{}; + unsigned char artifact[292]{}; + size_t artifact_size = 99; + Lardon3DCalibrationBootstrapOutput output{}; + CHECK(complete(&fixture, views, kViewCount, checks, kCoordinateCount, entries, + artifact, sizeof(artifact), &artifact_size, &output) == + LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH); + CHECK(artifact_size == 0 && output.scope.scope_id == 0 && + is_calibration_without_scope(fixture)); + close_fixture(&fixture); + + CHECK(make_fixture(&fixture)); + artifact_size = 99; + std::memset(&output, 0x7f, sizeof(output)); + CHECK(complete(&fixture, views, kViewCount - 1, checks, kCoordinateCount, + entries, artifact, sizeof(artifact), &artifact_size, &output) == + LARDON3D_CALIBRATION_WORKFLOW_CAPACITY); + CHECK(artifact_size == 0 && output.scope.scope_id == 0 && + is_calibration_without_scope(fixture)); + close_fixture(&fixture); + return true; +} + +bool tooling_science_rejection_is_not_ready() { + Fixture fixture; + // The external bundle is structurally and provenance-valid, but 0.8 px is + // deliberately above Tooling's frozen hold-out RMSE acceptance threshold. + CHECK(make_fixture(&fixture, 0.8)); + Lardon3DCalibrationToolingView views[kViewCount]{}; + Lardon3DCalibrationToolingCoordinateCheck checks[kCoordinateCount]{}; + Lardon3DCalibrationToolingEntry entries[1]{}; + unsigned char artifact[292]{}; + size_t artifact_size = 99; + Lardon3DCalibrationBootstrapOutput output{}; + CHECK(complete(&fixture, views, kViewCount, checks, kCoordinateCount, entries, + artifact, sizeof(artifact), &artifact_size, &output) == + LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE); + CHECK(artifact_size == 0 && output.scope.scope_id == 0 && + is_calibration_without_scope(fixture)); + close_fixture(&fixture); + return true; +} + +} // namespace + +int main() { + if (!happy_path_and_exact_retry() || + !pre_import_failures_preserve_calibration() || + !tooling_science_rejection_is_not_ready()) + return 1; + std::puts("CALIBRATION_WORKFLOW_READY_V1=PASS"); + return 0; +}