From 1a96bcce1f8e7111e8e184017a076bfbbb9cc3bf Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 3 Sep 2026 14:35:46 +0200 Subject: [PATCH] feat: bridge calibration workflow to autofocus study --- docs/architecture/calibration_af_study.md | 41 ++++ docs/roadmap/roadmap.md | 6 +- .../lardon3d/calibration_af_study_workflow.h | 51 +++++ meson.build | 17 ++ prompt/02_CURRENT_FROZEN_STATE.md | 1 + prompt/12_CALIBRATION.md | 1 + prompt/31_IMPLEMENTATION_ORDER.md | 2 +- src/calibration_af_study_workflow.c | 211 ++++++++++++++++++ tests/test_calibration_af_study_workflow.c | 190 ++++++++++++++++ 9 files changed, 517 insertions(+), 3 deletions(-) create mode 100644 include/lardon3d/calibration_af_study_workflow.h create mode 100644 src/calibration_af_study_workflow.c create mode 100644 tests/test_calibration_af_study_workflow.c diff --git a/docs/architecture/calibration_af_study.md b/docs/architecture/calibration_af_study.md index 466abb7..4ac2d3f 100644 --- a/docs/architecture/calibration_af_study.md +++ b/docs/architecture/calibration_af_study.md @@ -147,3 +147,44 @@ This v1 boundary does not: The future physical study supplies the evidence needed to decide whether the A6000 + E PZ 16-50 supports one domain, discrete domains, or exact focus only. + +## Materialized Workflow bridge v1 + +**Status: PASS / FROZEN.** + +`CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1` is the additive conversion boundary +between the already-FROZEN Calibration Workflow materializer and AF-study +samples. + +It accepts a `Lardon3DCalibrationWorkflowExternalEvidence` that has already +passed the existing immutable-file, solver-bundle and provenance checks. It +does not parse `solve.json` or any other solver file again. + +The bridge: + +```text +materialized Calibration Workflow evidence ++ caller-supplied exact focus token ++ caller-supplied FIT/HOLDOUT role +-> Lardon3DCalibrationAfStudySample +``` + +The sample publishes `repeated_parameters[0]` only after checking that all three +retained full solves remain exactly equal. + +Its `calibration_evidence_sha256` is independent of the focus token and +FIT/HOLDOUT role and binds: + +- target identity; +- optical-state identity; +- solver executable/configuration identity; +- initialization evidence; +- validation evidence; +- exact oriented dimensions; +- exact published binary64 intrinsics. + +Therefore relabelling one calibration result cannot manufacture independent +calibration evidence. + +The bridge performs no DB access, metadata interpretation, physical AF decision, +thresholding, interpolation or extrapolation. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index e1ba9f0..18963fc 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -950,5 +950,7 @@ CURRENT_NEXT PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VAL Implementation proceeds only through explicitly human-authorized tranches under `prompt.md` and the numbered `prompt/` execution contract. The current dependency is physical autofocus/optical applicability validation for real equipment, followed by a dedicated calibrated real campaign. -Generic autofocus applicability machinery is available, but the real -A6000 + E PZ 16-50 autofocus applicability remains blocked until physical evidence validates it. +Generic autofocus applicability machinery is available. AF-study evidence can now be produced from +already-materialized Calibration Workflow results without a second solver parser, while focus tokens +remain explicit study annotations. The real A6000 + E PZ 16-50 autofocus applicability remains +blocked until physical evidence validates it. diff --git a/include/lardon3d/calibration_af_study_workflow.h b/include/lardon3d/calibration_af_study_workflow.h new file mode 100644 index 0000000..78f650a --- /dev/null +++ b/include/lardon3d/calibration_af_study_workflow.h @@ -0,0 +1,51 @@ +#ifndef LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_H +#define LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK = 0, + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT, + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE, + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR, +} Lardon3DCalibrationAfStudyWorkflowResult; + +/* Convert one already validated/materialized Calibration Workflow result into + * one AF-study sample without re-parsing any solver file. + * + * `focus_token` and FIT/HOLDOUT role are study annotations. They are + * deliberately excluded from calibration_evidence_sha256, so relabelling the + * same calibration result cannot manufacture a new calibration-evidence + * identity. + * + * calibration_evidence_sha256 is domain-separated and binds: + * - target identity; + * - optical-state identity; + * - solver executable/configuration identity; + * - initialization and validation evidence; + * - exact oriented dimensions; + * - exact published binary64 fx,fy,cx,cy,k1,k2,p1,p2. + * + * The bridge checks the materialization invariants it consumes, including + * deterministic validation-evidence binding and equality of all three repeated + * full solves. It publishes repeated_parameters[0]. + * + * No Project DB access, metadata interpretation, physical AF applicability + * decision, interpolation, extrapolation, solver execution or thresholding + * occurs here. */ +Lardon3DCalibrationAfStudyWorkflowResult +lardon3d_calibration_af_study_sample_from_materialized_evidence( + const Lardon3DCalibrationWorkflowExternalEvidence *external, + Lardon3DCalibrationAfStudySampleRole role, const char *focus_token, + Lardon3DCalibrationAfStudySample *output); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index e985f4b..4d9e7b5 100644 --- a/meson.build +++ b/meson.build @@ -184,6 +184,7 @@ lardon3d_app = executable( 'src/calibration_tooling.c', 'src/calibration_tooling_v2.c', 'src/calibration_af_study.c', + 'src/calibration_af_study_workflow.c', 'src/calibration_workflow.cpp', 'src/calibration_workflow_materialize.cpp', 'src/calibration_workflow_bind.cpp', @@ -930,6 +931,22 @@ calibration_af_study_test = executable( test('calibration-af-study', calibration_af_study_test, timeout: 30) +calibration_af_study_workflow_test = executable( + 'test-calibration-af-study-workflow', + sources: [ + 'tests/test_calibration_af_study_workflow.c', + 'src/calibration_af_study_workflow.c', + ], + include_directories: include_directories('include'), + dependencies: [openssl, cc.find_library('m')], +) + +test( + 'calibration-af-study-workflow', + calibration_af_study_workflow_test, + timeout: 30, +) + calibration_workflow_v2_test = executable( 'test-calibration-workflow-v2', sources: [ diff --git a/prompt/02_CURRENT_FROZEN_STATE.md b/prompt/02_CURRENT_FROZEN_STATE.md index f86fee9..4deaeef 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -42,6 +42,7 @@ Calibration Workflow v2 PASS/FROZEN Adaptive capture settings semantics PASS/FROZEN Autofocus v2 foundation PASS/FROZEN Calibration AF study evidence v1 PASS/FROZEN +Calibration AF study Workflow bridge v1 PASS/FROZEN Calibration Tooling planarity alignment PASS/FROZEN ``` diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index 8bcfa81..3401900 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -24,6 +24,7 @@ CALIBRATION_V2_WORKFLOW_READY=PASS/FROZEN ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PASS/FROZEN AUTOFOCUS_V2_FOUNDATION=PASS/FROZEN CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN +CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1=PASS/FROZEN CURRENT_CALIBRATION_NEXT=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION ``` diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index a7ed391..8ce2b0b 100644 --- a/prompt/31_IMPLEMENTATION_ORDER.md +++ b/prompt/31_IMPLEMENTATION_ORDER.md @@ -26,7 +26,7 @@ Default dependency order: 4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — PASS/FROZEN; 5. Heterogeneous Workflow v2 truthful READY proof — PASS/FROZEN; 6. Adaptive capture settings / generic autofocus foundation — PASS/FROZEN; -7. physical autofocus/optical applicability validation and dedicated calibrated real campaign — CURRENT; `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` supplies deterministic measurement evidence but makes no physical applicability decision; +7. physical autofocus/optical applicability validation and dedicated calibrated real campaign — CURRENT; `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` supplies deterministic measurement evidence and `CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1=PASS/FROZEN` converts already-materialized Calibration Workflow evidence into AF-study samples without reparsing solver artifacts; neither makes a physical applicability decision; 8. real Sparse SfM proof; 9. durable Dense/OpenMVS orchestration; 10. mesh / refinement / texturing / export; diff --git a/src/calibration_af_study_workflow.c b/src/calibration_af_study_workflow.c new file mode 100644 index 0000000..1a35fb2 --- /dev/null +++ b/src/calibration_af_study_workflow.c @@ -0,0 +1,211 @@ +#include + +#include +#include +#include +#include +#include + +static bool nonzero_digest(const unsigned char value[32]) { + unsigned char any = 0; + for (size_t index = 0; index < 32; ++index) any |= value[index]; + return any != 0; +} + +static bool token_copy( + const char *input, + char output[LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY]) { + if (!input) return false; + size_t length = 0; + while (length < LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY && + input[length] != '\0') + ++length; + if (length == 0 || + length >= LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY) + return false; + memset(output, 0, LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY); + memcpy(output, input, length); + return true; +} + +static bool digest_begin(EVP_MD_CTX **ctx, const char *domain) { + *ctx = EVP_MD_CTX_new(); + return *ctx && + EVP_DigestInit_ex(*ctx, EVP_sha256(), NULL) == 1 && + EVP_DigestUpdate(*ctx, domain, strlen(domain)) == 1; +} + +static bool digest_add(EVP_MD_CTX *ctx, const void *bytes, size_t size) { + return EVP_DigestUpdate(ctx, bytes, size) == 1; +} + +static bool digest_finish(EVP_MD_CTX *ctx, unsigned char output[32]) { + unsigned int output_size = 0; + const bool ok = + EVP_DigestFinal_ex(ctx, output, &output_size) == 1 && output_size == 32; + EVP_MD_CTX_free(ctx); + return ok; +} + +static void encode_u32_le(uint32_t value, unsigned char output[4]) { + for (size_t index = 0; index < 4; ++index) + output[index] = (unsigned char)(value >> (8u * index)); +} + +static void encode_f64_le(double value, unsigned char output[8]) { + uint64_t bits = 0; + if (value == 0.0) value = 0.0; + memcpy(&bits, &value, sizeof(bits)); + for (size_t index = 0; index < 8; ++index) + output[index] = (unsigned char)(bits >> (8u * index)); +} + +static bool validation_binding( + const Lardon3DCalibrationWorkflowInputBoundary *boundary, + unsigned char output[32]) { + static const char domain[] = "L3DCAL_WORKFLOW_VALIDATION_V1\n"; + EVP_MD_CTX *ctx = NULL; + if (!digest_begin(&ctx, domain)) { + if (ctx) EVP_MD_CTX_free(ctx); + return false; + } + const bool ok = + digest_add(ctx, boundary->detection_sha256, 32) && + digest_add(ctx, boundary->solve_sha256, 32) && + digest_add(ctx, boundary->evidence_sha256, 32) && + digest_add(ctx, boundary->producer_sha256, 32); + if (!ok) { + EVP_MD_CTX_free(ctx); + return false; + } + return digest_finish(ctx, output); +} + +static bool sample_identity( + const Lardon3DCalibrationWorkflowExternalEvidence *external, + unsigned char output[32]) { + static const char domain[] = "L3DAF_CALIBRATION_SAMPLE_V1\n"; + EVP_MD_CTX *ctx = NULL; + if (!digest_begin(&ctx, domain)) { + if (ctx) EVP_MD_CTX_free(ctx); + return false; + } + + bool ok = + digest_add(ctx, external->target_sha256, 32) && + digest_add(ctx, external->optical_state_sha256, 32) && + digest_add(ctx, external->solver_executable_sha256, 32) && + digest_add(ctx, external->solver_configuration_sha256, 32) && + digest_add(ctx, external->initialization_evidence_sha256, 32) && + digest_add(ctx, external->validation_evidence_sha256, 32); + + unsigned char encoded_u32[4]; + encode_u32_le(external->oriented_width, encoded_u32); + ok = ok && digest_add(ctx, encoded_u32, sizeof(encoded_u32)); + encode_u32_le(external->oriented_height, encoded_u32); + ok = ok && digest_add(ctx, encoded_u32, sizeof(encoded_u32)); + + for (size_t parameter = 0; parameter < 8 && ok; ++parameter) { + unsigned char encoded_f64[8]; + encode_f64_le(external->repeated_parameters[0][parameter], encoded_f64); + ok = digest_add(ctx, encoded_f64, sizeof(encoded_f64)); + } + + if (!ok) { + EVP_MD_CTX_free(ctx); + return false; + } + return digest_finish(ctx, output); +} + +static bool boundary_provenance_valid( + const Lardon3DCalibrationWorkflowExternalEvidence *external) { + const Lardon3DCalibrationWorkflowInputBoundary *boundary = + &external->boundary; + + if (!nonzero_digest(boundary->session_sha256) || + !nonzero_digest(boundary->detection_sha256) || + !nonzero_digest(boundary->solve_sha256) || + !nonzero_digest(boundary->evidence_sha256) || + !nonzero_digest(boundary->producer_sha256) || + !nonzero_digest(boundary->optical_state_sha256) || + !nonzero_digest(boundary->solver_executable_sha256) || + !nonzero_digest(boundary->solver_configuration_sha256)) + return false; + + return memcmp(external->optical_state_sha256, + boundary->optical_state_sha256, 32) == 0 && + memcmp(external->solver_executable_sha256, + boundary->solver_executable_sha256, 32) == 0 && + memcmp(external->solver_configuration_sha256, + boundary->solver_configuration_sha256, 32) == 0 && + memcmp(external->initialization_evidence_sha256, + boundary->session_sha256, 32) == 0; +} + +static bool published_parameters_valid( + const Lardon3DCalibrationWorkflowExternalEvidence *external) { + const double *published = external->repeated_parameters[0]; + for (size_t parameter = 0; parameter < 8; ++parameter) { + if (!isfinite(published[parameter])) return false; + if (external->repeated_parameters[1][parameter] != published[parameter] || + external->repeated_parameters[2][parameter] != published[parameter]) + return false; + } + + return published[0] > 0.0 && published[1] > 0.0 && + published[2] >= 0.0 && published[3] >= 0.0 && + published[2] < (double)external->oriented_width && + published[3] < (double)external->oriented_height; +} + +Lardon3DCalibrationAfStudyWorkflowResult +lardon3d_calibration_af_study_sample_from_materialized_evidence( + const Lardon3DCalibrationWorkflowExternalEvidence *external, + Lardon3DCalibrationAfStudySampleRole role, const char *focus_token, + Lardon3DCalibrationAfStudySample *output) { + if (output) memset(output, 0, sizeof(*output)); + + if (!external || !focus_token || !output || + (role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT && + role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT)) + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT; + + Lardon3DCalibrationAfStudySample sample = {0}; + if (!token_copy(focus_token, sample.focus_token)) + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT; + + if (external->oriented_width == 0 || external->oriented_height == 0 || + !nonzero_digest(external->target_sha256) || + !nonzero_digest(external->optical_state_sha256) || + !nonzero_digest(external->solver_executable_sha256) || + !nonzero_digest(external->solver_configuration_sha256) || + !nonzero_digest(external->initialization_evidence_sha256) || + !nonzero_digest(external->validation_evidence_sha256) || + !boundary_provenance_valid(external) || + !published_parameters_valid(external)) + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE; + + unsigned char expected_validation[32] = {0}; + if (!validation_binding(&external->boundary, expected_validation)) + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR; + if (memcmp(external->validation_evidence_sha256, + expected_validation, 32) != 0) + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE; + + sample.role = role; + if (!sample_identity(external, sample.calibration_evidence_sha256)) + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR; + + sample.fx = external->repeated_parameters[0][0]; + sample.fy = external->repeated_parameters[0][1]; + sample.cx = external->repeated_parameters[0][2]; + sample.cy = external->repeated_parameters[0][3]; + sample.k1 = external->repeated_parameters[0][4]; + sample.k2 = external->repeated_parameters[0][5]; + sample.p1 = external->repeated_parameters[0][6]; + sample.p2 = external->repeated_parameters[0][7]; + + *output = sample; + return LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK; +} diff --git a/tests/test_calibration_af_study_workflow.c b/tests/test_calibration_af_study_workflow.c new file mode 100644 index 0000000..01b8b63 --- /dev/null +++ b/tests/test_calibration_af_study_workflow.c @@ -0,0 +1,190 @@ +#include + +#include +#include +#include +#include + +#define CHECK(expression) \ + do { \ + if (!(expression)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expression); \ + return 1; \ + } \ + } while (0) + +static void fill_digest(unsigned char output[32], unsigned char value) { + memset(output, value, 32); +} + +static int sha256_validation( + 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 0; + int ok = EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 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; +} + +static Lardon3DCalibrationWorkflowExternalEvidence fixture(void) { + Lardon3DCalibrationWorkflowExternalEvidence value = {0}; + + fill_digest(value.boundary.session_sha256, 0x11); + fill_digest(value.boundary.detection_sha256, 0x22); + fill_digest(value.boundary.solve_sha256, 0x33); + fill_digest(value.boundary.evidence_sha256, 0x44); + fill_digest(value.boundary.producer_sha256, 0x55); + fill_digest(value.boundary.campaign_state_sha256, 0x66); + fill_digest(value.boundary.optical_state_sha256, 0x77); + fill_digest(value.boundary.solver_executable_sha256, 0x88); + fill_digest(value.boundary.solver_configuration_sha256, 0x99); + + fill_digest(value.target_sha256, 0xA1); + memcpy(value.optical_state_sha256, value.boundary.optical_state_sha256, 32); + memcpy(value.solver_executable_sha256, + value.boundary.solver_executable_sha256, 32); + memcpy(value.solver_configuration_sha256, + value.boundary.solver_configuration_sha256, 32); + memcpy(value.initialization_evidence_sha256, + value.boundary.session_sha256, 32); + + (void)sha256_validation(&value.boundary, value.validation_evidence_sha256); + + value.oriented_width = 6000; + value.oriented_height = 4000; + const double params[8] = { + 4000.0, 4002.0, 3000.0, 2000.0, -0.1, 0.01, 0.001, -0.001, + }; + for (size_t run = 0; run < 3; ++run) + memcpy(value.repeated_parameters[run], params, sizeof(params)); + + return value; +} + +static int test_happy_path_and_identity_stability(void) { + Lardon3DCalibrationWorkflowExternalEvidence external = fixture(); + Lardon3DCalibrationAfStudySample fit = {0}; + Lardon3DCalibrationAfStudySample holdout = {0}; + + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, + "sony-focus:137", &fit) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK); + CHECK(fit.role == LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT); + CHECK(strcmp(fit.focus_token, "sony-focus:137") == 0); + CHECK(fit.fx == 4000.0 && fit.fy == 4002.0); + CHECK(fit.cx == 3000.0 && fit.cy == 2000.0); + + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT, + "different-focus-label", &holdout) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK); + + CHECK(memcmp(fit.calibration_evidence_sha256, + holdout.calibration_evidence_sha256, 32) == 0); + unsigned char zero[32] = {0}; + CHECK(memcmp(fit.calibration_evidence_sha256, zero, 32) != 0); + return 0; +} + +static int test_identity_binds_provenance_and_parameters(void) { + Lardon3DCalibrationWorkflowExternalEvidence a = fixture(); + Lardon3DCalibrationWorkflowExternalEvidence b = fixture(); + Lardon3DCalibrationAfStudySample sample_a = {0}; + Lardon3DCalibrationAfStudySample sample_b = {0}; + + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &a, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample_a) == LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK); + + b.target_sha256[0] ^= 1; + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &b, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample_b) == LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK); + CHECK(memcmp(sample_a.calibration_evidence_sha256, + sample_b.calibration_evidence_sha256, 32) != 0); + + b = fixture(); + for (size_t run = 0; run < 3; ++run) + b.repeated_parameters[run][0] += 1.0; + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &b, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample_b) == LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK); + CHECK(memcmp(sample_a.calibration_evidence_sha256, + sample_b.calibration_evidence_sha256, 32) != 0); + return 0; +} + +static int test_invalid_materialization_rejected(void) { + Lardon3DCalibrationWorkflowExternalEvidence external = fixture(); + Lardon3DCalibrationAfStudySample sample; + memset(&sample, 0xA5, sizeof(sample)); + + external.validation_evidence_sha256[0] ^= 1; + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE); + Lardon3DCalibrationAfStudySample zero = {0}; + CHECK(memcmp(&sample, &zero, sizeof(sample)) == 0); + + external = fixture(); + external.repeated_parameters[1][0] += 1.0; + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE); + + external = fixture(); + external.repeated_parameters[0][0] = NAN; + external.repeated_parameters[1][0] = NAN; + external.repeated_parameters[2][0] = NAN; + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE); + + external = fixture(); + external.optical_state_sha256[0] ^= 1; + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE); + return 0; +} + +static int test_invalid_arguments(void) { + Lardon3DCalibrationWorkflowExternalEvidence external = fixture(); + Lardon3DCalibrationAfStudySample sample = {0}; + + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + NULL, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:a", + &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT); + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, 0, "focus:a", &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT); + CHECK(lardon3d_calibration_af_study_sample_from_materialized_evidence( + &external, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "", + &sample) == + LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT); + return 0; +} + +int main(void) { + CHECK(test_happy_path_and_identity_stability() == 0); + CHECK(test_identity_binds_provenance_and_parameters() == 0); + CHECK(test_invalid_materialization_rejected() == 0); + CHECK(test_invalid_arguments() == 0); + puts("CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1=PASS"); + return 0; +}