diff --git a/docs/architecture/calibration_workflow.md b/docs/architecture/calibration_workflow.md index f94809d..c207e44 100644 --- a/docs/architecture/calibration_workflow.md +++ b/docs/architecture/calibration_workflow.md @@ -5,7 +5,8 @@ ```text CALIBRATION_WORKFLOW=IN_PROGRESS CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN -CURRENT_WORKFLOW_NEXT=EVIDENCE_MATERIALIZATION_V1 +CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN +CURRENT_WORKFLOW_NEXT=SELECTED_EXECUTION_BINDING_V1 ``` ## Authority @@ -84,6 +85,60 @@ Input Boundary v1 does not: - invoke Calibration Bootstrap; - change selected-execution state. +## Evidence Materialization v1 + +`CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN`. + +The implementation is additive: + +```text +src/calibration_workflow_materialize.cpp +tests/test_calibration_workflow_materialize.cpp +``` + +It consumes only inputs that first pass Input Boundary v1 and performs no +Project DB access or mutation. + +The caller owns bounded arrays for materialized views and coordinate checks. +On success the output borrows those arrays and retains: + +- exact target generator SHA-256 and physical target measurements; +- measured white border and categorical planarity evidence; +- exact optical-state SHA-256; +- exact solver executable and configuration SHA-256; +- exact accepted/rejected per-view classifications and rejection reasons; +- hold-out assignment, frame region, distance band and target coverage; +- retained per-view residual counts and metrics; +- coordinate-equivalence checks derived from the retained session points; +- all three exact full-solve parameter vectors; +- exact fit parameter vector; +- support image/observation counts; +- global RMSE, maximum residual and high-residual fraction; +- hold-out RMSE and maximum residual; +- maximum parameter delta and `validation_flags=0x0f`. + +The stage consumes published solver evidence; it does not reclassify views, +rerun calibration, average repeated solves or manufacture missing values. + +`initialization_evidence_sha256` is the exact `session.l3dcal` SHA-256. + +`validation_evidence_sha256` is deterministic and domain separated: + +```text +SHA256( + ASCII("L3DCAL_WORKFLOW_VALIDATION_V1\n") + || detection_sha256_raw32 + || solve_sha256_raw32 + || evidence_sha256_raw32 + || producer_sha256_raw32 +) +``` + +This boundary deliberately does not construct per-campaign +`Lardon3DCalibrationToolingEntry` rows. Those rows require Project DB proof of +the selected image identities, representation bytes/dimensions and explicit +Capture optical assignments, which belongs to Selected Execution Binding v1. + ## Campaign optical-state evidence Project DB v23 retains exact explicit optical configuration identity, including @@ -102,21 +157,25 @@ Absence or disagreement remains `CALIBRATION_UNAVAILABLE`. ## Current next boundary ```text -CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1 +CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1 ``` -The next stage parses the already validated bundle into bounded in-memory -Science v1 evidence: +The next stage may read Project DB, but must not mutate it. It must: -- exact per-view evidence; -- exact repeated full-solve parameters; -- fit parameters; -- global and hold-out validation metrics; -- coordinate-equivalence evidence; -- immutable provenance digests. +- 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. -It still performs no Project DB mutation. - -Only after that boundary passes may the workflow bind the exact selected -execution, campaign representations and optical assignments and invoke the -FROZEN Tooling/Bootstrap path. +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`. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 8c0de05..a82f3ca 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -529,6 +529,8 @@ The solver session contract now also requires explicit measured `white_border >= `CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN`: the workflow now has a bounded non-mutating input boundary for session, solver bundle and campaign optical-state evidence. It rejects special/symlink/oversize files, invalid JSON, provenance digest mismatches and incompatible optical state. The next boundary is Evidence Materialization v1. +`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. + 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 diff --git a/include/lardon3d/calibration_workflow.h b/include/lardon3d/calibration_workflow.h index ed876fd..a540002 100644 --- a/include/lardon3d/calibration_workflow.h +++ b/include/lardon3d/calibration_workflow.h @@ -4,6 +4,8 @@ #include #include +#include + #ifdef __cplusplus extern "C" { #endif @@ -72,6 +74,69 @@ Lardon3DCalibrationWorkflowResult lardon3d_calibration_workflow_validate_input_b const Lardon3DCalibrationWorkflowInputFiles *files, Lardon3DCalibrationWorkflowInputBoundary *boundary); + +typedef enum { + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_SOURCE_SIZE = 1, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_DECODE = 2, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_DECODED_DIMENSIONS = 3, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_INSUFFICIENT_CHARUCO = 4, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_INVALID_CHARUCO_ID = 5, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_OCCUPANCY = 6, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_TARGET_PHYSICAL_QUADRANTS = 7, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_CLIPPING = 8, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_PRE_SOLVE_CORNER_RMS = 9, + LARDON3D_CALIBRATION_WORKFLOW_REJECTION_COORDINATE_EQUIVALENCE = 10, +} Lardon3DCalibrationWorkflowRejectionReason; + +typedef struct { + Lardon3DCalibrationWorkflowInputBoundary boundary; + unsigned char target_sha256[32]; + unsigned char optical_state_sha256[32]; + unsigned char solver_executable_sha256[32]; + unsigned char solver_configuration_sha256[32]; + unsigned char initialization_evidence_sha256[32]; + unsigned char validation_evidence_sha256[32]; + uint32_t target_family; + uint32_t target_squares_x; + uint32_t target_squares_y; + double target_square_length_mm; + double target_marker_length_mm; + double target_active_width_mm; + double target_active_height_mm; + double target_white_border_mm; + double target_measurements_mm[LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS]; + double measurement_resolution_mm; + double target_flatness_mm; + double holdout_rmse_px; + double holdout_maximum_residual_px; + uint32_t extra_distortion_coefficient_count; + const Lardon3DCalibrationToolingView *views; + size_t view_count; + const Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks; + size_t coordinate_check_count; + double repeated_parameters[3][8]; + double fit_parameters[8]; + uint32_t support_images; + uint32_t support_observations; + double reprojection_rmse_px; + double maximum_residual_px; + double high_residual_fraction; + double maximum_parameter_delta; + uint32_t validation_flags; +} Lardon3DCalibrationWorkflowExternalEvidence; + +/* Materialize the already validated external session and solver bundle into + * bounded Science-v1 evidence. Caller owns `views` and `coordinate_checks`; + * output borrows those arrays on success. This stage performs no Project DB + * access, no Tooling import and no selected-execution mutation. */ +Lardon3DCalibrationWorkflowResult +lardon3d_calibration_workflow_materialize_external_evidence( + const Lardon3DCalibrationWorkflowInputFiles *files, + Lardon3DCalibrationToolingView *views, size_t view_capacity, + Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks, + size_t coordinate_check_capacity, + Lardon3DCalibrationWorkflowExternalEvidence *output); + #ifdef __cplusplus } #endif diff --git a/meson.build b/meson.build index fe5899e..de17a23 100644 --- a/meson.build +++ b/meson.build @@ -181,6 +181,7 @@ lardon3d_app = executable( 'src/calibration_bootstrap.c', 'src/calibration_tooling.c', 'src/calibration_workflow.cpp', + 'src/calibration_workflow_materialize.cpp', 'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_bundle_adjustment.cpp', @@ -908,6 +909,24 @@ calibration_workflow_test = executable( test('calibration-workflow', calibration_workflow_test, timeout: 30) + +calibration_workflow_materialization_test = executable( + 'test-calibration-workflow-materialization', + sources: [ + 'tests/test_calibration_workflow_materialize.cpp', + 'src/calibration_workflow.cpp', + 'src/calibration_workflow_materialize.cpp', + ], + include_directories: include_directories('include'), + dependencies: [openssl], +) + +test( + 'calibration-workflow-materialization', + calibration_workflow_materialization_test, + timeout: 30, +) + 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 97a3e1a..fef0cef 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_EVIDENCE_MATERIALIZATION +CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING ``` ## Authority @@ -32,6 +32,7 @@ Calibration solver producer identity PASS/FROZEN Calibration solver per-view evidence PASS/FROZEN Calibration solver bundle repair PASS/FROZEN Calibration workflow input boundary PASS/FROZEN +Calibration workflow evidence materialization PASS/FROZEN Calibration Tooling planarity alignment PASS/FROZEN ``` diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index d76c9a7..a2a4bb5 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -14,7 +14,8 @@ CALIBRATION_SOLVER_PER_VIEW_EVIDENCE_V1=PASS/FROZEN CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN CALIBRATION_WORKFLOW=IN_PROGRESS CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN -CURRENT_CALIBRATION_NEXT=WORKFLOW_EVIDENCE_MATERIALIZATION_V1 +CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN +CURRENT_CALIBRATION_NEXT=WORKFLOW_SELECTED_EXECUTION_BINDING_V1 ``` ## Authority @@ -23,7 +24,7 @@ CURRENT_CALIBRATION_NEXT=WORKFLOW_EVIDENCE_MATERIALIZATION_V1 `docs/architecture/calibration_tooling.md` is the specialized Tooling authority. -`docs/architecture/calibration_workflow.md` is the specialized workflow authority. The public API remains `include/lardon3d/calibration_tooling.h`. +`docs/architecture/calibration_workflow.md` is the specialized workflow authority. Its public API is `include/lardon3d/calibration_workflow.h`; the FROZEN Tooling API remains `include/lardon3d/calibration_tooling.h`. A bounded corrective review established that Calibration Science v1 defines target planarity as a categorical physical attestation, not a numeric flatness tolerance. Tooling preserves its public structure layout while requiring `target_flatness_mm` to be NaN, so callers cannot invent a millimetre measurement. The canonical session's `planarity PASS ` evidence is bound through immutable initialization evidence. @@ -57,7 +58,7 @@ dedicated physical calibration acquisition -> real Sparse SfM ``` -The workflow coordinator is now implemented through its first bounded checkpoint. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality without Project DB mutation. The current implementation gap is Evidence Materialization v1. It consumes the immutable `session.l3dcal` plus `detection.json`, `solve.json` and `evidence.json`, binds them to the exact selected execution and optical state, constructs the bounded Tooling evidence and never manufactures missing physical evidence. +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. ## REQUIRED_PRODUCT_TARGET diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index 252060f..c32e5ca 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_EVIDENCE_MATERIALIZATION +CURRENT_NEXT=1_CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING ``` ## 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 PASS/FROZEN; current next sub-boundary: Evidence Materialization v1; +1. final usable calibration workflow — IN PROGRESS; Input Boundary v1 and Evidence Materialization v1 PASS/FROZEN; current next sub-boundary: Selected Execution Binding v1; 2. dedicated physical calibrated real campaign; 3. real Sparse SfM proof; 4. durable Dense/OpenMVS orchestration; diff --git a/src/calibration_workflow_materialize.cpp b/src/calibration_workflow_materialize.cpp new file mode 100644 index 0000000..f871192 --- /dev/null +++ b/src/calibration_workflow_materialize.cpp @@ -0,0 +1,925 @@ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr size_t kShaBytes = 32; +constexpr size_t kMaxObservations = + LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS * 48u; + +struct ShaLess { + bool operator()(const std::array& a, + const std::array& b) const { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); + } +}; + + +bool hex_digit(char c, unsigned *value) { + if (c >= '0' && c <= '9') *value = static_cast(c - '0'); + else if (c >= 'a' && c <= 'f') *value = static_cast(c - 'a' + 10); + else if (c >= 'A' && c <= 'F') *value = static_cast(c - 'A' + 10); + else return false; + return true; +} + +bool parse_sha(std::string_view text, unsigned char output[32]) { + if (text.size() != 64) return false; + for (size_t i = 0; i < 32; ++i) { + unsigned hi = 0, lo = 0; + if (!hex_digit(text[2 * i], &hi) || !hex_digit(text[2 * i + 1], &lo)) + return false; + output[i] = static_cast((hi << 4u) | lo); + } + return true; +} + +std::array sha_array(const unsigned char value[32]) { + std::array out{}; + std::memcpy(out.data(), value, 32); + return out; +} + +bool digest(const unsigned char *data, size_t size, unsigned char output[32]) { + unsigned int length = 0; + return EVP_Digest(data, size, output, &length, EVP_sha256(), nullptr) == 1 && + length == 32; +} + +Lardon3DCalibrationWorkflowResult read_verified( + const char *path, const unsigned char expected[32], std::string *output) { + if (!path || !expected || !output) + return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT; + int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) { + if (errno == ELOOP) return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE; + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } + struct stat st{}; + if (fstat(fd, &st) != 0) { + close(fd); + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } + if (!S_ISREG(st.st_mode)) { + close(fd); + return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE; + } + if (st.st_size <= 0) { + close(fd); + return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE; + } + if (static_cast(st.st_size) > + LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) { + close(fd); + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + } + const size_t size = static_cast(st.st_size); + output->assign(size, '\0'); + size_t used = 0; + while (used < size) { + ssize_t n = pread(fd, output->data() + used, size - used, + static_cast(used)); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) { + close(fd); + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + } + used += static_cast(n); + } + if (close(fd) != 0) return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + unsigned char actual[32]{}; + if (!digest(reinterpret_cast(output->data()), + output->size(), actual)) + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + if (std::memcmp(actual, expected, 32) != 0) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + return LARDON3D_CALIBRATION_WORKFLOW_OK; +} + +bool parse_u64(std::string_view text, uint64_t *value) { + if (!value || text.empty()) return false; + uint64_t v = 0; + auto result = std::from_chars(text.data(), text.data() + text.size(), v, 10); + if (result.ec != std::errc() || result.ptr != text.data() + text.size()) + return false; + *value = v; + return true; +} + +bool parse_u32(std::string_view text, uint32_t *value) { + uint64_t v = 0; + if (!parse_u64(text, &v) || v > UINT32_MAX) return false; + *value = static_cast(v); + return true; +} + +bool parse_double_c(std::string_view text, double *value) { + if (!value || text.empty() || text.size() > 128) return false; + char buffer[129]; + std::memcpy(buffer, text.data(), text.size()); + buffer[text.size()] = '\0'; + locale_t locale = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if (!locale) return false; + char *end = nullptr; + errno = 0; + double v = strtod_l(buffer, &end, locale); + freelocale(locale); + if (errno == ERANGE || end != buffer + text.size() || !std::isfinite(v)) + return false; + *value = v; + return true; +} + +std::vector words(std::string_view line) { + std::vector out; + size_t at = 0; + while (at < line.size()) { + while (at < line.size() && (line[at] == ' ' || line[at] == '\t')) ++at; + if (at == line.size()) break; + size_t end = at; + while (end < line.size() && line[end] != ' ' && line[end] != '\t') ++end; + out.push_back(line.substr(at, end - at)); + at = end; + } + return out; +} + +bool token_ok(std::string_view token) { + if (token.empty() || + token.size() >= LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY) + return false; + for (unsigned char c : token) { + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' || + c == ':')) + return false; + } + return true; +} + +struct SessionView { + std::array sha{}; + uint32_t orientation = 0; + double pre_solve = -1.0; + double clipping = -1.0; + double distance = -1.0; + uint32_t distance_band = UINT32_MAX; + uint32_t coordinate_orientation = UINT32_MAX; + uint32_t coordinate_width = 0; + uint32_t coordinate_height = 0; + uint32_t coordinate_count = 0; + unsigned coordinate_coverage = 0; + std::vector checks; +}; + +struct SessionData { + std::string target_id; + std::string instrument; + std::string decoder; + std::string decoder_version; + std::string optical_token; + unsigned char generator_sha[32]{}; + unsigned char planarity_sha[32]{}; + unsigned char optical_sha[32]{}; + double measurements[10]{}; + double measurement_resolution = 0.0; + double white_border = 0.0; + std::vector views; +}; + +int coverage_bit(std::string_view label) { + static const std::array labels = { + "center", "top", "right", "bottom", "left", + "top_left", "top_right", "bottom_left", "bottom_right"}; + for (size_t i = 0; i < labels.size(); ++i) + if (label == labels[i]) return static_cast(i); + return -1; +} + +bool parse_session_materialization(std::string_view text, SessionData *out) { + if (!out || text.empty() || text.back() != '\n' || + text.find('\0') != std::string_view::npos || + text.find('\r') != std::string_view::npos) + return false; + std::map, size_t, ShaLess> index; + bool target = false, measurement = false, border = false, planarity = false; + bool decoder = false, optical = false; + size_t at = 0, line_number = 0; + while (at < text.size()) { + size_t end = text.find('\n', at); + if (end == std::string_view::npos) return false; + std::string_view line = text.substr(at, end - at); + at = end + 1; + ++line_number; + if (line_number == 1) { + if (line != "L3DCAL_SESSION_V1") return false; + continue; + } + if (line.empty()) continue; + auto w = words(line); + if (w.empty()) return false; + if (w[0] == "target") { + uint32_t sx = 0, sy = 0; + double square = 0.0, marker = 0.0; + if (target || w.size() != 8 || !token_ok(w[1]) || + !parse_sha(w[2], out->generator_sha) || + w[3] != "DICT_5X5_100" || !parse_u32(w[4], &sx) || + !parse_u32(w[5], &sy) || !parse_double_c(w[6], &square) || + !parse_double_c(w[7], &marker) || sx != 9 || sy != 7 || + square != 30.0 || marker != 21.0) + return false; + out->target_id.assign(w[1]); + target = true; + } else if (w[0] == "measurement") { + if (measurement || w.size() != 13 || !token_ok(w[1]) || + !parse_double_c(w[2], &out->measurement_resolution)) + return false; + out->instrument.assign(w[1]); + for (size_t i = 0; i < 10; ++i) + if (!parse_double_c(w[i + 3], &out->measurements[i])) return false; + measurement = true; + } else if (w[0] == "white_border") { + if (border || w.size() != 2 || !parse_double_c(w[1], &out->white_border)) + return false; + border = true; + } else if (w[0] == "planarity") { + if (planarity || w.size() != 3 || w[1] != "PASS" || + !parse_sha(w[2], out->planarity_sha)) + return false; + planarity = true; + } else if (w[0] == "decoder") { + if (decoder || w.size() != 3 || !token_ok(w[1]) || !token_ok(w[2])) + return false; + out->decoder.assign(w[1]); + out->decoder_version.assign(w[2]); + decoder = true; + } else if (w[0] == "optical_state") { + if (optical || w.size() != 3 || !parse_sha(w[1], out->optical_sha) || + !token_ok(w[2]) || w[2] == "UNKNOWN") + return false; + out->optical_token.assign(w[2]); + optical = true; + } else if (w[0] == "image") { + if (w.size() != 4 || w[1].empty() || w[1].size() > 4096) return false; + SessionView view; + if (!parse_sha(w[2], view.sha.data()) || !parse_u32(w[3], &view.orientation) || + (view.orientation != 0 && view.orientation != 90 && + view.orientation != 180 && view.orientation != 270)) + return false; + auto inserted = index.emplace(view.sha, out->views.size()); + if (!inserted.second || + out->views.size() >= LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS) + return false; + out->views.push_back(view); + } else if (w[0] == "pre_solve" || w[0] == "clipping" || + w[0] == "distance" || w[0] == "coordinate" || + w[0] == "coordinate_point") { + if (w.size() < 2) return false; + std::array sha{}; + if (!parse_sha(w[1], sha.data())) return false; + auto found = index.find(sha); + if (found == index.end()) return false; + SessionView& view = out->views[found->second]; + if (w[0] == "pre_solve") { + if (w.size() != 3 || view.pre_solve >= 0 || + !parse_double_c(w[2], &view.pre_solve)) + return false; + } else if (w[0] == "clipping") { + if (w.size() != 3 || view.clipping >= 0 || + !parse_double_c(w[2], &view.clipping)) + return false; + } else if (w[0] == "distance") { + if (w.size() != 4 || view.distance > 0 || + !parse_double_c(w[2], &view.distance) || + !parse_u32(w[3], &view.distance_band) || + view.distance <= 0 || view.distance_band > 2) + return false; + } else if (w[0] == "coordinate") { + double initial_dx = 0.0, initial_dy = 0.0; + if (w.size() != 10 || view.coordinate_count != 0 || + w[2] != out->decoder || w[3] != out->decoder_version || + !parse_u32(w[4], &view.coordinate_orientation) || + !parse_u32(w[5], &view.coordinate_width) || + !parse_u32(w[6], &view.coordinate_height) || + !parse_u32(w[7], &view.coordinate_count) || + !parse_double_c(w[8], &initial_dx) || + !parse_double_c(w[9], &initial_dy) || + view.coordinate_orientation != view.orientation || + view.coordinate_width == 0 || view.coordinate_height == 0 || + view.coordinate_count < 20 || view.coordinate_count > 48 || + initial_dx < 0 || initial_dy < 0) + return false; + view.checks.reserve(view.coordinate_count); + } else { + double sx = 0.0, sy = 0.0, fx = 0.0, fy = 0.0; + if (w.size() != 7 || view.coordinate_count == 0 || + view.checks.size() >= view.coordinate_count || + !parse_double_c(w[3], &sx) || !parse_double_c(w[4], &sy) || + !parse_double_c(w[5], &fx) || !parse_double_c(w[6], &fy)) + return false; + int bit = coverage_bit(w[2]); + if (bit < 0) return false; + view.coordinate_coverage |= 1u << static_cast(bit); + Lardon3DCalibrationToolingCoordinateCheck check{}; + std::memcpy(check.source_sha256, view.sha.data(), 32); + check.orientation_degrees = view.orientation; + check.dx_px = sx - fx; + check.dy_px = sy - fy; + view.checks.push_back(check); + } + } else { + return false; + } + } + if (!target || !measurement || !border || !planarity || !decoder || !optical || + out->views.empty()) + return false; + for (const SessionView& view : out->views) { + if (view.pre_solve < 0 || view.clipping < 0 || view.distance <= 0 || + view.distance_band > 2 || view.coordinate_count < 20 || + view.checks.size() != view.coordinate_count || + view.coordinate_coverage != 0x1ffu) + return false; + } + return true; +} + +class JsonCursor { + public: + explicit JsonCursor(std::string_view text) : text_(text) {} + void ws() { + while (at_ < text_.size() && + (text_[at_] == ' ' || text_[at_] == '\n' || + text_[at_] == '\r' || text_[at_] == '\t')) + ++at_; + } + bool ch(char expected) { + ws(); + if (at_ >= text_.size() || text_[at_] != expected) return false; + ++at_; + return true; + } + bool key(std::string_view expected) { + std::string value; + return string(&value) && value == expected && ch(':'); + } + bool string(std::string *output) { + ws(); + if (at_ >= text_.size() || text_[at_] != '"') return false; + ++at_; + output->clear(); + while (at_ < text_.size()) { + unsigned char c = static_cast(text_[at_++]); + if (c == '"') return true; + if (c < 0x20 || c == '\\') return false; + output->push_back(static_cast(c)); + } + return false; + } + bool u64(uint64_t *output) { + ws(); + size_t start = at_; + if (start >= text_.size() || text_[start] < '0' || text_[start] > '9') + return false; + while (at_ < text_.size() && text_[at_] >= '0' && text_[at_] <= '9') ++at_; + return parse_u64(text_.substr(start, at_ - start), output); + } + bool u32(uint32_t *output) { + uint64_t v = 0; + if (!u64(&v) || v > UINT32_MAX) return false; + *output = static_cast(v); + return true; + } + bool boolean(bool *output) { + ws(); + if (text_.substr(at_, 4) == "true") { + at_ += 4; *output = true; return true; + } + if (text_.substr(at_, 5) == "false") { + at_ += 5; *output = false; return true; + } + return false; + } + bool hex_double(double *output) { + std::string value; + return string(&value) && parse_double_c(value, output); + } + bool done() { + ws(); + return at_ == text_.size(); + } + private: + std::string_view text_; + size_t at_ = 0; +}; + +bool comma(JsonCursor *j) { return j->ch(','); } + +uint32_t rejection_reason(std::string_view reason) { + static const std::array reasons = { + "source_size", "decode", "decoded_dimensions", "insufficient_charuco", + "invalid_charuco_id", "occupancy", "target_physical_quadrants", + "clipping", "pre_solve_corner_rms", "coordinate_equivalence"}; + for (size_t i = 0; i < reasons.size(); ++i) + if (reason == reasons[i]) return static_cast(i + 1); + return 0; +} + +struct DetectionView { + Lardon3DCalibrationToolingView tooling{}; + uint32_t width = 0; + uint32_t height = 0; + bool coordinate_pass = false; + std::set corner_ids; +}; + +bool parse_detection(std::string_view text, const SessionData& session, + std::vector *views) { + JsonCursor j(text); + std::string format, decoder, decoder_version; + if (!j.ch('{') || !j.key("format") || !j.string(&format) || + format != "L3DCAL_DETECTION_V1" || !comma(&j) || + !j.key("decoder") || !j.string(&decoder) || decoder != session.decoder || + !comma(&j) || !j.key("decoder_version") || + !j.string(&decoder_version) || decoder_version != session.decoder_version || + !comma(&j) || !j.key("views") || !j.ch('[')) + return false; + + std::map, size_t, ShaLess> session_index; + for (size_t i = 0; i < session.views.size(); ++i) + session_index.emplace(session.views[i].sha, i); + std::set, ShaLess> seen; + std::array previous{}; + bool have_previous = false; + + j.ws(); + if (!j.ch(']')) { + while (true) { + DetectionView dv; + std::string sha_text, decision, reason; + uint32_t physical_quadrants = 0, comparison_points = 0; + double coordinate_dx = 0.0, coordinate_dy = 0.0; + bool coordinate_pass = false, holdout = false; + if (!j.ch('{') || !j.key("source_sha256") || !j.string(&sha_text) || + !parse_sha(sha_text, dv.tooling.source_sha256) || !comma(&j) || + !j.key("orientation") || !j.u32(&dv.tooling.orientation_degrees) || + !comma(&j) || !j.key("oriented_width") || !j.u32(&dv.width) || + !comma(&j) || !j.key("oriented_height") || !j.u32(&dv.height) || + !comma(&j) || !j.key("decision") || !j.string(&decision) || + !comma(&j) || !j.key("reason") || !j.string(&reason) || + !comma(&j) || !j.key("frame_region") || !j.u32(&dv.tooling.quadrant) || + !comma(&j) || !j.key("distance_band") || + !j.u32(&dv.tooling.distance_band) || !comma(&j) || + !j.key("holdout") || !j.boolean(&holdout) || !comma(&j) || + !j.key("target_occupancy") || !j.hex_double(&dv.tooling.target_occupancy) || + !comma(&j) || !j.key("normal_angle_degrees") || + !j.hex_double(&dv.tooling.normal_angle_degrees) || !comma(&j) || + !j.key("measured_distance_metres") || + !j.hex_double(&dv.tooling.distance_metres) || !comma(&j) || + !j.key("pre_solve_corner_rms_px") || + !j.hex_double(&dv.tooling.corner_rms_px) || !comma(&j) || + !j.key("clipping_fraction") || + !j.hex_double(&dv.tooling.clipped_fraction) || !comma(&j) || + !j.key("physical_target_quadrants") || !j.u32(&physical_quadrants) || + !comma(&j) || !j.key("target_corner_quadrant_mask") || + !j.u32(&dv.tooling.target_corner_quadrant_mask) || !comma(&j) || + !j.key("corner_count") || !j.u32(&dv.tooling.corner_count) || + !comma(&j) || !j.key("residual_count") || + !j.u32(&dv.tooling.residual_count) || !comma(&j) || + !j.key("high_residual_count") || + !j.u32(&dv.tooling.high_residual_count) || !comma(&j) || + !j.key("reprojection_rmse_px") || + !j.hex_double(&dv.tooling.reprojection_rmse_px) || !comma(&j) || + !j.key("maximum_residual_px") || + !j.hex_double(&dv.tooling.maximum_residual_px) || !comma(&j) || + !j.key("coordinate_equivalence") || !j.ch('{') || + !j.key("comparison_points") || !j.u32(&comparison_points) || + !comma(&j) || !j.key("max_abs_dx_px") || !j.hex_double(&coordinate_dx) || + !comma(&j) || !j.key("max_abs_dy_px") || !j.hex_double(&coordinate_dy) || + !comma(&j) || !j.key("pass") || !j.boolean(&coordinate_pass) || + !j.ch('}') || !comma(&j) || !j.key("corners") || !j.ch('[')) + return false; + + auto key = sha_array(dv.tooling.source_sha256); + auto sit = session_index.find(key); + if (sit == session_index.end() || !seen.insert(key).second) return false; + if (have_previous && + !std::lexicographical_compare(previous.begin(), previous.end(), + key.begin(), key.end())) + return false; + previous = key; + have_previous = true; + const SessionView& sv = session.views[sit->second]; + + size_t corner_count = 0; + j.ws(); + if (!j.ch(']')) { + while (true) { + uint32_t id = 0; + double x = 0.0, y = 0.0; + if (!j.ch('{') || !j.key("id") || !j.u32(&id) || !comma(&j) || + !j.key("x") || !j.hex_double(&x) || !comma(&j) || + !j.key("y") || !j.hex_double(&y) || !j.ch('}') || + !dv.corner_ids.insert(id).second) + return false; + ++corner_count; + j.ws(); + if (j.ch(']')) break; + if (!comma(&j)) return false; + } + } + if (!j.ch('}')) return false; + + dv.tooling.accepted = decision == "accepted" ? 1u : 0u; + if (decision != "accepted" && decision != "rejected") return false; + if (dv.tooling.accepted) { + if (reason != "-" || !coordinate_pass || dv.width == 0 || dv.height == 0 || + dv.tooling.orientation_degrees != sv.orientation || + dv.tooling.corner_rms_px != sv.pre_solve || + dv.tooling.distance_metres != sv.distance || + dv.tooling.distance_band != sv.distance_band || + comparison_points != sv.coordinate_count || + dv.tooling.corner_count != corner_count || + dv.tooling.residual_count != dv.tooling.corner_count) + return false; + } else { + dv.tooling.rejection_reason = rejection_reason(reason); + if (reason == "-" || dv.tooling.rejection_reason == 0 || holdout) + return false; + } + dv.tooling.holdout = holdout ? 1u : 0u; + unsigned pop = 0; + for (unsigned bit = 0; bit < 4; ++bit) + pop += (dv.tooling.target_corner_quadrant_mask >> bit) & 1u; + if (physical_quadrants != pop || + dv.tooling.high_residual_count > dv.tooling.residual_count || + coordinate_dx < 0 || coordinate_dy < 0) + return false; + dv.coordinate_pass = coordinate_pass; + views->push_back(std::move(dv)); + j.ws(); + if (j.ch(']')) break; + if (!comma(&j)) return false; + } + } + if (!j.ch('}') || !j.done() || views->size() != session.views.size()) + return false; + return true; +} + +bool parse_parameter_array(JsonCursor *j, double output[8]) { + if (!j->ch('[')) return false; + for (size_t i = 0; i < 8; ++i) { + if (!j->hex_double(&output[i])) return false; + if (i + 1 != 8 && !comma(j)) return false; + } + return j->ch(']'); +} + +bool parse_vec3(JsonCursor *j) { + if (!j->ch('[')) return false; + for (size_t i = 0; i < 3; ++i) { + double value = 0.0; + if (!j->hex_double(&value)) return false; + if (i + 1 != 3 && !comma(j)) return false; + } + return j->ch(']'); +} + +struct SolveData { + double repeated[3][8]{}; + double fit[8]{}; +}; + +bool parse_solve(std::string_view text, size_t accepted_count, SolveData *out) { + JsonCursor j(text); + std::string format; + if (!j.ch('{') || !j.key("format") || !j.string(&format) || + format != "L3DCAL_SOLVE_V1" || !comma(&j) || + !j.key("runs") || !j.ch('[')) + return false; + for (uint32_t run = 0; run < 3; ++run) { + uint32_t run_id = UINT32_MAX; + double opencv_rms = 0.0; + if (!j.ch('{') || !j.key("run") || !j.u32(&run_id) || run_id != run || + !comma(&j) || !j.key("params") || + !parse_parameter_array(&j, out->repeated[run]) || !comma(&j) || + !j.key("opencv_rms_px") || !j.hex_double(&opencv_rms) || + opencv_rms < 0 || !comma(&j) || !j.key("poses") || !j.ch('[')) + return false; + size_t poses = 0; + j.ws(); + if (!j.ch(']')) { + while (true) { + if (!j.ch('{') || !j.key("rvec") || !parse_vec3(&j) || + !comma(&j) || !j.key("tvec_m") || !parse_vec3(&j) || !j.ch('}')) + return false; + ++poses; + j.ws(); + if (j.ch(']')) break; + if (!comma(&j)) return false; + } + } + if (poses != accepted_count || !j.ch('}')) return false; + if (run != 2 && !comma(&j)) return false; + } + if (!j.ch(']') || !comma(&j) || !j.key("fit_params") || + !parse_parameter_array(&j, out->fit) || !j.ch('}') || !j.done()) + return false; + for (size_t run = 1; run < 3; ++run) + if (std::memcmp(out->repeated[0], out->repeated[run], + sizeof(out->repeated[0])) != 0) + return false; + return true; +} + +struct EvidenceData { + double global_rmse = 0.0; + double global_max = 0.0; + double high_fraction = 0.0; + double holdout_rmse = 0.0; + double holdout_max = 0.0; + double maximum_delta = 0.0; + uint32_t flags = 0; +}; + +bool parse_flags(std::string_view text, uint32_t *flags) { + if (text.size() != 3 || text[0] != '0' || text[1] != 'x') return false; + unsigned v = 0; + if (!hex_digit(text[2], &v) || v > UINT32_MAX) return false; + *flags = static_cast(v); + return true; +} + +bool parse_evidence(std::string_view text, const SessionData& session, + const std::vector& views, + EvidenceData *out) { + JsonCursor j(text); + std::string format, target_id, generator, instrument, planarity, optical, + optical_state, flags_text; + double resolution = 0.0; + bool deterministic = false; + if (!j.ch('{') || !j.key("format") || !j.string(&format) || + format != "L3DCAL_EVIDENCE_BUNDLE_V1" || !comma(&j) || + !j.key("target") || !j.ch('{') || + !j.key("id") || !j.string(&target_id) || !comma(&j) || + !j.key("generator_sha256") || !j.string(&generator) || !comma(&j) || + !j.key("instrument") || !j.string(&instrument) || !comma(&j) || + !j.key("resolution_mm") || !j.hex_double(&resolution) || !comma(&j) || + !j.key("planarity_evidence_sha256") || !j.string(&planarity) || + !j.ch('}') || !comma(&j) || + !j.key("optical_sha256") || !j.string(&optical) || !comma(&j) || + !j.key("optical_state") || !j.string(&optical_state) || !comma(&j) || + !j.key("validation_flags") || !j.string(&flags_text) || !comma(&j) || + !j.key("global_rmse_px") || !j.hex_double(&out->global_rmse) || + !comma(&j) || !j.key("maximum_residual_px") || + !j.hex_double(&out->global_max) || !comma(&j) || + !j.key("high_residual_fraction") || !j.hex_double(&out->high_fraction) || + !comma(&j) || !j.key("holdout") || !j.ch('{') || + !j.key("rmse_px") || !j.hex_double(&out->holdout_rmse) || !comma(&j) || + !j.key("maximum_px") || !j.hex_double(&out->holdout_max) || + !j.ch('}') || !comma(&j) || + !j.key("maximum_parameter_delta_px") || + !j.hex_double(&out->maximum_delta) || !comma(&j) || + !j.key("deterministic_full_solve_equality") || + !j.boolean(&deterministic) || !comma(&j) || + !j.key("residuals") || !j.ch('[')) + return false; + + unsigned char generator_sha[32]{}, planarity_sha[32]{}, optical_sha[32]{}; + if (target_id != session.target_id || instrument != session.instrument || + resolution != session.measurement_resolution || + !parse_sha(generator, generator_sha) || + !parse_sha(planarity, planarity_sha) || !parse_sha(optical, optical_sha) || + std::memcmp(generator_sha, session.generator_sha, 32) != 0 || + std::memcmp(planarity_sha, session.planarity_sha, 32) != 0 || + std::memcmp(optical_sha, session.optical_sha, 32) != 0 || + optical_state != session.optical_token || + !parse_flags(flags_text, &out->flags) || out->flags != 0x0fu || + !deterministic || out->global_rmse < 0 || out->global_max < 0 || + out->high_fraction < 0 || out->high_fraction > 1 || + out->holdout_rmse < 0 || out->holdout_max < 0 || + out->maximum_delta < 0) + return false; + + std::map, size_t, ShaLess> view_index; + for (size_t i = 0; i < views.size(); ++i) + view_index.emplace(sha_array(views[i].tooling.source_sha256), i); + std::vector counts(views.size(), 0), highs(views.size(), 0); + std::set> residual_ids; + uint64_t total = 0, total_high = 0; + + j.ws(); + if (!j.ch(']')) { + while (true) { + std::string sha_text; + uint32_t corner_id = 0; + double dx = 0.0, dy = 0.0, rmse = 0.0; + if (!j.ch('{') || !j.key("source_sha256") || !j.string(&sha_text) || + !comma(&j) || !j.key("corner_id") || !j.u32(&corner_id) || + !comma(&j) || !j.key("dx_px") || !j.hex_double(&dx) || + !comma(&j) || !j.key("dy_px") || !j.hex_double(&dy) || + !comma(&j) || !j.key("rmse_px") || !j.hex_double(&rmse) || + !j.ch('}')) + return false; + unsigned char sha[32]{}; + if (!parse_sha(sha_text, sha)) return false; + auto found = view_index.find(sha_array(sha)); + if (found == view_index.end()) return false; + size_t vi = found->second; + const DetectionView& dv = views[vi]; + if (!dv.tooling.accepted || dv.corner_ids.find(corner_id) == dv.corner_ids.end() || + !residual_ids.emplace(vi, corner_id).second || + rmse != dv.tooling.reprojection_rmse_px) + return false; + ++counts[vi]; + ++total; + const double squared = dx * dx + dy * dy; + if (!std::isfinite(squared)) return false; + if (squared > 1.0) { + ++highs[vi]; + ++total_high; + } + if (total > kMaxObservations) return false; + j.ws(); + if (j.ch(']')) break; + if (!comma(&j)) return false; + } + } + if (!j.ch('}') || !j.done() || total == 0) return false; + for (size_t i = 0; i < views.size(); ++i) { + if (views[i].tooling.accepted) { + if (counts[i] != views[i].tooling.residual_count || + highs[i] != views[i].tooling.high_residual_count) + return false; + } else if (counts[i] != 0) { + return false; + } + } + const double calculated_fraction = + static_cast(total_high) / static_cast(total); + if (calculated_fraction != out->high_fraction) return false; + return true; +} + +bool validation_binding(const Lardon3DCalibrationWorkflowInputBoundary& boundary, + unsigned char output[32]) { + static const char domain[] = "L3DCAL_WORKFLOW_VALIDATION_V1\n"; + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (!ctx) return false; + bool ok = EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) == 1 && + EVP_DigestUpdate(ctx, domain, sizeof(domain) - 1) == 1 && + EVP_DigestUpdate(ctx, boundary.detection_sha256, 32) == 1 && + EVP_DigestUpdate(ctx, boundary.solve_sha256, 32) == 1 && + EVP_DigestUpdate(ctx, boundary.evidence_sha256, 32) == 1 && + EVP_DigestUpdate(ctx, boundary.producer_sha256, 32) == 1; + unsigned int length = 0; + ok = ok && EVP_DigestFinal_ex(ctx, output, &length) == 1 && length == 32; + EVP_MD_CTX_free(ctx); + return ok; +} + +} // namespace + +extern "C" Lardon3DCalibrationWorkflowResult +lardon3d_calibration_workflow_materialize_external_evidence( + const Lardon3DCalibrationWorkflowInputFiles *files, + Lardon3DCalibrationToolingView *views, size_t view_capacity, + Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks, + size_t coordinate_check_capacity, + Lardon3DCalibrationWorkflowExternalEvidence *output) { + if (!files || !views || !coordinate_checks || !output) + return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT; + std::memset(output, 0, sizeof(*output)); + + Lardon3DCalibrationWorkflowInputBoundary boundary{}; + Lardon3DCalibrationWorkflowResult result = + lardon3d_calibration_workflow_validate_input_boundary(files, &boundary); + if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result; + + std::string session_text, detection_text, solve_text, evidence_text; + const struct { + const char *path; + const unsigned char *sha; + std::string *text; + } inputs[] = { + {files->session_path, boundary.session_sha256, &session_text}, + {files->detection_path, boundary.detection_sha256, &detection_text}, + {files->solve_path, boundary.solve_sha256, &solve_text}, + {files->evidence_path, boundary.evidence_sha256, &evidence_text}, + }; + for (const auto& input : inputs) { + result = read_verified(input.path, input.sha, input.text); + if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result; + } + + SessionData session; + if (!parse_session_materialization(session_text, &session)) + return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE; + if (std::memcmp(session.optical_sha, boundary.optical_state_sha256, 32) != 0 || + session.optical_token != boundary.optical_state_token) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + std::vector parsed_views; + if (!parse_detection(detection_text, session, &parsed_views)) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + size_t accepted = 0; + uint64_t support_observations = 0; + for (const DetectionView& view : parsed_views) { + if (view.tooling.accepted) { + ++accepted; + support_observations += view.tooling.residual_count; + if (support_observations > UINT32_MAX) + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + } + } + + SolveData solve; + if (!parse_solve(solve_text, accepted, &solve)) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + EvidenceData evidence; + if (!parse_evidence(evidence_text, session, parsed_views, &evidence)) + return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH; + + size_t check_count = 0; + for (const SessionView& view : session.views) { + if (SIZE_MAX - check_count < view.checks.size()) + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + check_count += view.checks.size(); + } + if (parsed_views.size() > view_capacity || + check_count > coordinate_check_capacity) + return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY; + + for (size_t i = 0; i < parsed_views.size(); ++i) + views[i] = parsed_views[i].tooling; + size_t at = 0; + for (const SessionView& view : session.views) + for (const auto& check : view.checks) coordinate_checks[at++] = check; + + output->boundary = boundary; + std::memcpy(output->target_sha256, session.generator_sha, 32); + std::memcpy(output->optical_state_sha256, boundary.optical_state_sha256, 32); + std::memcpy(output->solver_executable_sha256, + boundary.solver_executable_sha256, 32); + std::memcpy(output->solver_configuration_sha256, + boundary.solver_configuration_sha256, 32); + std::memcpy(output->initialization_evidence_sha256, + boundary.session_sha256, 32); + if (!validation_binding(boundary, output->validation_evidence_sha256)) + return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR; + + output->target_family = + LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100; + output->target_squares_x = 9; + output->target_squares_y = 7; + output->target_square_length_mm = 30.0; + output->target_marker_length_mm = 21.0; + output->target_active_width_mm = 270.0; + output->target_active_height_mm = 210.0; + output->target_white_border_mm = session.white_border; + for (size_t i = 0; i < 10; ++i) + output->target_measurements_mm[i] = session.measurements[i]; + output->measurement_resolution_mm = session.measurement_resolution; + output->target_flatness_mm = NAN; + output->holdout_rmse_px = evidence.holdout_rmse; + output->holdout_maximum_residual_px = evidence.holdout_max; + output->extra_distortion_coefficient_count = 0; + output->views = views; + output->view_count = parsed_views.size(); + output->coordinate_checks = coordinate_checks; + output->coordinate_check_count = check_count; + std::memcpy(output->repeated_parameters, solve.repeated, + sizeof(output->repeated_parameters)); + std::memcpy(output->fit_parameters, solve.fit, + sizeof(output->fit_parameters)); + output->support_images = static_cast(accepted); + output->support_observations = static_cast(support_observations); + output->reprojection_rmse_px = evidence.global_rmse; + output->maximum_residual_px = evidence.global_max; + output->high_residual_fraction = evidence.high_fraction; + output->maximum_parameter_delta = evidence.maximum_delta; + output->validation_flags = evidence.flags; + return LARDON3D_CALIBRATION_WORKFLOW_OK; +} diff --git a/tests/test_calibration_workflow_materialize.cpp b/tests/test_calibration_workflow_materialize.cpp new file mode 100644 index 0000000..da6a7d8 --- /dev/null +++ b/tests/test_calibration_workflow_materialize.cpp @@ -0,0 +1,267 @@ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if(!(x)){ std::fprintf(stderr,"materialize failure %d: %s\n",__LINE__,#x); return false; } } while(0) + +namespace { + +std::string hex_repeat(char c) { return std::string(64, c); } + +std::string hf(double value) { + std::ostringstream o; + o.imbue(std::locale::classic()); + o << std::hexfloat << value; + return o.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 sha_file(const std::filesystem::path& path, unsigned char out[32]) { + std::ifstream in(path, std::ios::binary); + std::ostringstream bytes; + bytes << in.rdbuf(); + const std::string data = bytes.str(); + unsigned int n = 0; + return in && EVP_Digest(data.data(), data.size(), out, &n, EVP_sha256(), nullptr) == 1 && + n == 32; +} + +std::string sha_file_hex(const std::filesystem::path& path) { + unsigned char value[32]{}; + if (!sha_file(path, value)) return {}; + 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] & 15]; + } + return out; +} + + +struct Fixture { + std::filesystem::path root, session, detection, solve, evidence, producer, campaign; + Lardon3DCalibrationWorkflowInputFiles files{}; +}; + +std::string coordinate_lines(const std::string& sha, double distance, unsigned band) { + static const std::array labels = { + "center","top","right","bottom","left","top_left","top_right","bottom_left","bottom_right"}; + std::ostringstream s; + s << "pre_solve " << sha << " 0.1\n" + << "clipping " << sha << " 0\n" + << "coordinate " << sha << " qualified_decoder 1 0 1000 800 20 0 0\n"; + for (int i = 0; i < 20; ++i) + s << "coordinate_point " << sha << " " << labels[static_cast(i)%labels.size()] + << " " << i << " " << i+1 << " " << i << " " << i+1 << "\n"; + s << "distance " << sha << " " << std::setprecision(17) + << std::defaultfloat << distance << " " << band << "\n"; + return s.str(); +} + +std::string detection_view(const std::string& sha, unsigned region, unsigned band, + double distance, bool last) { + std::ostringstream s; + s << "{\"source_sha256\":\"" << sha + << "\",\"orientation\":0,\"oriented_width\":1000,\"oriented_height\":800" + << ",\"decision\":\"accepted\",\"reason\":\"-\"" + << ",\"frame_region\":" << region + << ",\"distance_band\":" << band + << ",\"holdout\":false" + << ",\"target_occupancy\":\"" << hf(.3) + << "\",\"normal_angle_degrees\":\"" << hf(25.0) + << "\",\"measured_distance_metres\":\"" << hf(distance) + << "\",\"pre_solve_corner_rms_px\":\"" << hf(.1) + << "\",\"clipping_fraction\":\"" << hf(0.0) + << "\",\"physical_target_quadrants\":3" + << ",\"target_corner_quadrant_mask\":7" + << ",\"corner_count\":20,\"residual_count\":20,\"high_residual_count\":0" + << ",\"reprojection_rmse_px\":\"" << hf(.1) + << "\",\"maximum_residual_px\":\"" << hf(.2) + << "\",\"coordinate_equivalence\":{\"comparison_points\":20" + << ",\"max_abs_dx_px\":\"" << hf(0.0) + << "\",\"max_abs_dy_px\":\"" << hf(0.0) + << "\",\"pass\":true},\"corners\":["; + for (int i = 0; i < 20; ++i) { + if (i) s << ","; + s << "{\"id\":" << i << ",\"x\":\"" << hf(100.0+i) + << "\",\"y\":\"" << hf(200.0+i) << "\"}"; + } + s << "]}" << (last ? "\n" : " ,\n"); + return s.str(); +} + +std::string params(double first = 1000.0) { + const double p[8] = {first,1001,500,400,.01,-.01,.001,-.001}; + std::ostringstream s; + s << "["; + for (int i = 0; i < 8; ++i) { + if (i) s << ","; + s << "\"" << hf(p[i]) << "\""; + } + s << "]"; + return s.str(); +} + +std::string pose() { + return "{\"rvec\":[\"" + hf(0) + "\",\"" + hf(0) + "\",\"" + hf(0) + + "\"],\"tvec_m\":[\"" + hf(0) + "\",\"" + hf(0) + "\",\"" + hf(1) + "\"]}"; +} + +std::string solve_text(bool mismatch) { + std::ostringstream s; + s << "{\n\"format\":\"L3DCAL_SOLVE_V1\",\n\"runs\":[\n"; + for (int run = 0; run < 3; ++run) { + s << "{\"run\":" << run << ",\"params\":" << params(mismatch && run==1 ? 999.0 : 1000.0) + << ",\"opencv_rms_px\":\"" << hf(.1) << "\",\"poses\":[" + << pose() << "," << pose() << "]}" << (run==2 ? "\n" : " ,\n"); + } + s << "],\n\"fit_params\":" << params() << "\n}\n"; + return s.str(); +} + +std::string evidence_text(const std::string& optical, + const std::string& generator, + const std::string& planarity, + const std::string& a, + const std::string& b) { + std::ostringstream s; + s << "{\n\"format\":\"L3DCAL_EVIDENCE_BUNDLE_V1\",\n" + << "\"target\":{\"id\":\"board\",\"generator_sha256\":\"" << generator + << "\",\"instrument\":\"caliper\",\"resolution_mm\":\"" << hf(.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(.1) + << "\",\"maximum_residual_px\":\"" << hf(.2) + << "\",\"high_residual_fraction\":\"" << hf(0.0) << "\",\n" + << "\"holdout\":{\"rmse_px\":\"" << hf(.1) + << "\",\"maximum_px\":\"" << hf(.2) << "\"},\n" + << "\"maximum_parameter_delta_px\":\"" << hf(0.0) << "\",\n" + << "\"deterministic_full_solve_equality\":true,\n\"residuals\":["; + bool first = true; + for (const std::string* sha : {&a, &b}) { + for (int i = 0; i < 20; ++i) { + if (!first) s << ","; + first = false; + s << "{\"source_sha256\":\"" << *sha << "\",\"corner_id\":" << i + << ",\"dx_px\":\"" << hf(.1) << "\",\"dy_px\":\"" << hf(0.0) + << "\",\"rmse_px\":\"" << hf(.1) << "\"}"; + } + } + s << "]\n}\n"; + return s.str(); +} + +bool make_fixture(Fixture *f, bool mismatch_solve = false) { + char temp[]="/tmp/lardon3d-materialize-XXXXXX"; + char *r=mkdtemp(temp); if(!r) return false; + f->root=r; f->session=f->root/"session"; f->detection=f->root/"detection"; + f->solve=f->root/"solve"; f->evidence=f->root/"evidence"; + f->producer=f->root/"producer"; f->campaign=f->root/"campaign"; + const std::string optical=hex_repeat('1'), generator=hex_repeat('2'), + planarity=hex_repeat('3'), a=hex_repeat('4'), b=hex_repeat('5'); + 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" + << "image /a " << a << " 0\n" + << "image /b " << b << " 0\n" + << coordinate_lines(a,.3,0) << coordinate_lines(b,.5,1); + if(!write_text(f->session,session.str())) return false; + if(!write_text(f->detection, + "{\n\"format\":\"L3DCAL_DETECTION_V1\",\n\"decoder\":\"qualified_decoder\",\n" + "\"decoder_version\":\"1\",\n\"views\":[\n" + + detection_view(a,0,0,.3,false)+detection_view(b,1,1,.5,true)+"]\n}\n")) return false; + if(!write_text(f->solve,solve_text(mismatch_solve))) return false; + if(!write_text(f->evidence,evidence_text(optical,generator,planarity,a,b))) return false; + const std::string session_sha = sha_file_hex(f->session); + if (session_sha.empty()) return false; + if(!write_text(f->producer, + "{\n\"format\":\"L3DCAL_PRODUCER_V1\",\n" + "\"solver_executable_sha256\":\"" + hex_repeat('a') + "\",\n" + "\"solver_configuration_sha256\":\"" + hex_repeat('b') + "\",\n" + "\"session_sha256\":\"" + session_sha + "\",\n" + "\"opencv_version\":\"5.0.0\",\n" + "\"opencv_build_sha256\":\"" + hex_repeat('c') + "\",\n" + "\"threads\":1,\n\"rng_seed\":1278432342,\n" + "\"optical_sha256\":\"" + optical + "\"\n}\n")) return false; + if(!write_text(f->campaign, + "L3DCAL_CAMPAIGN_STATE_V1\n" + "execution 42\n" + "optical_configuration 7\n" + "optical_state " + optical + + " body_objective_zoom_focus_stabilization_format_pipeline\n" + "capture 0 101\ncapture 1 102\n")) return false; + + f->files={f->session.c_str(),f->detection.c_str(),f->solve.c_str(), + f->evidence.c_str(),f->producer.c_str(),f->campaign.c_str()}; + return true; +} + +bool valid() { + Fixture f; CHECK(make_fixture(&f)); + Lardon3DCalibrationToolingView views[2]{}; + Lardon3DCalibrationToolingCoordinateCheck checks[40]{}; + Lardon3DCalibrationWorkflowExternalEvidence out{}; + 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.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); + CHECK(views[0].accepted==1 && views[0].corner_count==20 && + views[1].distance_band==1); + CHECK(checks[0].dx_px==0.0 && checks[0].dy_px==0.0); + unsigned char session_sha[32]{}; + CHECK(sha_file(f.session, session_sha)); + CHECK(std::memcmp(out.initialization_evidence_sha256, session_sha, 32) == 0); + std::filesystem::remove_all(f.root); + return true; +} + +bool capacity_and_mismatch() { + Fixture f; CHECK(make_fixture(&f)); + Lardon3DCalibrationToolingView views[2]{}; + Lardon3DCalibrationToolingCoordinateCheck checks[40]{}; + Lardon3DCalibrationWorkflowExternalEvidence out{}; + CHECK(lardon3d_calibration_workflow_materialize_external_evidence( + &f.files,views,1,checks,40,&out)==LARDON3D_CALIBRATION_WORKFLOW_CAPACITY); + std::filesystem::remove_all(f.root); + + CHECK(make_fixture(&f,true)); + CHECK(lardon3d_calibration_workflow_materialize_external_evidence( + &f.files,views,2,checks,40,&out)==LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH); + std::filesystem::remove_all(f.root); + return true; +} + +} // namespace + +int main(){ + if(!valid()||!capacity_and_mismatch()) return 1; + std::puts("CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS"); + return 0; +}