Skip to content

Commit 84c06a2

Browse files
committed
fix(p037): close Phase B evidence provenance
Independent pushed-byte review of 460557e accepted the CH3-10/CH3-11 repairs but found a pre-existing, more fundamental defect: scrutinizing materialization_root's trustworthiness exposed that p037_evidence_b.record_problems() claimed, in its own docstring, to check "the same clauses" as the generic p037_evidence.record_problems(), but actually implemented only a small subset -- instrument_paths/ treatment_paths/subject_paths exact equality were never checked at all (only instrument_carve_outs, and only as a set, tolerating a reordered or duplicated closure); input_roots was never validated; instrument_identity was never cross-checked against a value freshly recomputed at the record's own source commit (comparison_problems's before==after equality cannot catch two independently forged-but-equal digests); population_commit's existence and ancestry were never checked; analysis_manifest/ support_manifest were never re-derived from the population commit, nor their sha256 fields checked against the carried manifest; reference_ profile/execution_profile were never checked; artifacts were never validated at all. Ported the full contract clause for clause, using the same pure, parameter-driven ev.* helpers the generic function itself calls (population_fields/analysis_manifest/support_manifest/_manifest_entries/ _manifest_digest/reference_profile_problems/commit_exists/is_ancestor -- none of these read EPOCH/INSTRUMENT_PATHS internally) and this module's own environment_id()/instrument_identity() for the two genuinely epoch-coupled checks. subject_paths was also missing from evidence_ fields()'s own returned dict (the SUBJECT_PATHS constant already existed, it just was never included) -- added and now validated too. One intentional divergence, named rather than silently kept: the old `materialized_root`/`population_intact()` branch is REMOVED, not renamed to the field verdict snapshots actually write (`materialization_root`) -- it was dead code (that typo'd field name is never written, so the branch never ran), and reintroducing it correctly would still make historical validation depend on an ephemeral materialized directory that need not still exist. Population integrity is proven durably instead, exactly as the generic function proves it: by re-deriving analysis_manifest/support_ manifest from the population commit and checking their digests. The same review found the field this dead branch was named after was itself the live vulnerability: p037_verdict_snapshot.py's compare() trusted the AFTER snapshot's own recorded `materialization_root` field directly as authority for the witness file-identity check (CH3-11's own fix). A manually altered snapshot can change BOTH `materialization_root` and `witness.site.file` to matching forged strings, and the CH3-11 exact- equality check between them would still pass -- two forged strings agreeing with each other, not with reality. compare() now computes canonical_root = ev.materialization_root(population_commit, analysis_ manifest_sha256).relative_to(ROOT).as_posix() and requires the recorded `materialization_root` to equal it, refusing (fail-closed) otherwise; the per-file witness-binding loop uses canonical_root, never the recorded field, to reconstruct expected_witness_file. New selftests: 16 record_problems() mutation hostiles (wrong/missing instrument_paths/treatment_paths/subject_paths/instrument_identity, invalid input_roots, nonexistent/non-ancestor population_commit, tampered analysis/support manifest, wrong analysis/support digest, empty execution_profile, four artifact-attestation mismatches), each derived by mutating ONE field of a single real record built from evidence_fields() against live HEAD (never two hand-crafted invalid records compared only to each other); two pairwise-forged-equal-digest controls proving comparison_ problems's before==after check is not mistaken for authenticity now that each side independently fails record_problems() on its own; and six canonical-materialization-root hostiles in p037_verdict_snapshot.py (correct case classifies, forged population/digest halves each refused, missing root refused, and the loop-closing case: a witness file forged to match a forged recorded root still fails against the canonical root). Sanity check against the two currently-committed R_B baselines (docs/ evidence/p037-b-baseline-verdict-{python,rust}.json): both fail only on the closure-identity fields (instrument_paths/carve_outs/treatment_paths/ subject_paths/instrument_identity), exactly the expected drift from INSTRUMENT_CARVE_OUTS growing since they were taken (dump.rs, verdict.rs, render.rs, own-cli/src/ownir.rs all joined after). Every deeper check -- population ancestry, manifest re-derivation, digest cross-check, artifact attestation -- passes cleanly on both real historical artifacts, so the kill gate (a substantive contract failure on a previously-valid artifact) does not fire; both files remain superseded for the unrelated reason this whole review arc already established. Zero production semantics: touches only scripts/p037_evidence_b.py and scripts/p037_verdict_snapshot.py. All three production_diff_gate units (rust, extractor, cli) report IDENTICAL against 460557e. ruff and strict mypy are clean; every module selftest passes; tests/test_p037_a2d_epoch.py and tests/test_p037_cumulative_evidence.py stay green. No T_B named, no R_B taken, no R1 commit/push/replay. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AN6xHbpovxrmZ4AMjS7WQA
1 parent 460557e commit 84c06a2

2 files changed

Lines changed: 449 additions & 27 deletions

File tree

‎scripts/p037_evidence_b.py‎

Lines changed: 321 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -481,14 +481,48 @@ def evidence_fields(
481481
"instrument_paths": list(INSTRUMENT_PATHS),
482482
"instrument_carve_outs": list(INSTRUMENT_CARVE_OUTS),
483483
"treatment_paths": list(TREATMENT_PATHS),
484+
"subject_paths": list(SUBJECT_PATHS),
484485
"instrument_identity": instrument_identity(source, repo=repo),
485486
**population,
486487
}
487488

488489

489490
def record_problems(record: dict[str, Any], *, repo: Path = ROOT) -> list[str]:
490491
"""Self-consistency of one Phase-B evidence record at its OWN source
491-
commit -- same clauses as p037_evidence.record_problems, epoch 'b'."""
492+
commit -- ported clause for clause from p037_evidence.record_problems
493+
(CH3-12, R1-review of 460557e: the prior version of this function
494+
claimed that parity in its own docstring but actually implemented only
495+
a small subset -- instrument_paths/treatment_paths/subject_paths exact
496+
equality were never checked at all (only instrument_carve_outs, and
497+
only as a SET, which also tolerates a reordered/duplicated closure);
498+
input_roots was never validated; instrument_identity(source) was never
499+
cross-checked against the recorded value (comparison_problems's own
500+
before==after check cannot catch two independently forged-but-equal
501+
digests); population_commit existence/ancestry was never checked;
502+
analysis_manifest/support_manifest were never re-derived from the
503+
population commit, nor their sha256 fields checked against the carried
504+
manifest; reference_profile/execution_profile were never checked;
505+
artifacts were never validated at all). Ported below using the SAME
506+
pure, parameter-driven ev.* helpers the generic function itself calls
507+
(population_fields/analysis_manifest/support_manifest/_manifest_
508+
entries/_manifest_digest/reference_profile_problems/commit_exists/
509+
is_ancestor -- confirmed against this module's own top docstring: none
510+
of these read EPOCH/INSTRUMENT_PATHS/TREATMENT_PATHS internally), and
511+
this module's OWN environment_id()/instrument_identity() (the two
512+
that ARE epoch-coupled, already defined above for exactly that reason,
513+
already used by evidence_fields()).
514+
515+
One intentional divergence from the generic function, named explicitly
516+
rather than silently kept: the old `materialized_root`/`population_
517+
intact()` branch is REMOVED, not merely renamed to the field verdict
518+
snapshots actually write (`materialization_root`) -- it was dead code
519+
(that typo'd field name is never written, so the branch never ran) and
520+
reintroducing it correctly would still make historical validation
521+
depend on an ephemeral materialized directory that need not still
522+
exist. Population integrity is proven durably instead, exactly as the
523+
generic function proves it: by re-deriving analysis_manifest/support_
524+
manifest from the population commit and checking their digests -- both
525+
already ported above, no separate directory inspection needed."""
492526
problems: list[str] = []
493527
source = record.get("source_commit")
494528
if not isinstance(source, str) or not source:
@@ -504,27 +538,121 @@ def record_problems(record: dict[str, Any], *, repo: Path = ROOT) -> list[str]:
504538
"record does not attest that the population stayed intact through the run")
505539
epoch = record.get("epoch")
506540
if epoch is None:
507-
problems.append(f"record carries no epoch: it predates {EPOCH} and is not eligible")
541+
problems.append(
542+
f"record carries no epoch: it predates {EPOCH} and is not eligible in it, "
543+
"as a before side or otherwise")
508544
elif epoch != EPOCH:
509545
problems.append(f"record names epoch {epoch!r}, not {EPOCH!r}")
546+
env_id = record.get("environment_id")
547+
if not isinstance(env_id, str) or not env_id:
548+
problems.append("record carries no environment_id")
549+
if record.get("instrument_paths") != list(INSTRUMENT_PATHS):
550+
problems.append("recorded instrument_paths differ from this tool's instrument closure")
551+
if record.get("instrument_carve_outs") != list(INSTRUMENT_CARVE_OUTS):
552+
problems.append("recorded instrument_carve_outs differ from this tool's")
553+
if record.get("treatment_paths") != list(TREATMENT_PATHS):
554+
problems.append("recorded treatment_paths differ from this tool's treatment closure")
555+
if record.get("subject_paths") != list(SUBJECT_PATHS):
556+
problems.append("recorded subject_paths differ from this tool's measurement closure")
557+
558+
roots_raw = record.get("input_roots")
559+
roots: tuple[str, ...] = ()
560+
if (
561+
isinstance(roots_raw, list)
562+
and roots_raw
563+
and all(isinstance(x, str) and x for x in roots_raw)
564+
):
565+
roots = tuple(str(x) for x in roots_raw)
566+
else:
567+
problems.append("record carries no valid input_roots")
568+
510569
if not ev.commit_exists(source, repo=repo):
511570
return [*problems, f"source commit {source[:12]} is not present in this checkout"]
512-
if not isinstance(record.get("instrument_carve_outs"), list) or (
513-
set(record["instrument_carve_outs"]) != set(INSTRUMENT_CARVE_OUTS)):
514-
problems.append("record's instrument_carve_outs differs from this module's own")
515-
env_id = record.get("environment_id")
571+
572+
try:
573+
expected_env = environment_id(source, repo=repo)
574+
except EvidenceRefused as exc:
575+
problems.append(str(exc))
576+
else:
577+
if isinstance(env_id, str) and env_id and env_id != expected_env:
578+
problems.append(
579+
f"record names environment {env_id!r}, not the epoch record's "
580+
f"{expected_env!r} at its source commit")
516581
try:
517-
want_env = environment_id(source, repo=repo)
582+
identity = instrument_identity(source, repo=repo)
518583
except EvidenceRefused as exc:
519584
problems.append(str(exc))
520585
else:
521-
if env_id != want_env:
586+
if record.get("instrument_identity") != identity:
522587
problems.append(
523-
f"record names environment {env_id!r}, not the epoch record's {want_env!r}")
524-
root = record.get("materialized_root")
525-
if isinstance(root, str) and root:
526-
tampered = ev.population_intact(record, Path(root), repo=repo)
527-
problems.extend(tampered)
588+
"recorded instrument_identity is not the instrument closure's digest at the "
589+
"source commit")
590+
591+
population = record.get("population_commit")
592+
if not isinstance(population, str) or not ev.commit_exists(population, repo=repo):
593+
problems.append("record names no population_commit present in this checkout")
594+
else:
595+
if not ev.is_ancestor(population, source, repo=repo):
596+
problems.append(
597+
f"population commit {population[:12]} is not an ancestor of (or equal to) "
598+
f"source commit {source[:12]}")
599+
try:
600+
recorded_analysis = ev._manifest_entries(record, "analysis_manifest")
601+
recorded_support = ev._manifest_entries(record, "support_manifest")
602+
except EvidenceRefused as exc:
603+
problems.append(str(exc))
604+
else:
605+
if roots:
606+
try:
607+
analysis = ev.analysis_manifest(population, roots, repo=repo)
608+
support = ev.support_manifest(population, analysis, repo=repo)
609+
except EvidenceRefused as exc:
610+
problems.append(str(exc))
611+
else:
612+
if recorded_analysis != analysis:
613+
problems.append(
614+
"recorded analysis_manifest does not match the population "
615+
"commit's exact C# input set")
616+
if recorded_support != support:
617+
problems.append(
618+
"recorded support_manifest does not match the population "
619+
"commit's semantic support closure")
620+
if record.get("analysis_manifest_sha256") != ev._manifest_digest(recorded_analysis):
621+
problems.append("analysis_manifest_sha256 does not name the carried manifest")
622+
if record.get("support_manifest_sha256") != ev._manifest_digest(recorded_support):
623+
problems.append("support_manifest_sha256 does not name the carried manifest")
624+
625+
problems.extend(ev.reference_profile_problems(record))
626+
627+
profile = record.get("execution_profile")
628+
if not isinstance(profile, dict) or not profile:
629+
problems.append("record carries no execution_profile")
630+
631+
artifacts = record.get("artifacts", {})
632+
if not isinstance(artifacts, dict):
633+
problems.append("artifacts is not an object")
634+
else:
635+
for name, artifact in artifacts.items():
636+
if not isinstance(artifact, dict):
637+
problems.append(f"artifact {name!r} is not an object")
638+
continue
639+
if artifact.get("source_commit") != source:
640+
problems.append(
641+
f"artifact {name!r} was not built from the record's source commit")
642+
if artifact.get("dirty") is not False:
643+
problems.append(f"artifact {name!r} was built on a dirty tree")
644+
if not isinstance(artifact.get("sha256"), str):
645+
problems.append(f"artifact {name!r} carries no sha256")
646+
executed = artifact.get("executed")
647+
if not isinstance(executed, dict):
648+
problems.append(f"artifact {name!r} carries no executed attestation")
649+
else:
650+
if executed.get("sha256") != artifact.get("sha256"):
651+
problems.append(
652+
f"artifact {name!r} executed a file other than the qualified build")
653+
if executed.get("post_run_intact") is not True:
654+
problems.append(
655+
f"artifact {name!r} does not attest it stayed intact through the run")
528656
return problems
529657

530658

@@ -873,6 +1001,186 @@ def selftest() -> int:
8731001
any("dump.rs" in p and "no production_diff_gate" in p for p in problems),
8741002
problems)
8751003

1004+
# --- CH3-12 (R1-review of 460557e, items 16/17): record_problems() was
1005+
# just ported clause-for-clause from the generic p037_evidence.record_
1006+
# problems (see this function's own docstring) after the review found it
1007+
# had claimed that parity without implementing it -- a hand-built
1008+
# "invalid record" fixture compared only to another hand-built one would
1009+
# risk re-committing exactly that mistake (both sides could drift from
1010+
# what a REAL take actually produces without either test noticing). So
1011+
# the base fixture below is the REAL current helper's own output --
1012+
# `evidence_fields()` called against live HEAD, the same call `take()`
1013+
# itself makes -- with only the handful of fields a full `_measure()` run
1014+
# adds afterwards (execution_profile/reference_profile/artifacts/
1015+
# post_run_*) filled in from the same real, parameter-driven ev.*
1016+
# helpers `_measure()` itself uses. `dirty`/`is_evidence` are forced
1017+
# rather than read from the live tree's actual state, because THIS
1018+
# selftest routinely runs on a dirty tree mid-repair -- record_problems()
1019+
# itself never re-derives "dirty" from git (that would check validation-
1020+
# time state, not take-time state), it only checks the field says
1021+
# `False`, so overriding it here is a fixture choice, not a cheat past
1022+
# the function under test. One mutation per test, per field, proving
1023+
# each ported clause is independently load-bearing -- never two
1024+
# hand-crafted invalid records compared only to each other.
1025+
_valid_provenance = evidence_fields(CORPUS_DIRS, population_commit="HEAD")
1026+
_valid_record: dict[str, Any] = {
1027+
**_valid_provenance,
1028+
"dirty": False,
1029+
"is_evidence": True,
1030+
"post_run_dirty": False,
1031+
"post_run_population_intact": True,
1032+
"execution_profile": ev.execution_profile(include_rust=False),
1033+
"reference_profile": {ev.EXTRA_REF_ENV: ev.REFERENCE_PROFILE_CLEAN,
1034+
"observed_extra_reference_lines": 0},
1035+
"artifacts": {},
1036+
}
1037+
_selfcheck("real-valid-record-passes-record-problems-cleanly",
1038+
record_problems(_valid_record) == [], record_problems(_valid_record))
1039+
1040+
_wrong_identity = {**_valid_record, "instrument_identity": "deadbeef"}
1041+
problems = record_problems(_wrong_identity)
1042+
_selfcheck("hostile-wrong-instrument-identity",
1043+
any("recorded instrument_identity is not the instrument closure" in p
1044+
for p in problems),
1045+
problems)
1046+
1047+
_wrong_instrument_paths = {**_valid_record, "instrument_paths": ["not/a/real/path"]}
1048+
problems = record_problems(_wrong_instrument_paths)
1049+
_selfcheck("hostile-wrong-instrument-paths",
1050+
any("recorded instrument_paths differ" in p for p in problems), problems)
1051+
1052+
_missing_treatment_paths = {k: v for k, v in _valid_record.items() if k != "treatment_paths"}
1053+
problems = record_problems(_missing_treatment_paths)
1054+
_selfcheck("hostile-missing-treatment-paths",
1055+
any("recorded treatment_paths differ" in p for p in problems), problems)
1056+
1057+
_wrong_subject_paths = {**_valid_record, "subject_paths": ["not/a/real/path"]}
1058+
problems = record_problems(_wrong_subject_paths)
1059+
_selfcheck("hostile-wrong-subject-paths",
1060+
any("recorded subject_paths differ" in p for p in problems), problems)
1061+
1062+
_invalid_input_roots = {**_valid_record, "input_roots": []}
1063+
problems = record_problems(_invalid_input_roots)
1064+
_selfcheck("hostile-invalid-input-roots",
1065+
any("no valid input_roots" in p for p in problems), problems)
1066+
1067+
_nonexistent_population = {**_valid_record, "population_commit": "0" * 40}
1068+
problems = record_problems(_nonexistent_population)
1069+
_selfcheck("hostile-nonexistent-population-commit",
1070+
any("no population_commit present" in p for p in problems), problems)
1071+
1072+
# population stays the real, current HEAD (a real, existing commit) but
1073+
# source moves to HEAD's own parent -- population is then a DESCENDANT
1074+
# of source, never its ancestor, without needing any commit outside this
1075+
# branch's own linear history.
1076+
_ancestor_hostile = {**_valid_record, "source_commit": ev.resolve_commit("HEAD~1")}
1077+
problems = record_problems(_ancestor_hostile)
1078+
_selfcheck("hostile-population-commit-not-ancestor-of-source",
1079+
any("is not an ancestor of" in p for p in problems), problems)
1080+
1081+
_tampered_analysis = {**_valid_record,
1082+
"analysis_manifest": [{"path": "fake.cs", "blob": "0" * 40}]}
1083+
problems = record_problems(_tampered_analysis)
1084+
_selfcheck("hostile-tampered-analysis-manifest",
1085+
any("recorded analysis_manifest does not match" in p for p in problems), problems)
1086+
1087+
_wrong_analysis_digest = {**_valid_record, "analysis_manifest_sha256": "deadbeef" * 8}
1088+
problems = record_problems(_wrong_analysis_digest)
1089+
_selfcheck("hostile-wrong-analysis-manifest-sha256",
1090+
any("analysis_manifest_sha256 does not name the carried manifest" in p
1091+
for p in problems),
1092+
problems)
1093+
1094+
_tampered_support = {**_valid_record,
1095+
"support_manifest": [{"path": "fake.xaml", "blob": "0" * 40,
1096+
"mechanism": "sibling-xaml"}]}
1097+
problems = record_problems(_tampered_support)
1098+
_selfcheck("hostile-tampered-support-manifest",
1099+
any("recorded support_manifest does not match" in p for p in problems), problems)
1100+
1101+
_wrong_support_digest = {**_valid_record, "support_manifest_sha256": "cafebabe" * 8}
1102+
problems = record_problems(_wrong_support_digest)
1103+
_selfcheck("hostile-wrong-support-manifest-sha256",
1104+
any("support_manifest_sha256 does not name the carried manifest" in p
1105+
for p in problems),
1106+
problems)
1107+
1108+
_empty_profile = {**_valid_record, "execution_profile": {}}
1109+
problems = record_problems(_empty_profile)
1110+
_selfcheck("hostile-empty-execution-profile",
1111+
any("carries no execution_profile" in p for p in problems), problems)
1112+
1113+
_good_artifact: dict[str, Any] = {
1114+
"source_commit": _valid_record["source_commit"], "dirty": False, "sha256": "a" * 64,
1115+
"executed": {"sha256": "a" * 64, "post_run_intact": True},
1116+
}
1117+
_base_with_artifact = {**_valid_record, "artifacts": {"own-cli": _good_artifact}}
1118+
_selfcheck("real-valid-record-plus-one-good-artifact-passes-cleanly",
1119+
record_problems(_base_with_artifact) == [], record_problems(_base_with_artifact))
1120+
1121+
_artifact_wrong_source = {**_base_with_artifact,
1122+
"artifacts": {"own-cli": {**_good_artifact,
1123+
"source_commit": "1" * 40}}}
1124+
problems = record_problems(_artifact_wrong_source)
1125+
_selfcheck("hostile-artifact-source-commit-mismatch",
1126+
any("was not built from the record's source commit" in p for p in problems),
1127+
problems)
1128+
1129+
_artifact_dirty = {**_base_with_artifact,
1130+
"artifacts": {"own-cli": {**_good_artifact, "dirty": True}}}
1131+
problems = record_problems(_artifact_dirty)
1132+
_selfcheck("hostile-artifact-dirty-true",
1133+
any("was built on a dirty tree" in p for p in problems), problems)
1134+
1135+
_artifact_wrong_executed_sha = {
1136+
**_base_with_artifact,
1137+
"artifacts": {"own-cli": {**_good_artifact,
1138+
"executed": {"sha256": "b" * 64, "post_run_intact": True}}},
1139+
}
1140+
problems = record_problems(_artifact_wrong_executed_sha)
1141+
_selfcheck("hostile-artifact-executed-sha256-mismatch",
1142+
any("executed a file other than the qualified build" in p for p in problems),
1143+
problems)
1144+
1145+
_artifact_not_intact = {
1146+
**_base_with_artifact,
1147+
"artifacts": {"own-cli": {**_good_artifact,
1148+
"executed": {"sha256": "a" * 64, "post_run_intact": False}}},
1149+
}
1150+
problems = record_problems(_artifact_not_intact)
1151+
_selfcheck("hostile-artifact-post-run-intact-false",
1152+
any("does not attest it stayed intact through the run" in p for p in problems),
1153+
problems)
1154+
1155+
# item 17: pairwise equality is not authenticity. Both sides carry the
1156+
# SAME forged value, so the (pre-existing) before==after equality check
1157+
# in comparison_problems() cannot see anything wrong -- only because
1158+
# record_problems() now independently recomputes and cross-checks each
1159+
# side's own value does the forgery surface, on BOTH sides, not just one.
1160+
_forged_identity_pair_problems = comparison_problems(
1161+
{**_valid_record, "instrument_identity": "deadbeefdeadbeef"},
1162+
{**_valid_record, "instrument_identity": "deadbeefdeadbeef"},
1163+
)
1164+
_selfcheck(
1165+
"pairwise-equal-but-forged-instrument-identity-still-refused-on-both-sides",
1166+
any(p.startswith("before: recorded instrument_identity is not")
1167+
for p in _forged_identity_pair_problems)
1168+
and any(p.startswith("after: recorded instrument_identity is not")
1169+
for p in _forged_identity_pair_problems),
1170+
_forged_identity_pair_problems)
1171+
1172+
_forged_digest_pair_problems = comparison_problems(
1173+
{**_valid_record, "analysis_manifest_sha256": "cafecafe" * 8},
1174+
{**_valid_record, "analysis_manifest_sha256": "cafecafe" * 8},
1175+
)
1176+
_selfcheck(
1177+
"pairwise-equal-but-forged-analysis-digest-still-refused-on-both-sides",
1178+
any(p.startswith("before: analysis_manifest_sha256 does not name")
1179+
for p in _forged_digest_pair_problems)
1180+
and any(p.startswith("after: analysis_manifest_sha256 does not name")
1181+
for p in _forged_digest_pair_problems),
1182+
_forged_digest_pair_problems)
1183+
8761184
if _failures:
8771185
print(f"RESULT: {_failures} check(s) failed")
8781186
return 1

0 commit comments

Comments
 (0)