JCR-1 ships ten conformance vectors with three independent verifier
implementations byte-equal. The triangle primitives (conservation,
cone, oracle, elicitor) had no equivalent — implementers in other
languages had to read the Rust source to know what "compliant" means.
This commit closes that gap. Each primitive gets:
conformance/vectors.json — the canonical fixture set
conformance/README.md — the conformance contract spec
tests/conformance.rs — Rust verifier wired into cargo test
Vectors:
conservation/1 — 5 vectors: conforming, below_spec, hallucination,
missing_delta, plus a multi-finding case
cone/1 — 6 vectors: every datum_default band entry,
jouleclaw_cascade_default L0, escalate-lut-to-
formulaic, escalate-from-apex (refused)
oracle/1 — 5 vectors: always_pass, always_fail, composite
all-pass with chain dedup, composite short-
circuit on first fail, empty-composite trivial-
pass
elicitor/1 — 5 vectors: empty-walker-done, single-required-
ask, two-required-fill-first-ask-second, fully-
filled-done, optional-slot-never-asked
Each verifier asserts byte-equality between the reference impl's
serde_json output and the vector's `expected_*`. Adding a vector
extends the conformance contract for every implementer.
The Python and Go reference verifiers (the JCR-1 multi-language
pattern) are roadmap, not part of this commit — the vectors are
language-neutral so other-language verifiers can be added without
changing the contract.
4 new tests, one per primitive — all green.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
78 lines
3.1 KiB
Rust
78 lines
3.1 KiB
Rust
//! Conservation conformance — the reference [`DeterministicVerifier`]
|
|
//! must produce exactly the findings listed in
|
|
//! `conformance/vectors.json` for every vector. Any independent
|
|
//! implementation (other language, FFI binding, alternate verifier) is
|
|
//! conformant iff it produces the same `Vec<VerificationFinding>` for
|
|
//! every input listed here, in the same order, byte-equal under
|
|
//! serde_json's snake_case-tagged enum serialization.
|
|
|
|
use jouleclaw_conservation::{
|
|
CanonicalJsonByteCount, ConservationClaim, DeterministicVerifier, Delta, Output, Spec,
|
|
Substrate, VerificationFinding, Verifier,
|
|
};
|
|
use serde_json::Value;
|
|
|
|
const VECTORS: &str = include_str!("../conformance/vectors.json");
|
|
|
|
fn check_vector(v: &Value) {
|
|
let n = v["n"].as_i64().expect("n");
|
|
let desc = v["description"].as_str().expect("description");
|
|
let input = &v["input"];
|
|
let expected = v["expected_findings"].as_array().expect("expected findings");
|
|
|
|
let measure = CanonicalJsonByteCount;
|
|
let spec_bytes = input["spec_bytes_utf8"].as_str().expect("spec_bytes_utf8").as_bytes();
|
|
let output_bytes = input["output_bytes_utf8"].as_str().expect("output_bytes_utf8").as_bytes();
|
|
let spec = Spec::from_bytes(spec_bytes, &measure);
|
|
let output = Output::from_bytes(output_bytes, &measure);
|
|
|
|
let mut delta = Delta::empty();
|
|
if let Some(fields) = input["delta_fields"].as_object() {
|
|
for (k, val) in fields {
|
|
delta.set(k.clone(), val.as_str().expect("delta value is string").to_string());
|
|
}
|
|
}
|
|
delta.recount(&measure);
|
|
|
|
let substrate = Substrate {
|
|
id: input["substrate_id"].as_str().unwrap_or("").to_string(),
|
|
arrangement_hash: input["substrate_arrangement_hash"]
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.to_string(),
|
|
used_info: jouleclaw_conservation::InformationContent(
|
|
input["substrate_used_info"].as_u64().unwrap_or(0),
|
|
),
|
|
};
|
|
|
|
let residual_tolerance = input["residual_tolerance"].as_u64().unwrap_or(0);
|
|
let required_slots: Vec<String> = input["required_slots"]
|
|
.as_array()
|
|
.expect("required_slots array")
|
|
.iter()
|
|
.map(|s| s.as_str().expect("required slot is string").to_string())
|
|
.collect();
|
|
|
|
let claim = ConservationClaim::seal(&measure, spec, output, delta, substrate, residual_tolerance)
|
|
.expect("seal");
|
|
let findings: Vec<VerificationFinding> =
|
|
DeterministicVerifier.verify(&claim, &required_slots);
|
|
|
|
let got = serde_json::to_value(&findings).expect("findings serialise");
|
|
let expected_v = Value::Array(expected.clone());
|
|
assert_eq!(
|
|
got, expected_v,
|
|
"vector {n} ({desc}): findings mismatch.\n got = {got}\n expected = {expected_v}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_conservation_vector_matches_the_reference_verifier() {
|
|
let suite: Value = serde_json::from_str(VECTORS).expect("vectors.json parses");
|
|
let count = suite["vector_count"].as_u64().expect("vector_count") as usize;
|
|
let vectors = suite["vectors"].as_array().expect("vectors array");
|
|
assert_eq!(vectors.len(), count, "vector_count mismatch");
|
|
for v in vectors {
|
|
check_vector(v);
|
|
}
|
|
}
|