scripts/review_protocol.py
scripts/review_protocol.pyBrowse 8 files
13,494 tokens
61,729 bytes
Token encoding: o200k_base
Snapshot 1d17ca4
← Back to SKILL.md
1#!/usr/bin/env python32"""Validate final-review packets, reviewer outputs, and verification receipts."""3 4from __future__ import annotations5 6import argparse7import hashlib8import json9import math10import re11from pathlib import Path12from typing import Any13 14from review_state import (15 _content_fingerprint,16 _NonRegularFileError,17 _read_regular_file,18 _repository_fingerprint,19)20 21PACKET_SOFT_LIMIT_BYTES = 12 * 102422SENTINELS = {"none", "not applicable"}23ROOT_CAUSE_ID = re.compile(r"[A-Z][A-Z0-9_-]*")24NEW_ROOT_CAUSE_ID = re.compile(r"NEW:[a-z0-9]+(?:-[a-z0-9]+)*")25SHA256 = re.compile(r"[0-9a-f]{64}")26PLACEHOLDER_TOKEN = re.compile(r"<(?=\S)[^<>\n]*\S>")27 28REQUIRED_PACKET_TEXT = (29 "task.id",30 "task.original_requirement",31 "task.risk_tier",32 "task.risk_reason",33 "scope_contract.required_behavior",34 "scope_contract.compatibility_requirements",35 "scope_contract.unsupported_cases",36 "scope_contract.supported_alternative",37 "repository.target",38 "repository.merge_base",39 "repository.head",40 "repository.release_boundary",41 "repository.status_evidence_id",42 "repository.complete_diff_command",43 "ledger.path",44 "manifests.task",45 "review_state.evidence_id",46 "review_state.revalidation_command",47 "verification.eligible_concurrent_gates",48 "verification.deferred_gates",49)50REVIEWER_OUTPUT_FIELDS = {51 "verdict",52 "reviewed_fingerprints",53 "checked_inventory_ids",54 "unchecked_inventory_ids",55 "high_risk_dimensions_checked",56 "focused_probes",57 "remaining_uncertainty",58 "findings",59 "sibling_scenario_scan",60 "inspection_call_count",61 "inspection_budget_reason",62}63FINDING_FIELDS = {64 "priority",65 "title",66 "location",67 "failure_scenario",68 "user_consequence",69 "support_basis",70 "baseline_patch_evidence",71 "smallest_safe_correction",72 "root_cause_id",73 "root_cause_evidence",74}75INVENTORY_FIELDS = {76 "contract": {77 "surface",78 "producers",79 "consumers",80 "behavior",81 "exports",82 "adjacent",83 "tests",84 },85 "await-boundary": {86 "operation",87 "state_snapshot",88 "blocking_point",89 "suspended_events",90 "monotonic_evidence",91 "revalidation",92 "side_effects_invariant",93 },94 "authority-data-flow": {95 "input_authority",96 "validation",97 "in_memory_state",98 "persisted_state",99 "retry_replay",100 "output",101 "exception_exposure",102 "cleanup_revocation",103 },104}105 106 107class ProtocolError(ValueError):108 """Raised when a review protocol artifact is incomplete or inconsistent."""109 110 111def _object(value: Any, context: str) -> dict[str, Any]:112 if not isinstance(value, dict):113 raise ProtocolError(f"{context} must be an object.")114 return value115 116 117def _require_exact_fields(value: dict[str, Any], expected: set[str], context: str) -> None:118 missing = sorted(expected - value.keys())119 unexpected = sorted(value.keys() - expected)120 if missing or unexpected:121 raise ProtocolError(122 f"{context} does not match the exact schema: "123 f"missing={missing}, unexpected={unexpected}."124 )125 126 127def _array(value: Any, context: str) -> list[Any]:128 if not isinstance(value, list):129 raise ProtocolError(f"{context} must be an array.")130 return value131 132 133def _text(value: Any, context: str, *, concrete: bool = False) -> str:134 if not isinstance(value, str) or not value.strip():135 raise ProtocolError(f"{context} must be a nonempty string.")136 if concrete and value.strip().lower() in SENTINELS:137 raise ProtocolError(f"{context} must contain concrete evidence.")138 return value139 140 141def _strings(value: Any, context: str) -> list[str]:142 result = [143 _text(item, f"{context}[{index}]") for index, item in enumerate(_array(value, context))144 ]145 if len(result) != len(set(result)):146 raise ProtocolError(f"{context} must not contain duplicates.")147 return result148 149 150def _integer(value: Any, context: str, *, minimum: int) -> int:151 if type(value) is not int or value < minimum:152 qualifier = "positive" if minimum == 1 else "nonnegative"153 raise ProtocolError(f"{context} must be a {qualifier} integer.")154 return value155 156 157def _at(value: dict[str, Any], dotted_path: str) -> Any:158 current: Any = value159 for part in dotted_path.split("."):160 if not isinstance(current, dict) or part not in current:161 raise ProtocolError(f"Missing required packet field: {dotted_path}.")162 current = current[part]163 return current164 165 166FileIdentity = tuple[int, int]167 168 169def _read_bytes(value: Any, context: str) -> tuple[Path, bytes, FileIdentity]:170 requested_path = Path(_text(value, context, concrete=True))171 if not requested_path.is_absolute():172 raise ProtocolError(f"{context} must be an absolute path: {requested_path}.")173 try:174 path = requested_path.resolve(strict=True)175 data, file_stat = _read_regular_file(path)176 except _NonRegularFileError as error:177 raise ProtocolError(f"{context} must be a regular file: {requested_path}.") from error178 except (OSError, ValueError) as error:179 raise ProtocolError(f"Cannot read {context} {requested_path}: {error}") from error180 return path, data, (file_stat.st_dev, file_stat.st_ino)181 182 183def _json_bytes(data: bytes, context: str) -> dict[str, Any]:184 def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:185 result: dict[str, Any] = {}186 for key, value in pairs:187 if key in result:188 raise ProtocolError(f"Duplicate JSON key in {context}: {key!r}.")189 result[key] = value190 return result191 192 def reject_constant(value: str) -> None:193 raise ProtocolError(f"Non-finite JSON number in {context}: {value}.")194 195 def finite_float(value: str) -> float:196 parsed = float(value)197 if not math.isfinite(parsed):198 raise ProtocolError(f"Non-finite JSON number in {context}: {value}.")199 return parsed200 201 try:202 value = json.loads(203 data,204 object_pairs_hook=unique_object,205 parse_constant=reject_constant,206 parse_float=finite_float,207 )208 except ProtocolError:209 raise210 except (RecursionError, UnicodeError, ValueError) as error:211 raise ProtocolError(f"Cannot read JSON object from {context}: {error}") from error212 return _object(value, context)213 214 215def _load_json(path: Path) -> dict[str, Any]:216 _, data, _ = _read_bytes(str(path.resolve()), str(path))217 return _json_bytes(data, str(path))218 219 220def _descriptor(value: Any, context: str) -> tuple[Path, bytes, str, FileIdentity]:221 descriptor = _object(value, context)222 path, data, identity = _read_bytes(descriptor.get("path"), f"{context}.path")223 expected = _text(descriptor.get("sha256"), f"{context}.sha256")224 if not SHA256.fullmatch(expected):225 raise ProtocolError(f"{context}.sha256 must be a lowercase SHA-256 digest.")226 actual = hashlib.sha256(data).hexdigest()227 if actual != expected:228 raise ProtocolError(f"{context} digest mismatch for {path}.")229 return path, data, actual, identity230 231 232def _read_unchanged(path: Path, expected_digest: str, context: str) -> bytes:233 _, data, _ = _read_bytes(str(path.resolve()), context)234 if hashlib.sha256(data).hexdigest() != expected_digest:235 raise ProtocolError(f"{context} changed during protocol validation.")236 return data237 238 239def _pathspec_file(value: Any, context: str) -> list[str]:240 _, data, _ = _read_bytes(value, context)241 try:242 lines = [line for line in data.decode().splitlines() if line]243 except UnicodeError as error:244 raise ProtocolError(f"Cannot decode {context}: {error}") from error245 if not lines or len(lines) != len(set(lines)):246 raise ProtocolError(f"{context} must contain unique nonempty pathspecs.")247 return lines248 249 250def _dependency_map(value: Any, component_names: set[str]) -> None:251 dependencies = _object(value, "manifests.dependency_map")252 if set(dependencies) != component_names:253 raise ProtocolError("manifests.dependency_map must cover the exact component names.")254 for component_name in sorted(component_names):255 context = f"manifests.dependency_map[{component_name!r}]"256 entries = _array(dependencies[component_name], context)257 if not entries:258 raise ProtocolError(f"{context} must contain at least one dependency.")259 pathspecs: set[str] = set()260 for index, raw_entry in enumerate(entries):261 entry_context = f"{context}[{index}]"262 entry = _object(raw_entry, entry_context)263 _require_exact_fields(entry, {"pathspec", "reason"}, entry_context)264 pathspec = _text(entry.get("pathspec"), f"{entry_context}.pathspec", concrete=True)265 _text(entry.get("reason"), f"{entry_context}.reason", concrete=True)266 if pathspec in pathspecs:267 raise ProtocolError(f"{context} contains duplicate pathspec {pathspec!r}.")268 pathspecs.add(pathspec)269 270 271def _command_result(value: Any, context: str) -> None:272 record = _object(value, context)273 _require_exact_fields(record, {"command", "result"}, context)274 command = _text(record.get("command"), f"{context}.command", concrete=True)275 _text(record.get("result"), f"{context}.result", concrete=True)276 if PLACEHOLDER_TOKEN.search(command):277 raise ProtocolError(f"{context}.command contains a placeholder token.")278 279 280def _sha256(value: Any, context: str) -> str:281 digest = _text(value, context)282 if not SHA256.fullmatch(digest):283 raise ProtocolError(f"{context} must be a lowercase SHA-256 digest.")284 return digest285 286 287def _inventory_digest(row: dict[str, Any]) -> str:288 content = {key: value for key, value in row.items() if key != "id"}289 canonical = json.dumps(content, sort_keys=True, separators=(",", ":"))290 return hashlib.sha256(canonical.encode()).hexdigest()291 292 293def _digest_map(value: Any, context: str, expected_ids: set[str]) -> dict[str, str]:294 digests = {295 _text(raw_id, f"{context} key", concrete=True): _sha256(digest, f"{context}.{raw_id}")296 for raw_id, digest in _object(value, context).items()297 }298 actual_ids = set(digests)299 if actual_ids != expected_ids:300 raise ProtocolError(301 f"{context} must bind the exact owned IDs: "302 f"missing={sorted(expected_ids - actual_ids)}, "303 f"unexpected={sorted(actual_ids - expected_ids)}."304 )305 return digests306 307 308def _workspace_entries(value: Any, context: str) -> dict[str, dict[str, Any]]:309 entries: dict[str, dict[str, Any]] = {}310 for index, raw_entry in enumerate(_array(value, context)):311 entry = _object(raw_entry, f"{context}[{index}]")312 path = _text(entry.get("path"), f"{context}[{index}].path")313 if path in entries:314 raise ProtocolError(f"{context} contains duplicate path {path!r}.")315 kind = entry.get("kind")316 required_fields = {317 "file": {"path", "kind", "executable", "sha256"},318 "symlink": {"path", "kind", "sha256"},319 "gitlink": {"path", "kind", "head"},320 "directory": {"path", "kind"},321 "missing": {"path", "kind"},322 }323 if kind not in required_fields:324 raise ProtocolError(f"{context}[{index}].kind is invalid: {kind!r}.")325 missing = sorted(required_fields[kind] - entry.keys())326 unexpected = sorted(entry.keys() - required_fields[kind])327 if missing or unexpected:328 raise ProtocolError(329 f"{context}[{index}] does not match the {kind} schema: "330 f"missing={missing}, unexpected={unexpected}."331 )332 if kind == "file" and type(entry["executable"]) is not bool:333 raise ProtocolError(f"{context}[{index}].executable must be a boolean.")334 if kind in {"file", "symlink"}:335 _sha256(entry["sha256"], f"{context}[{index}].sha256")336 if kind == "gitlink":337 head = _text(entry["head"], f"{context}[{index}].head")338 if not re.fullmatch(r"[0-9a-f]{40,64}", head):339 raise ProtocolError(f"{context}[{index}].head must be a Git object ID.")340 entries[path] = entry341 if list(entries) != sorted(entries):342 raise ProtocolError(f"{context} must be sorted by path.")343 return entries344 345 346def _workspace_paths(value: Any, context: str) -> set[str]:347 return set(_workspace_entries(value, context))348 349 350def _evidence_artifacts(packet: dict[str, Any]) -> dict[str, dict[str, Any]]:351 artifacts: dict[str, dict[str, Any]] = {}352 artifact_identities: dict[FileIdentity, str] = {}353 role_ids: dict[str, set[str]] = {354 "complete-diff": set(),355 "review-state": set(),356 "repository-status": set(),357 }358 for index, raw_artifact in enumerate(359 _array(packet.get("evidence_artifacts"), "evidence_artifacts")360 ):361 artifact = _object(raw_artifact, f"evidence_artifacts[{index}]")362 artifact_id = _text(artifact.get("id"), f"evidence_artifacts[{index}].id")363 if artifact_id in artifacts:364 raise ProtocolError(f"Duplicate evidence artifact ID: {artifact_id}.")365 path, data, digest, identity = _descriptor(artifact, f"evidence artifact {artifact_id}")366 existing_artifact = artifact_identities.get(identity)367 if existing_artifact is not None:368 raise ProtocolError(369 f"Duplicate evidence artifact file identity for "370 f"{existing_artifact} and {artifact_id}."371 )372 artifact_identities[identity] = artifact_id373 role = artifact.get("role")374 if role not in {"complete-diff", "review-state", "repository-status", "supporting"}:375 raise ProtocolError(f"Evidence artifact {artifact_id} has an invalid role: {role!r}.")376 _text(artifact.get("purpose"), f"evidence artifact {artifact_id}.purpose", concrete=True)377 if role in role_ids:378 role_ids[role].add(artifact_id)379 artifacts[artifact_id] = {380 "path": path,381 "data": data,382 "digest": digest,383 "role": role,384 }385 for role, ids in role_ids.items():386 if len(ids) != 1:387 raise ProtocolError(f"evidence_artifacts must contain exactly one {role} artifact.")388 return artifacts389 390 391def _review_state(392 packet: dict[str, Any], artifacts: dict[str, dict[str, Any]]393) -> tuple[dict[str, Any], str, dict[str, str]]:394 descriptor = _object(packet.get("review_state"), "review_state")395 if set(descriptor) != {"evidence_id", "revalidation_command"}:396 raise ProtocolError("review_state must contain only evidence_id and revalidation_command.")397 _command_result(398 {"command": descriptor.get("revalidation_command"), "result": "configured"},399 "review_state.revalidation",400 )401 evidence_id = _text(descriptor.get("evidence_id"), "review_state.evidence_id")402 artifact = artifacts.get(evidence_id)403 if artifact is None or artifact["role"] != "review-state":404 raise ProtocolError("review_state.evidence_id must name the review-state artifact.")405 state = _json_bytes(artifact["data"], str(artifact["path"]))406 base = _text(state.get("base"), "review_state.base")407 head = _text(state.get("head"), "review_state.head")408 combined = _sha256(state.get("content_fingerprint"), "review_state.content_fingerprint")409 if _sha256(state.get("fingerprint"), "review_state.fingerprint") != combined:410 raise ProtocolError("review_state.fingerprint must match content_fingerprint.")411 repository = _sha256(state.get("repository_fingerprint"), "review_state.repository_fingerprint")412 status = _sha256(state.get("status_sha256"), "review_state.status_sha256")413 tracked_diff = _sha256(state.get("tracked_diff_sha256"), "review_state.tracked_diff_sha256")414 complete_diff = _sha256(state.get("complete_diff_sha256"), "review_state.complete_diff_sha256")415 workspace = _workspace_entries(state.get("workspace"), "review_state.workspace")416 complete_diff_paths = _strings(417 state.get("complete_diff_paths"), "review_state.complete_diff_paths"418 )419 if complete_diff_paths != sorted(workspace):420 raise ProtocolError(421 "review_state.complete_diff_paths must exactly match the task workspace."422 )423 actual_combined = _content_fingerprint(base, list(workspace.values()))424 if combined != actual_combined:425 raise ProtocolError("review_state.content_fingerprint does not match its workspace.")426 components: dict[str, str] = {}427 component_owners: dict[str, str] = {}428 for name, raw_component in _object(state.get("components"), "review_state.components").items():429 component = _object(raw_component, f"review_state.components[{name!r}]")430 fingerprint = _sha256(431 component.get("content_fingerprint"),432 f"review_state.components[{name!r}].content_fingerprint",433 )434 _strings(component.get("pathspecs"), f"review_state.components[{name!r}].pathspecs")435 component_workspace = _workspace_entries(436 component.get("workspace"), f"review_state.components[{name!r}].workspace"437 )438 actual_fingerprint = _content_fingerprint(base, list(component_workspace.values()))439 if fingerprint != actual_fingerprint:440 raise ProtocolError(f"Component {name!r} fingerprint does not match its workspace.")441 for path, entry in component_workspace.items():442 if path not in workspace or entry != workspace[path]:443 raise ProtocolError(444 f"Component {name!r} workspace entry {path!r} differs from combined state."445 )446 if path in component_owners:447 raise ProtocolError(448 f"Components {component_owners[path]!r} and {name!r} overlap on {path!r}."449 )450 component_owners[path] = name451 components[name] = fingerprint452 if not components:453 raise ProtocolError("review_state.components must not be empty.")454 if set(component_owners) != set(workspace):455 raise ProtocolError("review_state component workspaces must partition combined workspace.")456 _strings(state.get("pathspecs"), "review_state.pathspecs")457 unfiltered = _object(state.get("unfiltered"), "review_state.unfiltered")458 unfiltered_status = _sha256(459 unfiltered.get("status_sha256"), "review_state.unfiltered.status_sha256"460 )461 unfiltered_workspace = _workspace_entries(462 unfiltered.get("workspace"), "review_state.unfiltered.workspace"463 )464 for path, entry in workspace.items():465 if unfiltered_workspace.get(path) != entry:466 raise ProtocolError(467 f"review_state.unfiltered.workspace does not preserve task entry {path!r}."468 )469 unfiltered_content = _content_fingerprint(base, list(unfiltered_workspace.values()))470 actual_repository = _repository_fingerprint(471 content_fingerprint=combined,472 head=head,473 status_sha256=status,474 tracked_diff_sha256=tracked_diff,475 complete_diff_sha256=complete_diff,476 unfiltered_status_sha256=unfiltered_status,477 unfiltered_content_fingerprint=unfiltered_content,478 )479 if repository != actual_repository:480 raise ProtocolError("review_state.repository_fingerprint does not match its state fields.")481 return state, combined, components482 483 484def validate_receipt_data(485 receipt: dict[str, Any],486 expected_combined: str,487 expected_components: dict[str, str],488 expected_repository: str,489 eligible_commands: set[str] | None = None,490) -> None:491 required = {492 "schema_version",493 "command",494 "environment",495 "exit_status",496 "non_mutation_basis",497 "before",498 "after",499 }500 _require_exact_fields(receipt, required, "Verification receipt")501 if type(receipt["schema_version"]) is not int or receipt["schema_version"] != 1:502 raise ProtocolError("Verification receipt schema_version must be integer 1.")503 if type(receipt["exit_status"]) is not int or receipt["exit_status"] != 0:504 raise ProtocolError("Verification receipt requires integer exit_status 0.")505 _command_result(506 {"command": receipt["command"], "result": receipt["non_mutation_basis"]},507 "verification receipt",508 )509 if eligible_commands is not None and receipt["command"] not in eligible_commands:510 raise ProtocolError(511 "Verification receipt command must exactly match a packet preflight command."512 )513 _text(receipt["environment"], "verification receipt environment", concrete=True)514 expected = {515 "combined": expected_combined,516 "components": expected_components,517 "repository": expected_repository,518 }519 for boundary in ("before", "after"):520 if receipt[boundary] != expected:521 raise ProtocolError(522 f"Verification receipt {boundary} fingerprints do not match the packet exactly."523 )524 525 526def validate_packet(527 path: Path,528 expected_task_id: str,529 expected_ledger_path: Path,530 prior_ledger_path: Path | None = None,531 prior_ledger_sha256: str | None = None,532) -> dict[str, Any]:533 packet_path, packet_data, _ = _read_bytes(str(path.resolve()), "packet")534 packet = _json_bytes(packet_data, str(packet_path))535 if type(packet.get("schema_version")) is not int or packet["schema_version"] != 1:536 raise ProtocolError("Packet schema_version must be integer 1.")537 for dotted_path in REQUIRED_PACKET_TEXT:538 _text(_at(packet, dotted_path), dotted_path)539 if _at(packet, "verification.eligible_concurrent_gates") != "none":540 raise ProtocolError(541 "verification.eligible_concurrent_gates must be 'none'; broad final gates start "542 "only after clean review."543 )544 if _at(packet, "verification.deferred_gates").strip().lower() in SENTINELS:545 raise ProtocolError(546 "verification.deferred_gates must list the applicable broad final gates."547 )548 if _at(packet, "task.risk_tier") not in {"normal", "elevated"}:549 raise ProtocolError("task.risk_tier must be 'normal' or 'elevated'.")550 expected_task_id = _text(expected_task_id, "expected task ID", concrete=True)551 expected_ledger_path = expected_ledger_path.resolve()552 if _at(packet, "task.id") != expected_task_id:553 raise ProtocolError("packet task.id must match the control-plane task ID.")554 555 packet_size = len(packet_data)556 overage_reason = _text(packet.get("packet_overage_reason"), "packet_overage_reason")557 if packet_size > PACKET_SOFT_LIMIT_BYTES and overage_reason.strip().lower() in SENTINELS:558 raise ProtocolError(559 f"Packet is {packet_size} bytes, above {PACKET_SOFT_LIMIT_BYTES}; "560 "provide an overage reason."561 )562 563 artifacts = _evidence_artifacts(packet)564 state, combined, components = _review_state(packet, artifacts)565 if _at(packet, "repository.merge_base") != state["base"]:566 raise ProtocolError("repository.merge_base must match the review-state base.")567 if _at(packet, "repository.head") != state["head"]:568 raise ProtocolError("repository.head must match the review-state head.")569 if PLACEHOLDER_TOKEN.search(_at(packet, "repository.complete_diff_command")):570 raise ProtocolError("repository.complete_diff_command contains a placeholder token.")571 status_evidence_id = _at(packet, "repository.status_evidence_id")572 status_artifact = artifacts.get(status_evidence_id)573 if status_artifact is None or status_artifact["role"] != "repository-status":574 raise ProtocolError(575 "repository.status_evidence_id must name the repository-status artifact."576 )577 if status_artifact["digest"] != state["unfiltered"]["status_sha256"]:578 raise ProtocolError(579 "The repository-status artifact must match review_state.unfiltered.status_sha256."580 )581 task_workspace = _workspace_paths(state["workspace"], "review_state.workspace")582 full_workspace = _workspace_paths(583 state["unfiltered"]["workspace"], "review_state.unfiltered.workspace"584 )585 exclusions: dict[str, str] = {}586 for index, raw_exclusion in enumerate(587 _array(_at(packet, "repository.exclusions"), "repository.exclusions")588 ):589 exclusion = _object(raw_exclusion, f"repository.exclusions[{index}]")590 excluded_path = _text(591 exclusion.get("path"), f"repository.exclusions[{index}].path", concrete=True592 )593 if excluded_path in exclusions:594 raise ProtocolError(f"Duplicate repository exclusion: {excluded_path}.")595 exclusions[excluded_path] = _text(596 exclusion.get("reason"), f"repository.exclusions[{index}].reason", concrete=True597 )598 expected_exclusions = full_workspace - task_workspace599 if set(exclusions) != expected_exclusions:600 raise ProtocolError(601 "repository.exclusions must exactly account for unfiltered changed paths outside "602 f"the task manifest: {sorted(expected_exclusions)}."603 )604 605 manifests = _object(packet.get("manifests"), "manifests")606 if _pathspec_file(manifests.get("task"), "manifests.task") != state["pathspecs"]:607 raise ProtocolError("manifests.task must match review_state.pathspecs exactly.")608 component_manifests = _object(manifests.get("components"), "manifests.components")609 if set(component_manifests) != set(components):610 raise ProtocolError("Component manifest and review-state names must match exactly.")611 _dependency_map(manifests.get("dependency_map"), set(components))612 for name, manifest_path in component_manifests.items():613 if (614 _pathspec_file(manifest_path, f"manifests.components[{name!r}]")615 != state["components"][name]["pathspecs"]616 ):617 raise ProtocolError(f"Component manifest {name!r} must match review state exactly.")618 619 inventory_ids: set[str] = set()620 inventory_digests: dict[str, str] = {}621 for index, raw_row in enumerate(_array(packet.get("inventory"), "inventory")):622 row = _object(raw_row, f"inventory[{index}]")623 row_id = _text(row.get("id"), f"inventory[{index}].id", concrete=True)624 if row_id in inventory_ids:625 raise ProtocolError(f"Duplicate inventory ID: {row_id}.")626 inventory_ids.add(row_id)627 kind = row.get("kind")628 if kind not in INVENTORY_FIELDS:629 raise ProtocolError(f"Inventory {row_id} has invalid kind.")630 _text(row.get("summary"), f"inventory[{index}].summary", concrete=True)631 missing_fields = sorted(INVENTORY_FIELDS[kind] - row.keys())632 if missing_fields:633 raise ProtocolError(f"Inventory {row_id} is missing {kind} fields: {missing_fields}.")634 for field in INVENTORY_FIELDS[kind]:635 _text(row[field], f"inventory[{index}].{field}")636 inventory_digests[row_id] = _inventory_digest(row)637 if not inventory_ids:638 raise ProtocolError("inventory must not be empty.")639 640 complete_diff_ids = {641 artifact_id642 for artifact_id, artifact in artifacts.items()643 if artifact["role"] == "complete-diff"644 }645 complete_diff_id = next(iter(complete_diff_ids))646 if artifacts[complete_diff_id]["digest"] != state["complete_diff_sha256"]:647 raise ProtocolError(648 f"Complete-diff artifact {complete_diff_id} must match "649 "review_state.complete_diff_sha256."650 )651 652 ledger = _object(packet.get("ledger"), "ledger")653 ledger_path, ledger_data, ledger_identity = _read_bytes(ledger.get("path"), "ledger.path")654 if ledger_path != expected_ledger_path:655 raise ProtocolError("ledger.path must match the control-plane ledger path.")656 if _json_bytes(ledger_data, str(ledger_path)) != ledger:657 raise ProtocolError("ledger.path content must match the packet ledger exactly.")658 if ledger.get("task_id") != expected_task_id:659 raise ProtocolError("ledger.task_id must match the control-plane task ID.")660 round_fingerprint = _sha256(661 ledger.get("round_fingerprint"),662 "ledger.round_fingerprint",663 )664 if round_fingerprint != combined:665 raise ProtocolError("ledger.round_fingerprint must match the packet fingerprint.")666 authorized_budgets = [667 _integer(value, f"ledger.authorized_round_budgets[{index}]", minimum=1)668 for index, value in enumerate(669 _array(ledger.get("authorized_round_budgets"), "ledger.authorized_round_budgets")670 )671 ]672 if not authorized_budgets:673 raise ProtocolError("ledger.authorized_round_budgets must not be empty.")674 current_round = _integer(ledger.get("current_round"), "ledger.current_round", minimum=1)675 remaining_budget = _integer(676 ledger.get("remaining_budget"), "ledger.remaining_budget", minimum=0677 )678 total_budget = sum(authorized_budgets)679 if current_round > total_budget or remaining_budget != total_budget - current_round:680 raise ProtocolError(681 "ledger current_round and remaining_budget must match the authorized budget history."682 )683 canonical_roots: dict[str, dict[str, Any]] = {}684 inventory_owners: dict[str, str] = {}685 owned_evidence_ids: set[str] = set()686 for index, raw_root in enumerate(_array(ledger.get("root_causes"), "ledger.root_causes")):687 root = _object(raw_root, f"ledger.root_causes[{index}]")688 root_id = _text(root.get("id"), f"ledger.root_causes[{index}].id")689 if not ROOT_CAUSE_ID.fullmatch(root_id) or root_id in canonical_roots:690 raise ProtocolError(f"Invalid or duplicate canonical root-cause ID: {root_id!r}.")691 if root.get("status") not in {"open", "closed"}:692 raise ProtocolError(f"Root cause {root_id} must be open or closed.")693 root_inventory = set(_strings(root.get("inventory_ids"), f"root {root_id} inventory"))694 root_evidence = set(_strings(root.get("contract_evidence_ids"), f"root {root_id} evidence"))695 if not root_inventory:696 raise ProtocolError(f"Root cause {root_id} must own at least one inventory ID.")697 unknown_inventory = sorted(root_inventory - inventory_ids)698 unknown_evidence = sorted(root_evidence - artifacts.keys())699 if unknown_inventory or unknown_evidence:700 raise ProtocolError(701 f"Root cause {root_id} has unknown inventory={unknown_inventory} "702 f"or evidence={unknown_evidence}."703 )704 for inventory_id in root_inventory:705 existing_root = inventory_owners.get(inventory_id)706 if existing_root is not None:707 raise ProtocolError(708 f"Canonical roots {existing_root} and {root_id} overlap on inventory "709 f"{inventory_id}."710 )711 inventory_owners[inventory_id] = root_id712 canonical_roots[root_id] = {713 "status": root["status"],714 "inventory_ids": root_inventory,715 "contract_evidence_ids": root_evidence,716 }717 owned_evidence_ids.update(root_evidence)718 if set(inventory_owners) != inventory_ids:719 raise ProtocolError(720 "Every inventory ID must have exactly one canonical root owner; "721 f"unowned={sorted(inventory_ids - set(inventory_owners))}."722 )723 evidence_bindings = _digest_map(724 ledger.get("contract_evidence_sha256"),725 "ledger.contract_evidence_sha256",726 owned_evidence_ids,727 )728 inventory_bindings = _digest_map(729 ledger.get("inventory_sha256"),730 "ledger.inventory_sha256",731 inventory_ids,732 )733 for evidence_id, digest in evidence_bindings.items():734 if artifacts[evidence_id]["digest"] != digest:735 raise ProtocolError(f"ledger evidence digest mismatch for {evidence_id}.")736 for inventory_id, digest in inventory_bindings.items():737 if inventory_digests[inventory_id] != digest:738 raise ProtocolError(f"ledger inventory digest mismatch for {inventory_id}.")739 740 if current_round > 1 and (prior_ledger_path is None or prior_ledger_sha256 is None):741 raise ProtocolError("Rounds after 1 require a digest-bound prior ledger snapshot.")742 if prior_ledger_path is not None or prior_ledger_sha256 is not None:743 if prior_ledger_path is None or prior_ledger_sha256 is None:744 raise ProtocolError("Prior ledger path and SHA-256 must be supplied together.")745 prior_path, prior_data, prior_identity = _read_bytes(746 str(prior_ledger_path), "prior ledger path"747 )748 if prior_identity == ledger_identity:749 raise ProtocolError("Prior ledger snapshot must be distinct from the current ledger.")750 if not SHA256.fullmatch(prior_ledger_sha256):751 raise ProtocolError("Prior ledger SHA-256 must be a lowercase SHA-256 digest.")752 if hashlib.sha256(prior_data).hexdigest() != prior_ledger_sha256:753 raise ProtocolError(f"Prior ledger digest mismatch for {prior_path}.")754 prior = _json_bytes(prior_data, str(prior_path))755 if prior.get("task_id") != expected_task_id:756 raise ProtocolError("Prior ledger task_id must match the control-plane task ID.")757 prior_internal_path = Path(_text(prior.get("path"), "prior ledger.path", concrete=True))758 if (759 not prior_internal_path.is_absolute()760 or prior_internal_path.resolve() != expected_ledger_path761 ):762 raise ProtocolError("Prior ledger.path must match the control-plane ledger path.")763 prior_budgets = [764 _integer(value, f"prior ledger.authorized_round_budgets[{index}]", minimum=1)765 for index, value in enumerate(766 _array(767 prior.get("authorized_round_budgets"),768 "prior ledger.authorized_round_budgets",769 )770 )771 ]772 prior_round = _integer(prior.get("current_round"), "prior ledger.current_round", minimum=1)773 prior_round_fingerprint = _sha256(774 prior.get("round_fingerprint"),775 "prior ledger.round_fingerprint",776 )777 prior_remaining = _integer(778 prior.get("remaining_budget"), "prior ledger.remaining_budget", minimum=0779 )780 if prior_remaining != sum(prior_budgets) - prior_round:781 raise ProtocolError("Prior ledger round state does not match its budget history.")782 if authorized_budgets[: len(prior_budgets)] != prior_budgets:783 raise ProtocolError("ledger.authorized_round_budgets must preserve the prior prefix.")784 if current_round not in {prior_round, prior_round + 1}:785 raise ProtocolError(786 "ledger.current_round must match the prior round or advance by exactly one."787 )788 if current_round == prior_round and authorized_budgets != prior_budgets:789 raise ProtocolError(790 "A same-round retry budget history must match the prior ledger snapshot."791 )792 if current_round == prior_round and round_fingerprint != prior_round_fingerprint:793 raise ProtocolError(794 "A same-round retry fingerprint must match the prior ledger snapshot."795 )796 prior_roots: dict[str, dict[str, Any]] = {}797 prior_owned_evidence_ids: set[str] = set()798 prior_owned_inventory_ids: set[str] = set()799 for index, raw_root in enumerate(800 _array(prior.get("root_causes"), "prior ledger.root_causes")801 ):802 prior_root = _object(raw_root, f"prior ledger.root_causes[{index}]")803 prior_id = _text(prior_root.get("id"), f"prior ledger.root_causes[{index}].id")804 if not ROOT_CAUSE_ID.fullmatch(prior_id) or prior_id in prior_roots:805 raise ProtocolError(f"Invalid prior canonical root-cause ID: {prior_id!r}.")806 if prior_root.get("status") not in {"open", "closed"}:807 raise ProtocolError(f"Prior root cause {prior_id} must be open or closed.")808 prior_roots[prior_id] = {809 "status": prior_root["status"],810 "inventory_ids": set(811 _strings(prior_root.get("inventory_ids"), f"prior root {prior_id} inventory")812 ),813 "contract_evidence_ids": set(814 _strings(815 prior_root.get("contract_evidence_ids"),816 f"prior root {prior_id} evidence",817 )818 ),819 }820 prior_owned_evidence_ids.update(prior_roots[prior_id]["contract_evidence_ids"])821 prior_owned_inventory_ids.update(prior_roots[prior_id]["inventory_ids"])822 prior_evidence_bindings = _digest_map(823 prior.get("contract_evidence_sha256"),824 "prior ledger.contract_evidence_sha256",825 prior_owned_evidence_ids,826 )827 prior_inventory_bindings = _digest_map(828 prior.get("inventory_sha256"),829 "prior ledger.inventory_sha256",830 prior_owned_inventory_ids,831 )832 for prior_id, prior_root in prior_roots.items():833 current_root = canonical_roots.get(prior_id)834 if current_root is None:835 raise ProtocolError(f"ledger removed prior canonical root {prior_id}.")836 new_inventory = current_root["inventory_ids"] - prior_root["inventory_ids"]837 new_evidence = (838 current_root["contract_evidence_ids"] - prior_root["contract_evidence_ids"]839 )840 if not prior_root["inventory_ids"].issubset(841 current_root["inventory_ids"]842 ) or not prior_root["contract_evidence_ids"].issubset(843 current_root["contract_evidence_ids"]844 ):845 raise ProtocolError(f"ledger regressed ownership for prior root {prior_id}.")846 for evidence_id in prior_root["contract_evidence_ids"]:847 if evidence_bindings[evidence_id] != prior_evidence_bindings[evidence_id]:848 raise ProtocolError(f"ledger changed prior evidence {evidence_id}.")849 for inventory_id in prior_root["inventory_ids"]:850 if inventory_bindings[inventory_id] != prior_inventory_bindings[inventory_id]:851 raise ProtocolError(f"ledger changed prior inventory {inventory_id}.")852 prior_evidence_digests = {853 prior_evidence_bindings[evidence_id]854 for evidence_id in prior_root["contract_evidence_ids"]855 }856 prior_inventory_digests = {857 prior_inventory_bindings[inventory_id]858 for inventory_id in prior_root["inventory_ids"]859 }860 content_new_evidence = {861 evidence_id862 for evidence_id in new_evidence863 if artifacts[evidence_id]["digest"] not in prior_evidence_digests864 }865 content_new_inventory = {866 inventory_id867 for inventory_id in new_inventory868 if inventory_bindings[inventory_id] not in prior_inventory_digests869 }870 if (871 prior_root["status"] == "closed"872 and current_root["status"] == "open"873 and not (content_new_inventory or content_new_evidence)874 ):875 raise ProtocolError(876 f"ledger reopened prior root {prior_id} without content-new evidence."877 )878 879 selected_dimensions = set(880 _strings(packet.get("selected_high_risk_dimensions"), "selected_high_risk_dimensions")881 )882 assignments = _array(packet.get("reviewer_assignments"), "reviewer_assignments")883 if len(assignments) != 2:884 raise ProtocolError("reviewer_assignments must contain exactly two reviewers.")885 reviewer_ids: set[str] = set()886 assigned_inventory: set[str] = set()887 assigned_dimensions: set[str] = set()888 primary_specialty_owners: dict[str, str] = {}889 high_risk_specialty_owners: dict[str, str] = {}890 for index, raw_assignment in enumerate(assignments):891 assignment = _object(raw_assignment, f"reviewer_assignments[{index}]")892 reviewer_id = _text(assignment.get("reviewer_id"), f"reviewer_assignments[{index}].id")893 if reviewer_id in reviewer_ids:894 raise ProtocolError(f"Duplicate reviewer ID: {reviewer_id}.")895 reviewer_ids.add(reviewer_id)896 reviewer_inventory = set(897 _strings(assignment.get("inventory_ids"), f"reviewer {reviewer_id} inventory")898 )899 primary_dimensions = _strings(900 assignment.get("primary_dimensions"), f"reviewer {reviewer_id} primary dimensions"901 )902 if not reviewer_inventory or not primary_dimensions:903 raise ProtocolError(904 f"Reviewer {reviewer_id} requires inventory and a primary specialty."905 )906 for dimension in primary_dimensions:907 normalized = dimension.strip().casefold()908 existing_reviewer = primary_specialty_owners.get(normalized)909 if existing_reviewer is not None:910 raise ProtocolError(911 f"Reviewers {existing_reviewer} and {reviewer_id} have an overlapping "912 f"primary specialty: {dimension!r}."913 )914 primary_specialty_owners[normalized] = reviewer_id915 assigned_inventory.update(reviewer_inventory)916 reviewer_dimensions = set(917 _strings(assignment.get("high_risk_dimensions"), f"reviewer {reviewer_id} dimensions")918 )919 for dimension in reviewer_dimensions:920 normalized = dimension.strip().casefold()921 existing_reviewer = high_risk_specialty_owners.get(normalized)922 if existing_reviewer is not None:923 raise ProtocolError(924 f"Reviewers {existing_reviewer} and {reviewer_id} have an overlapping "925 f"high-risk specialty: {dimension!r}."926 )927 high_risk_specialty_owners[normalized] = reviewer_id928 assigned_dimensions.update(reviewer_dimensions)929 reviewer_components = set(930 _strings(assignment.get("expected_components"), f"reviewer {reviewer_id} components")931 )932 reviewer_evidence = set(933 _strings(assignment.get("evidence_ids"), f"reviewer {reviewer_id} evidence")934 )935 required_control_evidence = {936 artifact_id937 for artifact_id, artifact in artifacts.items()938 if artifact["role"] in {"complete-diff", "review-state", "repository-status"}939 }940 if (941 reviewer_components != set(components)942 or not required_control_evidence.issubset(reviewer_evidence)943 or not reviewer_evidence.issubset(artifacts)944 ):945 raise ProtocolError(946 f"Reviewer {reviewer_id} must receive every component and control artifact "947 "without unknown evidence."948 )949 if assigned_inventory != inventory_ids or assigned_dimensions != selected_dimensions:950 raise ProtocolError("Reviewer assignments must cover the exact inventory and dimensions.")951 952 verification = _object(packet.get("verification"), "verification")953 preflight_commands: set[str] = set()954 for index, result in enumerate(955 _array(verification.get("preflight_results"), "verification.preflight_results")956 ):957 _command_result(result, f"verification.preflight_results[{index}]")958 command = result["command"]959 if command in preflight_commands:960 raise ProtocolError(f"Duplicate preflight command: {command!r}.")961 preflight_commands.add(command)962 receipt_identities: set[FileIdentity] = set()963 receipt_digests: set[str] = set()964 receipt_digests_by_path: dict[str, str] = {}965 receipt_commands: set[str] = set()966 for index, raw_receipt in enumerate(967 _array(verification.get("credited_receipts"), "verification.credited_receipts")968 ):969 receipt_path, receipt_data, receipt_digest, receipt_identity = _descriptor(970 raw_receipt, f"verification.credited_receipts[{index}]"971 )972 if receipt_identity in receipt_identities:973 raise ProtocolError(f"Duplicate credited receipt file identity: {receipt_path}.")974 if receipt_digest in receipt_digests:975 raise ProtocolError(f"Duplicate credited receipt digest: {receipt_digest}.")976 receipt_identities.add(receipt_identity)977 receipt_digests.add(receipt_digest)978 receipt_digests_by_path[str(receipt_path)] = receipt_digest979 receipt = _json_bytes(receipt_data, str(receipt_path))980 validate_receipt_data(981 receipt,982 combined,983 components,984 state["repository_fingerprint"],985 preflight_commands,986 )987 command = receipt["command"]988 if command in receipt_commands:989 raise ProtocolError(f"Duplicate credited receipt command: {command!r}.")990 receipt_commands.add(command)991 992 _strings(packet.get("architecture_references"), "architecture_references")993 return {994 "packet_path": str(packet_path),995 "packet_size_bytes": packet_size,996 "packet_sha256": hashlib.sha256(packet_data).hexdigest(),997 "ledger_sha256": hashlib.sha256(ledger_data).hexdigest(),998 "review_state_path": str(artifacts[_at(packet, "review_state.evidence_id")]["path"]),999 "combined_fingerprint": combined,1000 "components": components,1001 "inventory_ids": sorted(inventory_ids),1002 "reviewer_ids": sorted(reviewer_ids),1003 "credited_receipt_digests": dict(sorted(receipt_digests_by_path.items())),1004 "credited_receipt_paths": sorted(receipt_digests_by_path),1005 }1006 1007 1008def _revalidate_control_files(1009 packet_path: Path,1010 expected_ledger_path: Path,1011 prior_ledger_path: Path | None,1012 prior_ledger_sha256: str | None,1013 summary: dict[str, Any],1014) -> tuple[dict[str, Any], bytes | None]:1015 packet_data = _read_unchanged(packet_path, summary["packet_sha256"], "Packet")1016 packet = _json_bytes(packet_data, str(packet_path.resolve()))1017 _read_unchanged(expected_ledger_path, summary["ledger_sha256"], "Current ledger")1018 if prior_ledger_path is None:1019 return packet, None1020 if prior_ledger_sha256 is None:1021 raise ProtocolError("Prior ledger path and SHA-256 must be supplied together.")1022 prior_ledger_data = _read_unchanged(1023 prior_ledger_path,1024 prior_ledger_sha256,1025 "Prior ledger",1026 )1027 return packet, prior_ledger_data1028 1029 1030def validate_reviewer_output(1031 packet_path: Path,1032 reviewer_id: str,1033 output_path: Path,1034 expected_task_id: str,1035 expected_ledger_path: Path,1036 prior_ledger_path: Path | None = None,1037 prior_ledger_sha256: str | None = None,1038) -> dict[str, Any]:1039 summary = validate_packet(1040 packet_path,1041 expected_task_id,1042 expected_ledger_path,1043 prior_ledger_path,1044 prior_ledger_sha256,1045 )1046 packet, prior_ledger_data = _revalidate_control_files(1047 packet_path,1048 expected_ledger_path,1049 prior_ledger_path,1050 prior_ledger_sha256,1051 summary,1052 )1053 output = _load_json(output_path)1054 _require_exact_fields(output, REVIEWER_OUTPUT_FIELDS, "Reviewer output")1055 if output["verdict"] not in {1056 "clean",1057 "findings require fixes",1058 "complexity reset required",1059 "incomplete packet",1060 }:1061 raise ProtocolError(f"Invalid reviewer verdict: {output['verdict']!r}.")1062 expected_fingerprints = {1063 "packet": summary["packet_sha256"],1064 "combined": summary["combined_fingerprint"],1065 "components": summary["components"],1066 }1067 if output["reviewed_fingerprints"] != expected_fingerprints:1068 raise ProtocolError("Reviewer packet digest or fingerprints do not match exactly.")1069 assignment = next(1070 (item for item in packet["reviewer_assignments"] if item.get("reviewer_id") == reviewer_id),1071 None,1072 )1073 if assignment is None:1074 raise ProtocolError(f"Unknown reviewer ID: {reviewer_id}.")1075 1076 checked = set(_strings(output["checked_inventory_ids"], "checked_inventory_ids"))1077 unchecked: set[str] = set()1078 for index, raw_item in enumerate(1079 _array(output["unchecked_inventory_ids"], "unchecked_inventory_ids")1080 ):1081 item = _object(raw_item, f"unchecked_inventory_ids[{index}]")1082 _require_exact_fields(1083 item,1084 {"id", "reason"},1085 f"unchecked_inventory_ids[{index}]",1086 )1087 unchecked_id = _text(item.get("id"), f"unchecked_inventory_ids[{index}].id")1088 if unchecked_id in unchecked:1089 raise ProtocolError(f"Duplicate unchecked inventory ID: {unchecked_id}.")1090 unchecked.add(unchecked_id)1091 _text(item.get("reason"), f"unchecked_inventory_ids[{index}].reason", concrete=True)1092 if checked & unchecked or checked | unchecked != set(assignment["inventory_ids"]):1093 raise ProtocolError("Reviewer inventory accounting differs from the assignment.")1094 if set(_strings(output["high_risk_dimensions_checked"], "high_risk_dimensions_checked")) != set(1095 assignment["high_risk_dimensions"]1096 ):1097 raise ProtocolError("Reviewer high-risk dimension accounting differs from the assignment.")1098 for index, probe in enumerate(_array(output["focused_probes"], "focused_probes")):1099 _command_result(probe, f"focused_probes[{index}]")1100 1101 canonical_roots = {root["id"]: root for root in packet["ledger"]["root_causes"]}1102 prior_canonical_roots: dict[str, dict[str, Any]] = {}1103 prior_evidence_bindings: dict[str, str] = {}1104 prior_inventory_bindings: dict[str, str] = {}1105 if prior_ledger_data is not None:1106 prior_ledger = _json_bytes(prior_ledger_data, str(prior_ledger_path))1107 prior_canonical_roots = {root["id"]: root for root in prior_ledger["root_causes"]}1108 prior_evidence_bindings = prior_ledger["contract_evidence_sha256"]1109 prior_inventory_bindings = prior_ledger["inventory_sha256"]1110 evidence_digests = {1111 artifact["id"]: artifact["sha256"] for artifact in packet["evidence_artifacts"]1112 }1113 inventory_digests = packet["ledger"]["inventory_sha256"]1114 indexed_evidence = set(evidence_digests)1115 indexed_inventory = {row["id"] for row in packet["inventory"]}1116 owned_evidence = {1117 evidence_id1118 for root in canonical_roots.values()1119 for evidence_id in root["contract_evidence_ids"]1120 }1121 owned_evidence_digests = {evidence_digests[evidence_id] for evidence_id in owned_evidence}1122 inventory_owners = {1123 inventory_id: root_id1124 for root_id, root in canonical_roots.items()1125 for inventory_id in root["inventory_ids"]1126 }1127 findings = _array(output["findings"], "findings")1128 proposed_roots: set[str] = set()1129 proposed_evidence_owners: dict[str, str] = {}1130 for index, raw_finding in enumerate(findings):1131 finding = _object(raw_finding, f"findings[{index}]")1132 _require_exact_fields(finding, FINDING_FIELDS, f"Finding {index}")1133 if finding["priority"] not in {"P0", "P1", "P2", "P3"}:1134 raise ProtocolError(f"Finding {index} has an invalid priority.")1135 for field in FINDING_FIELDS - {"priority", "root_cause_id", "root_cause_evidence"}:1136 _text(finding[field], f"findings[{index}].{field}")1137 root_id = _text(finding["root_cause_id"], f"findings[{index}].root_cause_id")1138 evidence = _object(finding["root_cause_evidence"], f"findings[{index}].root_cause_evidence")1139 _require_exact_fields(1140 evidence,1141 {"new_contract_evidence_ids", "new_inventory_ids"},1142 f"findings[{index}].root_cause_evidence",1143 )1144 new_evidence = set(1145 _strings(evidence.get("new_contract_evidence_ids"), f"finding {index} evidence")1146 )1147 new_inventory = set(1148 _strings(evidence.get("new_inventory_ids"), f"finding {index} inventory")1149 )1150 if not new_evidence.issubset(indexed_evidence) or not new_inventory.issubset(1151 indexed_inventory1152 ):1153 raise ProtocolError(f"Finding {index} references unindexed root evidence.")1154 root = canonical_roots.get(root_id)1155 if root is not None:1156 foreign_inventory = sorted(1157 inventory_id1158 for inventory_id in new_inventory1159 if inventory_owners.get(inventory_id) != root_id1160 )1161 if foreign_inventory:1162 raise ProtocolError(1163 f"Finding {index} reassigns inventory owned by another canonical root: "1164 f"{foreign_inventory}."1165 )1166 prior_root = prior_canonical_roots.get(root_id)1167 prior_evidence = set(prior_root["contract_evidence_ids"]) if prior_root else set()1168 prior_inventory = set(prior_root["inventory_ids"]) if prior_root else set()1169 added_evidence = set(root["contract_evidence_ids"]) - prior_evidence1170 added_inventory = set(root["inventory_ids"]) - prior_inventory1171 if not new_evidence.issubset(added_evidence) or not new_inventory.issubset(1172 added_inventory1173 ):1174 raise ProtocolError(1175 f"Finding {index} root evidence must be new in the current ledger round."1176 )1177 prior_evidence_digests = {1178 prior_evidence_bindings[evidence_id] for evidence_id in prior_evidence1179 }1180 prior_inventory_digests = {1181 prior_inventory_bindings[inventory_id] for inventory_id in prior_inventory1182 }1183 content_new_evidence = {1184 evidence_id1185 for evidence_id in new_evidence1186 if evidence_digests[evidence_id] not in prior_evidence_digests1187 }1188 content_new_inventory = {1189 inventory_id1190 for inventory_id in new_inventory1191 if inventory_digests[inventory_id] not in prior_inventory_digests1192 }1193 new_for_root = bool(content_new_evidence or content_new_inventory)1194 if root["status"] == "closed" and not new_for_root:1195 raise ProtocolError(1196 f"Finding {index} reopens closed root {root_id} without content-new evidence."1197 )1198 elif not NEW_ROOT_CAUSE_ID.fullmatch(root_id):1199 raise ProtocolError(1200 f"Finding {index} must reuse a canonical root ID or propose NEW:<slug>."1201 )1202 else:1203 if new_inventory:1204 raise ProtocolError(1205 f"Finding {index} new root proposal cannot reuse canonical inventory: "1206 f"{sorted(new_inventory)}."1207 )1208 proposal_digests = {1209 evidence_digests[evidence_id]1210 for evidence_id in new_evidence1211 if evidence_digests[evidence_id] not in owned_evidence_digests1212 }1213 available_digests = {1214 digest1215 for digest in proposal_digests1216 if proposed_evidence_owners.get(digest) in {None, root_id}1217 }1218 if not available_digests:1219 existing_owners = sorted(1220 {1221 proposed_evidence_owners[digest]1222 for digest in proposal_digests1223 if digest in proposed_evidence_owners1224 }1225 )1226 if existing_owners:1227 raise ProtocolError(1228 f"Finding {index} reuses evidence owned by proposed root {existing_owners}."1229 )1230 raise ProtocolError(1231 f"Finding {index} proposes {root_id} without content-new evidence."1232 )1233 for digest in available_digests:1234 proposed_evidence_owners.setdefault(digest, root_id)1235 proposed_roots.add(root_id)1236 1237 uncertainty = _strings(output["remaining_uncertainty"], "remaining_uncertainty")1238 if (1239 output["verdict"] in {"findings require fixes", "complexity reset required"}1240 and not findings1241 ):1242 raise ProtocolError(f"Verdict {output['verdict']!r} requires at least one finding.")1243 if output["verdict"] == "clean" and (unchecked or uncertainty or findings):1244 raise ProtocolError("A clean verdict requires no unchecked IDs, uncertainty, or findings.")1245 inspection_count = _integer(output["inspection_call_count"], "inspection_call_count", minimum=0)1246 reason = _text(output["inspection_budget_reason"], "inspection_budget_reason")1247 if inspection_count > 12 and reason.strip().lower() in SENTINELS:1248 raise ProtocolError("Inspection counts above 12 require an inspection_budget_reason.")1249 for index, raw_scan in enumerate(1250 _array(output["sibling_scenario_scan"], "sibling_scenario_scan")1251 ):1252 scan = _object(raw_scan, f"sibling_scenario_scan[{index}]")1253 _require_exact_fields(1254 scan,1255 {"root_cause_id", "inventory_ids", "result"},1256 f"sibling_scenario_scan[{index}]",1257 )1258 root_id = _text(scan.get("root_cause_id"), f"sibling_scenario_scan[{index}].root_cause_id")1259 if root_id not in canonical_roots and root_id not in proposed_roots:1260 raise ProtocolError(1261 f"sibling_scenario_scan[{index}] must reference a canonical or proposed root."1262 )1263 if not set(1264 _strings(scan.get("inventory_ids"), f"sibling_scenario_scan[{index}].inventory_ids")1265 ).issubset(indexed_inventory):1266 raise ProtocolError(f"sibling_scenario_scan[{index}] references unknown inventory IDs.")1267 _text(scan.get("result"), f"sibling_scenario_scan[{index}].result", concrete=True)1268 return {1269 "reviewer_id": reviewer_id,1270 "verdict": output["verdict"],1271 "combined_fingerprint": summary["combined_fingerprint"],1272 "finding_count": len(findings),1273 }1274 1275 1276def _validate_credited_receipt(1277 packet_path: Path,1278 receipt_path: Path,1279 expected_task_id: str,1280 expected_ledger_path: Path,1281 prior_ledger_path: Path | None = None,1282 prior_ledger_sha256: str | None = None,1283) -> dict[str, Any]:1284 summary = validate_packet(1285 packet_path,1286 expected_task_id,1287 expected_ledger_path,1288 prior_ledger_path,1289 prior_ledger_sha256,1290 )1291 _revalidate_control_files(1292 packet_path,1293 expected_ledger_path,1294 prior_ledger_path,1295 prior_ledger_sha256,1296 summary,1297 )1298 canonical_receipt_path = receipt_path.resolve()1299 expected_digest = summary["credited_receipt_digests"].get(str(canonical_receipt_path))1300 if expected_digest is None:1301 raise ProtocolError("The receipt path is not indexed by the validated packet.")1302 _read_unchanged(canonical_receipt_path, expected_digest, "Receipt")1303 return {1304 "receipt_path": str(canonical_receipt_path),1305 "receipt_sha256": expected_digest,1306 "combined_fingerprint": summary["combined_fingerprint"],1307 "reusable": True,1308 }1309 1310 1311def main() -> None:1312 parser = argparse.ArgumentParser()1313 commands = parser.add_subparsers(dest="command", required=True)1314 packet_parser = commands.add_parser("packet")1315 packet_parser.add_argument("--packet", type=Path, required=True)1316 output_parser = commands.add_parser("reviewer-output")1317 output_parser.add_argument("--packet", type=Path, required=True)1318 output_parser.add_argument("--reviewer", required=True)1319 output_parser.add_argument("--output", type=Path, required=True)1320 receipt_parser = commands.add_parser("receipt")1321 receipt_parser.add_argument("--packet", type=Path, required=True)1322 receipt_parser.add_argument("--receipt", type=Path, required=True)1323 for command_parser in (packet_parser, output_parser, receipt_parser):1324 command_parser.add_argument("--task-id", required=True)1325 command_parser.add_argument("--ledger", type=Path, required=True)1326 command_parser.add_argument("--prior-ledger", type=Path)1327 command_parser.add_argument("--prior-ledger-sha256")1328 args = parser.parse_args()1329 try:1330 if args.command == "packet":1331 result = validate_packet(1332 args.packet,1333 args.task_id,1334 args.ledger,1335 args.prior_ledger,1336 args.prior_ledger_sha256,1337 )1338 elif args.command == "reviewer-output":1339 result = validate_reviewer_output(1340 args.packet,1341 args.reviewer,1342 args.output,1343 args.task_id,1344 args.ledger,1345 args.prior_ledger,1346 args.prior_ledger_sha256,1347 )1348 else:1349 result = _validate_credited_receipt(1350 args.packet,1351 args.receipt,1352 args.task_id,1353 args.ledger,1354 args.prior_ledger,1355 args.prior_ledger_sha256,1356 )1357 except ProtocolError as error:1358 parser.error(str(error))1359 print(json.dumps(result, indent=2, sort_keys=True))1360 1361 1362if __name__ == "__main__":1363 main()1364