From 6a52ef495756652ecad3451028e182ee1daa6ff8 Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 3 Sep 2026 14:46:15 +0200 Subject: [PATCH] feat: assemble autofocus study evidence --- docs/architecture/calibration_af_study.md | 35 +++ docs/roadmap/roadmap.md | 5 +- .../lardon3d/calibration_af_study_assembly.h | 61 +++++ meson.build | 19 ++ prompt/02_CURRENT_FROZEN_STATE.md | 1 + prompt/12_CALIBRATION.md | 1 + prompt/31_IMPLEMENTATION_ORDER.md | 2 +- src/calibration_af_study_assembly.c | 112 +++++++++ tests/test_calibration_af_study_assembly.c | 226 ++++++++++++++++++ 9 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 include/lardon3d/calibration_af_study_assembly.h create mode 100644 src/calibration_af_study_assembly.c create mode 100644 tests/test_calibration_af_study_assembly.c diff --git a/docs/architecture/calibration_af_study.md b/docs/architecture/calibration_af_study.md index 4ac2d3f..d71c39d 100644 --- a/docs/architecture/calibration_af_study.md +++ b/docs/architecture/calibration_af_study.md @@ -188,3 +188,38 @@ calibration evidence. The bridge performs no DB access, metadata interpretation, physical AF decision, thresholding, interpolation or extrapolation. + +## Materialized study assembly v1 + +**Status: PASS / FROZEN.** + +`CALIBRATION_AF_STUDY_ASSEMBLY_V1` composes the frozen Workflow bridge and +`L3DAFST1` producer so a physical AF study does not need caller-written sample +arrays. + +The caller supplies: + +```text +study_context_sha256 +2..64 entries { + materialized Calibration Workflow evidence + exact opaque focus token + FIT or HOLDOUT role +} +``` + +The assembly: + +1. converts every entry through the frozen Workflow bridge; +2. requires one exact oriented width/height for the whole study; +3. rejects a repeated `calibration_evidence_sha256` even when the caller changes + focus token or FIT/HOLDOUT role; +4. passes the resulting bounded samples to the frozen AF-study producer; +5. returns deterministic `L3DAFST1`, artifact SHA-256 and summary. + +`study_context_sha256` remains caller-retained and explicit. This layer does not +derive body/lens/focal/non-focus state from solver metadata and does not decide +whether different focus observations belong to one physically valid domain. + +No Project DB access, solver execution, metadata inference, thresholding, +interpolation or extrapolation is introduced. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 18963fc..1b92027 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -952,5 +952,6 @@ numbered `prompt/` execution contract. The current dependency is physical autofo applicability validation for real equipment, followed by a dedicated calibrated real campaign. 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. +remain explicit study annotations. Multiple independent materialized samples can now be assembled into +one deterministic L3DAFST1 artifact without caller-written sample arrays. The real +A6000 + E PZ 16-50 autofocus applicability remains blocked until physical evidence validates it. diff --git a/include/lardon3d/calibration_af_study_assembly.h b/include/lardon3d/calibration_af_study_assembly.h new file mode 100644 index 0000000..988a591 --- /dev/null +++ b/include/lardon3d/calibration_af_study_assembly.h @@ -0,0 +1,61 @@ +#ifndef LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_H +#define LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_OK = 0, + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT, + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE, + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CAPACITY, + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CRYPTO_ERROR, + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_ENCODING_ERROR, +} Lardon3DCalibrationAfStudyAssemblyResult; + +typedef struct { + const Lardon3DCalibrationWorkflowExternalEvidence *external; + Lardon3DCalibrationAfStudySampleRole role; + const char *focus_token; +} Lardon3DCalibrationAfStudyAssemblyEntry; + +typedef struct { + /* Exact caller-retained identity of the common body/lens/focal/non-focus + * geometric study context. The assembly does not derive or reinterpret it. */ + unsigned char study_context_sha256[ + LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE]; + const Lardon3DCalibrationAfStudyAssemblyEntry *entries; + size_t entry_count; +} Lardon3DCalibrationAfStudyAssemblyInput; + +/* Assemble 2..MAX_SAMPLES already-materialized Calibration Workflow results + * into one deterministic L3DAFST1 artifact. + * + * Each entry is converted through the frozen Workflow bridge; no solver file + * is parsed here. All samples must share exact oriented dimensions because one + * L3DAFST1 study has one image geometry. The common optical/non-focus identity + * remains the explicit study_context_sha256 supplied by the caller. + * + * Duplicate calibration_evidence_sha256 values are rejected regardless of + * focus token or FIT/HOLDOUT role. A single physical calibration result cannot + * therefore be relabelled to masquerade as independent AF evidence. + * + * This boundary performs no Project DB access, no metadata interpretation, no + * physical applicability decision and no acceptance thresholding. */ +Lardon3DCalibrationAfStudyAssemblyResult +lardon3d_calibration_af_study_assemble_materialized( + const Lardon3DCalibrationAfStudyAssemblyInput *input, + unsigned char *artifact, size_t artifact_capacity, size_t *written, + unsigned char artifact_sha256[ + LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE], + Lardon3DCalibrationAfStudySummary *summary); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 4d9e7b5..44783d8 100644 --- a/meson.build +++ b/meson.build @@ -185,6 +185,7 @@ lardon3d_app = executable( 'src/calibration_tooling_v2.c', 'src/calibration_af_study.c', 'src/calibration_af_study_workflow.c', + 'src/calibration_af_study_assembly.c', 'src/calibration_workflow.cpp', 'src/calibration_workflow_materialize.cpp', 'src/calibration_workflow_bind.cpp', @@ -947,6 +948,24 @@ test( timeout: 30, ) +calibration_af_study_assembly_test = executable( + 'test-calibration-af-study-assembly', + sources: [ + 'tests/test_calibration_af_study_assembly.c', + 'src/calibration_af_study_assembly.c', + 'src/calibration_af_study_workflow.c', + 'src/calibration_af_study.c', + ], + include_directories: include_directories('include'), + dependencies: [openssl, cc.find_library('m')], +) + +test( + 'calibration-af-study-assembly', + calibration_af_study_assembly_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 4deaeef..f4c52ab 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -43,6 +43,7 @@ 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 AF study assembly v1 PASS/FROZEN Calibration Tooling planarity alignment PASS/FROZEN ``` diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index 3401900..6046993 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -25,6 +25,7 @@ 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 +CALIBRATION_AF_STUDY_ASSEMBLY_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 8ce2b0b..b1826e6 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 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; +7. physical autofocus/optical applicability validation and dedicated calibrated real campaign — CURRENT; `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` supplies deterministic measurement evidence, `CALIBRATION_AF_STUDY_WORKFLOW_BRIDGE_V1=PASS/FROZEN` converts already-materialized Calibration Workflow evidence into AF-study samples without reparsing solver artifacts, and `CALIBRATION_AF_STUDY_ASSEMBLY_V1=PASS/FROZEN` assembles multiple independent materialized samples into one bounded L3DAFST1 artifact; none 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_assembly.c b/src/calibration_af_study_assembly.c new file mode 100644 index 0000000..b4065a2 --- /dev/null +++ b/src/calibration_af_study_assembly.c @@ -0,0 +1,112 @@ +#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 Lardon3DCalibrationAfStudyAssemblyResult map_bridge_result( + Lardon3DCalibrationAfStudyWorkflowResult result) { + switch (result) { + case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_ARGUMENT: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT; + case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_INVALID_EVIDENCE: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE; + case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_CRYPTO_ERROR: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CRYPTO_ERROR; + case LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK: + break; + } + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE; +} + +static Lardon3DCalibrationAfStudyAssemblyResult map_study_result( + Lardon3DCalibrationAfStudyResult result) { + switch (result) { + case LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT; + case LARDON3D_CALIBRATION_AF_STUDY_CAPACITY: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CAPACITY; + case LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE; + case LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR: + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_ENCODING_ERROR; + case LARDON3D_CALIBRATION_AF_STUDY_OK: + break; + } + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE; +} + +Lardon3DCalibrationAfStudyAssemblyResult +lardon3d_calibration_af_study_assemble_materialized( + const Lardon3DCalibrationAfStudyAssemblyInput *input, + unsigned char *artifact, size_t artifact_capacity, size_t *written, + unsigned char artifact_sha256[32], + Lardon3DCalibrationAfStudySummary *summary) { + if (written) *written = 0; + if (artifact_sha256) memset(artifact_sha256, 0, 32); + if (summary) memset(summary, 0, sizeof(*summary)); + + if (!input || !artifact || !written || !artifact_sha256 || !summary || + !input->entries || input->entry_count < 2 || + input->entry_count > LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES || + !nonzero_digest(input->study_context_sha256)) + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT; + + Lardon3DCalibrationAfStudySample + samples[LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES]; + memset(samples, 0, sizeof(samples)); + + uint32_t width = 0; + uint32_t height = 0; + + for (size_t index = 0; index < input->entry_count; ++index) { + const Lardon3DCalibrationAfStudyAssemblyEntry *entry = + &input->entries[index]; + if (!entry->external || !entry->focus_token) + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT; + + Lardon3DCalibrationAfStudyWorkflowResult bridge = + lardon3d_calibration_af_study_sample_from_materialized_evidence( + entry->external, entry->role, entry->focus_token, &samples[index]); + if (bridge != LARDON3D_CALIBRATION_AF_STUDY_WORKFLOW_OK) + return map_bridge_result(bridge); + + if (index == 0) { + width = entry->external->oriented_width; + height = entry->external->oriented_height; + } else if (entry->external->oriented_width != width || + entry->external->oriented_height != height) { + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE; + } + + /* Stronger than the raw L3DAFST1 producer's (token,digest) duplicate rule: + * an already-materialized calibration result is independent evidence only + * once, regardless of how the caller labels focus or study role. */ + for (size_t previous = 0; previous < index; ++previous) { + if (memcmp(samples[previous].calibration_evidence_sha256, + samples[index].calibration_evidence_sha256, 32) == 0) + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE; + } + } + + Lardon3DCalibrationAfStudyInput study = {0}; + memcpy(study.study_context_sha256, input->study_context_sha256, 32); + study.width = width; + study.height = height; + study.samples = samples; + study.sample_count = input->entry_count; + + Lardon3DCalibrationAfStudyResult result = + lardon3d_calibration_af_study_produce( + &study, artifact, artifact_capacity, written, artifact_sha256, + summary); + if (result != LARDON3D_CALIBRATION_AF_STUDY_OK) + return map_study_result(result); + + return LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_OK; +} diff --git a/tests/test_calibration_af_study_assembly.c b/tests/test_calibration_af_study_assembly.c new file mode 100644 index 0000000..b2f1d93 --- /dev/null +++ b/tests/test_calibration_af_study_assembly.c @@ -0,0 +1,226 @@ +#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 validation_sha( + 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( + unsigned char identity, double focal_offset) { + Lardon3DCalibrationWorkflowExternalEvidence value = {0}; + + fill_digest(value.boundary.session_sha256, identity); + fill_digest(value.boundary.detection_sha256, (unsigned char)(identity + 1)); + fill_digest(value.boundary.solve_sha256, (unsigned char)(identity + 2)); + fill_digest(value.boundary.evidence_sha256, (unsigned char)(identity + 3)); + fill_digest(value.boundary.producer_sha256, 0x55); + fill_digest(value.boundary.campaign_state_sha256, 0x66); + fill_digest(value.boundary.optical_state_sha256, + (unsigned char)(identity + 4)); + 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)validation_sha(&value.boundary, value.validation_evidence_sha256); + + value.oriented_width = 6000; + value.oriented_height = 4000; + const double params[8] = { + 4000.0 + focal_offset, 4002.0 + focal_offset, + 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 Lardon3DCalibrationAfStudyAssemblyInput make_input( + Lardon3DCalibrationAfStudyAssemblyEntry *entries, size_t count) { + Lardon3DCalibrationAfStudyAssemblyInput input = {0}; + fill_digest(input.study_context_sha256, 0xD1); + input.entries = entries; + input.entry_count = count; + return input; +} + +static int test_happy_path_and_order_independence(void) { + Lardon3DCalibrationWorkflowExternalEvidence first = fixture(0x11, 0.0); + Lardon3DCalibrationWorkflowExternalEvidence second = fixture(0x21, 8.0); + Lardon3DCalibrationWorkflowExternalEvidence third = fixture(0x31, 14.0); + + Lardon3DCalibrationAfStudyAssemblyEntry entries[3] = { + {&first, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:137"}, + {&second, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:151"}, + {&third, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT, "focus:165"}, + }; + Lardon3DCalibrationAfStudyAssemblyInput input = make_input(entries, 3); + + unsigned char artifact_a[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES]; + unsigned char artifact_b[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES]; + unsigned char sha_a[32]; + unsigned char sha_b[32]; + size_t written_a = 0; + size_t written_b = 0; + Lardon3DCalibrationAfStudySummary summary_a; + Lardon3DCalibrationAfStudySummary summary_b; + + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact_a, sizeof(artifact_a), &written_a, sha_a, + &summary_a) == LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_OK); + CHECK(written_a > 0); + CHECK(memcmp(artifact_a, "L3DAFST1", 8) == 0); + CHECK(summary_a.sample_count == 3); + CHECK(summary_a.fit_count == 2); + CHECK(summary_a.holdout_count == 1); + CHECK(summary_a.pair_count == 3); + + Lardon3DCalibrationAfStudyAssemblyEntry reordered[3] = { + entries[2], entries[0], entries[1], + }; + input.entries = reordered; + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact_b, sizeof(artifact_b), &written_b, sha_b, + &summary_b) == LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_OK); + CHECK(written_a == written_b); + CHECK(memcmp(artifact_a, artifact_b, written_a) == 0); + CHECK(memcmp(sha_a, sha_b, 32) == 0); + CHECK(memcmp(&summary_a, &summary_b, sizeof(summary_a)) == 0); + return 0; +} + +static int test_relabelled_duplicate_calibration_rejected(void) { + Lardon3DCalibrationWorkflowExternalEvidence same = fixture(0x11, 0.0); + Lardon3DCalibrationAfStudyAssemblyEntry entries[2] = { + {&same, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:137"}, + {&same, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT, "focus:165"}, + }; + Lardon3DCalibrationAfStudyAssemblyInput input = make_input(entries, 2); + unsigned char artifact[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES]; + unsigned char sha[32]; + size_t written = 999; + Lardon3DCalibrationAfStudySummary summary; + memset(sha, 0xA5, sizeof(sha)); + memset(&summary, 0xA5, sizeof(summary)); + + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact, sizeof(artifact), &written, sha, &summary) == + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE); + CHECK(written == 0); + unsigned char zero_sha[32] = {0}; + CHECK(memcmp(sha, zero_sha, 32) == 0); + Lardon3DCalibrationAfStudySummary zero_summary = {0}; + CHECK(memcmp(&summary, &zero_summary, sizeof(summary)) == 0); + return 0; +} + +static int test_dimension_mismatch_rejected(void) { + Lardon3DCalibrationWorkflowExternalEvidence first = fixture(0x11, 0.0); + Lardon3DCalibrationWorkflowExternalEvidence second = fixture(0x21, 8.0); + second.oriented_width = 5999; + + Lardon3DCalibrationAfStudyAssemblyEntry entries[2] = { + {&first, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:137"}, + {&second, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT, "focus:165"}, + }; + Lardon3DCalibrationAfStudyAssemblyInput input = make_input(entries, 2); + unsigned char artifact[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES]; + unsigned char sha[32]; + size_t written = 0; + Lardon3DCalibrationAfStudySummary summary; + + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact, sizeof(artifact), &written, sha, &summary) == + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE); + return 0; +} + +static int test_invalid_bridge_evidence_rejected(void) { + Lardon3DCalibrationWorkflowExternalEvidence first = fixture(0x11, 0.0); + Lardon3DCalibrationWorkflowExternalEvidence second = fixture(0x21, 8.0); + second.validation_evidence_sha256[0] ^= 1; + + Lardon3DCalibrationAfStudyAssemblyEntry entries[2] = { + {&first, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:137"}, + {&second, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT, "focus:165"}, + }; + Lardon3DCalibrationAfStudyAssemblyInput input = make_input(entries, 2); + unsigned char artifact[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES]; + unsigned char sha[32]; + size_t written = 0; + Lardon3DCalibrationAfStudySummary summary; + + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact, sizeof(artifact), &written, sha, &summary) == + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_EVIDENCE); + return 0; +} + +static int test_invalid_arguments_and_capacity(void) { + Lardon3DCalibrationWorkflowExternalEvidence first = fixture(0x11, 0.0); + Lardon3DCalibrationWorkflowExternalEvidence second = fixture(0x21, 8.0); + Lardon3DCalibrationAfStudyAssemblyEntry entries[2] = { + {&first, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT, "focus:137"}, + {&second, LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT, "focus:165"}, + }; + Lardon3DCalibrationAfStudyAssemblyInput input = make_input(entries, 1); + unsigned char artifact[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES]; + unsigned char sha[32]; + size_t written = 0; + Lardon3DCalibrationAfStudySummary summary; + + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact, sizeof(artifact), &written, sha, &summary) == + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_INVALID_ARGUMENT); + + input.entry_count = 2; + CHECK(lardon3d_calibration_af_study_assemble_materialized( + &input, artifact, 8, &written, sha, &summary) == + LARDON3D_CALIBRATION_AF_STUDY_ASSEMBLY_CAPACITY); + return 0; +} + +int main(void) { + CHECK(test_happy_path_and_order_independence() == 0); + CHECK(test_relabelled_duplicate_calibration_rejected() == 0); + CHECK(test_dimension_mismatch_rejected() == 0); + CHECK(test_invalid_bridge_evidence_rejected() == 0); + CHECK(test_invalid_arguments_and_capacity() == 0); + puts("CALIBRATION_AF_STUDY_ASSEMBLY_V1=PASS"); + return 0; +}