From df7b5891747a8b54708c267032dc8c2d6ea4ef8f Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 3 Sep 2026 10:39:52 +0200 Subject: [PATCH] feat: bind calibration workflow selected execution --- docs/architecture/calibration_workflow.md | 60 ++- docs/roadmap/roadmap.md | 9 +- include/lardon3d/calibration_workflow.h | 19 + meson.build | 22 + prompt/02_CURRENT_FROZEN_STATE.md | 3 +- prompt/12_CALIBRATION.md | 5 +- prompt/31_IMPLEMENTATION_ORDER.md | 4 +- src/calibration_workflow_bind.cpp | 365 ++++++++++++++++ src/calibration_workflow_materialize.cpp | 15 + tests/test_calibration_workflow_bind.cpp | 393 ++++++++++++++++++ .../test_calibration_workflow_materialize.cpp | 1 + 11 files changed, 868 insertions(+), 28 deletions(-) create mode 100644 src/calibration_workflow_bind.cpp create mode 100644 tests/test_calibration_workflow_bind.cpp diff --git a/docs/architecture/calibration_workflow.md b/docs/architecture/calibration_workflow.md index c207e44..5ff1caf 100644 --- a/docs/architecture/calibration_workflow.md +++ b/docs/architecture/calibration_workflow.md @@ -6,7 +6,8 @@ CALIBRATION_WORKFLOW=IN_PROGRESS CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN -CURRENT_WORKFLOW_NEXT=SELECTED_EXECUTION_BINDING_V1 +CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN +CURRENT_WORKFLOW_NEXT=TOOLING_BOOTSTRAP_READY ``` ## Authority @@ -154,28 +155,47 @@ explicit Project DB optical configuration. Absence or disagreement remains `CALIBRATION_UNAVAILABLE`. -## Current next boundary +## Selected Execution Binding v1 ```text -CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1 +CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN ``` -The next stage may read Project DB, but must not mutate it. It must: +The implementation is additive: -- load the exact selected execution and require the calibration/ready stage - appropriate to the existing FROZEN contract; -- require exact selected item count and order; -- match every campaign-state Capture row to the selected item; -- load each Capture's explicit v23 optical assignment and require the exact - declared optical configuration; -- load every selected image/asset identity; -- read the exact managed representation as a bounded regular file; -- require asset byte size and SHA-256 equality; -- decode the exact geometric representation and require oriented dimensions - compatible with the calibration evidence; -- construct the per-image `Lardon3DCalibrationToolingEntry` rows without - changing any scientific value. +```text +src/calibration_workflow_bind.cpp +tests/test_calibration_workflow_bind.cpp +``` -Only after that read-only binding passes may a final workflow boundary invoke -the FROZEN Tooling/Bootstrap path and transition the selected execution to -truthful `READY`. +This boundary is read-only. It may read Project DB and managed representation +bytes, but does not attach a calibration scope, invoke Calibration Tooling or +Bootstrap, or transition the selected execution to `READY`. It proves: + +- exact selected-execution stage, completion, item order and Capture mapping; +- exact explicit v23 optical configuration, including campaign-origin facts + where present; +- exact selected-image/Capture relation and READY image asset identity; +- managed representation size and SHA-256 through project-relative `openat` + descent that rejects absolute paths, dot components, symlinks, non-directory + components and non-regular final files; +- grayscale OpenCV decoded width/height equal to the accepted materialized + calibration geometry; and +- deterministic selected-item-order `Lardon3DCalibrationToolingEntry` + construction from the published solve values, without averaging or solver + recomputation. + +Entries are staged internally and published to caller storage only after every +selected item passes. Exact retries are read-only and deterministic. + +## Current next boundary + +The final calibration workflow boundary is: + +```text +validated input -> materialized evidence -> selected-execution binding +-> FROZEN Calibration Tooling -> FROZEN Calibration Bootstrap -> READY +``` + +It must preserve the binding's exact provenance and use only the FROZEN +Tooling/Bootstrap import path. No failure before import may mutate Project DB. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index a82f3ca..6651dbb 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_COORDINATOR +CURRENT_NEXT CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY ``` The current Project DB head is additive: @@ -531,6 +531,8 @@ The solver session contract now also requires explicit measured `white_border >= `CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN`: the validated external calibration inputs are now materialized into bounded Science v1 target, per-view, coordinate, repeated-solve, fit, residual, hold-out and provenance evidence without Project DB access or mutation. The next boundary is exact Selected Execution Binding v1 before any Tooling/Bootstrap import. +`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. + 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 @@ -900,9 +902,10 @@ PRODUCT_DEFINITION_V1 PASS/FROZEN PROMPT_TREE CURRENT CALIBRATION_EVIDENCE_SOLVER_V1 IMPLEMENTED/VALIDATED CALIBRATION_TOOLING_ALIGNMENT PASS/FROZEN -CURRENT_NEXT CALIBRATION_WORKFLOW_COORDINATOR +CURRENT_NEXT CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY ``` Implementation proceeds only through explicitly human-authorized tranches under `prompt.md` and the numbered `prompt/` execution contract. The current authorized dependency is the final usable -calibration workflow; its next missing sub-boundary is the workflow coordinator. +calibration workflow; its next missing sub-boundary composes the FROZEN Tooling +and Bootstrap importer to reach truthful READY from the validated binding. diff --git a/include/lardon3d/calibration_workflow.h b/include/lardon3d/calibration_workflow.h index a540002..07ab68a 100644 --- a/include/lardon3d/calibration_workflow.h +++ b/include/lardon3d/calibration_workflow.h @@ -25,6 +25,7 @@ typedef enum { LARDON3D_CALIBRATION_WORKFLOW_CAPACITY, LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE, LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH, + LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR, } Lardon3DCalibrationWorkflowResult; /* `campaign_state_path` is a canonical external acquisition manifest: @@ -110,6 +111,11 @@ typedef struct { double holdout_rmse_px; double holdout_maximum_residual_px; uint32_t extra_distortion_coefficient_count; + /* Exact decoded/oriented geometry shared by every accepted calibration view. + * It is retained from detection.json + coordinate evidence and is never + * inferred from camera parameters or campaign images. */ + uint32_t oriented_width; + uint32_t oriented_height; const Lardon3DCalibrationToolingView *views; size_t view_count; const Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks; @@ -137,6 +143,19 @@ lardon3d_calibration_workflow_materialize_external_evidence( size_t coordinate_check_capacity, Lardon3DCalibrationWorkflowExternalEvidence *output); +/* Bind already materialized external evidence to the exact durable selected + * execution. This stage is read-only: it verifies selected item order/Capture + * identity, explicit v23 optical assignment, managed representation + * size/SHA-256, safe project-relative regular-file access and the exact + * OpenCV-decoded oriented dimensions. Caller owns `entries`; `output` borrows + * them on success. No Tooling import or selected-execution mutation occurs. */ +Lardon3DCalibrationWorkflowResult +lardon3d_calibration_workflow_bind_selected_execution( + Lardon3DProjectDb *database, const char *project_path, + const Lardon3DCalibrationWorkflowExternalEvidence *external, + Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity, + Lardon3DCalibrationToolingEvidence *output); + #ifdef __cplusplus } #endif diff --git a/meson.build b/meson.build index de17a23..a4fb0d5 100644 --- a/meson.build +++ b/meson.build @@ -182,6 +182,7 @@ lardon3d_app = executable( 'src/calibration_tooling.c', 'src/calibration_workflow.cpp', 'src/calibration_workflow_materialize.cpp', + 'src/calibration_workflow_bind.cpp', 'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_bundle_adjustment.cpp', @@ -927,6 +928,27 @@ test( timeout: 30, ) + +calibration_workflow_binding_test = executable( + 'test-calibration-workflow-binding', + sources: [ + 'tests/test_calibration_workflow_bind.cpp', + 'src/calibration_workflow_bind.cpp', + 'src/optical_profiles.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, opencv], +) + +test( + 'calibration-workflow-binding', + calibration_workflow_binding_test, + timeout: 30, +) + 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 fef0cef..7b336a4 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_SELECTED_EXECUTION_BINDING +CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY ``` ## Authority @@ -33,6 +33,7 @@ Calibration solver per-view evidence PASS/FROZEN Calibration solver bundle repair PASS/FROZEN Calibration workflow input boundary PASS/FROZEN Calibration workflow evidence materialization PASS/FROZEN +Calibration workflow selected-execution binding PASS/FROZEN Calibration Tooling planarity alignment PASS/FROZEN ``` diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index a2a4bb5..c188e85 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -15,7 +15,8 @@ CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN CALIBRATION_WORKFLOW=IN_PROGRESS CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN -CURRENT_CALIBRATION_NEXT=WORKFLOW_SELECTED_EXECUTION_BINDING_V1 +CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN +CURRENT_CALIBRATION_NEXT=WORKFLOW_TOOLING_BOOTSTRAP_READY ``` ## Authority @@ -58,7 +59,7 @@ dedicated physical calibration acquisition -> real Sparse SfM ``` -The workflow coordinator now has two PASS/FROZEN non-mutating checkpoints. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality. Evidence Materialization v1 parses the retained session and solver bundle into bounded Science v1 target, per-view, coordinate, repeated-solve, fit, residual, hold-out and provenance evidence without opening Project DB. The current implementation gap is Selected Execution Binding v1: bind this external evidence to the exact selected execution, Capture optical assignments and campaign representation bytes, then construct the per-image Tooling entries. Missing or mismatched evidence is rejected; nothing is inferred. +The workflow coordinator now has three PASS/FROZEN non-mutating checkpoints. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality. Evidence Materialization v1 parses the retained session and solver bundle into bounded Science v1 target, per-view, coordinate, repeated-solve, fit, residual, hold-out and provenance evidence without opening Project DB. Selected Execution Binding v1 is read-only: it proves the exact selected execution and Capture mapping, explicit optical configuration, managed representation size/SHA-256, safe project-relative file containment, decoded geometry dimensions, and deterministic `Lardon3DCalibrationToolingEntry` construction. Missing or mismatched evidence is rejected; nothing is inferred. The final remaining boundary composes the validated binding with the FROZEN Tooling and Bootstrap import path to reach truthful `READY`. ## REQUIRED_PRODUCT_TARGET diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index c32e5ca..1abd468 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_SELECTED_EXECUTION_BINDING +CURRENT_NEXT=1_CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY ``` ## 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 and Evidence Materialization v1 PASS/FROZEN; current next sub-boundary: Selected Execution Binding v1; +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; 2. dedicated physical calibrated real campaign; 3. real Sparse SfM proof; 4. durable Dense/OpenMVS orchestration; diff --git a/src/calibration_workflow_bind.cpp b/src/calibration_workflow_bind.cpp new file mode 100644 index 0000000..856dcf2 --- /dev/null +++ b/src/calibration_workflow_bind.cpp @@ -0,0 +1,365 @@ +// Calibration Workflow reaches Project DB through frozen Tooling/Bootstrap +// headers. Establish the C ABI before that transitive include path is guarded; +// project_db.c provides these symbols with C linkage. +extern "C" { +#include +} + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class OwnedFd { + public: + OwnedFd() = default; + explicit OwnedFd(int fd) : fd_(fd) {} + ~OwnedFd() { + if (fd_ >= 0) (void)close(fd_); + } + OwnedFd(const OwnedFd&) = delete; + OwnedFd& operator=(const OwnedFd&) = delete; + OwnedFd(OwnedFd&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + OwnedFd& operator=(OwnedFd&& other) noexcept { + if (this != &other) { + if (fd_ >= 0) (void)close(fd_); + fd_ = other.fd_; + other.fd_ = -1; + } + return *this; + } + int get() const { return fd_; } + bool valid() const { return fd_ >= 0; } + private: + int fd_ = -1; +}; + +Lardon3DCalibrationWorkflowResult db_result(Lardon3DProjectDbResult result) { + if (result == LARDON3D_PROJECT_DB_OK) + return LARDON3D_CALIBRATION_WORKFLOW_OK; + if (result == LARDON3D_PROJECT_DB_NOT_FOUND || + result == LARDON3D_PROJECT_DB_CONSTRAINT) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + return LARDON3D_CALIBRATION_WORKFLOW_PROJECT_DB_ERROR; +} + +bool path_component_valid(std::string_view component) { + return !component.empty() && component != "." && component != ".."; +} + +Lardon3DCalibrationWorkflowResult open_project_asset( + const char *project_path, const char *relative_path, uint64_t expected_size, + const unsigned char expected_sha256[32], std::vector *bytes) { + if (!project_path || !*project_path || !relative_path || !*relative_path || + !expected_sha256 || !bytes) + return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT; + + std::string_view path(relative_path); + if (path.front() == '/' || path.back() == '/') + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + int root = open(project_path, + O_RDONLY | O_DIRECTORY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW); + if (root < 0) { + if (errno == ELOOP || errno == ENOTDIR) + return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE; + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } + OwnedFd current(root); + + size_t at = 0; + while (at < path.size()) { + const size_t slash = path.find('/', at); + const bool last = slash == std::string_view::npos; + const size_t end = last ? path.size() : slash; + const std::string_view component = path.substr(at, end - at); + if (!path_component_valid(component)) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + const std::string name(component); + int flags = O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW; + if (!last) flags |= O_DIRECTORY; + int next = openat(current.get(), name.c_str(), flags); + if (next < 0) { + if (errno == ELOOP || errno == ENOTDIR) + return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE; + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } + OwnedFd opened(next); + + struct stat st{}; + if (fstat(opened.get(), &st) != 0) + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + + if (!last) { + if (!S_ISDIR(st.st_mode)) + return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE; + current = std::move(opened); + at = slash + 1; + continue; + } + + if (!S_ISREG(st.st_mode)) + return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE; + if (st.st_size <= 0 || expected_size == 0 || + static_cast(st.st_size) != expected_size) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + if (expected_size > LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + + bytes->assign(static_cast(expected_size), 0); + size_t used = 0; + while (used < bytes->size()) { + ssize_t count = pread(opened.get(), bytes->data() + used, + bytes->size() - used, static_cast(used)); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + used += static_cast(count); + } + + unsigned char actual[32]{}; + unsigned int digest_size = 0; + if (EVP_Digest(bytes->data(), bytes->size(), actual, &digest_size, + EVP_sha256(), nullptr) != 1 || + digest_size != 32) + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + if (std::memcmp(actual, expected_sha256, 32) != 0) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + return LARDON3D_CALIBRATION_WORKFLOW_OK; + } + + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; +} + +bool execution_stage_valid(const Lardon3DProjectDbSelectedExecution& execution) { + if (execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION) + return !execution.has_calibration_scope; + if (execution.stage == LARDON3D_SELECTED_EXECUTION_READY) + return execution.has_calibration_scope && execution.calibration_scope_id != 0; + return false; +} + +void fill_entry(const Lardon3DCalibrationWorkflowExternalEvidence& external, + const Lardon3DProjectDbSelectedExecutionItem& item, + const Lardon3DProjectDbImageAsset& asset, + Lardon3DCalibrationToolingEntry *entry) { + std::memset(entry, 0, sizeof(*entry)); + entry->image_id = item.image_id; + std::memcpy(entry->representation_sha256, asset.sha256, 32); + std::memcpy(entry->optical_state_sha256, external.optical_state_sha256, 32); + entry->width = external.oriented_width; + entry->height = external.oriented_height; + + const double *p = external.repeated_parameters[0]; + entry->fx = p[0]; + entry->fy = p[1]; + entry->cx = p[2]; + entry->cy = p[3]; + entry->k1 = p[4]; + entry->k2 = p[5]; + entry->p1 = p[6]; + entry->p2 = p[7]; + + const double *fit = external.fit_parameters; + entry->fit_fx = fit[0]; + entry->fit_fy = fit[1]; + entry->fit_cx = fit[2]; + entry->fit_cy = fit[3]; + entry->fit_k1 = fit[4]; + entry->fit_k2 = fit[5]; + entry->fit_p1 = fit[6]; + entry->fit_p2 = fit[7]; + + std::memcpy(entry->repeated_parameters, external.repeated_parameters, + sizeof(entry->repeated_parameters)); + entry->support_images = external.support_images; + entry->support_observations = external.support_observations; + entry->reprojection_rmse_px = external.reprojection_rmse_px; + entry->maximum_parameter_delta = external.maximum_parameter_delta; + entry->validation_flags = external.validation_flags; +} + +void fill_tooling_evidence( + const Lardon3DCalibrationWorkflowExternalEvidence& external, + const Lardon3DCalibrationToolingEntry *entries, size_t entry_count, + Lardon3DCalibrationToolingEvidence *output) { + std::memset(output, 0, sizeof(*output)); + std::memcpy(output->target_sha256, external.target_sha256, 32); + std::memcpy(output->optical_state_sha256, external.optical_state_sha256, 32); + std::memcpy(output->solver_executable_sha256, + external.solver_executable_sha256, 32); + std::memcpy(output->solver_configuration_sha256, + external.solver_configuration_sha256, 32); + std::memcpy(output->initialization_evidence_sha256, + external.initialization_evidence_sha256, 32); + std::memcpy(output->validation_evidence_sha256, + external.validation_evidence_sha256, 32); + + output->target_family = external.target_family; + output->target_squares_x = external.target_squares_x; + output->target_squares_y = external.target_squares_y; + output->target_square_length_mm = external.target_square_length_mm; + output->target_marker_length_mm = external.target_marker_length_mm; + output->target_active_width_mm = external.target_active_width_mm; + output->target_active_height_mm = external.target_active_height_mm; + output->target_white_border_mm = external.target_white_border_mm; + std::memcpy(output->target_measurements_mm, external.target_measurements_mm, + sizeof(output->target_measurements_mm)); + output->measurement_resolution_mm = external.measurement_resolution_mm; + output->target_flatness_mm = external.target_flatness_mm; + output->holdout_rmse_px = external.holdout_rmse_px; + output->holdout_maximum_residual_px = + external.holdout_maximum_residual_px; + output->extra_distortion_coefficient_count = + external.extra_distortion_coefficient_count; + output->views = external.views; + output->view_count = external.view_count; + output->entries = entries; + output->entry_count = entry_count; + output->coordinate_checks = external.coordinate_checks; + output->coordinate_check_count = external.coordinate_check_count; +} + +Lardon3DCalibrationWorkflowResult bind_selected_execution_impl( + Lardon3DProjectDb *database, const char *project_path, + const Lardon3DCalibrationWorkflowExternalEvidence *external, + Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity, + Lardon3DCalibrationToolingEvidence *output) { + if (output) std::memset(output, 0, sizeof(*output)); + if (!database || !project_path || !*project_path || !external || !entries || + !output) + return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT; + + const uint32_t item_count = external->boundary.capture_count; + if (item_count == 0 || + item_count > LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS || + item_count > entry_capacity) + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + if (!external->views || external->view_count == 0 || + external->view_count > LARDON3D_CALIBRATION_TOOLING_MAX_VIEWS || + !external->coordinate_checks || external->coordinate_check_count == 0 || + external->coordinate_check_count > + LARDON3D_CALIBRATION_TOOLING_MAX_COORDINATE_CHECKS || + external->oriented_width == 0 || external->oriented_height == 0) + return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE; + if (std::memcmp(external->optical_state_sha256, + external->boundary.optical_state_sha256, 32) != 0) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + std::vector staged(item_count); + + Lardon3DProjectDbSelectedExecution execution{}; + Lardon3DProjectDbResult dbr = lardon3d_project_db_load_selected_execution( + database, external->boundary.selected_execution_id, &execution); + if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr); + if (!execution_stage_valid(execution) || + execution.next_item_index != execution.item_count || + execution.item_count != item_count) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + Lardon3DOpticalConfiguration configuration{}; + dbr = lardon3d_optical_configuration_load( + database, external->boundary.optical_configuration_id, &configuration); + if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr); + if (configuration.optical_configuration_id != + external->boundary.optical_configuration_id) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + for (uint32_t index = 0; index < item_count; ++index) { + Lardon3DProjectDbSelectedExecutionItem item{}; + dbr = lardon3d_project_db_load_selected_execution_item( + database, execution.execution_id, index, &item); + if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr); + if (item.item_index != index || !item.has_image || item.image_id == 0 || + item.capture_id != external->boundary.capture_ids[index]) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + Lardon3DOpticalCaptureAssignment assignment{}; + dbr = lardon3d_optical_capture_assignment_load( + database, item.capture_id, &assignment); + if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr); + if (assignment.capture_id != item.capture_id || + assignment.optical_configuration_id != + external->boundary.optical_configuration_id) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + if (assignment.has_campaign_origin && + (assignment.campaign_task_id != execution.campaign_task_id || + assignment.campaign_group_id != item.campaign_group_id)) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + Lardon3DProjectDbCapture image_capture{}; + dbr = lardon3d_project_db_find_capture_for_image( + database, item.image_id, &image_capture); + if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr); + if (image_capture.capture_id != item.capture_id) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + Lardon3DProjectDbImage image{}; + Lardon3DProjectDbImageAsset asset{}; + dbr = lardon3d_project_db_load_image(database, item.image_id, &image, &asset); + if (dbr != LARDON3D_PROJECT_DB_OK) return db_result(dbr); + if (image.image_id != item.image_id || image.asset_id != asset.asset_id || + asset.state != LARDON3D_DB_IMAGE_ASSET_READY) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + std::vector bytes; + Lardon3DCalibrationWorkflowResult result = + open_project_asset(project_path, asset.path, asset.size_bytes, + asset.sha256, &bytes); + if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result; + + try { + cv::Mat decoded = cv::imdecode(bytes, cv::IMREAD_GRAYSCALE); + if (decoded.empty() || decoded.cols <= 0 || decoded.rows <= 0 || + static_cast(decoded.cols) != external->oriented_width || + static_cast(decoded.rows) != external->oriented_height) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + } catch (const cv::Exception&) { + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + } catch (...) { + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } + + fill_entry(*external, item, asset, &staged[index]); + } + + std::memcpy(entries, staged.data(), + static_cast(item_count) * sizeof(*entries)); + fill_tooling_evidence(*external, entries, item_count, output); + return LARDON3D_CALIBRATION_WORKFLOW_OK; +} + +} // namespace + +extern "C" Lardon3DCalibrationWorkflowResult +lardon3d_calibration_workflow_bind_selected_execution( + Lardon3DProjectDb *database, const char *project_path, + const Lardon3DCalibrationWorkflowExternalEvidence *external, + Lardon3DCalibrationToolingEntry *entries, size_t entry_capacity, + Lardon3DCalibrationToolingEvidence *output) { + try { + return bind_selected_execution_impl(database, project_path, external, + entries, entry_capacity, output); + } catch (const std::bad_alloc&) { + if (output) std::memset(output, 0, sizeof(*output)); + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + } catch (...) { + if (output) std::memset(output, 0, sizeof(*output)); + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } +} diff --git a/src/calibration_workflow_materialize.cpp b/src/calibration_workflow_materialize.cpp index f871192..4d5ab14 100644 --- a/src/calibration_workflow_materialize.cpp +++ b/src/calibration_workflow_materialize.cpp @@ -554,6 +554,8 @@ bool parse_detection(std::string_view text, const SessionData& session, if (decision != "accepted" && decision != "rejected") return false; if (dv.tooling.accepted) { if (reason != "-" || !coordinate_pass || dv.width == 0 || dv.height == 0 || + dv.width != sv.coordinate_width || + dv.height != sv.coordinate_height || dv.tooling.orientation_degrees != sv.orientation || dv.tooling.corner_rms_px != sv.pre_solve || dv.tooling.distance_metres != sv.distance || @@ -846,14 +848,25 @@ lardon3d_calibration_workflow_materialize_external_evidence( size_t accepted = 0; uint64_t support_observations = 0; + uint32_t oriented_width = 0; + uint32_t oriented_height = 0; for (const DetectionView& view : parsed_views) { if (view.tooling.accepted) { + if (accepted == 0) { + oriented_width = view.width; + oriented_height = view.height; + } else if (view.width != oriented_width || + view.height != oriented_height) { + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + } ++accepted; support_observations += view.tooling.residual_count; if (support_observations > UINT32_MAX) return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; } } + if (accepted == 0 || oriented_width == 0 || oriented_height == 0) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; SolveData solve; if (!parse_solve(solve_text, accepted, &solve)) @@ -906,6 +919,8 @@ lardon3d_calibration_workflow_materialize_external_evidence( output->holdout_rmse_px = evidence.holdout_rmse; output->holdout_maximum_residual_px = evidence.holdout_max; output->extra_distortion_coefficient_count = 0; + output->oriented_width = oriented_width; + output->oriented_height = oriented_height; output->views = views; output->view_count = parsed_views.size(); output->coordinate_checks = coordinate_checks; diff --git a/tests/test_calibration_workflow_bind.cpp b/tests/test_calibration_workflow_bind.cpp new file mode 100644 index 0000000..8ca82ab --- /dev/null +++ b/tests/test_calibration_workflow_bind.cpp @@ -0,0 +1,393 @@ +// The fixture calls the C Project DB API directly. Include it with 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 + +#define CHECK(x) do { if (!(x)) { \ + std::fprintf(stderr, "workflow binding failure %d: %s\n", __LINE__, #x); \ + return false; \ +} } while (0) + +namespace { + +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; +} + +bool digest(const std::vector& bytes, unsigned char output[32]) { + unsigned int size = 0; + return EVP_Digest(bytes.data(), bytes.size(), output, &size, + EVP_sha256(), nullptr) == 1 && size == 32; +} + +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; +} + +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); +} + +struct Fixture { + std::filesystem::path root; + std::filesystem::path db_path; + std::filesystem::path image_path; + std::string image_asset_path; + Lardon3DProjectDb *database = nullptr; + uint64_t execution_id = 0; + uint64_t configuration_id = 0; + std::vector image_bytes; + Lardon3DCalibrationToolingView view{}; + Lardon3DCalibrationToolingCoordinateCheck check{}; + Lardon3DCalibrationWorkflowExternalEvidence external{}; +}; + +void close_fixture(Fixture *fixture) { + if (fixture->database) { + lardon3d_project_db_close(fixture->database); + fixture->database = nullptr; + } + if (!fixture->root.empty()) { + std::error_code ec; + std::filesystem::remove_all(fixture->root, ec); + } +} + +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,'test.png','/source/test.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, "Binding body", sizeof("Binding body")); + std::memcpy(body_input.name, "Binding body", sizeof("Binding 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, "Binding 35", sizeof("Binding 35")); + std::memcpy(lens_input.name, "Binding 35 mm", sizeof("Binding 35 mm")); + Lardon3DOpticalLensProfile lens{}; + if (lardon3d_optical_lens_create( + fixture->database, &lens_input, &lens) != LARDON3D_PROJECT_DB_OK) + return false; + + Lardon3DOpticalConfiguration config_input{}; + config_input.camera_body_profile_id = body.camera_body_profile_id; + config_input.lens_profile_id = lens.lens_profile_id; + config_input.has_focal_length = true; + config_input.focal_length_um = 35000; + Lardon3DOpticalConfiguration configuration{}; + if (lardon3d_optical_configuration_create( + fixture->database, &config_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; +} + +void populate_external(Fixture *fixture, uint32_t width, uint32_t height) { + auto& e = fixture->external; + std::memset(&e, 0, sizeof(e)); + e.boundary.selected_execution_id = fixture->execution_id; + e.boundary.optical_configuration_id = fixture->configuration_id; + e.boundary.capture_count = 1; + e.boundary.capture_ids[0] = 1; + std::memset(e.boundary.optical_state_sha256, 0x11, 32); + std::memset(e.optical_state_sha256, 0x11, 32); + std::memset(e.target_sha256, 0x21, 32); + std::memset(e.solver_executable_sha256, 0x31, 32); + std::memset(e.solver_configuration_sha256, 0x41, 32); + std::memset(e.initialization_evidence_sha256, 0x51, 32); + std::memset(e.validation_evidence_sha256, 0x61, 32); + + e.target_family = + LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100; + e.target_squares_x = 9; + e.target_squares_y = 7; + e.target_square_length_mm = 30.0; + e.target_marker_length_mm = 21.0; + e.target_active_width_mm = 270.0; + e.target_active_height_mm = 210.0; + e.target_white_border_mm = 30.0; + for (double& measurement : e.target_measurements_mm) measurement = 30.0; + e.measurement_resolution_mm = 0.1; + e.target_flatness_mm = NAN; + e.holdout_rmse_px = 0.1; + e.holdout_maximum_residual_px = 0.2; + e.oriented_width = width; + e.oriented_height = height; + + fixture->view.accepted = 1; + fixture->view.corner_count = 40; + fixture->view.residual_count = 40; + fixture->view.target_corner_quadrant_mask = 15; + fixture->view.target_occupancy = 0.3; + fixture->view.normal_angle_degrees = 25.0; + fixture->view.distance_metres = 0.4; + fixture->view.reprojection_rmse_px = 0.1; + fixture->view.maximum_residual_px = 0.2; + fixture->view.quadrant = 0; + fixture->view.distance_band = 1; + e.views = &fixture->view; + e.view_count = 1; + + e.coordinate_checks = &fixture->check; + e.coordinate_check_count = 1; + + const double parameters[8] = { + 80.0, 81.0, 50.0, 40.0, 0.01, -0.01, 0.001, -0.001}; + for (size_t run = 0; run < 3; ++run) + std::memcpy(e.repeated_parameters[run], parameters, sizeof(parameters)); + std::memcpy(e.fit_parameters, parameters, sizeof(parameters)); + e.support_images = 40; + e.support_observations = 1600; + e.reprojection_rmse_px = 0.1; + e.maximum_residual_px = 0.2; + e.maximum_parameter_delta = 0.0; + e.validation_flags = LARDON3D_CALIBRATION_TOOLING_VALIDATION_FLAGS; +} + +bool make_fixture(Fixture *fixture, uint32_t width = 100, + uint32_t height = 80, bool complete = true) { + char temp[] = "/tmp/lardon3d-workflow-bind-XXXXXX"; + char *root = mkdtemp(temp); + if (!root) return false; + fixture->root = root; + fixture->db_path = fixture->root / "project.db"; + + cv::Mat image(static_cast(height), static_cast(width), + 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, image_sha)) return false; + const std::string image_hex = hex(image_sha); + 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; + item.source_asset_id = 0; + 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 (complete && + lardon3d_project_db_record_selected_representation( + fixture->database, fixture->execution_id, 0, 1, 1) != + LARDON3D_PROJECT_DB_OK) + return false; + + populate_external(fixture, width, height); + return true; +} + +bool valid_binding() { + Fixture fixture; + CHECK(make_fixture(&fixture)); + + Lardon3DProjectDbSelectedExecution before{}; + CHECK(lardon3d_project_db_load_selected_execution( + fixture.database, fixture.execution_id, &before) == + LARDON3D_PROJECT_DB_OK); + CHECK(before.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION); + CHECK(!before.has_calibration_scope); + + Lardon3DCalibrationToolingEntry entries[1]{}; + Lardon3DCalibrationToolingEvidence tooling{}; + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &fixture.external, + entries, 1, &tooling) == LARDON3D_CALIBRATION_WORKFLOW_OK); + + CHECK(tooling.entries == entries && tooling.entry_count == 1); + CHECK(tooling.views == fixture.external.views && tooling.view_count == 1); + CHECK(entries[0].image_id == 1); + CHECK(entries[0].width == 100 && entries[0].height == 80); + CHECK(entries[0].fx == 80.0 && entries[0].fy == 81.0); + CHECK(entries[0].fit_fx == 80.0); + CHECK(entries[0].support_images == 40 && + entries[0].support_observations == 1600); + CHECK(entries[0].validation_flags == + LARDON3D_CALIBRATION_TOOLING_VALIDATION_FLAGS); + + Lardon3DCalibrationToolingEntry second[1]{}; + Lardon3DCalibrationToolingEvidence second_tooling{}; + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &fixture.external, + second, 1, &second_tooling) == LARDON3D_CALIBRATION_WORKFLOW_OK); + CHECK(std::memcmp(entries, second, sizeof(entries)) == 0); + + Lardon3DProjectDbSelectedExecution after{}; + CHECK(lardon3d_project_db_load_selected_execution( + fixture.database, fixture.execution_id, &after) == + LARDON3D_PROJECT_DB_OK); + CHECK(after.stage == before.stage && + after.next_item_index == before.next_item_index && + after.has_calibration_scope == before.has_calibration_scope && + after.calibration_scope_id == before.calibration_scope_id); + + close_fixture(&fixture); + return true; +} + +bool mismatch_rejections() { + Fixture fixture; + CHECK(make_fixture(&fixture)); + Lardon3DCalibrationToolingEntry entries[1]{}; + Lardon3DCalibrationToolingEvidence tooling{}; + + auto changed = fixture.external; + changed.boundary.capture_ids[0] = 999; + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &changed, + entries, 1, &tooling) == + LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH); + + changed = fixture.external; + ++changed.boundary.optical_configuration_id; + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &changed, + entries, 1, &tooling) == + LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH); + + changed = fixture.external; + ++changed.oriented_width; + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &changed, + entries, 1, &tooling) == + LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH); + + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &fixture.external, + entries, 0, &tooling) == LARDON3D_CALIBRATION_WORKFLOW_CAPACITY); + + const auto backing = fixture.image_path.parent_path() / "backing.png"; + std::error_code rename_error; + std::filesystem::rename(fixture.image_path, backing, rename_error); + CHECK(!rename_error); + CHECK(symlink("backing.png", fixture.image_path.c_str()) == 0); + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &fixture.external, + entries, 1, &tooling) == + LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE); + CHECK(unlink(fixture.image_path.c_str()) == 0); + rename_error.clear(); + std::filesystem::rename(backing, fixture.image_path, rename_error); + CHECK(!rename_error); + + close_fixture(&fixture); + + CHECK(make_fixture(&fixture, 100, 80, false)); + CHECK(lardon3d_calibration_workflow_bind_selected_execution( + fixture.database, fixture.root.c_str(), &fixture.external, + entries, 1, &tooling) == + LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH); + close_fixture(&fixture); + return true; +} + +} // namespace + +int main() { + if (!valid_binding() || !mismatch_rejections()) return 1; + std::puts("CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS"); + return 0; +} diff --git a/tests/test_calibration_workflow_materialize.cpp b/tests/test_calibration_workflow_materialize.cpp index da6a7d8..50a7781 100644 --- a/tests/test_calibration_workflow_materialize.cpp +++ b/tests/test_calibration_workflow_materialize.cpp @@ -229,6 +229,7 @@ bool valid() { CHECK(lardon3d_calibration_workflow_materialize_external_evidence( &f.files,views,2,checks,40,&out)==LARDON3D_CALIBRATION_WORKFLOW_OK); CHECK(out.view_count==2 && out.coordinate_check_count==40); + CHECK(out.oriented_width==1000 && out.oriented_height==800); CHECK(out.support_images==2 && out.support_observations==40); CHECK(out.validation_flags==15 && out.target_white_border_mm==30.0); CHECK(out.repeated_parameters[0][0]==1000.0 && out.fit_parameters[0]==1000.0);