mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
917860b0f8
* feat: add non-ranking revision roadmap contract (#670) * fix: isolate revision authority prompt budget
2720 lines
143 KiB
Python
2720 lines
143 KiB
Python
#!/usr/bin/env python3
|
||
"""Recompute the #576 Spec B three-gate re-review contract from its artifacts.
|
||
|
||
Stdlib-only, same architecture class as ``check_panel_synthesis.py`` (#510:
|
||
recompute every layer from the primary artifacts; any mismatch voids the
|
||
synthesis). Normative source:
|
||
``docs/design/2026-07-27-576-spec-b-re-review-precommitment-contract-spec.md``
|
||
§5 (artifact shapes), §6 (decision derivation), §10 (reviewer-label
|
||
normalization grammar), §11 (input manifest + apply-chain witness), §13
|
||
(checker responsibilities).
|
||
|
||
Responsibilities (spec §13):
|
||
|
||
1. Schema-validate the three phase artifacts + manifest (self-contained
|
||
validators), and replay the hash-bound #670 Revision-Evidence Bundle.
|
||
2. Hash-chain: ``input_manifest_hash`` -> ``precommitment_hash`` ->
|
||
``verdict_record_hash`` verbatim binding; criterion inheritance binding
|
||
(roadmap_text / letter_text byte comparison + R<n> ordinal recomputation
|
||
with the §5.1 contiguity degradation).
|
||
3. Consumer-side apply-chain witness (§11), including mandatory report-1.3
|
||
``patch_digest`` content-bound positional pairing. Older reports are
|
||
accepted only by the isolated archived loader.
|
||
4. Recompute invariants: coverage cardinalities, applied_criterion rules,
|
||
adjustment-chain grammar, deferral-loop referential integrity
|
||
(reapplications / resolutions / adjudications / acceptances / pending
|
||
rebuttal upgrades), the G2 deferral biconditional, DecisionInputs
|
||
operand equality, and the §6 Step 1-3 derivation (Steps 2-3 recomputed
|
||
BOTH from DecisionInputs AND independently from the raw records).
|
||
5. Goalpost witness: the frozen new-issue set is byte-identical across
|
||
§5.2/§5.3 and only ``regression`` attribution enters the decision path.
|
||
|
||
Exit codes (graded, spec §13):
|
||
|
||
0 pass
|
||
1 recomputation mismatch -> ``[RE-REVIEW-ABORT: synthesis_mismatch]``
|
||
(referential-integrity failures are recomputation mismatches and
|
||
always exit 1)
|
||
2 schema or manifest invalid -> the matching §3.5 abort reason
|
||
(``manifest_incomplete`` / ``manifest_hash_mismatch`` for the
|
||
manifest layer; ``phase1_lint_failed`` / ``phase2a_lint_failed`` /
|
||
``phase2b_lint_failed`` for a phase artifact that fails its schema)
|
||
|
||
Classification note (documented design decision): the three hash-chain
|
||
links and the manifest-side §11 rules (artifact byte hashes, freshness,
|
||
patch_digest pairing, apply-chain witness ``fail``) are the same-inputs
|
||
proof — their violation is graded exit 2 with ``manifest_hash_mismatch``,
|
||
mirroring §11's "-> manifest_hash_mismatch, G0". Content recomputation on
|
||
top of a sound binding (criterion quotes, letter ordinals, chains,
|
||
decision) is graded exit 1.
|
||
|
||
Hashing convention: sha256 hex over JSON Canonical Form (RFC 8785 / JCS,
|
||
the v3.6.3 passport convention). For this contract's value domain
|
||
(objects/arrays/strings/integers/booleans/null) Python's
|
||
``json.dumps(obj, sort_keys=True, separators=(",", ":"),
|
||
ensure_ascii=False)`` produces the JCS byte stream.
|
||
|
||
Usage:
|
||
python scripts/check_re_review_synthesis.py \
|
||
--manifest M.json --precommitment P.json \
|
||
--verdict-record V.json --traceability T.json \
|
||
--roadmap roadmap.json --author-adjudication author.json \
|
||
--revision-evidence-bundle bundle.json \
|
||
[--revision-evidence-root bundle-root] \
|
||
[--letter letter.md] \
|
||
[--apply-report R1.json ...]
|
||
|
||
The roadmap, author sidecar, Revision-Evidence Bundle, optional letter, and
|
||
ordered apply reports are exact §11 files the checker reads. Bundle replay
|
||
uses only its explicitly declared local artifacts under the supplied root.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import json
|
||
import math
|
||
import re
|
||
import sys
|
||
import unicodedata
|
||
from pathlib import Path
|
||
|
||
if __package__ in (None, ""): # pragma: no cover - direct CLI invocation
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||
|
||
from scripts.revision_roadmap import ContractError as RevisionContractError
|
||
from scripts.revision_roadmap import validate_bundle
|
||
|
||
EXIT_PASS = 0
|
||
EXIT_SYNTHESIS = 1
|
||
EXIT_INVALID = 2
|
||
|
||
VERDICTS = ("FULLY_ADDRESSED", "PARTIALLY_ADDRESSED", "NOT_ADDRESSED", "MADE_WORSE", "CANNOT_VERIFY")
|
||
JUDGE_VERDICTS = ("FULLY_ADDRESSED", "PARTIALLY_ADDRESSED", "NOT_ADDRESSED", "MADE_WORSE")
|
||
OBLIGATION_CLASSES = ("must_fix", "should_fix", "consider")
|
||
ROW_OBLIGATION_MAP = {"MUST_FIX": "must_fix", "SHOULD_FIX": "should_fix", "CONSIDER": "consider"}
|
||
SEVERITIES = ("critical", "major", "minor")
|
||
RESIDUAL_OBLIGATION_CLASSES = ("must_fix", "should_fix", "consider")
|
||
SEAT_LABELS = ("EIC", "R1", "R2", "R3", "DA")
|
||
VERIFIER_SEATS = ("EIC", "R1", "R2", "R3")
|
||
ADJUSTMENT_BASES = (
|
||
"author_pointer_located_evidence",
|
||
"valid_rebuttal",
|
||
"scope_correction",
|
||
"user_accepted_fail_closed",
|
||
"cross_model_adjudication",
|
||
)
|
||
ATTRIBUTIONS = ("regression", "previously_missed", "indeterminate")
|
||
DECISION_STATES = ("Accept", "Minor Revision", "Major Revision", "user_review_required", "aborted")
|
||
ABORT_REASONS = (
|
||
"phase1_lint_failed",
|
||
"phase2a_lint_failed",
|
||
"phase2b_lint_failed",
|
||
"manifest_incomplete",
|
||
"manifest_hash_mismatch",
|
||
"criteria_drift",
|
||
"synthesis_mismatch",
|
||
)
|
||
WITNESS_STATES = ("pass", "fail", "not_run_no_reports")
|
||
DECISION_ORDER = {"Accept": 0, "Minor Revision": 1, "Major Revision": 2}
|
||
# §5.3 mechanical 5->4 map (MADE_WORSE -> NO: the concern stands unresolved —
|
||
# verification happened, outcome negative).
|
||
VERIFIED_MAP = {
|
||
"FULLY_ADDRESSED": "YES",
|
||
"PARTIALLY_ADDRESSED": "PARTIAL",
|
||
"NOT_ADDRESSED": "NO",
|
||
"MADE_WORSE": "NO",
|
||
"CANNOT_VERIFY": "CANNOT_VERIFY",
|
||
}
|
||
ESCALATION_CLASSES = ("research_integrity", "ethics", "safety", "legal_compliance", "fatal_validity")
|
||
|
||
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||
_HASH12_RE = re.compile(r"^[0-9a-f]{12}$")
|
||
_REPORT_VERSION_RE = re.compile(r"^[0-9]+(\.[0-9]+)*$")
|
||
_ITEM_ID_RE = re.compile(r"^REV-[A-Za-z0-9-]+$")
|
||
_CONCERN_ID_RE = re.compile(r"^[RSN][1-9][0-9]*$")
|
||
_LETTER_REF_RE = re.compile(r"^R([1-9][0-9]*)$")
|
||
_ANCHOR_RE = re.compile(r"^(text|table|figure|equation|dataset|absence): .+$")
|
||
_ARTIFACT_REF_RE = re.compile(r"^(passport|path):.+$")
|
||
_ANSWER_REF_RE = re.compile(r"^(adjudication:DIS-[1-9][0-9]*|intent:INT-[1-9][0-9]*)$")
|
||
_CRITERION_REF_RE = re.compile(r"^(phase1:REV-[A-Za-z0-9-]+|dissent:DIS-[1-9][0-9]*)$")
|
||
_SOURCE_REF_RE = re.compile(r"^(reapplication:RAP-[1-9][0-9]*|acceptance:ACC-[1-9][0-9]*)$")
|
||
_CHECK_ADJUDICATED_RE = re.compile(r"^adjudicated:(RADJ-[1-9][0-9]*)$")
|
||
_AUTHOR_EVENT_RE = re.compile(r"^AUTHOR-EVENT-[A-Za-z0-9._-]+$")
|
||
_BLOCK_ID_RE = re.compile(r"^(?:B[0-9]{4,}|DOC-BODY-START)$")
|
||
_CLAIM_AUTH_RE = re.compile(r"^CLAIM-AUTH-[A-Za-z0-9._-]+$")
|
||
_CLAIM_SURFACE_RE = re.compile(r"^CLAIM-SURFACE-[A-Za-z0-9._-]+$")
|
||
_CLAIM_ID_RE = re.compile(r"^C-[0-9]{3,}$")
|
||
_MANIFEST_ID_RE = re.compile(
|
||
r"^M-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z-[0-9a-f]{4}$"
|
||
)
|
||
_ID_RES = {
|
||
"NS": re.compile(r"^NS-[1-9][0-9]*$"),
|
||
"NEW": re.compile(r"^NEW-[1-9][0-9]*$"),
|
||
"DIS": re.compile(r"^DIS-[1-9][0-9]*$"),
|
||
"ESC": re.compile(r"^ESC-[1-9][0-9]*$"),
|
||
"ADJ": re.compile(r"^ADJ-[1-9][0-9]*$"),
|
||
"INT": re.compile(r"^INT-[1-9][0-9]*$"),
|
||
"RES": re.compile(r"^RES-[1-9][0-9]*$"),
|
||
"RADJ": re.compile(r"^RADJ-[1-9][0-9]*$"),
|
||
"ACC": re.compile(r"^ACC-[1-9][0-9]*$"),
|
||
"RAP": re.compile(r"^RAP-[1-9][0-9]*$"),
|
||
"PRB": re.compile(r"^PRB-[1-9][0-9]*$"),
|
||
}
|
||
|
||
|
||
class ManifestError(Exception):
|
||
"""Manifest-layer failure -> exit 2 with a §3.5 G0 reason."""
|
||
|
||
def __init__(self, reason: str, message: str):
|
||
self.reason = reason
|
||
super().__init__(message)
|
||
|
||
|
||
class ArtifactSchemaError(Exception):
|
||
"""Phase-artifact schema failure -> exit 2 with the phase lint reason."""
|
||
|
||
def __init__(self, reason: str, message: str):
|
||
self.reason = reason
|
||
super().__init__(message)
|
||
|
||
|
||
def canonical_hash(obj) -> str:
|
||
"""sha256 hex over the object's JSON Canonical Form (RFC 8785 / JCS)."""
|
||
payload = json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||
|
||
|
||
def _strict_json_bytes(raw: bytes, label: str):
|
||
"""Parse interoperable JSON: UTF-8, unique keys, no NaN/Infinity."""
|
||
def unique_object(pairs):
|
||
value = {}
|
||
for key, child in pairs:
|
||
if key in value:
|
||
raise ValueError(f"duplicate object key {key!r}")
|
||
value[key] = child
|
||
return value
|
||
|
||
def reject_constant(token):
|
||
raise ValueError(f"non-finite JSON constant {token!r}")
|
||
|
||
try:
|
||
return json.loads(
|
||
raw.decode("utf-8"),
|
||
object_pairs_hook=unique_object,
|
||
parse_constant=reject_constant,
|
||
)
|
||
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
||
raise ValueError(f"{label}: invalid interoperable JSON ({exc})") from exc
|
||
|
||
|
||
# --- §10 normalization grammar -------------------------------------------------
|
||
|
||
_PAREN_RE = re.compile(r"\([^()]*\)")
|
||
_SEPARATOR_RE = re.compile(r",|/|;| and |&")
|
||
_R_TOKEN_RES = (
|
||
re.compile(r"^r([1-3])$"),
|
||
re.compile(r"^reviewer ([1-3])$"),
|
||
re.compile(r"^peer reviewer ([1-3])$"),
|
||
)
|
||
|
||
|
||
def normalize_reviewer_labels(raw: str):
|
||
"""§10 emitter-side normalization grammar, in declared order.
|
||
|
||
(1a) strip every parenthetical group FIRST; (1b) THEN strip any
|
||
" — <suffix>" tail from the residual (order normative and
|
||
non-commutative); (2) split on {",", "/", ";", " and ", "&"};
|
||
(3) trim whitespace; case-fold. A stripped token normalizes by
|
||
WHOLE-TOKEN EXACT MATCH (no substring extraction); unrecognized
|
||
tokens are DROPPED, never guessed. Returns ``(labels, parse_failure)``
|
||
where labels is the deduplicated, original-order result and
|
||
``parse_failure`` is True when a non-empty input produced zero
|
||
recognized tokens (§10: routes to EIC but counts as unmapped — a
|
||
distinct state from a genuinely empty list).
|
||
"""
|
||
text = unicodedata.normalize("NFC", raw)
|
||
prev = None
|
||
while prev != text: # nested groups collapse outside-in
|
||
prev = text
|
||
text = _PAREN_RE.sub("", text)
|
||
dash_idx = text.find(" — ")
|
||
if dash_idx != -1:
|
||
text = text[:dash_idx]
|
||
labels = []
|
||
for token in _SEPARATOR_RE.split(text):
|
||
token = token.strip().casefold()
|
||
if not token:
|
||
continue
|
||
label = None
|
||
if token in ("eic", "editor-in-chief"):
|
||
label = "EIC"
|
||
elif token in ("da", "devil's advocate", "devil’s advocate"):
|
||
label = "DA"
|
||
else:
|
||
for pattern in _R_TOKEN_RES:
|
||
match = pattern.match(token)
|
||
if match:
|
||
label = "R" + match.group(1)
|
||
break
|
||
if label is not None and label not in labels:
|
||
labels.append(label)
|
||
parse_failure = bool(raw.strip()) and not labels
|
||
return labels, parse_failure
|
||
|
||
|
||
# --- validation helpers --------------------------------------------------------
|
||
|
||
|
||
def _is_str(value, nonempty=True) -> bool:
|
||
return isinstance(value, str) and (not nonempty or len(value) > 0)
|
||
|
||
|
||
class _V:
|
||
"""Accumulating validator for one artifact; raises on first failure."""
|
||
|
||
def __init__(self, reason: str, artifact_name: str):
|
||
self.reason = reason
|
||
self.artifact = artifact_name
|
||
|
||
def fail(self, path: str, message: str):
|
||
raise ArtifactSchemaError(self.reason, f"{self.artifact}: {path}: {message}")
|
||
|
||
def obj(self, value, path, required, optional=()):
|
||
if not isinstance(value, dict):
|
||
self.fail(path, "must be an object")
|
||
allowed = set(required) | set(optional)
|
||
for key in value:
|
||
if key not in allowed:
|
||
self.fail(path, f"unexpected field {key!r}")
|
||
for key in required:
|
||
if key not in value:
|
||
self.fail(path, f"missing required field {key!r}")
|
||
|
||
def string(self, value, path, nonempty=True):
|
||
if not _is_str(value, nonempty):
|
||
self.fail(path, "must be a non-empty string" if nonempty else "must be a string")
|
||
|
||
def enum(self, value, path, allowed):
|
||
if value not in allowed:
|
||
self.fail(path, f"must be one of {sorted(allowed)!r}, got {value!r}")
|
||
|
||
def pattern(self, value, path, regex, label):
|
||
if not isinstance(value, str) or not regex.match(value):
|
||
self.fail(path, f"must match {label}, got {value!r}")
|
||
|
||
def integer(self, value, path, minimum=None, maximum=None):
|
||
if not isinstance(value, int) or isinstance(value, bool):
|
||
self.fail(path, "must be an integer")
|
||
if minimum is not None and value < minimum:
|
||
self.fail(path, f"must be >= {minimum}")
|
||
if maximum is not None and value > maximum:
|
||
self.fail(path, f"must be <= {maximum}")
|
||
|
||
def array(self, value, path):
|
||
if not isinstance(value, list):
|
||
self.fail(path, "must be an array")
|
||
|
||
|
||
def _validate_residual_gap(v: _V, gap, path):
|
||
v.obj(gap, path, ["text", "residual_obligation_class"])
|
||
v.string(gap["text"], f"{path}.text")
|
||
v.enum(gap["residual_obligation_class"], f"{path}.residual_obligation_class", RESIDUAL_OBLIGATION_CLASSES)
|
||
|
||
|
||
def _validate_author_targets(v: _V, targets, path):
|
||
v.array(targets, path)
|
||
seen = set()
|
||
operation_order = {"replace_block": 0, "insert_after": 1, "delete_block": 2}
|
||
prior_key = None
|
||
for i, target in enumerate(targets):
|
||
row_path = f"{path}[{i}]"
|
||
v.obj(target, row_path, ["block_id", "allowed_operations"])
|
||
v.pattern(target["block_id"], f"{row_path}.block_id", _BLOCK_ID_RE, "B<n> | DOC-BODY-START")
|
||
if target["block_id"] in seen:
|
||
v.fail(path, f"duplicate block_id {target['block_id']!r}")
|
||
seen.add(target["block_id"])
|
||
v.array(target["allowed_operations"], f"{row_path}.allowed_operations")
|
||
if not target["allowed_operations"]:
|
||
v.fail(f"{row_path}.allowed_operations", "must not be empty")
|
||
if len(target["allowed_operations"]) != len(set(target["allowed_operations"])):
|
||
v.fail(f"{row_path}.allowed_operations", "must be duplicate-free")
|
||
for op in target["allowed_operations"]:
|
||
v.enum(op, f"{row_path}.allowed_operations", tuple(operation_order))
|
||
if target["allowed_operations"] != sorted(
|
||
target["allowed_operations"], key=operation_order.__getitem__
|
||
):
|
||
v.fail(f"{row_path}.allowed_operations", "must use canonical operation order")
|
||
block_order = -1 if target["block_id"] == "DOC-BODY-START" else int(target["block_id"][1:])
|
||
key = (block_order, target["block_id"])
|
||
if prior_key is not None and key < prior_key:
|
||
v.fail(path, "must use canonical block order")
|
||
prior_key = key
|
||
|
||
|
||
def _validate_claim_authorizations(v: _V, authorizations, path):
|
||
v.array(authorizations, path)
|
||
seen = set()
|
||
required = [
|
||
"authorization_id",
|
||
"author_event_id",
|
||
"surface_id",
|
||
"scoped_manifest_id",
|
||
"claim_id",
|
||
"block_id",
|
||
"original_text_sha256",
|
||
"replacement_text",
|
||
"replacement_text_sha256",
|
||
"from_rung",
|
||
"to_rung",
|
||
"direction",
|
||
"reason",
|
||
]
|
||
for i, authorization in enumerate(authorizations):
|
||
row_path = f"{path}[{i}]"
|
||
v.obj(authorization, row_path, required)
|
||
v.pattern(authorization["authorization_id"], f"{row_path}.authorization_id", _CLAIM_AUTH_RE, "CLAIM-AUTH-<id>")
|
||
if authorization["authorization_id"] in seen:
|
||
v.fail(path, f"duplicate authorization_id {authorization['authorization_id']!r}")
|
||
seen.add(authorization["authorization_id"])
|
||
v.pattern(authorization["author_event_id"], f"{row_path}.author_event_id", _AUTHOR_EVENT_RE, "AUTHOR-EVENT-<id>")
|
||
v.pattern(authorization["surface_id"], f"{row_path}.surface_id", _CLAIM_SURFACE_RE, "CLAIM-SURFACE-<id>")
|
||
v.pattern(authorization["scoped_manifest_id"], f"{row_path}.scoped_manifest_id", _MANIFEST_ID_RE, "scoped manifest id")
|
||
v.pattern(authorization["claim_id"], f"{row_path}.claim_id", _CLAIM_ID_RE, "C-<n>")
|
||
if authorization["block_id"] == "DOC-BODY-START":
|
||
v.fail(f"{row_path}.block_id", "claim surfaces require an anchored B<n> block")
|
||
v.pattern(authorization["block_id"], f"{row_path}.block_id", _BLOCK_ID_RE, "B<n>")
|
||
for field in ("original_text_sha256", "replacement_text_sha256"):
|
||
v.pattern(authorization[field], f"{row_path}.{field}", _SHA256_RE, "sha256 hex")
|
||
v.string(authorization["replacement_text"], f"{row_path}.replacement_text", nonempty=False)
|
||
if hashlib.sha256(authorization["replacement_text"].encode("utf-8")).hexdigest() != authorization["replacement_text_sha256"]:
|
||
v.fail(row_path, "replacement_text_sha256 does not match replacement_text")
|
||
for field in ("from_rung", "to_rung", "reason"):
|
||
v.string(authorization[field], f"{row_path}.{field}")
|
||
if authorization["from_rung"] == authorization["to_rung"]:
|
||
v.fail(row_path, "from_rung and to_rung must differ")
|
||
v.enum(authorization["direction"], f"{row_path}.direction", ("strengthen", "weaken"))
|
||
|
||
|
||
def _validate_tagged_anchors(v: _V, anchors, path):
|
||
v.array(anchors, path)
|
||
if not anchors:
|
||
v.fail(path, "must carry at least one anchor")
|
||
for i, entry in enumerate(anchors):
|
||
v.obj(entry, f"{path}[{i}]", ["anchor", "anchor_artifact"])
|
||
v.pattern(entry["anchor"], f"{path}[{i}].anchor", _ANCHOR_RE, "the Schema 6 typed-anchor grammar")
|
||
v.enum(entry["anchor_artifact"], f"{path}[{i}].anchor_artifact", ("manuscript", "letter"))
|
||
|
||
|
||
def _validate_new_issue(v: _V, rec, path):
|
||
v.obj(rec, path, [
|
||
"new_issue_id", "description", "location_anchor", "severity", "found_by",
|
||
"confidence", "competence_basis", "attribution", "attribution_evidence",
|
||
"nearest_roadmap_item", "non_match_rationale",
|
||
])
|
||
v.pattern(rec["new_issue_id"], f"{path}.new_issue_id", _ID_RES["NEW"], "NEW-<n>")
|
||
v.string(rec["description"], f"{path}.description")
|
||
v.pattern(rec["location_anchor"], f"{path}.location_anchor", _ANCHOR_RE, "typed anchor")
|
||
v.enum(rec["severity"], f"{path}.severity", SEVERITIES)
|
||
v.enum(rec["found_by"], f"{path}.found_by", VERIFIER_SEATS)
|
||
v.integer(rec["confidence"], f"{path}.confidence", 1, 5)
|
||
v.string(rec["competence_basis"], f"{path}.competence_basis")
|
||
v.enum(rec["attribution"], f"{path}.attribution", ATTRIBUTIONS)
|
||
v.string(rec["attribution_evidence"], f"{path}.attribution_evidence")
|
||
nearest = rec["nearest_roadmap_item"]
|
||
if nearest is not None:
|
||
v.pattern(nearest, f"{path}.nearest_roadmap_item", _ITEM_ID_RE, "REV-<id> or null")
|
||
v.string(rec["non_match_rationale"], f"{path}.non_match_rationale")
|
||
|
||
|
||
def validate_precommitment(art: dict):
|
||
v = _V("phase1_lint_failed", "precommitment")
|
||
v.obj(art, "$", ["contract_version", "round_id", "input_manifest_hash", "items", "new_standards"])
|
||
v.enum(art["contract_version"], "$.contract_version", ("1.1",))
|
||
v.string(art["round_id"], "$.round_id")
|
||
v.pattern(art["input_manifest_hash"], "$.input_manifest_hash", _SHA256_RE, "sha256 hex")
|
||
v.array(art["items"], "$.items")
|
||
for i, rec in enumerate(art["items"]):
|
||
path = f"$.items[{i}]"
|
||
v.obj(rec, path, [
|
||
"item_id", "obligation_class", "inherited_criterion", "operationalization",
|
||
"expected_change_surface", "equivalence_policy", "source_reviewer",
|
||
"source_reviewer_labels",
|
||
])
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.enum(rec["obligation_class"], f"{path}.obligation_class", ("must_fix", "should_fix"))
|
||
crit = rec["inherited_criterion"]
|
||
v.obj(crit, f"{path}.inherited_criterion", ["roadmap_text"], ["letter_text", "letter_item_ref"])
|
||
v.string(crit["roadmap_text"], f"{path}.inherited_criterion.roadmap_text")
|
||
has_text = "letter_text" in crit
|
||
has_ref = "letter_item_ref" in crit
|
||
if has_text != has_ref:
|
||
v.fail(f"{path}.inherited_criterion", "letter_text and letter_item_ref are biconditional (§5.1)")
|
||
if has_text:
|
||
v.string(crit["letter_text"], f"{path}.inherited_criterion.letter_text")
|
||
v.pattern(crit["letter_item_ref"], f"{path}.inherited_criterion.letter_item_ref", _LETTER_REF_RE, "R<n>")
|
||
if rec["obligation_class"] != "must_fix":
|
||
v.fail(f"{path}.inherited_criterion", "letter fields exist ONLY on must_fix items (§5.1)")
|
||
op = rec["operationalization"]
|
||
v.obj(op, f"{path}.operationalization", ["fully_addressed"], ["partially_addressed", "made_worse_discriminator"])
|
||
v.string(op["fully_addressed"], f"{path}.operationalization.fully_addressed")
|
||
if rec["obligation_class"] == "must_fix":
|
||
for field in ("partially_addressed", "made_worse_discriminator"):
|
||
if field not in op:
|
||
v.fail(f"{path}.operationalization", f"must_fix items require {field!r} (§5.1)")
|
||
v.string(op[field], f"{path}.operationalization.{field}")
|
||
else:
|
||
for field in ("partially_addressed", "made_worse_discriminator"):
|
||
if field in op:
|
||
v.fail(f"{path}.operationalization", f"should_fix lighter form forbids {field!r} (§5.1)")
|
||
v.string(rec["expected_change_surface"], f"{path}.expected_change_surface")
|
||
v.enum(rec["equivalence_policy"], f"{path}.equivalence_policy", ("allowed",))
|
||
v.string(rec["source_reviewer"], f"{path}.source_reviewer")
|
||
v.array(rec["source_reviewer_labels"], f"{path}.source_reviewer_labels")
|
||
seen = set()
|
||
for j, label in enumerate(rec["source_reviewer_labels"]):
|
||
v.enum(label, f"{path}.source_reviewer_labels[{j}]", SEAT_LABELS)
|
||
if label in seen:
|
||
v.fail(f"{path}.source_reviewer_labels", f"duplicate label {label!r}")
|
||
seen.add(label)
|
||
v.array(art["new_standards"], "$.new_standards")
|
||
ns_ids = set()
|
||
for i, rec in enumerate(art["new_standards"]):
|
||
path = f"$.new_standards[{i}]"
|
||
v.obj(rec, path, ["new_standard_id", "item_id", "standard_text", "why_not_in_round1", "classification"])
|
||
v.pattern(rec["new_standard_id"], f"{path}.new_standard_id", _ID_RES["NS"], "NS-<n>")
|
||
if rec["new_standard_id"] in ns_ids:
|
||
v.fail(path, f"duplicate new_standard_id {rec['new_standard_id']!r}")
|
||
ns_ids.add(rec["new_standard_id"])
|
||
if rec["item_id"] != "global":
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, 'REV-<id> or "global"')
|
||
v.string(rec["standard_text"], f"{path}.standard_text")
|
||
v.string(rec["why_not_in_round1"], f"{path}.why_not_in_round1")
|
||
v.enum(rec["classification"], f"{path}.classification", ("advisory", "escalation_requested"))
|
||
|
||
|
||
def validate_verdict_record(art: dict):
|
||
v = _V("phase2a_lint_failed", "verdict_record")
|
||
v.obj(art, "$", ["contract_version", "round_id", "precommitment_hash", "items", "new_issues", "dissents", "escalation_exceptions"])
|
||
v.enum(art["contract_version"], "$.contract_version", ("1.1",))
|
||
v.string(art["round_id"], "$.round_id")
|
||
v.pattern(art["precommitment_hash"], "$.precommitment_hash", _SHA256_RE, "sha256 hex")
|
||
v.array(art["items"], "$.items")
|
||
for i, rec in enumerate(art["items"]):
|
||
path = f"$.items[{i}]"
|
||
v.obj(rec, path, ["item_id", "verdict", "change_summary", "verified_by", "applied_criterion"],
|
||
["evidence_anchor", "cannot_verify_reason", "residual_gap"])
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.enum(rec["verdict"], f"{path}.verdict", VERDICTS)
|
||
if rec["verdict"] == "CANNOT_VERIFY":
|
||
if "cannot_verify_reason" not in rec or "evidence_anchor" in rec:
|
||
v.fail(path, "CANNOT_VERIFY carries cannot_verify_reason and no evidence_anchor (§3.3)")
|
||
v.string(rec["cannot_verify_reason"], f"{path}.cannot_verify_reason")
|
||
else:
|
||
if "evidence_anchor" not in rec or "cannot_verify_reason" in rec:
|
||
v.fail(path, "non-CANNOT_VERIFY verdicts carry evidence_anchor and no cannot_verify_reason (§3.3)")
|
||
v.array(rec["evidence_anchor"], f"{path}.evidence_anchor")
|
||
if not rec["evidence_anchor"]:
|
||
v.fail(f"{path}.evidence_anchor", "must carry at least one anchor")
|
||
for j, anchor in enumerate(rec["evidence_anchor"]):
|
||
v.pattern(anchor, f"{path}.evidence_anchor[{j}]", _ANCHOR_RE, "typed anchor")
|
||
if rec["verdict"] == "PARTIALLY_ADDRESSED":
|
||
if "residual_gap" not in rec:
|
||
v.fail(path, "PARTIALLY_ADDRESSED requires residual_gap (§3.3)")
|
||
_validate_residual_gap(v, rec["residual_gap"], f"{path}.residual_gap")
|
||
elif "residual_gap" in rec:
|
||
v.fail(path, "residual_gap is required iff verdict = PARTIALLY_ADDRESSED")
|
||
v.string(rec["change_summary"], f"{path}.change_summary")
|
||
v.enum(rec["verified_by"], f"{path}.verified_by", VERIFIER_SEATS)
|
||
applied = rec["applied_criterion"]
|
||
if applied not in ("precommitted", "not_precommitted") and not (
|
||
isinstance(applied, str) and re.match(r"^dissented:DIS-[1-9][0-9]*$", applied)
|
||
):
|
||
v.fail(f"{path}.applied_criterion", f"invalid value {applied!r}")
|
||
v.array(art["new_issues"], "$.new_issues")
|
||
for i, rec in enumerate(art["new_issues"]):
|
||
_validate_new_issue(v, rec, f"$.new_issues[{i}]")
|
||
v.array(art["dissents"], "$.dissents")
|
||
for i, rec in enumerate(art["dissents"]):
|
||
path = f"$.dissents[{i}]"
|
||
v.obj(rec, path, [
|
||
"dissent_id", "item_id", "criterion_hash", "reason_code",
|
||
"original_operationalization", "replacement_operationalization",
|
||
"evidence", "decision_impact_note",
|
||
])
|
||
v.pattern(rec["dissent_id"], f"{path}.dissent_id", _ID_RES["DIS"], "DIS-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.pattern(rec["criterion_hash"], f"{path}.criterion_hash", _SHA256_RE, "sha256 hex")
|
||
v.enum(rec["reason_code"], f"{path}.reason_code",
|
||
("criterion_ambiguous", "criterion_infeasible_as_written", "evidence_surface_moved", "criterion_error"))
|
||
for field in ("original_operationalization", "replacement_operationalization", "evidence", "decision_impact_note"):
|
||
v.string(rec[field], f"{path}.{field}")
|
||
v.array(art["escalation_exceptions"], "$.escalation_exceptions")
|
||
for i, rec in enumerate(art["escalation_exceptions"]):
|
||
path = f"$.escalation_exceptions[{i}]"
|
||
v.obj(rec, path, [
|
||
"exception_id", "escalation_class", "reason_code", "evidence_anchor",
|
||
"why_round1_missed_it", "mechanical_decision_impact", "approval_state",
|
||
], ["new_standard_ref"])
|
||
v.pattern(rec["exception_id"], f"{path}.exception_id", _ID_RES["ESC"], "ESC-<n>")
|
||
if "new_standard_ref" in rec:
|
||
v.pattern(rec["new_standard_ref"], f"{path}.new_standard_ref", _ID_RES["NS"], "NS-<n>")
|
||
v.enum(rec["escalation_class"], f"{path}.escalation_class", ESCALATION_CLASSES)
|
||
v.string(rec["reason_code"], f"{path}.reason_code")
|
||
v.pattern(rec["evidence_anchor"], f"{path}.evidence_anchor", _ANCHOR_RE, "typed anchor")
|
||
v.string(rec["why_round1_missed_it"], f"{path}.why_round1_missed_it")
|
||
v.enum(rec["mechanical_decision_impact"], f"{path}.mechanical_decision_impact", ("Minor Revision", "Major Revision"))
|
||
v.enum(rec["approval_state"], f"{path}.approval_state", ("pending",))
|
||
|
||
|
||
def _validate_adjustment_body(v: _V, rec, path, *, drafted: bool):
|
||
required = ["item_id", "from_verdict", "to_verdict", "basis", "rationale"]
|
||
optional = ["evidence_anchor", "cannot_verify_reason", "residual_gap"]
|
||
if drafted:
|
||
pass # drafted bodies carry no booking-assigned fields
|
||
else:
|
||
required.insert(0, "adjustment_id")
|
||
optional += ["critical_rebuttal_check", "source_ref", "supersedes_adjustment_id"]
|
||
v.obj(rec, path, required, optional)
|
||
if not drafted:
|
||
v.pattern(rec["adjustment_id"], f"{path}.adjustment_id", _ID_RES["ADJ"], "ADJ-<n>")
|
||
if "supersedes_adjustment_id" in rec:
|
||
v.pattern(rec["supersedes_adjustment_id"], f"{path}.supersedes_adjustment_id", _ID_RES["ADJ"], "ADJ-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.enum(rec["from_verdict"], f"{path}.from_verdict", VERDICTS)
|
||
v.enum(rec["to_verdict"], f"{path}.to_verdict", VERDICTS)
|
||
if drafted:
|
||
v.enum(rec["basis"], f"{path}.basis", ("valid_rebuttal",))
|
||
else:
|
||
v.enum(rec["basis"], f"{path}.basis", ADJUSTMENT_BASES)
|
||
v.string(rec["rationale"], f"{path}.rationale")
|
||
if rec["to_verdict"] == "CANNOT_VERIFY":
|
||
if "cannot_verify_reason" not in rec or "evidence_anchor" in rec:
|
||
v.fail(path, "to_verdict CANNOT_VERIFY carries cannot_verify_reason and no evidence_anchor")
|
||
v.string(rec["cannot_verify_reason"], f"{path}.cannot_verify_reason")
|
||
if rec["basis"] != "user_accepted_fail_closed":
|
||
v.fail(path, "user_accepted_fail_closed is the ONLY basis that may land on CANNOT_VERIFY (§3.4)")
|
||
else:
|
||
if "evidence_anchor" not in rec or "cannot_verify_reason" in rec:
|
||
v.fail(path, "non-CANNOT_VERIFY adjustments carry evidence_anchor and no cannot_verify_reason")
|
||
_validate_tagged_anchors(v, rec["evidence_anchor"], f"{path}.evidence_anchor")
|
||
if rec["basis"] in ("author_pointer_located_evidence", "scope_correction"):
|
||
for j, anchor in enumerate(rec["evidence_anchor"]):
|
||
if anchor["anchor_artifact"] != "manuscript":
|
||
v.fail(f"{path}.evidence_anchor[{j}]",
|
||
f"basis {rec['basis']} requires manuscript-side anchors (§5.3)")
|
||
if rec["to_verdict"] == "PARTIALLY_ADDRESSED":
|
||
if "residual_gap" not in rec:
|
||
v.fail(path, "to_verdict PARTIALLY_ADDRESSED requires residual_gap (§5.3)")
|
||
_validate_residual_gap(v, rec["residual_gap"], f"{path}.residual_gap")
|
||
elif "residual_gap" in rec:
|
||
v.fail(path, "residual_gap is required iff to_verdict = PARTIALLY_ADDRESSED")
|
||
# §3.4 Direction column. valid_rebuttal — the only basis allowing
|
||
# letter-side anchors — is "upgrade to FULLY_ADDRESSED"; anything else
|
||
# would let a persuasive letter move a verdict sideways/downward, the
|
||
# exact channel §1 exists to close. author_pointer_located_evidence is
|
||
# an UPGRADE: located evidence satisfies the Phase-1 operationalization
|
||
# (fully or partially), so the target is PARTIALLY/FULLY and strictly
|
||
# better than the source (MADE_WORSE/CANNOT_VERIFY rank lowest).
|
||
if rec["basis"] == "valid_rebuttal":
|
||
if rec["to_verdict"] != "FULLY_ADDRESSED" or rec["from_verdict"] == "FULLY_ADDRESSED":
|
||
v.fail(path, "valid_rebuttal adjustments upgrade to FULLY_ADDRESSED (§3.4 Direction)")
|
||
elif rec["basis"] == "author_pointer_located_evidence":
|
||
rank = {"MADE_WORSE": 0, "CANNOT_VERIFY": 0, "NOT_ADDRESSED": 1, "PARTIALLY_ADDRESSED": 2, "FULLY_ADDRESSED": 3}
|
||
if rec["to_verdict"] not in ("PARTIALLY_ADDRESSED", "FULLY_ADDRESSED") or rank[rec["to_verdict"]] <= rank[rec["from_verdict"]]:
|
||
v.fail(path, "author_pointer_located_evidence adjustments are upgrades to PARTIALLY/FULLY_ADDRESSED (§3.4 Direction)")
|
||
if drafted:
|
||
return
|
||
source_ref = rec.get("source_ref")
|
||
if rec["basis"] == "cross_model_adjudication":
|
||
if not (isinstance(source_ref, str) and source_ref.startswith("reapplication:")):
|
||
v.fail(path, 'basis cross_model_adjudication requires source_ref "reapplication:<id>"')
|
||
elif rec["basis"] == "user_accepted_fail_closed":
|
||
if not (isinstance(source_ref, str) and source_ref.startswith("acceptance:")):
|
||
v.fail(path, 'basis user_accepted_fail_closed requires source_ref "acceptance:<id>"')
|
||
elif source_ref is not None:
|
||
v.fail(path, f"source_ref is FORBIDDEN on basis {rec['basis']} (§5.3)")
|
||
if source_ref is not None:
|
||
v.pattern(source_ref, f"{path}.source_ref", _SOURCE_REF_RE, "reapplication:RAP-<n> | acceptance:ACC-<n>")
|
||
check = rec.get("critical_rebuttal_check")
|
||
if check is not None:
|
||
if rec["basis"] != "valid_rebuttal":
|
||
v.fail(path, "critical_rebuttal_check exists only on valid_rebuttal adjustments (§3.4)")
|
||
if check not in ("single_family_disclosed", "pass_unavailable_disclosed") and not _CHECK_ADJUDICATED_RE.match(check):
|
||
v.fail(f"{path}.critical_rebuttal_check", f"invalid value {check!r}")
|
||
|
||
|
||
def validate_traceability(art: dict):
|
||
v = _V("phase2b_lint_failed", "traceability")
|
||
v.obj(art, "$", [
|
||
"contract_version", "round_id", "revision", "verdict_record_hash", "rows", "adjustments",
|
||
"new_issues", "post_letter_observations", "dissent_adjudications",
|
||
"resolution_intents", "cross_model_resolutions", "rebuttal_adjudications",
|
||
"g2d_acceptances", "pending_rebuttal_upgrades", "escalation_approvals",
|
||
"reapplications", "decision_inputs", "decision_state",
|
||
], ["supersedes_hash", "abort_reason"])
|
||
v.enum(art["contract_version"], "$.contract_version", ("1.1",))
|
||
v.string(art["round_id"], "$.round_id")
|
||
v.integer(art["revision"], "$.revision", minimum=1)
|
||
if art["revision"] == 1:
|
||
if "supersedes_hash" in art:
|
||
v.fail("$", "revision 1 must not carry supersedes_hash")
|
||
else:
|
||
if "supersedes_hash" not in art:
|
||
v.fail("$", "revision >= 2 requires supersedes_hash")
|
||
v.pattern(art["supersedes_hash"], "$.supersedes_hash", _SHA256_RE, "sha256 hex")
|
||
v.pattern(art["verdict_record_hash"], "$.verdict_record_hash", _SHA256_RE, "sha256 hex")
|
||
v.enum(art["decision_state"], "$.decision_state", DECISION_STATES)
|
||
if art["decision_state"] == "aborted":
|
||
if "abort_reason" not in art:
|
||
v.fail("$", "decision_state aborted requires abort_reason")
|
||
v.enum(art["abort_reason"], "$.abort_reason", ABORT_REASONS)
|
||
elif "abort_reason" in art:
|
||
v.fail("$", "abort_reason is required iff decision_state = aborted")
|
||
|
||
v.array(art["rows"], "$.rows")
|
||
for i, row in enumerate(art["rows"]):
|
||
path = f"$.rows[{i}]"
|
||
v.obj(row, path, [
|
||
"item_id", "concern_id", "obligation_class", "original_comment", "authors_claim",
|
||
"revision_location", "verified", "status", "quality_assessment",
|
||
"final_verdict", "phase2a_verdict", "verified_by", "author_triage",
|
||
"authorized_targets", "claim_strength_authorizations",
|
||
], ["adjustment_id", "addressed_by_rebuttal", "cross_model_status", "cross_model_verdict", "author_reason"])
|
||
v.pattern(row["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.pattern(row["concern_id"], f"{path}.concern_id", _CONCERN_ID_RE, "R<n>/S<n>/N<n>")
|
||
v.enum(row["obligation_class"], f"{path}.obligation_class", tuple(ROW_OBLIGATION_MAP))
|
||
for field in ("original_comment", "authors_claim", "revision_location", "quality_assessment"):
|
||
v.string(row[field], f"{path}.{field}")
|
||
v.enum(row["verified"], f"{path}.verified", ("YES", "PARTIAL", "NO", "CANNOT_VERIFY"))
|
||
v.enum(row["status"], f"{path}.status", VERDICTS)
|
||
v.enum(row["final_verdict"], f"{path}.final_verdict", VERDICTS)
|
||
v.enum(row["phase2a_verdict"], f"{path}.phase2a_verdict", VERDICTS)
|
||
v.enum(row["verified_by"], f"{path}.verified_by", VERIFIER_SEATS)
|
||
v.enum(
|
||
row["author_triage"],
|
||
f"{path}.author_triage",
|
||
("will_address", "wont_address", "not_on_point"),
|
||
)
|
||
_validate_author_targets(v, row["authorized_targets"], f"{path}.authorized_targets")
|
||
_validate_claim_authorizations(
|
||
v,
|
||
row["claim_strength_authorizations"],
|
||
f"{path}.claim_strength_authorizations",
|
||
)
|
||
if row["author_triage"] in ("wont_address", "not_on_point"):
|
||
if "author_reason" not in row:
|
||
v.fail(path, "declined author triage requires author_reason")
|
||
v.string(row["author_reason"], f"{path}.author_reason")
|
||
if row["authorized_targets"] or row["claim_strength_authorizations"]:
|
||
v.fail(path, "declined author triage carries no work/claim authority")
|
||
elif "author_reason" in row:
|
||
v.fail(path, "will_address rows do not carry an inferred author_reason")
|
||
if "adjustment_id" in row:
|
||
v.pattern(row["adjustment_id"], f"{path}.adjustment_id", _ID_RES["ADJ"], "ADJ-<n>")
|
||
if "addressed_by_rebuttal" in row and row["addressed_by_rebuttal"] is not True:
|
||
v.fail(f"{path}.addressed_by_rebuttal", "when present the marker is the literal true")
|
||
has_status = "cross_model_status" in row
|
||
has_cmv = "cross_model_verdict" in row
|
||
if row["obligation_class"] == "MUST_FIX":
|
||
if not has_status:
|
||
v.fail(path, "MUST_FIX rows always carry cross_model_status (§5.3 / #539)")
|
||
elif has_status or has_cmv:
|
||
v.fail(path, "SHOULD_FIX/CONSIDER rows carry neither cross-model field (#539 scope)")
|
||
if has_status:
|
||
v.enum(row["cross_model_status"], f"{path}.cross_model_status",
|
||
("agree", "diverges", "unavailable", "not_configured"))
|
||
if row["cross_model_status"] in ("agree", "diverges"):
|
||
if not has_cmv:
|
||
v.fail(path, "evaluated (agree/diverges) rows carry cross_model_verdict")
|
||
v.enum(row["cross_model_verdict"], f"{path}.cross_model_verdict", JUDGE_VERDICTS)
|
||
elif has_cmv:
|
||
v.fail(path, "unavailable/not_configured rows carry no cross_model_verdict")
|
||
|
||
v.array(art["adjustments"], "$.adjustments")
|
||
for i, rec in enumerate(art["adjustments"]):
|
||
_validate_adjustment_body(v, rec, f"$.adjustments[{i}]", drafted=False)
|
||
|
||
v.array(art["new_issues"], "$.new_issues")
|
||
for i, rec in enumerate(art["new_issues"]):
|
||
_validate_new_issue(v, rec, f"$.new_issues[{i}]")
|
||
|
||
v.array(art["post_letter_observations"], "$.post_letter_observations")
|
||
for i, entry in enumerate(art["post_letter_observations"]):
|
||
v.string(entry, f"$.post_letter_observations[{i}]")
|
||
|
||
v.array(art["dissent_adjudications"], "$.dissent_adjudications")
|
||
for i, rec in enumerate(art["dissent_adjudications"]):
|
||
path = f"$.dissent_adjudications[{i}]"
|
||
v.obj(rec, path, ["dissent_id", "adjudicator", "outcome", "rationale"])
|
||
v.pattern(rec["dissent_id"], f"{path}.dissent_id", _ID_RES["DIS"], "DIS-<n>")
|
||
v.enum(rec["adjudicator"], f"{path}.adjudicator", ("cross_model", "user"))
|
||
v.enum(rec["outcome"], f"{path}.outcome", ("replacement_approved", "original_upheld"))
|
||
v.string(rec["rationale"], f"{path}.rationale")
|
||
|
||
v.array(art["resolution_intents"], "$.resolution_intents")
|
||
for i, rec in enumerate(art["resolution_intents"]):
|
||
path = f"$.resolution_intents[{i}]"
|
||
v.obj(rec, path, ["intent_id", "item_id", "answered_by"], ["guidance_note"])
|
||
v.pattern(rec["intent_id"], f"{path}.intent_id", _ID_RES["INT"], "INT-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.enum(rec["answered_by"], f"{path}.answered_by", ("system", "user"))
|
||
if "guidance_note" in rec:
|
||
if rec["answered_by"] == "system":
|
||
v.fail(path, "guidance_note exists only on user intents (§5.3)")
|
||
v.string(rec["guidance_note"], f"{path}.guidance_note")
|
||
|
||
v.array(art["cross_model_resolutions"], "$.cross_model_resolutions")
|
||
for i, rec in enumerate(art["cross_model_resolutions"]):
|
||
path = f"$.cross_model_resolutions[{i}]"
|
||
v.obj(rec, path, ["resolution_id", "item_id", "intent_id", "reapplication_id", "state", "resolved_by", "rationale"])
|
||
v.pattern(rec["resolution_id"], f"{path}.resolution_id", _ID_RES["RES"], "RES-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.pattern(rec["intent_id"], f"{path}.intent_id", _ID_RES["INT"], "INT-<n>")
|
||
v.pattern(rec["reapplication_id"], f"{path}.reapplication_id", _ID_RES["RAP"], "RAP-<n>")
|
||
v.enum(rec["state"], f"{path}.state", ("primary_upheld", "primary_revised"))
|
||
v.enum(rec["resolved_by"], f"{path}.resolved_by", ("system", "user"))
|
||
if rec["resolved_by"] == "user" and rec["state"] != "primary_upheld":
|
||
v.fail(path, "the user acceptance form is always primary_upheld — the user never hand-sets a verdict (§5.3)")
|
||
v.string(rec["rationale"], f"{path}.rationale")
|
||
|
||
v.array(art["rebuttal_adjudications"], "$.rebuttal_adjudications")
|
||
for i, rec in enumerate(art["rebuttal_adjudications"]):
|
||
path = f"$.rebuttal_adjudications[{i}]"
|
||
v.obj(rec, path, ["rebuttal_adjudication_id", "item_id", "verdict", "rationale"])
|
||
v.pattern(rec["rebuttal_adjudication_id"], f"{path}.rebuttal_adjudication_id", _ID_RES["RADJ"], "RADJ-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.enum(rec["verdict"], f"{path}.verdict", ("upheld", "challenged"))
|
||
v.string(rec["rationale"], f"{path}.rationale")
|
||
|
||
v.array(art["g2d_acceptances"], "$.g2d_acceptances")
|
||
for i, rec in enumerate(art["g2d_acceptances"]):
|
||
path = f"$.g2d_acceptances[{i}]"
|
||
v.obj(rec, path, ["acceptance_id", "item_id", "reapplication_id", "accepted_by"])
|
||
v.pattern(rec["acceptance_id"], f"{path}.acceptance_id", _ID_RES["ACC"], "ACC-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.pattern(rec["reapplication_id"], f"{path}.reapplication_id", _ID_RES["RAP"], "RAP-<n>")
|
||
v.enum(rec["accepted_by"], f"{path}.accepted_by", ("user",))
|
||
|
||
v.array(art["pending_rebuttal_upgrades"], "$.pending_rebuttal_upgrades")
|
||
for i, rec in enumerate(art["pending_rebuttal_upgrades"]):
|
||
path = f"$.pending_rebuttal_upgrades[{i}]"
|
||
v.obj(rec, path, ["proposal_id", "item_id", "drafted_adjustment", "disposition"])
|
||
v.pattern(rec["proposal_id"], f"{path}.proposal_id", _ID_RES["PRB"], "PRB-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
_validate_adjustment_body(v, rec["drafted_adjustment"], f"{path}.drafted_adjustment", drafted=True)
|
||
disposition = rec["disposition"]
|
||
if not isinstance(disposition, str) or not re.match(
|
||
r"^(booked:ADJ-[1-9][0-9]*|challenged:RADJ-[1-9][0-9]*|booked_disclosed:ADJ-[1-9][0-9]*|booked_single_family:ADJ-[1-9][0-9]*)$",
|
||
disposition,
|
||
):
|
||
v.fail(f"{path}.disposition", f"invalid disposition {disposition!r}")
|
||
|
||
v.array(art["escalation_approvals"], "$.escalation_approvals")
|
||
for i, rec in enumerate(art["escalation_approvals"]):
|
||
path = f"$.escalation_approvals[{i}]"
|
||
v.obj(rec, path, ["exception_id", "approval_state", "approved_by"])
|
||
v.pattern(rec["exception_id"], f"{path}.exception_id", _ID_RES["ESC"], "ESC-<n>")
|
||
v.enum(rec["approval_state"], f"{path}.approval_state", ("approved", "rejected"))
|
||
v.enum(rec["approved_by"], f"{path}.approved_by", ("user",))
|
||
|
||
v.array(art["reapplications"], "$.reapplications")
|
||
for i, rec in enumerate(art["reapplications"]):
|
||
path = f"$.reapplications[{i}]"
|
||
v.obj(rec, path, [
|
||
"reapplication_id", "item_id", "answer_refs", "pre_reapplication_verdict",
|
||
"reapplied_verdict", "rationale", "criterion_ref",
|
||
], ["supersedes_reapplication_id", "evidence_anchor", "cannot_verify_reason", "residual_gap"])
|
||
v.pattern(rec["reapplication_id"], f"{path}.reapplication_id", _ID_RES["RAP"], "RAP-<n>")
|
||
v.pattern(rec["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.array(rec["answer_refs"], f"{path}.answer_refs")
|
||
if not rec["answer_refs"]:
|
||
v.fail(f"{path}.answer_refs", "must carry at least one tagged ref")
|
||
seen_refs = set()
|
||
for j, ref in enumerate(rec["answer_refs"]):
|
||
v.pattern(ref, f"{path}.answer_refs[{j}]", _ANSWER_REF_RE, "adjudication:DIS-<n> | intent:INT-<n>")
|
||
if ref in seen_refs:
|
||
v.fail(f"{path}.answer_refs", f"duplicate ref {ref!r}")
|
||
seen_refs.add(ref)
|
||
if "supersedes_reapplication_id" in rec:
|
||
v.pattern(rec["supersedes_reapplication_id"], f"{path}.supersedes_reapplication_id", _ID_RES["RAP"], "RAP-<n>")
|
||
v.enum(rec["pre_reapplication_verdict"], f"{path}.pre_reapplication_verdict", VERDICTS)
|
||
v.enum(rec["reapplied_verdict"], f"{path}.reapplied_verdict", VERDICTS)
|
||
if rec["reapplied_verdict"] == "CANNOT_VERIFY":
|
||
if "cannot_verify_reason" not in rec or "evidence_anchor" in rec:
|
||
v.fail(path, "CANNOT_VERIFY carries cannot_verify_reason and no evidence_anchor")
|
||
v.string(rec["cannot_verify_reason"], f"{path}.cannot_verify_reason")
|
||
else:
|
||
if "evidence_anchor" not in rec or "cannot_verify_reason" in rec:
|
||
v.fail(path, "non-CANNOT_VERIFY reapplications carry evidence_anchor and no cannot_verify_reason")
|
||
_validate_tagged_anchors(v, rec["evidence_anchor"], f"{path}.evidence_anchor")
|
||
if rec["reapplied_verdict"] == "PARTIALLY_ADDRESSED":
|
||
if "residual_gap" not in rec:
|
||
v.fail(path, "reapplied PARTIALLY_ADDRESSED requires residual_gap")
|
||
_validate_residual_gap(v, rec["residual_gap"], f"{path}.residual_gap")
|
||
elif "residual_gap" in rec:
|
||
v.fail(path, "residual_gap is required iff reapplied_verdict = PARTIALLY_ADDRESSED")
|
||
v.string(rec["rationale"], f"{path}.rationale")
|
||
v.pattern(rec["criterion_ref"], f"{path}.criterion_ref", _CRITERION_REF_RE, "phase1:REV-<id> | dissent:DIS-<n>")
|
||
|
||
di = art["decision_inputs"]
|
||
v.obj(di, "$.decision_inputs", [
|
||
"per_item", "verdict_counts", "residual_obligation_class_counts", "should_fix_addressed_rate",
|
||
"regressions", "non_regression_new_issue_ids", "escalations", "apply_chain_witness",
|
||
], ["reject_recommended"])
|
||
v.array(di["per_item"], "$.decision_inputs.per_item")
|
||
for i, entry in enumerate(di["per_item"]):
|
||
path = f"$.decision_inputs.per_item[{i}]"
|
||
v.obj(entry, path, ["item_id", "final_verdict", "driving_severity"], ["residual_obligation_class"])
|
||
v.pattern(entry["item_id"], f"{path}.item_id", _ITEM_ID_RE, "REV-<id>")
|
||
v.enum(entry["final_verdict"], f"{path}.final_verdict", VERDICTS)
|
||
if entry["driving_severity"] is not None:
|
||
v.enum(entry["driving_severity"], f"{path}.driving_severity", SEVERITIES)
|
||
if "residual_obligation_class" in entry:
|
||
v.enum(entry["residual_obligation_class"], f"{path}.residual_obligation_class", RESIDUAL_OBLIGATION_CLASSES)
|
||
for bucket_name in ("verdict_counts", "residual_obligation_class_counts"):
|
||
bucket = di[bucket_name]
|
||
v.obj(bucket, f"$.decision_inputs.{bucket_name}", list(OBLIGATION_CLASSES))
|
||
keys = VERDICTS if bucket_name == "verdict_counts" else RESIDUAL_OBLIGATION_CLASSES
|
||
for prio in OBLIGATION_CLASSES:
|
||
v.obj(bucket[prio], f"$.decision_inputs.{bucket_name}.{prio}", list(keys))
|
||
for key in keys:
|
||
v.integer(bucket[prio][key], f"$.decision_inputs.{bucket_name}.{prio}.{key}", minimum=0)
|
||
rate = di["should_fix_addressed_rate"]
|
||
v.obj(rate, "$.decision_inputs.should_fix_addressed_rate", ["numerator", "denominator"])
|
||
v.integer(rate["numerator"], "$.decision_inputs.should_fix_addressed_rate.numerator", minimum=0)
|
||
v.integer(rate["denominator"], "$.decision_inputs.should_fix_addressed_rate.denominator", minimum=0)
|
||
v.array(di["regressions"], "$.decision_inputs.regressions")
|
||
for i, entry in enumerate(di["regressions"]):
|
||
path = f"$.decision_inputs.regressions[{i}]"
|
||
v.obj(entry, path, ["new_issue_id", "severity"])
|
||
v.pattern(entry["new_issue_id"], f"{path}.new_issue_id", _ID_RES["NEW"], "NEW-<n>")
|
||
v.enum(entry["severity"], f"{path}.severity", SEVERITIES)
|
||
v.array(di["non_regression_new_issue_ids"], "$.decision_inputs.non_regression_new_issue_ids")
|
||
for i, entry in enumerate(di["non_regression_new_issue_ids"]):
|
||
v.pattern(entry, f"$.decision_inputs.non_regression_new_issue_ids[{i}]", _ID_RES["NEW"], "NEW-<n>")
|
||
v.array(di["escalations"], "$.decision_inputs.escalations")
|
||
for i, entry in enumerate(di["escalations"]):
|
||
path = f"$.decision_inputs.escalations[{i}]"
|
||
v.obj(entry, path, ["exception_id", "effective_approval_state", "escalation_class", "mechanical_decision_impact"])
|
||
v.pattern(entry["exception_id"], f"{path}.exception_id", _ID_RES["ESC"], "ESC-<n>")
|
||
v.enum(entry["effective_approval_state"], f"{path}.effective_approval_state", ("pending", "approved", "rejected"))
|
||
v.enum(entry["escalation_class"], f"{path}.escalation_class", ESCALATION_CLASSES)
|
||
v.enum(entry["mechanical_decision_impact"], f"{path}.mechanical_decision_impact", ("Minor Revision", "Major Revision"))
|
||
if "reject_recommended" in di and not isinstance(di["reject_recommended"], bool):
|
||
v.fail("$.decision_inputs.reject_recommended", "must be a boolean")
|
||
v.enum(di["apply_chain_witness"], "$.decision_inputs.apply_chain_witness", WITNESS_STATES)
|
||
|
||
|
||
# --- manifest ------------------------------------------------------------------
|
||
|
||
|
||
def validate_manifest(manifest: dict):
|
||
"""Structural manifest validation (§11). Failures -> manifest_incomplete."""
|
||
|
||
def fail(message: str):
|
||
raise ManifestError("manifest_incomplete", f"manifest: {message}")
|
||
|
||
if not isinstance(manifest, dict):
|
||
fail("must be an object")
|
||
expected_top = {"contract_version", "round_id", "cross_model_active", "artifacts"}
|
||
if set(manifest) != expected_top:
|
||
fail(f"top-level fields must be exactly {sorted(expected_top)}")
|
||
if manifest["contract_version"] != "1.1":
|
||
fail("contract_version must be current 1.1; legacy/mixed chains use the archived loader")
|
||
if not _is_str(manifest["round_id"]):
|
||
fail("round_id must be a non-empty string")
|
||
if not isinstance(manifest["cross_model_active"], bool):
|
||
fail("cross_model_active must be a boolean")
|
||
artifacts = manifest["artifacts"]
|
||
single_keys = (
|
||
"original_manuscript", "revised_manuscript", "revision_roadmap",
|
||
"author_adjudication", "revision_evidence_bundle",
|
||
"editorial_decision_letter", "response_to_reviewers",
|
||
"round1_findings", "round1_config_cards",
|
||
)
|
||
array_keys = ("revision_patches", "apply_reports")
|
||
if not isinstance(artifacts, dict) or set(artifacts) != set(single_keys) | set(array_keys):
|
||
fail("artifacts must carry exactly the eleven current §11 keys")
|
||
|
||
def check_entry_fields(entry, key, path):
|
||
ref = entry.get("path_or_passport_ref")
|
||
if not _is_str(ref) or not _ARTIFACT_REF_RE.match(ref):
|
||
fail(f"{path}: path_or_passport_ref must be passport:<ref> | path:<relative path>")
|
||
if ref.startswith("path:"):
|
||
rel = ref[len("path:"):]
|
||
segments = re.split(r"[/\\]", rel)
|
||
if (
|
||
rel.startswith(("/", "\\"))
|
||
or re.match(r"^[A-Za-z]:", rel)
|
||
or any(segment == ".." for segment in segments)
|
||
or not rel
|
||
):
|
||
fail(f"{path}: path: refs carry a RELATIVE path — no absolute, drive-letter, or traversal segments (§11)")
|
||
sha = entry.get("sha256")
|
||
if not isinstance(sha, str) or not _SHA256_RE.match(sha):
|
||
fail(f"{path}: sha256 must be 64-hex")
|
||
for field in ("version_label", "origin_date"):
|
||
value = entry.get(field, "MISSING")
|
||
if value == "MISSING":
|
||
fail(f"{path}: missing {field}")
|
||
if value is not None and not _is_str(value):
|
||
fail(f"{path}: {field} must be a non-empty string or null")
|
||
if value is None and entry["path_or_passport_ref"].startswith("passport:"):
|
||
fail(f"{path}: null {field} on a passport:-tagged entry (§11)")
|
||
|
||
for key in single_keys:
|
||
entry = artifacts[key]
|
||
if not isinstance(entry, dict) or not isinstance(entry.get("present"), bool):
|
||
fail(f"artifacts.{key}: present-discriminated union required")
|
||
if entry["present"]:
|
||
if set(entry) != {"present", "path_or_passport_ref", "sha256", "version_label", "origin_date"}:
|
||
fail(f"artifacts.{key}: present entry carries exactly ref/sha256/version_label/origin_date")
|
||
check_entry_fields(entry, key, f"artifacts.{key}")
|
||
elif set(entry) != {"present"}:
|
||
fail(f"artifacts.{key}: an absent artifact carries NO ref, hash, or freshness fields (§11)")
|
||
for key in array_keys:
|
||
entry = artifacts[key]
|
||
if not isinstance(entry, dict) or not isinstance(entry.get("present"), bool):
|
||
fail(f"artifacts.{key}: present-discriminated union required")
|
||
if entry["present"]:
|
||
if set(entry) != {"present", "items"}:
|
||
fail(f"artifacts.{key}: present array entry carries exactly items")
|
||
items = entry["items"]
|
||
if not isinstance(items, list) or not items:
|
||
fail(f"artifacts.{key}: present: false is the canonical empty encoding (§11)")
|
||
for i, element in enumerate(items):
|
||
if not isinstance(element, dict) or set(element) != {"path_or_passport_ref", "sha256", "version_label", "origin_date"}:
|
||
fail(f"artifacts.{key}[{i}]: every element FULLY populated (no per-element absence)")
|
||
check_entry_fields(element, key, f"artifacts.{key}[{i}]")
|
||
elif set(entry) != {"present"}:
|
||
fail(f"artifacts.{key}: an absent artifact carries NO ref, hash, or freshness fields (§11)")
|
||
|
||
# Required set + array pairing (G0).
|
||
for key in (
|
||
"original_manuscript",
|
||
"revised_manuscript",
|
||
"revision_roadmap",
|
||
"author_adjudication",
|
||
"revision_evidence_bundle",
|
||
):
|
||
if not artifacts[key]["present"]:
|
||
raise ManifestError("manifest_incomplete", f"manifest: hard-required artifact {key} absent (§11)")
|
||
patches = artifacts["revision_patches"]
|
||
reports = artifacts["apply_reports"]
|
||
if patches["present"] != reports["present"]:
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
"manifest: revision_patches and apply_reports must travel together (§11)",
|
||
)
|
||
if reports["present"]:
|
||
if len(reports["items"]) != len(patches["items"]):
|
||
raise ManifestError("manifest_incomplete", "manifest: apply_reports / revision_patches length mismatch (§11)")
|
||
|
||
# Freshness: a roadmap older than the letter it should pair with fails
|
||
# closed (§11). Comparable only when both origin_dates are non-null.
|
||
roadmap_entry = artifacts["revision_roadmap"]
|
||
letter_entry = artifacts["editorial_decision_letter"]
|
||
if letter_entry["present"] and roadmap_entry["origin_date"] is not None and letter_entry["origin_date"] is not None:
|
||
if roadmap_entry["origin_date"] < letter_entry["origin_date"]:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"manifest: revision_roadmap origin_date predates editorial_decision_letter (freshness, §11)",
|
||
)
|
||
|
||
|
||
def _hash_prefix(sha256_hex: str) -> str:
|
||
"""§11: apply reports use the 12-hex base_draft_hash format."""
|
||
return sha256_hex[:12]
|
||
|
||
|
||
def compute_apply_chain_witness(manifest: dict, report_payloads):
|
||
"""§11 apply-chain witness over the ordered report payloads.
|
||
|
||
Returns (witness, notes). A broken checked link raises ManifestError
|
||
(manifest_hash_mismatch, G0) — positive breakage evidence outranks the
|
||
absence markers, matching the declared precedence
|
||
fail > not_run_no_reports > pass.
|
||
"""
|
||
artifacts = manifest["artifacts"]
|
||
notes = []
|
||
if not report_payloads:
|
||
return "not_run_no_reports", notes
|
||
patches = artifacts["revision_patches"]["items"]
|
||
for i, report in enumerate(report_payloads):
|
||
if not isinstance(report, dict):
|
||
raise ManifestError("manifest_incomplete", f"apply report [{i}]: must be a JSON object")
|
||
for field in ("report_format_version", "base_draft_hash", "output_draft_hash"):
|
||
if not _is_str(report.get(field)):
|
||
raise ManifestError("manifest_incomplete", f"apply report [{i}]: missing {field}")
|
||
if not _REPORT_VERSION_RE.match(report["report_format_version"]):
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
f"apply report [{i}]: report_format_version {report['report_format_version']!r} is not a numeric "
|
||
"dotted version; current contract requires exact report format 1.3 (§11)",
|
||
)
|
||
for field in ("base_draft_hash", "output_draft_hash"):
|
||
if not _HASH12_RE.match(report[field]):
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
f"apply report [{i}]: {field} must be the 12-hex base_draft_hash format (§11)",
|
||
)
|
||
version = tuple(int(part) for part in report["report_format_version"].split("."))
|
||
if version != (1, 3):
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
f"apply report [{i}]: current contract requires report format 1.3; legacy reports use the archived loader",
|
||
)
|
||
digest = report.get("patch_digest")
|
||
if not isinstance(digest, str) or not _SHA256_RE.match(digest):
|
||
raise ManifestError("manifest_incomplete", f"apply report [{i}]: format 1.3 requires patch_digest")
|
||
if digest != patches[i]["sha256"]:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
f"apply report [{i}]: patch_digest does not equal the paired revision_patches[{i}].sha256 (§11 content-bound pairing)",
|
||
)
|
||
authorization = report.get("authorization_witness")
|
||
if not isinstance(authorization, dict) or authorization.get("status") not in (
|
||
"pass",
|
||
"not_applicable_integrity_correction",
|
||
):
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
f"apply report [{i}]: format 1.3 requires a current replayed authorization_witness",
|
||
)
|
||
if authorization.get("unregistered_claim_drift_review_required") is not True:
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
f"apply report [{i}]: authorization witness must surface the E6 unregistered-claim review boundary",
|
||
)
|
||
original = artifacts["original_manuscript"]
|
||
if not original["present"]:
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
"apply-chain witness: current contract hard-requires original_manuscript",
|
||
)
|
||
if report_payloads[0]["base_draft_hash"] != _hash_prefix(original["sha256"]):
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"apply-chain witness: first report base_draft_hash does not equal the original_manuscript hash prefix (§11)",
|
||
)
|
||
for i in range(1, len(report_payloads)):
|
||
if report_payloads[i]["base_draft_hash"] != report_payloads[i - 1]["output_draft_hash"]:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
f"apply-chain witness: report [{i}] base_draft_hash does not equal report [{i - 1}] output_draft_hash (§11)",
|
||
)
|
||
revised = artifacts["revised_manuscript"]
|
||
if report_payloads[-1]["output_draft_hash"] != _hash_prefix(revised["sha256"]):
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"apply-chain witness: last report output_draft_hash does not equal the revised_manuscript hash prefix (§11)",
|
||
)
|
||
return "pass", notes
|
||
|
||
|
||
def validate_revision_bundle_binding(bundle: dict, manifest: dict) -> None:
|
||
"""Bind the replayed bundle to the exact §11 current-round artifacts."""
|
||
artifacts = manifest["artifacts"]
|
||
revised_sha = artifacts["revised_manuscript"]["sha256"]
|
||
if bundle["final_draft"]["sha256"] != revised_sha:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"revision_evidence_bundle.final_draft does not equal revised_manuscript",
|
||
)
|
||
|
||
roadmap_sha = artifacts["revision_roadmap"]["sha256"]
|
||
author_sha = artifacts["author_adjudication"]["sha256"]
|
||
partial_matches = []
|
||
exact_matches = []
|
||
for index, row in enumerate(bundle["rounds"]):
|
||
if row["kind"] not in ("review_roadmap", "review_noop"):
|
||
continue
|
||
roadmap_match = row["revision_roadmap"]["sha256"] == roadmap_sha
|
||
author_match = row["author_adjudication"]["sha256"] == author_sha
|
||
if roadmap_match != author_match:
|
||
partial_matches.append(index)
|
||
if roadmap_match and author_match:
|
||
exact_matches.append((index, row))
|
||
if partial_matches:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"revision_evidence_bundle splits the current roadmap/author pair across rounds",
|
||
)
|
||
if len(exact_matches) != 1:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"revision_evidence_bundle must contain exactly one round with the exact current roadmap/author pair",
|
||
)
|
||
|
||
_index, current = exact_matches[0]
|
||
original = artifacts["original_manuscript"]
|
||
if original["present"] and current["pre_round_draft"]["sha256"] != original["sha256"]:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"revision_evidence_bundle current round pre draft differs from original_manuscript",
|
||
)
|
||
bundle_pairs = [
|
||
(row["revision_patch"]["sha256"], row["apply_report"]["sha256"])
|
||
for row in bundle["rounds"]
|
||
if row["kind"] in ("review_roadmap", "integrity_correction")
|
||
]
|
||
patches = artifacts["revision_patches"]
|
||
reports = artifacts["apply_reports"]
|
||
if patches["present"] != reports["present"]:
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
"revision_evidence_bundle cannot bind asymmetric revision_patches/apply_reports state",
|
||
)
|
||
manifest_pairs = []
|
||
if patches["present"] and reports["present"]:
|
||
manifest_pairs = [
|
||
(patch["sha256"], report["sha256"])
|
||
for patch, report in zip(patches["items"], reports["items"])
|
||
]
|
||
if bundle_pairs != manifest_pairs:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"revision_evidence_bundle ordered write patch/report pairs differ from the input manifest",
|
||
)
|
||
|
||
|
||
# --- letter + roadmap parsing --------------------------------------------------
|
||
|
||
_LETTER_SECTION_RE = re.compile(r"^### Required Item Details\s*$")
|
||
_LETTER_BLOCK_RE = re.compile(r"^\*\*(R[1-9][0-9]*): .*\*\*\s*$")
|
||
_LETTER_CRITERIA_RE = re.compile(r"^- \*\*Acceptance criteria\*\*: (.*)$")
|
||
|
||
|
||
def parse_letter_blocks(text: str):
|
||
"""Extract the letter's Required Item Details blocks in document order.
|
||
|
||
Returns an ordered list of (rid, acceptance_text) where acceptance_text
|
||
is the single-line payload after the ``- **Acceptance criteria**:``
|
||
bullet, or None when the block carries no such bullet (a block without
|
||
an Acceptance-criteria field offers nothing to inherit — treated as
|
||
no-block-at-ordinal for the §5.1 letter-fields rule; its id still
|
||
participates in the contiguity check).
|
||
"""
|
||
lines = text.splitlines()
|
||
blocks = []
|
||
in_section = False
|
||
current = None
|
||
for line in lines:
|
||
if _LETTER_SECTION_RE.match(line):
|
||
in_section = True
|
||
continue
|
||
if not in_section:
|
||
continue
|
||
if line.startswith("## "):
|
||
break
|
||
header = _LETTER_BLOCK_RE.match(line)
|
||
if header:
|
||
current = [header.group(1), None]
|
||
blocks.append(current)
|
||
continue
|
||
if current is not None and current[1] is None:
|
||
criteria = _LETTER_CRITERIA_RE.match(line)
|
||
if criteria:
|
||
current[1] = criteria.group(1)
|
||
return [(rid, text_value) for rid, text_value in blocks]
|
||
|
||
|
||
def letter_ordinals_sound(blocks, must_fix_count: int) -> bool:
|
||
"""§5.1 contiguity: ids EXACTLY the contiguous sequence R1..Rk, k <= n.
|
||
|
||
A gap, duplicate, or extra id (beyond the must_fix count) makes the
|
||
ordinal derivation unsound and degrades the WHOLE letter layer to
|
||
absent; a mere count shortfall (contiguous prefix) does not.
|
||
"""
|
||
for i, (rid, _text) in enumerate(blocks):
|
||
if rid != f"R{i + 1}":
|
||
return False
|
||
return len(blocks) <= must_fix_count
|
||
|
||
|
||
def load_roadmap(payload: dict):
|
||
"""Minimal Schema 7 machine-form validation for checker consumption."""
|
||
|
||
def fail(message: str):
|
||
raise ManifestError("manifest_incomplete", f"revision_roadmap: {message}")
|
||
|
||
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
|
||
fail("must be an object with an items array (Schema 7)")
|
||
items = []
|
||
seen = set()
|
||
for i, item in enumerate(payload["items"]):
|
||
if not isinstance(item, dict):
|
||
fail(f"items[{i}] must be an object")
|
||
for field in ("id", "obligation_class", "verification_criteria", "reviewer"):
|
||
if not _is_str(item.get(field)):
|
||
fail(f"items[{i}].{field} must be a non-empty string")
|
||
if not _ITEM_ID_RE.match(item["id"]):
|
||
fail(f"items[{i}].id must be REV-<id>")
|
||
if item["id"] in seen:
|
||
fail(f"duplicate roadmap id {item['id']!r}")
|
||
seen.add(item["id"])
|
||
if item["obligation_class"] not in OBLIGATION_CLASSES:
|
||
fail(f"items[{i}].obligation_class must be one of {OBLIGATION_CLASSES}")
|
||
severity = item.get("severity")
|
||
if severity is not None and severity not in SEVERITIES:
|
||
fail(f"items[{i}].severity must be one of {SEVERITIES} when present")
|
||
source_kind = item.get("source_kind")
|
||
if source_kind is not None and source_kind not in ("question", "editorial"):
|
||
fail(f"items[{i}].source_kind must be question|editorial when present")
|
||
transported = {
|
||
"severity",
|
||
"severity_source",
|
||
"evidence_anchor",
|
||
"confidence",
|
||
"competence_basis",
|
||
"confidence_source",
|
||
"corroborating_sources",
|
||
}
|
||
if source_kind is None:
|
||
for field in ("severity", "evidence_anchor", "confidence", "competence_basis"):
|
||
if field not in item:
|
||
fail(f"items[{i}] finding-driven row requires transported {field}")
|
||
elif transported & set(item):
|
||
fail(f"items[{i}] source_kind row must not carry transported finding fields")
|
||
items.append(item)
|
||
return items
|
||
|
||
|
||
def load_author_adjudication(
|
||
payload: dict,
|
||
*,
|
||
roadmap_items: list[dict],
|
||
roadmap_sha256: str,
|
||
original_manuscript_sha256: str | None,
|
||
):
|
||
"""Validate the #670 sidecar projection this consumer copies.
|
||
|
||
Full target/claim-surface replay already occurs at patch apply and bundle
|
||
validation. This consumer independently validates the closed sidecar shape,
|
||
exact roadmap/base bindings, explicit user-event references, full item
|
||
cardinality, and the author-owned fields Schema 11 must copy unchanged.
|
||
"""
|
||
|
||
def fail(message: str):
|
||
raise ManifestError("manifest_incomplete", f"author_adjudication: {message}")
|
||
|
||
expected_top = {
|
||
"schema_version",
|
||
"revision_round",
|
||
"roadmap_sha256",
|
||
"base_draft_sha256",
|
||
"claim_surface_manifest_sha256",
|
||
"adjudication_status",
|
||
"author_events",
|
||
"display_order",
|
||
"author_adjudications",
|
||
"collateral_authorizations",
|
||
}
|
||
if not isinstance(payload, dict) or set(payload) != expected_top:
|
||
fail(f"top-level fields must be exactly {sorted(expected_top)}")
|
||
if payload["schema_version"] != "author-adjudication/1.0":
|
||
fail("schema_version must be author-adjudication/1.0")
|
||
if not isinstance(payload["revision_round"], int) or isinstance(payload["revision_round"], bool) or payload["revision_round"] < 1:
|
||
fail("revision_round must be an integer >= 1")
|
||
for field in ("roadmap_sha256", "base_draft_sha256", "claim_surface_manifest_sha256"):
|
||
if not isinstance(payload[field], str) or not _SHA256_RE.match(payload[field]):
|
||
fail(f"{field} must be 64-hex")
|
||
if payload["roadmap_sha256"] != roadmap_sha256:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"author_adjudication.roadmap_sha256 does not match the exact roadmap bytes",
|
||
)
|
||
if (
|
||
original_manuscript_sha256 is not None
|
||
and payload["base_draft_sha256"] != original_manuscript_sha256
|
||
):
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"author_adjudication.base_draft_sha256 does not match original_manuscript bytes",
|
||
)
|
||
if payload["adjudication_status"] != "complete":
|
||
fail("adjudication_status must be complete")
|
||
|
||
events = payload["author_events"]
|
||
if not isinstance(events, list):
|
||
fail("author_events must be an array")
|
||
event_ids = set()
|
||
for index, event in enumerate(events):
|
||
if not isinstance(event, dict) or set(event) != {
|
||
"event_id",
|
||
"source",
|
||
"actor_role",
|
||
"input_sha256",
|
||
}:
|
||
fail(f"author_events[{index}] has an invalid closed shape")
|
||
if not _AUTHOR_EVENT_RE.match(event["event_id"]):
|
||
fail(f"author_events[{index}].event_id is invalid")
|
||
if event["event_id"] in event_ids:
|
||
fail(f"duplicate author event {event['event_id']!r}")
|
||
event_ids.add(event["event_id"])
|
||
if event["source"] != "explicit_session_user_message" or event["actor_role"] != "author":
|
||
fail(f"author_events[{index}] is not an explicit author session event")
|
||
if not isinstance(event["input_sha256"], str) or not _SHA256_RE.match(event["input_sha256"]):
|
||
fail(f"author_events[{index}].input_sha256 must be 64-hex")
|
||
|
||
roadmap_ids = [item["id"] for item in roadmap_items]
|
||
display = payload["display_order"]
|
||
if not isinstance(display, dict) or set(display) != {"mode", "item_ids", "author_event_id"}:
|
||
fail("display_order has an invalid closed shape")
|
||
if display["mode"] not in ("source_traceability", "user_selected"):
|
||
fail("display_order.mode is invalid")
|
||
if (
|
||
not isinstance(display["item_ids"], list)
|
||
or len(display["item_ids"]) != len(set(display["item_ids"]))
|
||
or set(display["item_ids"]) != set(roadmap_ids)
|
||
):
|
||
fail("display_order.item_ids must be a full unique roadmap permutation")
|
||
if display["mode"] == "source_traceability" and display["item_ids"] != roadmap_ids:
|
||
fail("source_traceability display order must equal immutable roadmap order")
|
||
if display["author_event_id"] not in event_ids:
|
||
fail("display_order.author_event_id does not resolve")
|
||
|
||
records = payload["author_adjudications"]
|
||
if not isinstance(records, list):
|
||
fail("author_adjudications must be an array")
|
||
by_item = {}
|
||
for index, record in enumerate(records):
|
||
required = {
|
||
"item_id",
|
||
"author_event_id",
|
||
"author_triage",
|
||
"authorized_targets",
|
||
"claim_strength_authorizations",
|
||
}
|
||
allowed = required | {"author_reason"}
|
||
if not isinstance(record, dict) or not required.issubset(record) or not set(record).issubset(allowed):
|
||
fail(f"author_adjudications[{index}] has an invalid closed shape")
|
||
item_id = record["item_id"]
|
||
if item_id in by_item:
|
||
fail(f"duplicate author adjudication for {item_id!r}")
|
||
by_item[item_id] = record
|
||
if record["author_event_id"] not in event_ids:
|
||
fail(f"author adjudication {item_id}: author_event_id does not resolve")
|
||
if record["author_triage"] not in ("will_address", "wont_address", "not_on_point"):
|
||
fail(f"author adjudication {item_id}: invalid author_triage")
|
||
try:
|
||
validator = _V("manifest_incomplete", f"author adjudication {item_id}")
|
||
_validate_author_targets(validator, record["authorized_targets"], "$.authorized_targets")
|
||
_validate_claim_authorizations(
|
||
validator,
|
||
record["claim_strength_authorizations"],
|
||
"$.claim_strength_authorizations",
|
||
)
|
||
except ArtifactSchemaError as exc:
|
||
fail(str(exc))
|
||
if record["author_triage"] == "will_address":
|
||
if not record["authorized_targets"]:
|
||
fail(f"author adjudication {item_id}: will_address requires exact targets")
|
||
if "author_reason" in record:
|
||
fail(f"author adjudication {item_id}: will_address must not invent author_reason")
|
||
else:
|
||
if not _is_str(record.get("author_reason")):
|
||
fail(f"author adjudication {item_id}: declined choice requires author_reason")
|
||
if record["authorized_targets"] or record["claim_strength_authorizations"]:
|
||
fail(f"author adjudication {item_id}: declined choice carries no authority")
|
||
if set(by_item) != set(roadmap_ids):
|
||
fail("complete sidecar must carry exactly one author adjudication per roadmap item")
|
||
|
||
collateral = payload["collateral_authorizations"]
|
||
if not isinstance(collateral, list):
|
||
fail("collateral_authorizations must be an array")
|
||
for index, record in enumerate(collateral):
|
||
required = {
|
||
"authorization_id",
|
||
"author_event_id",
|
||
"authorizing_item_id",
|
||
"constrained_item_id",
|
||
"block_id",
|
||
"operation",
|
||
"reason",
|
||
}
|
||
if not isinstance(record, dict) or set(record) != required:
|
||
fail(f"collateral_authorizations[{index}] has an invalid closed shape")
|
||
if record["author_event_id"] not in event_ids:
|
||
fail(f"collateral_authorizations[{index}].author_event_id does not resolve")
|
||
if record["authorizing_item_id"] not in by_item or record["constrained_item_id"] not in by_item:
|
||
fail(f"collateral_authorizations[{index}] item reference does not resolve")
|
||
if by_item[record["authorizing_item_id"]]["author_triage"] != "will_address":
|
||
fail(f"collateral_authorizations[{index}] authorizing item is not will_address")
|
||
if by_item[record["constrained_item_id"]]["author_triage"] not in (
|
||
"wont_address",
|
||
"not_on_point",
|
||
):
|
||
fail(f"collateral_authorizations[{index}] constrained item is not declined")
|
||
if not _BLOCK_ID_RE.match(record["block_id"]):
|
||
fail(f"collateral_authorizations[{index}].block_id is invalid")
|
||
if record["operation"] not in ("replace_block", "insert_after", "delete_block"):
|
||
fail(f"collateral_authorizations[{index}].operation is invalid")
|
||
if not _is_str(record["reason"]):
|
||
fail(f"collateral_authorizations[{index}].reason must be non-empty")
|
||
return by_item
|
||
|
||
|
||
# --- synthesis recomputation ---------------------------------------------------
|
||
|
||
|
||
class Failures:
|
||
def __init__(self):
|
||
self.messages = []
|
||
|
||
def add(self, message: str):
|
||
self.messages.append(message)
|
||
|
||
|
||
def _decision_max(*decisions):
|
||
return max(decisions, key=lambda d: DECISION_ORDER[d])
|
||
|
||
|
||
def derive_decision(p1_items, p2_partial_magnitudes, p2_made_worse, rate_num, rate_den, regression_severities, escalations):
|
||
"""§6 Steps 2-3. ``p1_items`` = [(final_verdict, driving_severity, residual_obligation_class)].
|
||
|
||
Returns (decision, reject_recommended, base_rule).
|
||
"""
|
||
p1_count = len(p1_items)
|
||
rate_below_80 = rate_den > 0 and rate_num * 5 < rate_den * 4 # < 80%, integer-exact
|
||
base = None
|
||
rule = None
|
||
if any(v == "MADE_WORSE" and sev == "critical" for v, sev, _m in p1_items) or ("critical" in regression_severities):
|
||
base, rule = "Major Revision", "B1"
|
||
elif p1_count > 0 and 2 * sum(1 for v, _s, _m in p1_items if v in ("NOT_ADDRESSED", "MADE_WORSE")) >= p1_count:
|
||
base, rule = "Major Revision", "B2"
|
||
elif any(v in ("NOT_ADDRESSED", "MADE_WORSE", "CANNOT_VERIFY") for v, _s, _m in p1_items) or ("major" in regression_severities):
|
||
base, rule = "Major Revision", "B3"
|
||
elif any(v == "PARTIALLY_ADDRESSED" and m == "must_fix" for v, _s, m in p1_items) or ("must_fix" in p2_partial_magnitudes):
|
||
base, rule = "Major Revision", "B4"
|
||
elif (
|
||
any(v == "PARTIALLY_ADDRESSED" and m in ("should_fix", "consider") for v, _s, m in p1_items)
|
||
or rate_below_80
|
||
or p2_made_worse
|
||
or ("minor" in regression_severities)
|
||
):
|
||
base, rule = "Minor Revision", "B5"
|
||
else:
|
||
base, rule = "Accept", "B6"
|
||
decision = base
|
||
reject = rule in ("B1", "B2")
|
||
for esc in escalations:
|
||
if esc["effective_approval_state"] == "approved":
|
||
decision = _decision_max(decision, esc["mechanical_decision_impact"])
|
||
if esc["escalation_class"] == "research_integrity":
|
||
reject = True
|
||
return decision, reject, rule
|
||
|
||
|
||
def _adjustment_chains(traceability, fails: Failures):
|
||
"""Build per-item ordered adjustment chains (§5.3 grammar).
|
||
|
||
Returns {item_id: [records head..tail]} (empty dict entries omitted);
|
||
reports every grammar violation into ``fails``.
|
||
"""
|
||
by_id = {}
|
||
for rec in traceability["adjustments"]:
|
||
if rec["adjustment_id"] in by_id:
|
||
fails.add(f"adjustment ids must be unique: {rec['adjustment_id']}")
|
||
return {}
|
||
by_id[rec["adjustment_id"]] = rec
|
||
chains = {}
|
||
by_item = {}
|
||
for rec in traceability["adjustments"]:
|
||
by_item.setdefault(rec["item_id"], []).append(rec)
|
||
for item_id, records in by_item.items():
|
||
superseded = {}
|
||
heads = []
|
||
for rec in records:
|
||
prev = rec.get("supersedes_adjustment_id")
|
||
if prev is None:
|
||
heads.append(rec)
|
||
else:
|
||
prev_rec = by_id.get(prev)
|
||
if prev_rec is None or prev_rec["item_id"] != item_id:
|
||
fails.add(f"adjustment {rec['adjustment_id']}: supersedes_adjustment_id {prev!r} does not resolve on item {item_id}")
|
||
return {}
|
||
if prev in superseded:
|
||
fails.add(f"adjustment chain on {item_id}: {prev} superseded twice")
|
||
return {}
|
||
superseded[prev] = rec
|
||
if len(heads) != 1:
|
||
fails.add(f"adjustment chain on {item_id}: exactly one head required, found {len(heads)}")
|
||
return {}
|
||
chain = [heads[0]]
|
||
while chain[-1]["adjustment_id"] in superseded:
|
||
chain.append(superseded[chain[-1]["adjustment_id"]])
|
||
if len(chain) != len(records):
|
||
fails.add(f"adjustment chain on {item_id}: records outside the linear chain (cycle or fork)")
|
||
return {}
|
||
for prev_rec, next_rec in zip(chain, chain[1:]):
|
||
if next_rec["from_verdict"] != prev_rec["to_verdict"]:
|
||
fails.add(
|
||
f"adjustment chain on {item_id}: {next_rec['adjustment_id']}.from_verdict "
|
||
f"!= {prev_rec['adjustment_id']}.to_verdict (head-to-tail join)"
|
||
)
|
||
chains[item_id] = chain
|
||
return chains
|
||
|
||
|
||
def check(
|
||
manifest,
|
||
precommitment,
|
||
verdict_record,
|
||
traceability,
|
||
roadmap_items,
|
||
author_by_item,
|
||
letter_blocks,
|
||
witness,
|
||
witness_notes,
|
||
):
|
||
"""All §13 recomputation on schema-valid, hash-bound inputs.
|
||
|
||
Returns (failures, warnings, recomputed_outcome) where
|
||
recomputed_outcome is the Step 1-3 emission expectation.
|
||
"""
|
||
fails = Failures()
|
||
warnings = list(witness_notes)
|
||
|
||
roadmap_by_id = {item["id"]: item for item in roadmap_items}
|
||
must_fix_order = [item["id"] for item in roadmap_items if item["obligation_class"] == "must_fix"]
|
||
p12_ids = {item["id"] for item in roadmap_items if item["obligation_class"] in ("must_fix", "should_fix")}
|
||
cross_model_active = manifest["cross_model_active"]
|
||
|
||
# round_id equality across all artifacts.
|
||
for name, art in (("precommitment", precommitment), ("verdict_record", verdict_record), ("traceability", traceability)):
|
||
if art["round_id"] != manifest["round_id"]:
|
||
fails.add(f"{name}.round_id != manifest.round_id")
|
||
if art["contract_version"] != manifest["contract_version"]:
|
||
fails.add(f"{name}.contract_version != manifest.contract_version")
|
||
|
||
# --- Phase 1 coverage + criterion binding (§4/§5.1) ---
|
||
pre_by_item = {}
|
||
for rec in precommitment["items"]:
|
||
if rec["item_id"] in pre_by_item:
|
||
fails.add(f"precommitment: duplicate record for {rec['item_id']}")
|
||
pre_by_item[rec["item_id"]] = rec
|
||
if set(pre_by_item) != p12_ids:
|
||
fails.add(
|
||
"precommitment coverage: records must exist for exactly the must_fix/should_fix roadmap items "
|
||
f"(missing {sorted(p12_ids - set(pre_by_item))}, extra {sorted(set(pre_by_item) - p12_ids)})"
|
||
)
|
||
letter_present = manifest["artifacts"]["editorial_decision_letter"]["present"]
|
||
letter_sound = letter_blocks is not None and letter_ordinals_sound(letter_blocks, len(must_fix_order))
|
||
if letter_present and letter_blocks is not None and not letter_sound:
|
||
warnings.append("[CRITERIA-LAYER-ABSENT: letter/roadmap ordinal mismatch]")
|
||
if letter_present and letter_sound and not letter_blocks:
|
||
warnings.append(
|
||
"NOTE: editorial decision letter present but no Required Item Details blocks parsed — the level-2 "
|
||
"criteria layer is empty (template drift or a genuinely block-less letter)"
|
||
)
|
||
block_by_rid = {}
|
||
if letter_sound and letter_blocks:
|
||
block_by_rid = {rid: text for rid, text in letter_blocks}
|
||
for item_id, rec in pre_by_item.items():
|
||
item = roadmap_by_id.get(item_id)
|
||
if item is None:
|
||
continue
|
||
if rec["obligation_class"] != item["obligation_class"]:
|
||
fails.add(f"precommitment {item_id}: obligation_class {rec['obligation_class']} != roadmap {item['obligation_class']}")
|
||
if rec["inherited_criterion"]["roadmap_text"] != item["verification_criteria"]:
|
||
fails.add(f"precommitment {item_id}: inherited_criterion.roadmap_text does not match the roadmap verbatim (§4)")
|
||
if rec["source_reviewer"] != item["reviewer"]:
|
||
fails.add(
|
||
f"precommitment {item_id}: source_reviewer is a VERBATIM copy of the Schema 7 reviewer field "
|
||
f"({rec['source_reviewer']!r} != {item['reviewer']!r}, §5.1)"
|
||
)
|
||
labels, _parse_failure = normalize_reviewer_labels(rec["source_reviewer"])
|
||
if rec["source_reviewer_labels"] != labels:
|
||
fails.add(
|
||
f"precommitment {item_id}: source_reviewer_labels {rec['source_reviewer_labels']!r} "
|
||
f"!= §10 normalization of source_reviewer ({labels!r})"
|
||
)
|
||
has_letter_fields = "letter_text" in rec["inherited_criterion"]
|
||
if item["obligation_class"] == "must_fix":
|
||
ordinal = must_fix_order.index(item_id) + 1
|
||
derived_ref = f"R{ordinal}"
|
||
block_text = block_by_rid.get(derived_ref) if letter_present and letter_sound else None
|
||
if block_text is not None:
|
||
if not has_letter_fields:
|
||
fails.add(f"precommitment {item_id}: letter carries block {derived_ref} — letter_text/letter_item_ref REQUIRED (§5.1)")
|
||
else:
|
||
if rec["inherited_criterion"]["letter_item_ref"] != derived_ref:
|
||
fails.add(
|
||
f"precommitment {item_id}: letter_item_ref {rec['inherited_criterion']['letter_item_ref']!r} "
|
||
f"!= derived ordinal {derived_ref!r} (§5.1 — the ref is DERIVED, never chosen)"
|
||
)
|
||
if rec["inherited_criterion"]["letter_text"] != block_text:
|
||
fails.add(f"precommitment {item_id}: letter_text does not match the letter's Acceptance criteria byte-for-byte (§13)")
|
||
elif has_letter_fields:
|
||
fails.add(f"precommitment {item_id}: letter layer absent at this item — letter fields must be absent (§5.1/§13)")
|
||
elif has_letter_fields:
|
||
fails.add(f"precommitment {item_id}: letter fields on a non-must_fix item (§5.1)")
|
||
|
||
# --- Phase 2A coverage + applied_criterion (§5.2) ---
|
||
verdict_by_item = {}
|
||
for rec in verdict_record["items"]:
|
||
if rec["item_id"] in verdict_by_item:
|
||
fails.add(f"verdict_record: duplicate record for {rec['item_id']}")
|
||
verdict_by_item[rec["item_id"]] = rec
|
||
if set(verdict_by_item) != set(roadmap_by_id):
|
||
fails.add(
|
||
"verdict_record coverage: exactly one record per roadmap item, ALL priorities "
|
||
f"(missing {sorted(set(roadmap_by_id) - set(verdict_by_item))}, extra {sorted(set(verdict_by_item) - set(roadmap_by_id))})"
|
||
)
|
||
dissent_by_id = {}
|
||
for rec in verdict_record["dissents"]:
|
||
if rec["dissent_id"] in dissent_by_id:
|
||
fails.add(f"verdict_record: duplicate dissent_id {rec['dissent_id']}")
|
||
dissent_by_id[rec["dissent_id"]] = rec
|
||
pre = pre_by_item.get(rec["item_id"])
|
||
if pre is None:
|
||
fails.add(f"dissent {rec['dissent_id']}: item {rec['item_id']} has no pre-commitment record")
|
||
elif rec["criterion_hash"] != canonical_hash(pre):
|
||
fails.add(f"dissent {rec['dissent_id']}: criterion_hash does not recompute from the Phase-1 record (§7)")
|
||
# Reverse witness (§7): a dissent swaps the criterion BEFORE the
|
||
# verdict — "The item's verdict record then carries
|
||
# applied_criterion: dissented:<dissent_id>". A ghost dissent that
|
||
# was never applied cannot exist, trip the §7 bound, or authorize
|
||
# an original_upheld re-application of a criterion that was in
|
||
# fact applied at 2A.
|
||
applied = verdict_by_item.get(rec["item_id"], {}).get("applied_criterion")
|
||
if applied != f"dissented:{rec['dissent_id']}":
|
||
fails.add(
|
||
f"dissent {rec['dissent_id']}: the item's verdict record must carry "
|
||
f"applied_criterion dissented:{rec['dissent_id']} — a dissent that was never applied is a ghost (§7)"
|
||
)
|
||
# §7 bounds (SD-5): dissent on a must_fix item, or dissents on > ceil(N/3) of
|
||
# all items, trips independent adjudication of EVERY dissent record.
|
||
total_items = len(roadmap_items)
|
||
bound_tripped = any(
|
||
roadmap_by_id.get(dissent["item_id"], {}).get("obligation_class") == "must_fix"
|
||
for dissent in dissent_by_id.values()
|
||
) or (len(dissent_by_id) > math.ceil(total_items / 3))
|
||
for item_id, rec in verdict_by_item.items():
|
||
item = roadmap_by_id.get(item_id)
|
||
if item is None:
|
||
continue
|
||
applied = rec["applied_criterion"]
|
||
if item["obligation_class"] == "consider":
|
||
if applied != "not_precommitted":
|
||
fails.add(f"verdict {item_id}: consider items carry applied_criterion not_precommitted (§5.2)")
|
||
else:
|
||
if applied == "not_precommitted":
|
||
fails.add(f"verdict {item_id}: not_precommitted is valid ONLY for consider items (§5.2)")
|
||
elif applied.startswith("dissented:"):
|
||
dissent = dissent_by_id.get(applied.split(":", 1)[1])
|
||
if dissent is None or dissent["item_id"] != item_id:
|
||
fails.add(f"verdict {item_id}: applied_criterion {applied!r} does not resolve to a dissent on this item")
|
||
|
||
ns_by_id = {rec["new_standard_id"]: rec for rec in precommitment["new_standards"]}
|
||
exceptions_by_id = {}
|
||
for rec in verdict_record["escalation_exceptions"]:
|
||
if rec["exception_id"] in exceptions_by_id:
|
||
fails.add(f"verdict_record: duplicate exception_id {rec['exception_id']}")
|
||
exceptions_by_id[rec["exception_id"]] = rec
|
||
ref = rec.get("new_standard_ref")
|
||
if ref is not None:
|
||
target = ns_by_id.get(ref)
|
||
if target is None:
|
||
fails.add(f"escalation exception {rec['exception_id']}: new_standard_ref {ref!r} does not resolve")
|
||
elif target["classification"] != "escalation_requested":
|
||
fails.add(f"escalation exception {rec['exception_id']}: new_standard_ref target is not escalation_requested (§5.1)")
|
||
|
||
# --- new-issue freeze witness (§13.5) ---
|
||
if traceability["new_issues"] != verdict_record["new_issues"]:
|
||
fails.add("new_issues: §5.3 set is not WHOLE-RECORD byte-identical to the frozen §5.2 set (§3.3 freeze witness)")
|
||
new_issue_by_id = {}
|
||
for rec in verdict_record["new_issues"]:
|
||
if rec["new_issue_id"] in new_issue_by_id:
|
||
fails.add(f"verdict_record: duplicate new_issue_id {rec['new_issue_id']}")
|
||
new_issue_by_id[rec["new_issue_id"]] = rec
|
||
nearest = rec["nearest_roadmap_item"]
|
||
if nearest is not None and nearest not in roadmap_by_id:
|
||
fails.add(f"new issue {rec['new_issue_id']}: nearest_roadmap_item {nearest!r} does not resolve")
|
||
if rec["attribution"] == "regression" and nearest is not None:
|
||
warnings.append(
|
||
f"ADVISORY: regression {rec['new_issue_id']} names nearest_roadmap_item {nearest} — "
|
||
"review for roadmap-item overlap (§8)"
|
||
)
|
||
# --- rows (§5.3) ---
|
||
row_by_item = {}
|
||
for row in traceability["rows"]:
|
||
if row["item_id"] in row_by_item:
|
||
fails.add(f"traceability: duplicate row for {row['item_id']}")
|
||
row_by_item[row["item_id"]] = row
|
||
if set(row_by_item) != set(roadmap_by_id):
|
||
fails.add(
|
||
"row coverage: EVERY roadmap item, all priorities, has exactly one row "
|
||
f"(missing {sorted(set(roadmap_by_id) - set(row_by_item))}, extra {sorted(set(row_by_item) - set(roadmap_by_id))})"
|
||
)
|
||
for item_id, row in row_by_item.items():
|
||
item = roadmap_by_id.get(item_id)
|
||
if item is None:
|
||
continue
|
||
if ROW_OBLIGATION_MAP[row["obligation_class"]] != item["obligation_class"]:
|
||
fails.add(f"row {item_id}: obligation_class {row['obligation_class']} does not match the roadmap ({item['obligation_class']})")
|
||
author = author_by_item.get(item_id)
|
||
if author is None:
|
||
fails.add(f"row {item_id}: no hash-bound author adjudication exists")
|
||
else:
|
||
for field in (
|
||
"author_triage",
|
||
"authorized_targets",
|
||
"claim_strength_authorizations",
|
||
):
|
||
if row[field] != author[field]:
|
||
fails.add(
|
||
f"row {item_id}: {field} is not an exact copy of the hash-bound author adjudication"
|
||
)
|
||
row_has_reason = "author_reason" in row
|
||
author_has_reason = "author_reason" in author
|
||
if row_has_reason != author_has_reason or (
|
||
row_has_reason and row["author_reason"] != author["author_reason"]
|
||
):
|
||
fails.add(
|
||
f"row {item_id}: author_reason presence/value is not an exact author-adjudication copy"
|
||
)
|
||
vrec = verdict_by_item.get(item_id)
|
||
if vrec is not None:
|
||
if row["phase2a_verdict"] != vrec["verdict"]:
|
||
fails.add(f"row {item_id}: phase2a_verdict != committed §5.2 verdict")
|
||
if row["verified_by"] != vrec["verified_by"]:
|
||
fails.add(f"row {item_id}: verified_by not copied from the §5.2 verdict record")
|
||
if row["status"] != row["final_verdict"]:
|
||
fails.add(f"row {item_id}: status must map 1:1 from final_verdict (§5.3)")
|
||
if row["verified"] != VERIFIED_MAP[row["final_verdict"]]:
|
||
fails.add(f"row {item_id}: verified must follow the mechanical 5->4 map (§5.3)")
|
||
if "cross_model_verdict" in row:
|
||
derived = "agree" if row["cross_model_verdict"] == row["final_verdict"] else "diverges"
|
||
if row["cross_model_status"] != derived:
|
||
fails.add(f"row {item_id}: cross_model_status {row['cross_model_status']!r} != per-emission derivation {derived!r} (§5.3)")
|
||
|
||
# --- adjustment chains (§5.3) ---
|
||
chains = _adjustment_chains(traceability, fails)
|
||
for item_id, row in row_by_item.items():
|
||
chain = chains.get(item_id)
|
||
if chain:
|
||
if row.get("adjustment_id") != chain[-1]["adjustment_id"]:
|
||
fails.add(f"row {item_id}: adjustment_id must name the chain's LATEST record")
|
||
if chain[0]["from_verdict"] != row["phase2a_verdict"]:
|
||
fails.add(f"adjustment chain on {item_id}: head from_verdict != phase2a_verdict")
|
||
if chain[-1]["to_verdict"] != row["final_verdict"]:
|
||
fails.add(f"adjustment chain on {item_id}: tail to_verdict != final_verdict")
|
||
else:
|
||
if "adjustment_id" in row:
|
||
fails.add(f"row {item_id}: adjustment_id present with no adjustment chain")
|
||
tail_rebuttal = bool(chain) and chain[-1]["basis"] == "valid_rebuttal" and chain[-1]["to_verdict"] == "FULLY_ADDRESSED"
|
||
if bool(row.get("addressed_by_rebuttal")) != tail_rebuttal:
|
||
fails.add(f"row {item_id}: addressed_by_rebuttal marker must reflect a valid_rebuttal chain-tail upgrade to FULLY_ADDRESSED (§3.4)")
|
||
for item_id in chains:
|
||
if item_id not in row_by_item:
|
||
fails.add(f"adjustment chain on {item_id}: no matrix row exists")
|
||
|
||
# --- deferral-loop referential integrity (§5.3/§6) ---
|
||
intents_by_id = {}
|
||
for rec in traceability["resolution_intents"]:
|
||
if rec["intent_id"] in intents_by_id:
|
||
fails.add(f"duplicate intent_id {rec['intent_id']}")
|
||
intents_by_id[rec["intent_id"]] = rec
|
||
if rec["answered_by"] == "system":
|
||
# §5.3: system intents are emitted for every must_fix diverges row —
|
||
# the row must be an evaluated must_fix row (cross_model_verdict
|
||
# present implies an active configuration).
|
||
intent_row = row_by_item.get(rec["item_id"])
|
||
if intent_row is None or intent_row["obligation_class"] != "MUST_FIX" or "cross_model_verdict" not in intent_row:
|
||
fails.add(
|
||
f"resolution intent {rec['intent_id']}: system intents exist only for evaluated must_fix diverges "
|
||
"rows under ACTIVE cross-model (§5.3)"
|
||
)
|
||
adjudications_by_dissent = {}
|
||
for rec in traceability["dissent_adjudications"]:
|
||
if rec["dissent_id"] in adjudications_by_dissent:
|
||
fails.add(f"duplicate dissent adjudication for {rec['dissent_id']}")
|
||
adjudications_by_dissent[rec["dissent_id"]] = rec
|
||
if rec["dissent_id"] not in dissent_by_id:
|
||
fails.add(f"dissent adjudication {rec['dissent_id']}: no such dissent record")
|
||
if rec["adjudicator"] == "cross_model" and not cross_model_active:
|
||
fails.add(f"dissent adjudication {rec['dissent_id']}: cross_model adjudicator on an inactive configuration (§11)")
|
||
if not bound_tripped:
|
||
fails.add(
|
||
f"dissent adjudication {rec['dissent_id']}: no §7 bound tripped — dissents below the bound stand "
|
||
"unadjudicated by design (§7)"
|
||
)
|
||
dissent = dissent_by_id.get(rec["dissent_id"])
|
||
dissent_item = roadmap_by_id.get(dissent["item_id"]) if dissent else None
|
||
if rec["adjudicator"] == "cross_model":
|
||
if dissent_item is None or dissent_item["obligation_class"] != "must_fix":
|
||
fails.add(
|
||
f"dissent adjudication {rec['dissent_id']}: the judge's adjudication scope EQUALS the §9 pass's "
|
||
"must_fix coverage — dissents on non-must_fix items always take the G2(a) user path, even on an active "
|
||
"setup (§7)"
|
||
)
|
||
# A `user` adjudication on an active-setup must_fix dissent stays legal:
|
||
# the §6 deferral loop records "a user-adjudicated DissentAdjudication
|
||
# ... DIRECTLY" without an activity qualifier, and the §9 pass can be
|
||
# per-row unavailable — rejecting the user path would make a
|
||
# judge-transport failure unrecoverable (adjudicated against the
|
||
# stricter one-way reading; see PR #605 round-4/5 record).
|
||
reapp_by_id = {}
|
||
for rec in traceability["reapplications"]:
|
||
if rec["reapplication_id"] in reapp_by_id:
|
||
fails.add(f"duplicate reapplication_id {rec['reapplication_id']}")
|
||
reapp_by_id[rec["reapplication_id"]] = rec
|
||
superseded_reapps = {}
|
||
for rec in traceability["reapplications"]:
|
||
prev = rec.get("supersedes_reapplication_id")
|
||
if prev is not None:
|
||
prev_rec = reapp_by_id.get(prev)
|
||
if prev_rec is None or prev_rec["item_id"] != rec["item_id"]:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: supersedes_reapplication_id {prev!r} does not resolve on the same item")
|
||
elif prev in superseded_reapps:
|
||
fails.add(f"reapplication supersession: {prev} superseded twice")
|
||
else:
|
||
superseded_reapps[prev] = rec["reapplication_id"]
|
||
# acyclicity of supersession
|
||
for rec in traceability["reapplications"]:
|
||
seen_ids = set()
|
||
cursor = rec
|
||
while cursor is not None:
|
||
if cursor["reapplication_id"] in seen_ids:
|
||
fails.add(f"reapplication supersession cycle at {cursor['reapplication_id']}")
|
||
break
|
||
seen_ids.add(cursor["reapplication_id"])
|
||
prev = cursor.get("supersedes_reapplication_id")
|
||
cursor = reapp_by_id.get(prev) if prev is not None else None
|
||
current_reapps = [rec for rec in traceability["reapplications"] if rec["reapplication_id"] not in superseded_reapps]
|
||
|
||
def _chain_tail_verdict(item_id):
|
||
chain = chains.get(item_id)
|
||
if chain:
|
||
return chain[-1]["to_verdict"]
|
||
row = row_by_item.get(item_id)
|
||
return row["phase2a_verdict"] if row else None
|
||
|
||
for rec in traceability["reapplications"]:
|
||
item_id = rec["item_id"]
|
||
has_adjudication_ref = False
|
||
has_intent_ref = False
|
||
for ref in rec["answer_refs"]:
|
||
kind, ref_id = ref.split(":", 1)
|
||
if kind == "intent":
|
||
has_intent_ref = True
|
||
if kind == "adjudication":
|
||
has_adjudication_ref = True
|
||
adjudication = adjudications_by_dissent.get(ref_id)
|
||
dissent = dissent_by_id.get(ref_id)
|
||
if adjudication is None or dissent is None:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: answer ref {ref!r} does not resolve")
|
||
continue
|
||
if adjudication["outcome"] != "original_upheld":
|
||
fails.add(f"reapplication {rec['reapplication_id']}: adjudication ref {ref!r} must resolve to original_upheld (§6)")
|
||
if dissent["item_id"] != item_id:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: adjudication ref {ref!r} is on a different item")
|
||
else:
|
||
intent = intents_by_id.get(ref_id)
|
||
if intent is None:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: answer ref {ref!r} does not resolve")
|
||
elif intent["item_id"] != item_id:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: intent ref {ref!r} is on a different item")
|
||
# effective-criterion rule (§6)
|
||
replacement_dissents = [
|
||
dissent_id for dissent_id, adjudication in adjudications_by_dissent.items()
|
||
if adjudication["outcome"] == "replacement_approved"
|
||
and dissent_by_id.get(dissent_id, {}).get("item_id") == item_id
|
||
]
|
||
if has_adjudication_ref:
|
||
expected_ref = f"phase1:{item_id}"
|
||
elif replacement_dissents:
|
||
expected_ref = f"dissent:{replacement_dissents[0]}"
|
||
else:
|
||
expected_ref = f"phase1:{item_id}"
|
||
if rec["criterion_ref"] != expected_ref:
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: criterion_ref {rec['criterion_ref']!r} "
|
||
f"violates the §6 effective-criterion rule (expected {expected_ref!r})"
|
||
)
|
||
# derived adjustment (§6 loop step 2)
|
||
derived = [
|
||
adj for adj in traceability["adjustments"]
|
||
if adj.get("source_ref") == f"reapplication:{rec['reapplication_id']}"
|
||
]
|
||
# §6 trigger binding: a reapplication whose answer_refs carry ONLY
|
||
# intent refs is a DIVERGENCE re-verification — it exists only for
|
||
# an evaluated must_fix row (one carrying cross_model_verdict, which
|
||
# itself implies an active configuration): "diverges rows exist
|
||
# only under ACTIVE cross-model (a not_configured run has none)".
|
||
# A G2(d) retry is unaffected — its refs are CUMULATIVE and always
|
||
# include the mandating adjudication.
|
||
if has_intent_ref and not has_adjudication_ref:
|
||
row = row_by_item.get(item_id)
|
||
if row is None or row["obligation_class"] != "MUST_FIX" or "cross_model_verdict" not in row:
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: a divergence-only re-application exists only for an "
|
||
"evaluated must_fix row under active cross-model (§6/§5.3) — no committed verdict moves without its "
|
||
"triggering divergence"
|
||
)
|
||
elif row["cross_model_verdict"] == rec["pre_reapplication_verdict"]:
|
||
# §6 dispatch order is normative: diverges is identified
|
||
# FIRST (judge verdict vs the row's verdict), THEN the
|
||
# system intent is emitted and the 2B' call dispatched. A
|
||
# judge verdict equal to the dispatch-time pre-value means
|
||
# no divergence existed to resolve — the chain manufactures
|
||
# its own diverges status after the fact.
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: no divergence existed at dispatch — the judge's "
|
||
"verdict equals the dispatch-time pre_reapplication_verdict (§6)"
|
||
)
|
||
if not any(
|
||
intents_by_id.get(ref.split(":", 1)[1], {}).get("answered_by") == "system"
|
||
for ref in rec["answer_refs"]
|
||
if ref.startswith("intent:")
|
||
):
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: a divergence re-application chain carries the "
|
||
"ORIGINAL mandating system intent (§6 — divergence resolution is MECHANICAL by default; user "
|
||
"intents exist only on the retry paths, cumulatively alongside it)"
|
||
)
|
||
# §5.3 letter-tag condition: a letter-tagged anchor on a
|
||
# reapplication (and hence on its mechanically-copied
|
||
# cross_model_adjudication adjustment) is valid EXACTLY when the
|
||
# re-examined chain projection carried a booked valid_rebuttal
|
||
# record — the §3.4 machine witness that an assertion in the letter
|
||
# with no locatable manuscript evidence changes nothing. The 2B'
|
||
# input set withholds the Response Letter itself, so a booked
|
||
# rebuttal anchor is the only legal letter-side source.
|
||
if rec["reapplied_verdict"] != "CANNOT_VERIFY" and any(
|
||
anchor["anchor_artifact"] == "letter" for anchor in rec["evidence_anchor"]
|
||
):
|
||
letter_tag_allowed = False
|
||
for chain_adj in chains.get(item_id, []):
|
||
if len(derived) == 1 and chain_adj["adjustment_id"] == derived[0]["adjustment_id"]:
|
||
break
|
||
if chain_adj["basis"] == "valid_rebuttal":
|
||
letter_tag_allowed = True
|
||
break
|
||
if not letter_tag_allowed:
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: letter-tagged anchors are valid exactly when the "
|
||
"re-examined chain carries a booked valid_rebuttal record (§5.3/§3.4)"
|
||
)
|
||
changing = rec["reapplied_verdict"] != "CANNOT_VERIFY" and rec["reapplied_verdict"] != rec["pre_reapplication_verdict"]
|
||
if rec["reapplied_verdict"] == "CANNOT_VERIFY":
|
||
if derived:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: CANNOT_VERIFY appends NOTHING mechanically (§6)")
|
||
elif changing:
|
||
if len(derived) != 1:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: a verdict-changing reapplication requires exactly one derived adjustment (§13)")
|
||
else:
|
||
adj = derived[0]
|
||
if adj["item_id"] != item_id or adj["basis"] != "cross_model_adjudication":
|
||
fails.add(f"reapplication {rec['reapplication_id']}: derived adjustment must be a cross_model_adjudication on the same item")
|
||
if adj["from_verdict"] != rec["pre_reapplication_verdict"] or adj["to_verdict"] != rec["reapplied_verdict"]:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: derived adjustment endpoints must bind to the RECORDED pre-value (§6)")
|
||
if adj["rationale"] != rec["rationale"]:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: derived adjustment rationale must be MECHANICALLY COPIED (§6)")
|
||
if adj.get("residual_gap") != rec.get("residual_gap"):
|
||
fails.add(f"reapplication {rec['reapplication_id']}: derived adjustment residual_gap must be MECHANICALLY COPIED (§6)")
|
||
if adj.get("evidence_anchor") != rec.get("evidence_anchor"):
|
||
fails.add(f"reapplication {rec['reapplication_id']}: derived adjustment anchors must be MECHANICALLY COPIED, tags included (§5.3)")
|
||
else:
|
||
if derived:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: no verdict change, so no derived adjustment may exist (§6)")
|
||
|
||
# every original_upheld adjudication + every intent appears in exactly one CURRENT record's answer_refs
|
||
def _answers_in_current(ref: str):
|
||
return [rec for rec in current_reapps if ref in rec["answer_refs"]]
|
||
|
||
for dissent_id, adjudication in adjudications_by_dissent.items():
|
||
if adjudication["outcome"] == "original_upheld":
|
||
holders = _answers_in_current(f"adjudication:{dissent_id}")
|
||
if len(holders) != 1:
|
||
fails.add(
|
||
f"dissent adjudication {dissent_id}: original_upheld must appear in exactly one CURRENT reapplication's "
|
||
f"answer_refs (found {len(holders)}) — silent non-reapplication cannot pass (§6)"
|
||
)
|
||
for intent_id in intents_by_id:
|
||
holders = _answers_in_current(f"intent:{intent_id}")
|
||
if len(holders) != 1:
|
||
fails.add(
|
||
f"resolution intent {intent_id}: must appear in exactly one CURRENT reapplication's answer_refs "
|
||
f"(found {len(holders)}) (§6)"
|
||
)
|
||
|
||
# --- cross-model resolutions (§5.3/§9) ---
|
||
resolutions_by_reapp = {}
|
||
for rec in traceability["cross_model_resolutions"]:
|
||
if rec["reapplication_id"] in resolutions_by_reapp:
|
||
fails.add(f"reapplication {rec['reapplication_id']}: referenced by more than one CrossModelResolution")
|
||
resolutions_by_reapp[rec["reapplication_id"]] = rec
|
||
# §6 loop step 2: when an intent-mandated re-application derives a state
|
||
# (reapplied_verdict != CANNOT_VERIFY) the resolution is created
|
||
# MECHANICALLY — its absence means the pipeline skipped a mechanical step.
|
||
for rec in current_reapps:
|
||
has_intent_ref = any(ref.startswith("intent:") for ref in rec["answer_refs"])
|
||
if has_intent_ref and rec["reapplied_verdict"] != "CANNOT_VERIFY":
|
||
resolution = resolutions_by_reapp.get(rec["reapplication_id"])
|
||
if resolution is None or resolution["resolved_by"] != "system":
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: a derived state mandates a mechanically-created "
|
||
"system CrossModelResolution (§6 loop step 2)"
|
||
)
|
||
for rec in traceability["cross_model_resolutions"]:
|
||
intent = intents_by_id.get(rec["intent_id"])
|
||
reapp = reapp_by_id.get(rec["reapplication_id"])
|
||
if intent is None or intent["item_id"] != rec["item_id"]:
|
||
fails.add(f"resolution {rec['resolution_id']}: intent_id does not resolve on the same item")
|
||
if reapp is None or reapp["item_id"] != rec["item_id"]:
|
||
fails.add(f"resolution {rec['resolution_id']}: reapplication_id does not resolve on the same item")
|
||
continue
|
||
if f"intent:{rec['intent_id']}" not in reapp["answer_refs"]:
|
||
fails.add(
|
||
f"resolution {rec['resolution_id']}: reapplication {rec['reapplication_id']} did not answer "
|
||
f"intent {rec['intent_id']} — the resolution derives FROM the mandated re-application, never from "
|
||
"an unrelated same-item record (§6 loop step 2)"
|
||
)
|
||
if rec["resolved_by"] == "system":
|
||
if reapp["reapplied_verdict"] == "CANNOT_VERIFY":
|
||
fails.add(f"resolution {rec['resolution_id']}: a CANNOT_VERIFY reapplication derives no system resolution (§5.3)")
|
||
else:
|
||
expected_state = "primary_upheld" if reapp["reapplied_verdict"] == reapp["pre_reapplication_verdict"] else "primary_revised"
|
||
if rec["state"] != expected_state:
|
||
fails.add(f"resolution {rec['resolution_id']}: state {rec['state']!r} != §9 derivation ({expected_state!r})")
|
||
else:
|
||
if reapp["reapplied_verdict"] != "CANNOT_VERIFY":
|
||
fails.add(f"resolution {rec['resolution_id']}: the user acceptance form requires a CANNOT_VERIFY reapplication (§5.3)")
|
||
|
||
# --- G2(d) acceptances ---
|
||
acceptances_by_id = {}
|
||
for rec in traceability["g2d_acceptances"]:
|
||
if rec["acceptance_id"] in acceptances_by_id:
|
||
fails.add(f"duplicate acceptance_id {rec['acceptance_id']}")
|
||
acceptances_by_id[rec["acceptance_id"]] = rec
|
||
reapp = reapp_by_id.get(rec["reapplication_id"])
|
||
if reapp is None or reapp["item_id"] != rec["item_id"]:
|
||
fails.add(f"acceptance {rec['acceptance_id']}: reapplication_id does not resolve on the same item")
|
||
elif reapp["reapplied_verdict"] != "CANNOT_VERIFY":
|
||
fails.add(f"acceptance {rec['acceptance_id']}: the accepted reapplication must be CANNOT_VERIFY (§6 G2(d))")
|
||
if rec["reapplication_id"] in superseded_reapps:
|
||
fails.add(
|
||
f"acceptance {rec['acceptance_id']}: references a SUPERSEDED reapplication — an acceptance binds "
|
||
"to the CURRENT failed attempt, never a stale one (§6 G2(d))"
|
||
)
|
||
row = row_by_item.get(rec["item_id"])
|
||
if row is not None and row["final_verdict"] != "CANNOT_VERIFY":
|
||
fails.add(f"acceptance {rec['acceptance_id']}: the accepted item's final_verdict must be CANNOT_VERIFY (§6 G2(d))")
|
||
backing = [
|
||
adj for adj in traceability["adjustments"]
|
||
if adj["basis"] == "user_accepted_fail_closed"
|
||
and adj.get("source_ref") == f"acceptance:{rec['acceptance_id']}"
|
||
]
|
||
if len(backing) != 1:
|
||
fails.add(
|
||
f"acceptance {rec['acceptance_id']}: a G2(d) acceptance backs exactly ONE user_accepted_fail_closed "
|
||
f"adjustment (found {len(backing)}) — the one legal CANNOT_VERIFY append; an orphan acceptance cannot "
|
||
"clear the deferral (§6 G2(d)/§3.4)"
|
||
)
|
||
acceptance_adj_by_reapp = {}
|
||
for adj in traceability["adjustments"]:
|
||
if adj["basis"] == "user_accepted_fail_closed":
|
||
acc_id = adj["source_ref"].split(":", 1)[1]
|
||
acceptance = acceptances_by_id.get(acc_id)
|
||
if acceptance is None or acceptance["item_id"] != adj["item_id"]:
|
||
fails.add(f"adjustment {adj['adjustment_id']}: acceptance ref does not resolve on the same item")
|
||
continue
|
||
acceptance_adj_by_reapp[acceptance["reapplication_id"]] = adj
|
||
reapp = reapp_by_id.get(acceptance["reapplication_id"])
|
||
if reapp is not None and reapp.get("cannot_verify_reason") != adj.get("cannot_verify_reason"):
|
||
fails.add(f"adjustment {adj['adjustment_id']}: cannot_verify_reason must be copied from the accepted reapplication (§3.4)")
|
||
elif adj["basis"] == "cross_model_adjudication":
|
||
rap_id = adj["source_ref"].split(":", 1)[1]
|
||
if rap_id not in reapp_by_id:
|
||
fails.add(f"adjustment {adj['adjustment_id']}: reapplication ref does not resolve")
|
||
|
||
# pre_reapplication_verdict consistency (§13): the recorded pre-value is
|
||
# the TIME ANCHOR — it equals this reapplication's own derived
|
||
# adjustment's from_verdict when one exists; on the G2(d) path it equals
|
||
# the acceptance-backed adjustment's from_verdict (that adjustment
|
||
# consumed this reapplication, so the tail moved AFTER dispatch); else
|
||
# the current chain tail; else phase2a_verdict when the item has no chain.
|
||
for rec in traceability["reapplications"]:
|
||
derived = [
|
||
adj for adj in traceability["adjustments"]
|
||
if adj.get("source_ref") == f"reapplication:{rec['reapplication_id']}"
|
||
]
|
||
if rec["reapplication_id"] in superseded_reapps:
|
||
# UNCONDITIONAL for every superseded record — a derived
|
||
# adjustment must not bypass this branch. Supersession is
|
||
# defined for FAILED attempts only (§6 — "a retry names the
|
||
# failed attempt it supersedes"), and a failed attempt appends
|
||
# nothing, so its dispatch tail equals its direct retry's
|
||
# recorded pre-value. The current-tail fallback never applies
|
||
# to a historical record (the tail may have moved since).
|
||
successor = reapp_by_id[superseded_reapps[rec["reapplication_id"]]]
|
||
if rec["reapplied_verdict"] != "CANNOT_VERIFY":
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: only FAILED (CANNOT_VERIFY) attempts are superseded "
|
||
"by a retry (§6)"
|
||
)
|
||
if rec["pre_reapplication_verdict"] != successor["pre_reapplication_verdict"]:
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: a failed attempt appends nothing, so its dispatch "
|
||
f"tail equals its direct retry's pre_reapplication_verdict "
|
||
f"({rec['pre_reapplication_verdict']!r} != {successor['pre_reapplication_verdict']!r}, §6/§13)"
|
||
)
|
||
continue
|
||
if len(derived) == 1:
|
||
expected_pre = derived[0]["from_verdict"]
|
||
elif rec["reapplication_id"] in acceptance_adj_by_reapp:
|
||
expected_pre = acceptance_adj_by_reapp[rec["reapplication_id"]]["from_verdict"]
|
||
else:
|
||
expected_pre = _chain_tail_verdict(rec["item_id"])
|
||
if expected_pre is not None and rec["pre_reapplication_verdict"] != expected_pre:
|
||
fails.add(
|
||
f"reapplication {rec['reapplication_id']}: pre_reapplication_verdict {rec['pre_reapplication_verdict']!r} "
|
||
f"!= the chain tail as it stood at dispatch ({expected_pre!r}, §13)"
|
||
)
|
||
|
||
# --- critical-rebuttal machinery (§3.4/§5.3) ---
|
||
radj_by_id = {}
|
||
for rec in traceability["rebuttal_adjudications"]:
|
||
if rec["rebuttal_adjudication_id"] in radj_by_id:
|
||
fails.add(f"duplicate rebuttal_adjudication_id {rec['rebuttal_adjudication_id']}")
|
||
radj_by_id[rec["rebuttal_adjudication_id"]] = rec
|
||
if not cross_model_active:
|
||
fails.add(f"rebuttal adjudication {rec['rebuttal_adjudication_id']}: the §3.4 pass exists only under active cross-model (§11)")
|
||
adjustments_by_id = {adj["adjustment_id"]: adj for adj in traceability["adjustments"]}
|
||
|
||
def _is_critical(item_id):
|
||
item = roadmap_by_id.get(item_id)
|
||
return item is not None and item.get("severity") == "critical"
|
||
|
||
for adj in traceability["adjustments"]:
|
||
should_have_check = adj["basis"] == "valid_rebuttal" and _is_critical(adj["item_id"])
|
||
has_check = "critical_rebuttal_check" in adj
|
||
if should_have_check != has_check:
|
||
fails.add(
|
||
f"adjustment {adj['adjustment_id']}: critical_rebuttal_check present iff the adjustment is a critical "
|
||
f"valid_rebuttal (§3.4)"
|
||
)
|
||
if has_check:
|
||
check_value = adj["critical_rebuttal_check"]
|
||
match = _CHECK_ADJUDICATED_RE.match(check_value) if isinstance(check_value, str) else None
|
||
if match:
|
||
radj = radj_by_id.get(match.group(1))
|
||
if radj is None or radj["verdict"] != "upheld" or radj["item_id"] != adj["item_id"]:
|
||
fails.add(f"adjustment {adj['adjustment_id']}: adjudicated:* must resolve to an upheld RebuttalAdjudication on the same item (§3.4)")
|
||
elif check_value == "single_family_disclosed" and cross_model_active:
|
||
fails.add(f"adjustment {adj['adjustment_id']}: single_family_disclosed is invalid on an active cross-model run (§3.4)")
|
||
|
||
proposals_by_id = {}
|
||
booked_by_adjustment = {}
|
||
seen_drafted_bodies = {}
|
||
challenged_bodies = []
|
||
for rec in traceability["pending_rebuttal_upgrades"]:
|
||
if rec["proposal_id"] in proposals_by_id:
|
||
fails.add(f"duplicate proposal_id {rec['proposal_id']}")
|
||
proposals_by_id[rec["proposal_id"]] = rec
|
||
kind, ref_id = rec["disposition"].split(":", 1)
|
||
if rec["drafted_adjustment"]["item_id"] != rec["item_id"]:
|
||
fails.add(f"proposal {rec['proposal_id']}: drafted_adjustment.item_id must equal the proposal's item_id (§5.3)")
|
||
body_key = (rec["item_id"], canonical_hash(rec["drafted_adjustment"]))
|
||
if body_key in seen_drafted_bodies:
|
||
fails.add(
|
||
f"proposal {rec['proposal_id']}: duplicate PendingRebuttalUpgrade drafted body on item {rec['item_id']} "
|
||
f"(same canonical body as {seen_drafted_bodies[body_key]}) — one proposal per drafted upgrade (§5.3)"
|
||
)
|
||
seen_drafted_bodies[body_key] = rec["proposal_id"]
|
||
if kind == "challenged":
|
||
challenged_bodies.append((rec["proposal_id"], rec["item_id"], rec["drafted_adjustment"]))
|
||
if kind == "challenged":
|
||
radj = radj_by_id.get(ref_id)
|
||
if radj is None or radj["item_id"] != rec["item_id"]:
|
||
fails.add(f"proposal {rec['proposal_id']}: challenged ref does not resolve on the same item")
|
||
elif radj["verdict"] != "challenged":
|
||
fails.add(f"proposal {rec['proposal_id']}: challenged disposition must reference a challenged adjudication")
|
||
else:
|
||
adj = adjustments_by_id.get(ref_id)
|
||
if adj is None or adj["item_id"] != rec["item_id"] or adj["basis"] != "valid_rebuttal":
|
||
fails.add(f"proposal {rec['proposal_id']}: booked ref must resolve to a valid_rebuttal adjustment on the same item")
|
||
continue
|
||
if ref_id in booked_by_adjustment:
|
||
fails.add(f"proposal {rec['proposal_id']}: adjustment {ref_id} already booked by {booked_by_adjustment[ref_id]}")
|
||
booked_by_adjustment[ref_id] = rec["proposal_id"]
|
||
drafted = dict(rec["drafted_adjustment"])
|
||
booked = {
|
||
key: value for key, value in adj.items()
|
||
if key not in ("adjustment_id", "supersedes_adjustment_id", "critical_rebuttal_check")
|
||
}
|
||
if drafted != booked:
|
||
fails.add(
|
||
f"proposal {rec['proposal_id']}: booked adjustment must content-equal the drafted body "
|
||
"field-for-field, excluding EXACTLY {adjustment_id, supersedes_adjustment_id, critical_rebuttal_check} (§5.3)"
|
||
)
|
||
check_value = adj.get("critical_rebuttal_check")
|
||
if kind == "booked":
|
||
match = _CHECK_ADJUDICATED_RE.match(check_value) if isinstance(check_value, str) else None
|
||
if not match or radj_by_id.get(match.group(1), {}).get("verdict") != "upheld":
|
||
fails.add(f"proposal {rec['proposal_id']}: booked disposition requires an upheld adjudicated:* check (§3.4)")
|
||
elif kind == "booked_disclosed":
|
||
if check_value != "pass_unavailable_disclosed":
|
||
fails.add(f"proposal {rec['proposal_id']}: booked_disclosed requires critical_rebuttal_check pass_unavailable_disclosed")
|
||
if not cross_model_active:
|
||
fails.add(f"proposal {rec['proposal_id']}: booked_disclosed exists only under active cross-model (§11)")
|
||
elif kind == "booked_single_family":
|
||
if check_value != "single_family_disclosed":
|
||
fails.add(f"proposal {rec['proposal_id']}: booked_single_family requires critical_rebuttal_check single_family_disclosed")
|
||
if cross_model_active:
|
||
fails.add(f"proposal {rec['proposal_id']}: booked_single_family is invalid under active cross-model (§3.4)")
|
||
for adj in traceability["adjustments"]:
|
||
if adj["basis"] == "valid_rebuttal" and _is_critical(adj["item_id"]) and adj["adjustment_id"] not in booked_by_adjustment:
|
||
fails.add(
|
||
f"adjustment {adj['adjustment_id']}: every critical valid_rebuttal adjustment traces to exactly one "
|
||
"booked* PendingRebuttalUpgrade (§5.3)"
|
||
)
|
||
if adj["basis"] == "valid_rebuttal":
|
||
booked_view = {
|
||
key: value for key, value in adj.items()
|
||
if key not in ("adjustment_id", "supersedes_adjustment_id", "critical_rebuttal_check")
|
||
}
|
||
for proposal_id, item_id, drafted in challenged_bodies:
|
||
if adj["item_id"] == item_id and booked_view == drafted:
|
||
fails.add(
|
||
f"adjustment {adj['adjustment_id']}: content-equals CHALLENGED proposal {proposal_id}'s "
|
||
"drafted body — a challenged upgrade is NEVER booked (§3.4)"
|
||
)
|
||
|
||
# --- escalation approvals ---
|
||
approvals_by_exception = {}
|
||
for rec in traceability["escalation_approvals"]:
|
||
if rec["exception_id"] in approvals_by_exception:
|
||
fails.add(f"duplicate escalation approval for {rec['exception_id']}")
|
||
approvals_by_exception[rec["exception_id"]] = rec
|
||
if rec["exception_id"] not in exceptions_by_id:
|
||
fails.add(f"escalation approval {rec['exception_id']}: no such exception record")
|
||
|
||
# --- cross-model activity consistency (§11) ---
|
||
if not cross_model_active:
|
||
for item_id, row in row_by_item.items():
|
||
if row.get("cross_model_status") in ("agree", "diverges", "unavailable"):
|
||
fails.add(f"row {item_id}: cross_model_status {row['cross_model_status']!r} implies an active configuration (§11)")
|
||
|
||
# --- DecisionInputs recomputation (§13) ---
|
||
di = traceability["decision_inputs"]
|
||
|
||
def _final_residual_obligation_class(item_id):
|
||
row = row_by_item.get(item_id)
|
||
if row is None or row["final_verdict"] != "PARTIALLY_ADDRESSED":
|
||
return None
|
||
chain = chains.get(item_id)
|
||
record = chain[-1] if chain else verdict_by_item.get(item_id)
|
||
if record is None:
|
||
return None
|
||
gap = record.get("residual_gap")
|
||
return gap["residual_obligation_class"] if gap else None
|
||
|
||
expected_per_item = []
|
||
for item_id in must_fix_order:
|
||
row = row_by_item.get(item_id)
|
||
if row is None:
|
||
continue
|
||
item = roadmap_by_id[item_id]
|
||
severity = item.get("severity")
|
||
# §5.3: a null driving_severity is legal ONLY for source_kind items
|
||
# and legacy roadmaps. A must_fix item carrying other transported markers
|
||
# (#574 A2/A3) but no severity is a half-transported finding-driven
|
||
# item, not a legacy one — B1's critical join would be silently
|
||
# suppressed, so it fails here.
|
||
if (
|
||
severity is None
|
||
and item.get("source_kind") is None
|
||
and any(item.get(field) is not None for field in ("evidence_anchor", "confidence", "competence_basis"))
|
||
):
|
||
fails.add(
|
||
f"roadmap item {item_id}: transported fields present but severity absent — a non-legacy "
|
||
"finding-driven must_fix item cannot carry driving_severity null (§5.3)"
|
||
)
|
||
entry = {
|
||
"item_id": item_id,
|
||
"final_verdict": row["final_verdict"],
|
||
"driving_severity": severity if severity in SEVERITIES else None,
|
||
}
|
||
magnitude = _final_residual_obligation_class(item_id)
|
||
if magnitude is not None:
|
||
entry["residual_obligation_class"] = magnitude
|
||
expected_per_item.append(entry)
|
||
if di["per_item"] != expected_per_item:
|
||
fails.add("decision_inputs.per_item does not equal the recomputed per-must_fix operand list (immutable roadmap order, §13)")
|
||
|
||
expected_counts = {prio: {verdict: 0 for verdict in VERDICTS} for prio in OBLIGATION_CLASSES}
|
||
expected_magnitudes = {prio: {mag: 0 for mag in RESIDUAL_OBLIGATION_CLASSES} for prio in OBLIGATION_CLASSES}
|
||
for item_id, row in row_by_item.items():
|
||
item = roadmap_by_id.get(item_id)
|
||
if item is None:
|
||
continue
|
||
expected_counts[item["obligation_class"]][row["final_verdict"]] += 1
|
||
if row["final_verdict"] == "PARTIALLY_ADDRESSED":
|
||
magnitude = _final_residual_obligation_class(item_id)
|
||
if magnitude is not None:
|
||
expected_magnitudes[item["obligation_class"]][magnitude] += 1
|
||
if di["verdict_counts"] != expected_counts:
|
||
fails.add("decision_inputs.verdict_counts does not recompute from the final row verdicts (§13)")
|
||
if di["residual_obligation_class_counts"] != expected_magnitudes:
|
||
fails.add("decision_inputs.residual_obligation_class_counts does not recompute from the PARTIALLY_ADDRESSED rows (§13)")
|
||
|
||
p2_ids = [item["id"] for item in roadmap_items if item["obligation_class"] == "should_fix"]
|
||
expected_num = sum(
|
||
1 for item_id in p2_ids
|
||
if row_by_item.get(item_id, {}).get("final_verdict") in ("FULLY_ADDRESSED", "PARTIALLY_ADDRESSED")
|
||
)
|
||
if di["should_fix_addressed_rate"] != {"numerator": expected_num, "denominator": len(p2_ids)}:
|
||
fails.add("decision_inputs.should_fix_addressed_rate does not recompute over final should_fix verdicts (§6)")
|
||
|
||
expected_regressions = [
|
||
{"new_issue_id": rec["new_issue_id"], "severity": rec["severity"]}
|
||
for rec in verdict_record["new_issues"] if rec["attribution"] == "regression"
|
||
]
|
||
expected_non_regression = [
|
||
rec["new_issue_id"] for rec in verdict_record["new_issues"] if rec["attribution"] != "regression"
|
||
]
|
||
if di["regressions"] != expected_regressions:
|
||
fails.add("decision_inputs.regressions does not equal the regression-attributed frozen new issues (§8/§13)")
|
||
if di["non_regression_new_issue_ids"] != expected_non_regression:
|
||
fails.add("decision_inputs.non_regression_new_issue_ids does not equal the frozen previously_missed/indeterminate ids (§8/§13)")
|
||
|
||
expected_escalations = []
|
||
for rec in verdict_record["escalation_exceptions"]:
|
||
approval = approvals_by_exception.get(rec["exception_id"])
|
||
expected_escalations.append({
|
||
"exception_id": rec["exception_id"],
|
||
"effective_approval_state": approval["approval_state"] if approval else "pending",
|
||
"escalation_class": rec["escalation_class"],
|
||
"mechanical_decision_impact": rec["mechanical_decision_impact"],
|
||
})
|
||
if di["escalations"] != expected_escalations:
|
||
fails.add("decision_inputs.escalations does not equal the joined exception/approval summary (§5.3)")
|
||
|
||
if di["apply_chain_witness"] != witness:
|
||
fails.add(f"decision_inputs.apply_chain_witness {di['apply_chain_witness']!r} != recomputed witness {witness!r} (§11)")
|
||
|
||
# --- §6 Step 1 recomputation (gates) ---
|
||
g1 = any(
|
||
row["final_verdict"] != row["phase2a_verdict"] and row["item_id"] not in chains
|
||
for row in traceability["rows"]
|
||
)
|
||
pending = []
|
||
# bound_tripped computed once, next to dissent_by_id (§7 SD-5).
|
||
if bound_tripped:
|
||
for dissent_id in dissent_by_id:
|
||
if dissent_id not in adjudications_by_dissent:
|
||
pending.append(f"G2(a): dissent {dissent_id} lacks its DissentAdjudication (bound tripped — covering cardinality)")
|
||
|
||
def _covering_resolution(item_id, final_verdict):
|
||
for rec in traceability["cross_model_resolutions"]:
|
||
if rec["item_id"] != item_id:
|
||
continue
|
||
reapp = reapp_by_id.get(rec["reapplication_id"])
|
||
if reapp is None:
|
||
continue
|
||
anchor = reapp["reapplied_verdict"] if rec["resolved_by"] == "system" else reapp["pre_reapplication_verdict"]
|
||
if anchor == final_verdict:
|
||
return True
|
||
return False
|
||
|
||
for item_id, row in row_by_item.items():
|
||
if row.get("cross_model_status") == "diverges" and ROW_OBLIGATION_MAP[row["obligation_class"]] == "must_fix":
|
||
if not _covering_resolution(item_id, row["final_verdict"]):
|
||
pending.append(f"G2(b): must_fix diverges row {item_id} has no covering CrossModelResolution")
|
||
for rec in verdict_record["escalation_exceptions"]:
|
||
if rec["exception_id"] not in approvals_by_exception:
|
||
pending.append(f"G2(c): escalation exception {rec['exception_id']} is pending user approval")
|
||
accepted_reapps = {rec["reapplication_id"] for rec in traceability["g2d_acceptances"]}
|
||
for dissent_id, adjudication in adjudications_by_dissent.items():
|
||
if adjudication["outcome"] != "original_upheld":
|
||
continue
|
||
holders = _answers_in_current(f"adjudication:{dissent_id}")
|
||
if len(holders) != 1:
|
||
continue # already a synthesis failure above
|
||
current = holders[0]
|
||
if current["reapplied_verdict"] == "CANNOT_VERIFY" and current["reapplication_id"] not in accepted_reapps:
|
||
pending.append(f"G2(d): original_upheld dissent {dissent_id} fail-closed reapplication awaits user resolution")
|
||
|
||
# --- emission-kind-scoped equality set (§13) ---
|
||
recomputed = {"pending": pending, "g1": g1}
|
||
emitted_state = traceability["decision_state"]
|
||
if emitted_state == "aborted":
|
||
# §13: an aborted emission carries its true root-cause abort_reason
|
||
# and is EXEMPT from the deferral biconditional and Steps 2-3 (abort
|
||
# precedence over deferral). The only checker-recomputable reason is
|
||
# criteria_drift; the phase-lint reasons are fenced-call facts the
|
||
# checker cannot re-derive and are accepted as recorded (the
|
||
# manifest-class reasons would already have exited 2 above).
|
||
reason = traceability.get("abort_reason")
|
||
recomputed["expect"] = ("aborted", reason)
|
||
if "reject_recommended" in di:
|
||
fails.add("decision_inputs.reject_recommended must be ABSENT on a gated emission (§5.3 presence biconditional)")
|
||
if reason in ("manifest_incomplete", "manifest_hash_mismatch", "synthesis_mismatch"):
|
||
fails.add(
|
||
f"aborted emission claims {reason!r} as root cause, but the checker reached recomputation — the "
|
||
"§11 manifest layer validated against these same hash-bound inputs, so the claimed abort cannot be "
|
||
"the true root cause (§13: an aborted emission carries its TRUE root-cause abort_reason)"
|
||
)
|
||
if g1 and reason != "criteria_drift":
|
||
fails.add("Step 1 recomputes G1 (silent verdict change) — the aborted emission's true root cause is criteria_drift (§13)")
|
||
if reason == "criteria_drift" and not g1:
|
||
fails.add("the emission claims [RE-REVIEW-ABORT: criteria_drift] but no silent verdict change recomputes (§13)")
|
||
elif g1:
|
||
recomputed["expect"] = ("aborted", "criteria_drift")
|
||
fails.add("Step 1 recomputes G1 (silent verdict change) — the emission must be [RE-REVIEW-ABORT: criteria_drift]")
|
||
elif pending:
|
||
recomputed["expect"] = ("user_review_required", None)
|
||
if emitted_state != "user_review_required":
|
||
fails.add(
|
||
"G2 deferral biconditional: pending user-input state(s) exist, so user_review_required is the only "
|
||
"checker-valid decision_state — " + "; ".join(pending)
|
||
)
|
||
if "reject_recommended" in di:
|
||
fails.add("decision_inputs.reject_recommended must be ABSENT on a gated emission (§5.3 presence biconditional)")
|
||
else:
|
||
if emitted_state == "user_review_required":
|
||
fails.add(
|
||
"Step 1 recomputes no gate, but the emission carries decision_state 'user_review_required' "
|
||
"(the G2 biconditional runs both directions)"
|
||
)
|
||
else:
|
||
# Steps 2-3, recomputed independently from the raw records...
|
||
p1_operands = []
|
||
for item_id in must_fix_order:
|
||
row = row_by_item.get(item_id)
|
||
if row is None:
|
||
continue
|
||
item = roadmap_by_id[item_id]
|
||
severity = item.get("severity") if item.get("severity") in SEVERITIES else None
|
||
p1_operands.append((row["final_verdict"], severity, _final_residual_obligation_class(item_id)))
|
||
p2_partial_magnitudes = [
|
||
_final_residual_obligation_class(item_id) for item_id in p2_ids
|
||
if row_by_item.get(item_id, {}).get("final_verdict") == "PARTIALLY_ADDRESSED"
|
||
]
|
||
p2_made_worse = any(
|
||
row_by_item.get(item_id, {}).get("final_verdict") == "MADE_WORSE" for item_id in p2_ids
|
||
)
|
||
regression_severities = {entry["severity"] for entry in expected_regressions}
|
||
raw_decision, raw_reject, raw_rule = derive_decision(
|
||
p1_operands, p2_partial_magnitudes, p2_made_worse,
|
||
expected_num, len(p2_ids), regression_severities, expected_escalations,
|
||
)
|
||
# ...AND from the emitted DecisionInputs copies.
|
||
di_p1 = [
|
||
(entry["final_verdict"], entry["driving_severity"], entry.get("residual_obligation_class"))
|
||
for entry in di["per_item"]
|
||
]
|
||
di_p2_partials = [
|
||
mag for mag in RESIDUAL_OBLIGATION_CLASSES
|
||
for _ in range(di["residual_obligation_class_counts"]["should_fix"][mag])
|
||
]
|
||
di_decision, di_reject, _di_rule = derive_decision(
|
||
di_p1,
|
||
di_p2_partials,
|
||
di["verdict_counts"]["should_fix"]["MADE_WORSE"] > 0,
|
||
di["should_fix_addressed_rate"]["numerator"],
|
||
di["should_fix_addressed_rate"]["denominator"],
|
||
{entry["severity"] for entry in di["regressions"]},
|
||
di["escalations"],
|
||
)
|
||
recomputed["expect"] = (raw_decision, None)
|
||
recomputed["rule"] = raw_rule
|
||
if emitted_state != raw_decision:
|
||
fails.add(f"decision_state {emitted_state!r} != Steps 2-3 recomputed from the raw records ({raw_decision!r}, base {raw_rule})")
|
||
if di_decision != raw_decision:
|
||
fails.add(f"Steps 2-3 from DecisionInputs ({di_decision!r}) diverge from the raw-record recomputation ({raw_decision!r})")
|
||
if "reject_recommended" not in di:
|
||
fails.add("decision_inputs.reject_recommended must be PRESENT on a non-gated emission (§5.3 presence biconditional)")
|
||
else:
|
||
if di["reject_recommended"] != raw_reject:
|
||
fails.add(f"reject_recommended {di['reject_recommended']!r} != recomputed {raw_reject!r} (§6)")
|
||
if di_reject != raw_reject:
|
||
fails.add("reject_recommended recomputed from DecisionInputs diverges from the raw-record recomputation")
|
||
|
||
return fails, warnings, recomputed
|
||
|
||
|
||
# --- CLI -----------------------------------------------------------------------
|
||
|
||
|
||
def _load_json(path: Path, reason: str, label: str):
|
||
try:
|
||
return _strict_json_bytes(path.read_bytes(), label)
|
||
except (OSError, ValueError) as exc:
|
||
if reason.startswith("manifest"):
|
||
raise ManifestError(reason, f"{label}: unreadable or invalid JSON ({exc})")
|
||
raise ArtifactSchemaError(reason, f"{label}: unreadable or invalid JSON ({exc})")
|
||
|
||
|
||
def _verify_file_hash(path: Path, expected_sha256: str, label: str) -> bytes:
|
||
try:
|
||
raw = path.read_bytes()
|
||
except OSError as exc:
|
||
raise ManifestError("manifest_incomplete", f"{label}: unreadable ({exc})")
|
||
actual = hashlib.sha256(raw).hexdigest()
|
||
if actual != expected_sha256:
|
||
raise ManifestError("manifest_hash_mismatch", f"{label}: file bytes do not match the manifest sha256 (§11)")
|
||
return raw
|
||
|
||
|
||
def run(argv=None) -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||
parser.add_argument("--manifest", type=Path, required=True)
|
||
parser.add_argument("--precommitment", type=Path, required=True)
|
||
parser.add_argument("--verdict-record", type=Path, required=True)
|
||
parser.add_argument("--traceability", type=Path, required=True)
|
||
parser.add_argument("--roadmap", type=Path, required=True,
|
||
help="the file the manifest's revision_roadmap entry hash-binds (Schema 7 machine form)")
|
||
parser.add_argument(
|
||
"--author-adjudication",
|
||
type=Path,
|
||
required=True,
|
||
help="the exact #670 author sidecar hash-bound by the current input manifest",
|
||
)
|
||
parser.add_argument(
|
||
"--revision-evidence-bundle",
|
||
type=Path,
|
||
required=True,
|
||
help="the exact #670 bundle hash-bound by the current input manifest",
|
||
)
|
||
parser.add_argument(
|
||
"--revision-evidence-root",
|
||
type=Path,
|
||
default=None,
|
||
help="root for paths declared inside the bundle (default: bundle parent)",
|
||
)
|
||
parser.add_argument("--letter", type=Path, default=None,
|
||
help="the file the manifest's editorial_decision_letter entry hash-binds (markdown)")
|
||
parser.add_argument("--apply-report", type=Path, action="append", default=[], dest="apply_reports",
|
||
help="apply-report file(s), in the manifest's apply_reports[] order")
|
||
args = parser.parse_args(argv)
|
||
|
||
try:
|
||
manifest = _load_json(args.manifest, "manifest_incomplete", "manifest")
|
||
validate_manifest(manifest)
|
||
artifacts = manifest["artifacts"]
|
||
|
||
roadmap_raw = _verify_file_hash(args.roadmap, artifacts["revision_roadmap"]["sha256"], "revision_roadmap file")
|
||
try:
|
||
roadmap_payload = _strict_json_bytes(roadmap_raw, "revision_roadmap file")
|
||
except ValueError as exc:
|
||
raise ManifestError("manifest_incomplete", str(exc)) from exc
|
||
roadmap_items = load_roadmap(roadmap_payload)
|
||
author_raw = _verify_file_hash(
|
||
args.author_adjudication,
|
||
artifacts["author_adjudication"]["sha256"],
|
||
"author_adjudication file",
|
||
)
|
||
try:
|
||
author_payload = _strict_json_bytes(author_raw, "author_adjudication file")
|
||
except ValueError as exc:
|
||
raise ManifestError("manifest_incomplete", str(exc)) from exc
|
||
original_sha = (
|
||
artifacts["original_manuscript"]["sha256"]
|
||
if artifacts["original_manuscript"]["present"]
|
||
else None
|
||
)
|
||
author_by_item = load_author_adjudication(
|
||
author_payload,
|
||
roadmap_items=roadmap_items,
|
||
roadmap_sha256=hashlib.sha256(roadmap_raw).hexdigest(),
|
||
original_manuscript_sha256=original_sha,
|
||
)
|
||
|
||
letter_blocks = None
|
||
if artifacts["editorial_decision_letter"]["present"]:
|
||
if args.letter is None:
|
||
raise ManifestError("manifest_incomplete", "manifest declares editorial_decision_letter present — --letter is required")
|
||
letter_raw = _verify_file_hash(args.letter, artifacts["editorial_decision_letter"]["sha256"], "editorial_decision_letter file")
|
||
letter_blocks = parse_letter_blocks(letter_raw.decode("utf-8"))
|
||
elif args.letter is not None:
|
||
raise ManifestError("manifest_incomplete", "--letter provided but the manifest declares the letter absent")
|
||
|
||
report_entries = artifacts["apply_reports"]["items"] if artifacts["apply_reports"]["present"] else []
|
||
if len(args.apply_reports) != len(report_entries):
|
||
raise ManifestError(
|
||
"manifest_incomplete",
|
||
f"manifest declares {len(report_entries)} apply report(s); {len(args.apply_reports)} provided via --apply-report",
|
||
)
|
||
report_payloads = []
|
||
for i, (path, entry) in enumerate(zip(args.apply_reports, report_entries)):
|
||
raw = _verify_file_hash(path, entry["sha256"], f"apply report [{i}]")
|
||
try:
|
||
report_payloads.append(_strict_json_bytes(raw, f"apply report [{i}]"))
|
||
except ValueError as exc:
|
||
raise ManifestError("manifest_incomplete", str(exc)) from exc
|
||
witness, witness_notes = compute_apply_chain_witness(manifest, report_payloads)
|
||
|
||
bundle_entry = artifacts["revision_evidence_bundle"]
|
||
bundle_raw = _verify_file_hash(
|
||
args.revision_evidence_bundle,
|
||
bundle_entry["sha256"],
|
||
"revision_evidence_bundle file",
|
||
)
|
||
try:
|
||
bundle = _strict_json_bytes(bundle_raw, "revision_evidence_bundle file")
|
||
except ValueError as exc:
|
||
raise ManifestError("manifest_incomplete", str(exc)) from exc
|
||
try:
|
||
validate_bundle(
|
||
bundle,
|
||
root=args.revision_evidence_root or args.revision_evidence_bundle.parent,
|
||
)
|
||
except RevisionContractError as exc:
|
||
raise ManifestError(
|
||
"manifest_hash_mismatch",
|
||
"revision_evidence_bundle replay failed: " + "; ".join(exc.failures),
|
||
) from exc
|
||
validate_revision_bundle_binding(bundle, manifest)
|
||
|
||
precommitment = _load_json(args.precommitment, "phase1_lint_failed", "precommitment")
|
||
validate_precommitment(precommitment)
|
||
verdict_record = _load_json(args.verdict_record, "phase2a_lint_failed", "verdict_record")
|
||
validate_verdict_record(verdict_record)
|
||
traceability = _load_json(args.traceability, "phase2b_lint_failed", "traceability")
|
||
validate_traceability(traceability)
|
||
|
||
# Hash chain (§11): the same-inputs proof.
|
||
if precommitment["input_manifest_hash"] != canonical_hash(manifest):
|
||
raise ManifestError("manifest_hash_mismatch", "precommitment.input_manifest_hash does not recompute from the manifest (§11 chain)")
|
||
if verdict_record["precommitment_hash"] != canonical_hash(precommitment):
|
||
raise ManifestError("manifest_hash_mismatch", "verdict_record.precommitment_hash does not recompute from the precommitment artifact (§11 chain)")
|
||
if traceability["verdict_record_hash"] != canonical_hash(verdict_record):
|
||
raise ManifestError("manifest_hash_mismatch", "traceability.verdict_record_hash does not recompute from the verdict_record artifact (§11 chain)")
|
||
except ManifestError as exc:
|
||
print(f"[RE-REVIEW-ABORT: {exc.reason}]")
|
||
print(f"FAIL: {exc}")
|
||
return EXIT_INVALID
|
||
except ArtifactSchemaError as exc:
|
||
print(f"[RE-REVIEW-ABORT: {exc.reason}]")
|
||
print(f"FAIL: {exc}")
|
||
return EXIT_INVALID
|
||
|
||
fails, warnings, recomputed = check(
|
||
manifest, precommitment, verdict_record, traceability,
|
||
roadmap_items, author_by_item, letter_blocks, witness, witness_notes,
|
||
)
|
||
for note in warnings:
|
||
print(note, file=sys.stderr)
|
||
if fails.messages:
|
||
print("[RE-REVIEW-ABORT: synthesis_mismatch]")
|
||
for message in fails.messages:
|
||
print(f"MISMATCH: {message}")
|
||
return EXIT_SYNTHESIS
|
||
expect = recomputed.get("expect")
|
||
outcome = expect[0] if expect else traceability["decision_state"]
|
||
print(
|
||
f"re-review synthesis ok: round {manifest['round_id']!r}, revision {traceability['revision']}, "
|
||
f"decision_state {outcome!r}, apply_chain_witness {witness!r}"
|
||
)
|
||
return EXIT_PASS
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(run())
|