diff --git a/docs/validation/dfg_re20.md b/docs/validation/dfg_re20.md index 1f1dc55..20cf080 100644 --- a/docs/validation/dfg_re20.md +++ b/docs/validation/dfg_re20.md @@ -48,5 +48,7 @@ The uniform D80 control separates method error from refinement error. `--plan` w (per case: lattice parameters, every sample, window ranges, status, verdict). Receipts: `plan.json` and `result.json` carry `schema` 2 and `provenance` (HEAD, src tree, runner hash; clean checkout required, refused if it changes during the run). Receipts -written before this (schema 1, e.g. evidence 70f6617) are a bare list of cases. +written before this (schema 1, e.g. evidence 70f6617) are a bare list of cases. Read both +with `run_dfg_re20.read_result(path)` (`["cases"][0]` replaces the old `[0]`); existing +evidence scripts stay valid for schema-1 receipts and need this reader for new ones. Host contract tests: `tests/unit/test_dfg_re20.py`. diff --git a/tests/unit/test_dfg_re20.py b/tests/unit/test_dfg_re20.py index 6036fbc..fb85577 100644 --- a/tests/unit/test_dfg_re20.py +++ b/tests/unit/test_dfg_re20.py @@ -85,6 +85,23 @@ class Re20Provenance(unittest.TestCase): self.assertFalse(record["checkout_dirty"]) self.assertEqual(v.RESULT_SCHEMA, 2) + def test_reader_accepts_both_schemas_and_rejects_unknown(self): + import json + import tempfile + from pathlib import Path + case = dict(case="refined", status="NOT_STEADY", verdict=None, rows=[]) + with tempfile.TemporaryDirectory() as d: + old, new, bad = Path(d, "old.json"), Path(d, "new.json"), Path(d, "bad.json") + old.write_text(json.dumps([case])) + new.write_text(json.dumps(dict(schema=2, provenance=dict(src_tree="a" * 40), + cases=[case]))) + bad.write_text(json.dumps(dict(schema=3, cases=[case]))) + self.assertEqual(v.read_result(old), dict(schema=1, provenance=None, cases=[case])) + self.assertEqual(v.read_result(new)["cases"][0], case) + self.assertEqual(v.read_result(new)["provenance"]["src_tree"], "a" * 40) + with self.assertRaisesRegex(ValueError, "unknown DFG Re20 result schema"): + v.read_result(bad) + def test_dirty_checkout_is_refused(self): from unittest.mock import patch with patch("source_provenance.subprocess.check_output", diff --git a/tests/validation/run_dfg_re20.py b/tests/validation/run_dfg_re20.py index 190cad6..9e6099d 100644 --- a/tests/validation/run_dfg_re20.py +++ b/tests/validation/run_dfg_re20.py @@ -108,6 +108,21 @@ def summarize(rows): verdict=inside) +def read_result(path): + """Cases and provenance from a result.json of either schema. + + Schema 1 (receipts before RESULT_SCHEMA=2, e.g. evidence 70f6617) is a bare list of + cases without provenance; schema 2 is {schema, provenance, cases}. Readers that + indexed [0] on schema 1 must use read_result(path)["cases"][0]. + """ + data = json.loads(Path(path).read_text()) + if isinstance(data, list): + return dict(schema=1, provenance=None, cases=data) + if not isinstance(data, dict) or data.get("schema") != RESULT_SCHEMA: + raise ValueError(f"unknown DFG Re20 result schema: {data.get('schema') if isinstance(data, dict) else type(data).__name__}") + return dict(schema=RESULT_SCHEMA, provenance=data["provenance"], cases=data["cases"]) + + def healthy(row): return row["max_mach"] <= 0.2 and row["rho_min"] >= 0.5 and row["rho_max"] <= 1.5