scripts/review_protocol.py
scripts/review_protocol.pyBrowse 10 files
13,841 tokens
63,994 bytes
Token encoding: o200k_base
Snapshot 506f736
← 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}]")144 for index, item in enumerate(_array(value, context))145 ]146 if len(result) != len(set(result)):147 raise ProtocolError(f"{context} must not contain duplicates.")148 return result149 150 151def _integer(value: Any, context: str, *, minimum: int) -> int:152 if type(value) is not int or value < minimum:153 qualifier = "positive" if minimum == 1 else "nonnegative"154 raise ProtocolError(f"{context} must be a {qualifier} integer.")155 return value156 157 158def _at(value: dict[str, Any], dotted_path: str) -> Any:159 current: Any = value160 for part in dotted_path.split("."):161 if not isinstance(current, dict) or part not in current:162 raise ProtocolError(f"Missing required packet field: {dotted_path}.")163 current = current[part]164 return current165 166 167FileIdentity = tuple[int, int]168 169 170def _read_bytes(value: Any, context: str) -> tuple[Path, bytes, FileIdentity]:171 requested_path = Path(_text(value, context, concrete=True))172 if not requested_path.is_absolute():173 raise ProtocolError(f"{context} must be an absolute path: {requested_path}.")174 try:175 path = requested_path.resolve(strict=True)176 data, file_stat = _read_regular_file(path)177 except _NonRegularFileError as error:178 raise ProtocolError(f"{context} must be a regular file: {requested_path}.") from error179 except (OSError, ValueError) as error:180 raise ProtocolError(f"Cannot read {context} {requested_path}: {error}") from error181 return path, data, (file_stat.st_dev, file_stat.st_ino)182 183 184def _json_bytes(data: bytes, context: str) -> dict[str, Any]:185 def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:186 result: dict[str, Any] = {}187 for key, value in pairs:188 if key in result:189 raise ProtocolError(f"Duplicate JSON key in {context}: {key!r}.")190 result[key] = value191 return result192 193 def reject_constant(value: str) -> None:194 raise ProtocolError(f"Non-finite JSON number in {context}: {value}.")195 196 def finite_float(value: str) -> float:197 parsed = float(value)198 if not math.isfinite(parsed):199 raise ProtocolError(f"Non-finite JSON number in {context}: {value}.")200 return parsed201 202 try:203 value = json.loads(204 data,205 object_pairs_hook=unique_object,206 parse_constant=reject_constant,207 parse_float=finite_float,208 )209 except ProtocolError:210 raise211 except (RecursionError, UnicodeError, ValueError) as error:212 raise ProtocolError(f"Cannot read JSON object from {context}: {error}") from error213 return _object(value, context)214 215 216def _load_json(path: Path) -> dict[str, Any]:217 _, data, _ = _read_bytes(str(path.resolve()), str(path))218 return _json_bytes(data, str(path))219 220 221def _descriptor(value: Any, context: str) -> tuple[Path, bytes, str, FileIdentity]:222 descriptor = _object(value, context)223 path, data, identity = _read_bytes(descriptor.get("path"), f"{context}.path")224 expected = _text(descriptor.get("sha256"), f"{context}.sha256")225 if not SHA256.fullmatch(expected):226 raise ProtocolError(f"{context}.sha256 must be a lowercase SHA-256 digest.")227 actual = hashlib.sha256(data).hexdigest()228 if actual != expected:229 raise ProtocolError(f"{context} digest mismatch for {path}.")230 return path, data, actual, identity231 232 233def _read_unchanged(path: Path, expected_digest: str, context: str) -> bytes:234 _, data, _ = _read_bytes(str(path.resolve()), context)235 if hashlib.sha256(data).hexdigest() != expected_digest:236 raise ProtocolError(f"{context} changed during protocol validation.")237 return data238 239 240def _pathspec_file(value: Any, context: str) -> list[str]:241 _, data, _ = _read_bytes(value, context)242 try:243 lines = [line for line in data.decode().splitlines() if line]244 except UnicodeError as error:245 raise ProtocolError(f"Cannot decode {context}: {error}") from error246 if not lines or len(lines) != len(set(lines)):247 raise ProtocolError(f"{context} must contain unique nonempty pathspecs.")248 return lines249 250 251def _dependency_map(value: Any, component_names: set[str]) -> None:252 dependencies = _object(value, "manifests.dependency_map")253 if set(dependencies) != component_names:254 raise ProtocolError("manifests.dependency_map must cover the exact component names.")255 for component_name in sorted(component_names):256 context = f"manifests.dependency_map[{component_name!r}]"257 entries = _array(dependencies[component_name], context)258 if not entries:259 raise ProtocolError(f"{context} must contain at least one dependency.")260 pathspecs: set[str] = set()261 for index, raw_entry in enumerate(entries):262 entry_context = f"{context}[{index}]"263 entry = _object(raw_entry, entry_context)264 _require_exact_fields(entry, {"pathspec", "reason"}, entry_context)265 pathspec = _text(entry.get("pathspec"), f"{entry_context}.pathspec", concrete=True)266 _text(entry.get("reason"), f"{entry_context}.reason", concrete=True)267 if pathspec in pathspecs:268 raise ProtocolError(f"{context} contains duplicate pathspec {pathspec!r}.")269 pathspecs.add(pathspec)270 271 272def _command_result(value: Any, context: str) -> None:273 record = _object(value, context)274 _require_exact_fields(record, {"command", "result"}, context)275 command = _text(record.get("command"), f"{context}.command", concrete=True)276 _text(record.get("result"), f"{context}.result", concrete=True)277 if PLACEHOLDER_TOKEN.search(command):278 raise ProtocolError(f"{context}.command contains a placeholder token.")279 280 281def _sha256(value: Any, context: str) -> str:282 digest = _text(value, context)283 if not SHA256.fullmatch(digest):284 raise ProtocolError(f"{context} must be a lowercase SHA-256 digest.")285 return digest286 287 288def _inventory_digest(row: dict[str, Any]) -> str:289 content = {key: value for key, value in row.items() if key != "id"}290 canonical = json.dumps(content, sort_keys=True, separators=(",", ":"))291 return hashlib.sha256(canonical.encode()).hexdigest()292 293 294def _digest_map(value: Any, context: str, expected_ids: set[str]) -> dict[str, str]:295 digests = {296 _text(raw_id, f"{context} key", concrete=True): _sha256(digest, f"{context}.{raw_id}")297 for raw_id, digest in _object(value, context).items()298 }299 actual_ids = set(digests)300 if actual_ids != expected_ids:301 raise ProtocolError(302 f"{context} must bind the exact owned IDs: "303 f"missing={sorted(expected_ids - actual_ids)}, "304 f"unexpected={sorted(actual_ids - expected_ids)}."305 )306 return digests307 308 309def _workspace_entries(value: Any, context: str) -> dict[str, dict[str, Any]]:310 entries: dict[str, dict[str, Any]] = {}311 for index, raw_entry in enumerate(_array(value, context)):312 entry = _object(raw_entry, f"{context}[{index}]")313 path = _text(entry.get("path"), f"{context}[{index}].path")314 if path in entries:315 raise ProtocolError(f"{context} contains duplicate path {path!r}.")316 kind = entry.get("kind")317 required_fields = {318 "file": {"path", "kind", "executable", "sha256"},319 "symlink": {"path", "kind", "sha256"},320 "gitlink": {"path", "kind", "head"},321 "directory": {"path", "kind"},322 "missing": {"path", "kind"},323 }324 if kind not in required_fields:325 raise ProtocolError(f"{context}[{index}].kind is invalid: {kind!r}.")326 missing = sorted(required_fields[kind] - entry.keys())327 unexpected = sorted(entry.keys() - required_fields[kind])328 if missing or unexpected:329 raise ProtocolError(330 f"{context}[{index}] does not match the {kind} schema: "331 f"missing={missing}, unexpected={unexpected}."332 )333 if kind == "file" and type(entry["executable"]) is not bool:334 raise ProtocolError(f"{context}[{index}].executable must be a boolean.")335 if kind in {"file", "symlink"}:336 _sha256(entry["sha256"], f"{context}[{index}].sha256")337 if kind == "gitlink":338 head = _text(entry["head"], f"{context}[{index}].head")339 if not re.fullmatch(r"[0-9a-f]{40,64}", head):340 raise ProtocolError(f"{context}[{index}].head must be a Git object ID.")341 entries[path] = entry342 if list(entries) != sorted(entries):343 raise ProtocolError(f"{context} must be sorted by path.")344 return entries345 346 347def _workspace_paths(value: Any, context: str) -> set[str]:348 return set(_workspace_entries(value, context))349 350 351def _evidence_artifacts(packet: dict[str, Any]) -> dict[str, dict[str, Any]]:352 artifacts: dict[str, dict[str, Any]] = {}353 artifact_identities: dict[FileIdentity, str] = {}354 role_ids: dict[str, set[str]] = {355 "complete-diff": set(),356 "review-state": set(),357 "repository-status": set(),358 }359 for index, raw_artifact in enumerate(360 _array(packet.get("evidence_artifacts"), "evidence_artifacts")361 ):362 artifact = _object(raw_artifact, f"evidence_artifacts[{index}]")363 artifact_id = _text(artifact.get("id"), f"evidence_artifacts[{index}].id")364 if artifact_id in artifacts:365 raise ProtocolError(f"Duplicate evidence artifact ID: {artifact_id}.")366 path, data, digest, identity = _descriptor(artifact, f"evidence artifact {artifact_id}")367 existing_artifact = artifact_identities.get(identity)368 if existing_artifact is not None:369 raise ProtocolError(370 f"Duplicate evidence artifact file identity for "371 f"{existing_artifact} and {artifact_id}."372 )373 artifact_identities[identity] = artifact_id374 role = artifact.get("role")375 if role not in {376 "complete-diff",377 "review-state",378 "repository-status",379 "supporting",380 }:381 raise ProtocolError(382 f"Evidence artifact {artifact_id} has an invalid role: {role!r}."383 )384 _text(385 artifact.get("purpose"),386 f"evidence artifact {artifact_id}.purpose",387 concrete=True,388 )389 if role in role_ids:390 role_ids[role].add(artifact_id)391 artifacts[artifact_id] = {392 "path": path,393 "data": data,394 "digest": digest,395 "role": role,396 }397 for role, ids in role_ids.items():398 if len(ids) != 1:399 raise ProtocolError(400 f"evidence_artifacts must contain exactly one {role} artifact."401 )402 return artifacts403 404 405def _review_state(406 packet: dict[str, Any], artifacts: dict[str, dict[str, Any]]407) -> tuple[dict[str, Any], str, dict[str, str]]:408 descriptor = _object(packet.get("review_state"), "review_state")409 if set(descriptor) != {"evidence_id", "revalidation_command"}:410 raise ProtocolError(411 "review_state must contain only evidence_id and revalidation_command."412 )413 revalidation_command = _text(414 descriptor.get("revalidation_command"),415 "review_state.revalidation_command",416 concrete=True,417 )418 if not revalidation_command.startswith("PYTHONDONTWRITEBYTECODE=1 "):419 raise ProtocolError(420 "review_state.revalidation_command must disable Python bytecode writes."421 )422 _command_result(423 {"command": revalidation_command, "result": "configured"},424 "review_state.revalidation",425 )426 evidence_id = _text(descriptor.get("evidence_id"), "review_state.evidence_id")427 artifact = artifacts.get(evidence_id)428 if artifact is None or artifact["role"] != "review-state":429 raise ProtocolError(430 "review_state.evidence_id must name the review-state artifact."431 )432 state = _json_bytes(artifact["data"], str(artifact["path"]))433 base = _text(state.get("base"), "review_state.base")434 head = _text(state.get("head"), "review_state.head")435 combined = _sha256(436 state.get("content_fingerprint"), "review_state.content_fingerprint"437 )438 if _sha256(state.get("fingerprint"), "review_state.fingerprint") != combined:439 raise ProtocolError("review_state.fingerprint must match content_fingerprint.")440 repository = _sha256(441 state.get("repository_fingerprint"), "review_state.repository_fingerprint"442 )443 status = _sha256(state.get("status_sha256"), "review_state.status_sha256")444 tracked_diff = _sha256(445 state.get("tracked_diff_sha256"), "review_state.tracked_diff_sha256"446 )447 complete_diff = _sha256(448 state.get("complete_diff_sha256"), "review_state.complete_diff_sha256"449 )450 workspace = _workspace_entries(state.get("workspace"), "review_state.workspace")451 complete_diff_paths = _strings(452 state.get("complete_diff_paths"), "review_state.complete_diff_paths"453 )454 if complete_diff_paths != sorted(workspace):455 raise ProtocolError(456 "review_state.complete_diff_paths must exactly match the task workspace."457 )458 actual_combined = _content_fingerprint(base, list(workspace.values()))459 if combined != actual_combined:460 raise ProtocolError(461 "review_state.content_fingerprint does not match its workspace."462 )463 components: dict[str, str] = {}464 component_owners: dict[str, str] = {}465 for name, raw_component in _object(466 state.get("components"), "review_state.components"467 ).items():468 component = _object(raw_component, f"review_state.components[{name!r}]")469 fingerprint = _sha256(470 component.get("content_fingerprint"),471 f"review_state.components[{name!r}].content_fingerprint",472 )473 _strings(474 component.get("pathspecs"), f"review_state.components[{name!r}].pathspecs"475 )476 component_workspace = _workspace_entries(477 component.get("workspace"), f"review_state.components[{name!r}].workspace"478 )479 actual_fingerprint = _content_fingerprint(480 base, list(component_workspace.values())481 )482 if fingerprint != actual_fingerprint:483 raise ProtocolError(484 f"Component {name!r} fingerprint does not match its workspace."485 )486 for path, entry in component_workspace.items():487 if path not in workspace or entry != workspace[path]:488 raise ProtocolError(489 f"Component {name!r} workspace entry {path!r} differs from combined state."490 )491 if path in component_owners:492 raise ProtocolError(493 f"Components {component_owners[path]!r} and {name!r} overlap on {path!r}."494 )495 component_owners[path] = name496 components[name] = fingerprint497 if not components:498 raise ProtocolError("review_state.components must not be empty.")499 if set(component_owners) != set(workspace):500 raise ProtocolError(501 "review_state component workspaces must partition combined workspace."502 )503 _strings(state.get("pathspecs"), "review_state.pathspecs")504 unfiltered = _object(state.get("unfiltered"), "review_state.unfiltered")505 unfiltered_status = _sha256(506 unfiltered.get("status_sha256"), "review_state.unfiltered.status_sha256"507 )508 unfiltered_workspace = _workspace_entries(509 unfiltered.get("workspace"), "review_state.unfiltered.workspace"510 )511 for path, entry in workspace.items():512 if unfiltered_workspace.get(path) != entry:513 raise ProtocolError(514 f"review_state.unfiltered.workspace does not preserve task entry {path!r}."515 )516 unfiltered_content = _content_fingerprint(base, list(unfiltered_workspace.values()))517 actual_repository = _repository_fingerprint(518 content_fingerprint=combined,519 head=head,520 status_sha256=status,521 tracked_diff_sha256=tracked_diff,522 complete_diff_sha256=complete_diff,523 unfiltered_status_sha256=unfiltered_status,524 unfiltered_content_fingerprint=unfiltered_content,525 )526 if repository != actual_repository:527 raise ProtocolError(528 "review_state.repository_fingerprint does not match its state fields."529 )530 return state, combined, components531 532 533def validate_receipt_data(534 receipt: dict[str, Any],535 expected_combined: str,536 expected_components: dict[str, str],537 expected_repository: str,538 eligible_commands: set[str] | None = None,539) -> None:540 required = {541 "schema_version",542 "command",543 "environment",544 "exit_status",545 "non_mutation_basis",546 "before",547 "after",548 }549 _require_exact_fields(receipt, required, "Verification receipt")550 if type(receipt["schema_version"]) is not int or receipt["schema_version"] != 1:551 raise ProtocolError("Verification receipt schema_version must be integer 1.")552 if type(receipt["exit_status"]) is not int or receipt["exit_status"] != 0:553 raise ProtocolError("Verification receipt requires integer exit_status 0.")554 _command_result(555 {"command": receipt["command"], "result": receipt["non_mutation_basis"]},556 "verification receipt",557 )558 if eligible_commands is not None and receipt["command"] not in eligible_commands:559 raise ProtocolError(560 "Verification receipt command must exactly match a packet preflight command."561 )562 _text(receipt["environment"], "verification receipt environment", concrete=True)563 expected = {564 "combined": expected_combined,565 "components": expected_components,566 "repository": expected_repository,567 }568 for boundary in ("before", "after"):569 if receipt[boundary] != expected:570 raise ProtocolError(571 f"Verification receipt {boundary} fingerprints do not match the packet exactly."572 )573 574 575def validate_packet(576 path: Path,577 expected_task_id: str,578 expected_ledger_path: Path,579 prior_ledger_path: Path | None = None,580 prior_ledger_sha256: str | None = None,581) -> dict[str, Any]:582 packet_path, packet_data, _ = _read_bytes(str(path.resolve()), "packet")583 packet = _json_bytes(packet_data, str(packet_path))584 if type(packet.get("schema_version")) is not int or packet["schema_version"] != 1:585 raise ProtocolError("Packet schema_version must be integer 1.")586 for dotted_path in REQUIRED_PACKET_TEXT:587 _text(_at(packet, dotted_path), dotted_path)588 if _at(packet, "verification.eligible_concurrent_gates") != "none":589 raise ProtocolError(590 "verification.eligible_concurrent_gates must be 'none'; broad final gates start "591 "only after clean review."592 )593 if _at(packet, "verification.deferred_gates").strip().lower() in SENTINELS:594 raise ProtocolError(595 "verification.deferred_gates must list the applicable broad final gates."596 )597 if _at(packet, "task.risk_tier") not in {"normal", "elevated"}:598 raise ProtocolError("task.risk_tier must be 'normal' or 'elevated'.")599 expected_task_id = _text(expected_task_id, "expected task ID", concrete=True)600 expected_ledger_path = expected_ledger_path.resolve()601 if _at(packet, "task.id") != expected_task_id:602 raise ProtocolError("packet task.id must match the control-plane task ID.")603 604 packet_size = len(packet_data)605 overage_reason = _text(packet.get("packet_overage_reason"), "packet_overage_reason")606 if (607 packet_size > PACKET_SOFT_LIMIT_BYTES608 and overage_reason.strip().lower() in SENTINELS609 ):610 raise ProtocolError(611 f"Packet is {packet_size} bytes, above {PACKET_SOFT_LIMIT_BYTES}; "612 "provide an overage reason."613 )614 615 artifacts = _evidence_artifacts(packet)616 state, combined, components = _review_state(packet, artifacts)617 if _at(packet, "repository.merge_base") != state["base"]:618 raise ProtocolError("repository.merge_base must match the review-state base.")619 if _at(packet, "repository.head") != state["head"]:620 raise ProtocolError("repository.head must match the review-state head.")621 if PLACEHOLDER_TOKEN.search(_at(packet, "repository.complete_diff_command")):622 raise ProtocolError(623 "repository.complete_diff_command contains a placeholder token."624 )625 status_evidence_id = _at(packet, "repository.status_evidence_id")626 status_artifact = artifacts.get(status_evidence_id)627 if status_artifact is None or status_artifact["role"] != "repository-status":628 raise ProtocolError(629 "repository.status_evidence_id must name the repository-status artifact."630 )631 if status_artifact["digest"] != state["unfiltered"]["status_sha256"]:632 raise ProtocolError(633 "The repository-status artifact must match review_state.unfiltered.status_sha256."634 )635 task_workspace = _workspace_paths(state["workspace"], "review_state.workspace")636 full_workspace = _workspace_paths(637 state["unfiltered"]["workspace"], "review_state.unfiltered.workspace"638 )639 exclusions: dict[str, str] = {}640 for index, raw_exclusion in enumerate(641 _array(_at(packet, "repository.exclusions"), "repository.exclusions")642 ):643 exclusion = _object(raw_exclusion, f"repository.exclusions[{index}]")644 excluded_path = _text(645 exclusion.get("path"), f"repository.exclusions[{index}].path", concrete=True646 )647 if excluded_path in exclusions:648 raise ProtocolError(f"Duplicate repository exclusion: {excluded_path}.")649 exclusions[excluded_path] = _text(650 exclusion.get("reason"),651 f"repository.exclusions[{index}].reason",652 concrete=True,653 )654 expected_exclusions = full_workspace - task_workspace655 if set(exclusions) != expected_exclusions:656 raise ProtocolError(657 "repository.exclusions must exactly account for unfiltered changed paths outside "658 f"the task manifest: {sorted(expected_exclusions)}."659 )660 661 manifests = _object(packet.get("manifests"), "manifests")662 if _pathspec_file(manifests.get("task"), "manifests.task") != state["pathspecs"]:663 raise ProtocolError("manifests.task must match review_state.pathspecs exactly.")664 component_manifests = _object(manifests.get("components"), "manifests.components")665 if set(component_manifests) != set(components):666 raise ProtocolError("Component manifest and review-state names must match exactly.")667 _dependency_map(manifests.get("dependency_map"), set(components))668 for name, manifest_path in component_manifests.items():669 if (670 _pathspec_file(manifest_path, f"manifests.components[{name!r}]")671 != state["components"][name]["pathspecs"]672 ):673 raise ProtocolError(674 f"Component manifest {name!r} must match review state exactly."675 )676 677 inventory_ids: set[str] = set()678 inventory_digests: dict[str, str] = {}679 for index, raw_row in enumerate(_array(packet.get("inventory"), "inventory")):680 row = _object(raw_row, f"inventory[{index}]")681 row_id = _text(row.get("id"), f"inventory[{index}].id", concrete=True)682 if row_id in inventory_ids:683 raise ProtocolError(f"Duplicate inventory ID: {row_id}.")684 inventory_ids.add(row_id)685 kind = row.get("kind")686 if kind not in INVENTORY_FIELDS:687 raise ProtocolError(f"Inventory {row_id} has invalid kind.")688 _text(row.get("summary"), f"inventory[{index}].summary", concrete=True)689 missing_fields = sorted(INVENTORY_FIELDS[kind] - row.keys())690 if missing_fields:691 raise ProtocolError(692 f"Inventory {row_id} is missing {kind} fields: {missing_fields}."693 )694 for field in INVENTORY_FIELDS[kind]:695 _text(row[field], f"inventory[{index}].{field}")696 inventory_digests[row_id] = _inventory_digest(row)697 if not inventory_ids:698 raise ProtocolError("inventory must not be empty.")699 700 complete_diff_ids = {701 artifact_id702 for artifact_id, artifact in artifacts.items()703 if artifact["role"] == "complete-diff"704 }705 complete_diff_id = next(iter(complete_diff_ids))706 if artifacts[complete_diff_id]["digest"] != state["complete_diff_sha256"]:707 raise ProtocolError(708 f"Complete-diff artifact {complete_diff_id} must match "709 "review_state.complete_diff_sha256."710 )711 712 ledger = _object(packet.get("ledger"), "ledger")713 ledger_path, ledger_data, ledger_identity = _read_bytes(ledger.get("path"), "ledger.path")714 if ledger_path != expected_ledger_path:715 raise ProtocolError("ledger.path must match the control-plane ledger path.")716 if _json_bytes(ledger_data, str(ledger_path)) != ledger:717 raise ProtocolError("ledger.path content must match the packet ledger exactly.")718 if ledger.get("task_id") != expected_task_id:719 raise ProtocolError("ledger.task_id must match the control-plane task ID.")720 round_fingerprint = _sha256(721 ledger.get("round_fingerprint"),722 "ledger.round_fingerprint",723 )724 if round_fingerprint != combined:725 raise ProtocolError("ledger.round_fingerprint must match the packet fingerprint.")726 authorized_budgets = [727 _integer(value, f"ledger.authorized_round_budgets[{index}]", minimum=1)728 for index, value in enumerate(729 _array(730 ledger.get("authorized_round_budgets"),731 "ledger.authorized_round_budgets",732 )733 )734 ]735 if not authorized_budgets:736 raise ProtocolError("ledger.authorized_round_budgets must not be empty.")737 current_round = _integer(738 ledger.get("current_round"), "ledger.current_round", minimum=1739 )740 remaining_budget = _integer(741 ledger.get("remaining_budget"), "ledger.remaining_budget", minimum=0742 )743 total_budget = sum(authorized_budgets)744 if current_round > total_budget or remaining_budget != total_budget - current_round:745 raise ProtocolError(746 "ledger current_round and remaining_budget must match the authorized budget history."747 )748 canonical_roots: dict[str, dict[str, Any]] = {}749 inventory_owners: dict[str, str] = {}750 owned_evidence_ids: set[str] = set()751 for index, raw_root in enumerate(_array(ledger.get("root_causes"), "ledger.root_causes")):752 root = _object(raw_root, f"ledger.root_causes[{index}]")753 root_id = _text(root.get("id"), f"ledger.root_causes[{index}].id")754 if not ROOT_CAUSE_ID.fullmatch(root_id) or root_id in canonical_roots:755 raise ProtocolError(756 f"Invalid or duplicate canonical root-cause ID: {root_id!r}."757 )758 if root.get("status") not in {"open", "closed"}:759 raise ProtocolError(f"Root cause {root_id} must be open or closed.")760 root_inventory = set(761 _strings(root.get("inventory_ids"), f"root {root_id} inventory")762 )763 root_evidence = set(764 _strings(root.get("contract_evidence_ids"), f"root {root_id} evidence")765 )766 if not root_inventory:767 raise ProtocolError(768 f"Root cause {root_id} must own at least one inventory ID."769 )770 unknown_inventory = sorted(root_inventory - inventory_ids)771 unknown_evidence = sorted(root_evidence - artifacts.keys())772 if unknown_inventory or unknown_evidence:773 raise ProtocolError(774 f"Root cause {root_id} has unknown inventory={unknown_inventory} "775 f"or evidence={unknown_evidence}."776 )777 for inventory_id in root_inventory:778 existing_root = inventory_owners.get(inventory_id)779 if existing_root is not None:780 raise ProtocolError(781 f"Canonical roots {existing_root} and {root_id} overlap on inventory "782 f"{inventory_id}."783 )784 inventory_owners[inventory_id] = root_id785 canonical_roots[root_id] = {786 "status": root["status"],787 "inventory_ids": root_inventory,788 "contract_evidence_ids": root_evidence,789 }790 owned_evidence_ids.update(root_evidence)791 if set(inventory_owners) != inventory_ids:792 raise ProtocolError(793 "Every inventory ID must have exactly one canonical root owner; "794 f"unowned={sorted(inventory_ids - set(inventory_owners))}."795 )796 evidence_bindings = _digest_map(797 ledger.get("contract_evidence_sha256"),798 "ledger.contract_evidence_sha256",799 owned_evidence_ids,800 )801 inventory_bindings = _digest_map(802 ledger.get("inventory_sha256"),803 "ledger.inventory_sha256",804 inventory_ids,805 )806 for evidence_id, digest in evidence_bindings.items():807 if artifacts[evidence_id]["digest"] != digest:808 raise ProtocolError(f"ledger evidence digest mismatch for {evidence_id}.")809 for inventory_id, digest in inventory_bindings.items():810 if inventory_digests[inventory_id] != digest:811 raise ProtocolError(f"ledger inventory digest mismatch for {inventory_id}.")812 813 if current_round > 1 and (prior_ledger_path is None or prior_ledger_sha256 is None):814 raise ProtocolError(815 "Rounds after 1 require a digest-bound prior ledger snapshot."816 )817 if prior_ledger_path is not None or prior_ledger_sha256 is not None:818 if prior_ledger_path is None or prior_ledger_sha256 is None:819 raise ProtocolError("Prior ledger path and SHA-256 must be supplied together.")820 prior_path, prior_data, prior_identity = _read_bytes(821 str(prior_ledger_path), "prior ledger path"822 )823 if prior_identity == ledger_identity:824 raise ProtocolError("Prior ledger snapshot must be distinct from the current ledger.")825 if not SHA256.fullmatch(prior_ledger_sha256):826 raise ProtocolError(827 "Prior ledger SHA-256 must be a lowercase SHA-256 digest."828 )829 if hashlib.sha256(prior_data).hexdigest() != prior_ledger_sha256:830 raise ProtocolError(f"Prior ledger digest mismatch for {prior_path}.")831 prior = _json_bytes(prior_data, str(prior_path))832 if prior.get("task_id") != expected_task_id:833 raise ProtocolError(834 "Prior ledger task_id must match the control-plane task ID."835 )836 prior_internal_path = Path(837 _text(prior.get("path"), "prior ledger.path", concrete=True)838 )839 if (840 not prior_internal_path.is_absolute()841 or prior_internal_path.resolve() != expected_ledger_path842 ):843 raise ProtocolError(844 "Prior ledger.path must match the control-plane ledger path."845 )846 prior_budgets = [847 _integer(848 value, f"prior ledger.authorized_round_budgets[{index}]", minimum=1849 )850 for index, value in enumerate(851 _array(852 prior.get("authorized_round_budgets"),853 "prior ledger.authorized_round_budgets",854 )855 )856 ]857 prior_round = _integer(prior.get("current_round"), "prior ledger.current_round", minimum=1)858 prior_round_fingerprint = _sha256(859 prior.get("round_fingerprint"),860 "prior ledger.round_fingerprint",861 )862 prior_remaining = _integer(863 prior.get("remaining_budget"), "prior ledger.remaining_budget", minimum=0864 )865 if prior_remaining != sum(prior_budgets) - prior_round:866 raise ProtocolError(867 "Prior ledger round state does not match its budget history."868 )869 if authorized_budgets[: len(prior_budgets)] != prior_budgets:870 raise ProtocolError(871 "ledger.authorized_round_budgets must preserve the prior prefix."872 )873 if current_round not in {prior_round, prior_round + 1}:874 raise ProtocolError(875 "ledger.current_round must match the prior round or advance by exactly one."876 )877 if current_round == prior_round and authorized_budgets != prior_budgets:878 raise ProtocolError(879 "A same-round retry budget history must match the prior ledger snapshot."880 )881 if current_round == prior_round and round_fingerprint != prior_round_fingerprint:882 raise ProtocolError(883 "A same-round retry fingerprint must match the prior ledger snapshot."884 )885 prior_roots: dict[str, dict[str, Any]] = {}886 prior_owned_evidence_ids: set[str] = set()887 prior_owned_inventory_ids: set[str] = set()888 for index, raw_root in enumerate(889 _array(prior.get("root_causes"), "prior ledger.root_causes")890 ):891 prior_root = _object(raw_root, f"prior ledger.root_causes[{index}]")892 prior_id = _text(893 prior_root.get("id"), f"prior ledger.root_causes[{index}].id"894 )895 if not ROOT_CAUSE_ID.fullmatch(prior_id) or prior_id in prior_roots:896 raise ProtocolError(897 f"Invalid prior canonical root-cause ID: {prior_id!r}."898 )899 if prior_root.get("status") not in {"open", "closed"}:900 raise ProtocolError(901 f"Prior root cause {prior_id} must be open or closed."902 )903 prior_roots[prior_id] = {904 "status": prior_root["status"],905 "inventory_ids": set(906 _strings(907 prior_root.get("inventory_ids"),908 f"prior root {prior_id} inventory",909 )910 ),911 "contract_evidence_ids": set(912 _strings(913 prior_root.get("contract_evidence_ids"),914 f"prior root {prior_id} evidence",915 )916 ),917 }918 prior_owned_evidence_ids.update(prior_roots[prior_id]["contract_evidence_ids"])919 prior_owned_inventory_ids.update(prior_roots[prior_id]["inventory_ids"])920 prior_evidence_bindings = _digest_map(921 prior.get("contract_evidence_sha256"),922 "prior ledger.contract_evidence_sha256",923 prior_owned_evidence_ids,924 )925 prior_inventory_bindings = _digest_map(926 prior.get("inventory_sha256"),927 "prior ledger.inventory_sha256",928 prior_owned_inventory_ids,929 )930 for prior_id, prior_root in prior_roots.items():931 current_root = canonical_roots.get(prior_id)932 if current_root is None:933 raise ProtocolError(f"ledger removed prior canonical root {prior_id}.")934 new_inventory = current_root["inventory_ids"] - prior_root["inventory_ids"]935 new_evidence = (936 current_root["contract_evidence_ids"]937 - prior_root["contract_evidence_ids"]938 )939 if not prior_root["inventory_ids"].issubset(940 current_root["inventory_ids"]941 ) or not prior_root["contract_evidence_ids"].issubset(942 current_root["contract_evidence_ids"]943 ):944 raise ProtocolError(f"ledger regressed ownership for prior root {prior_id}.")945 for evidence_id in prior_root["contract_evidence_ids"]:946 if evidence_bindings[evidence_id] != prior_evidence_bindings[evidence_id]:947 raise ProtocolError(f"ledger changed prior evidence {evidence_id}.")948 for inventory_id in prior_root["inventory_ids"]:949 if inventory_bindings[inventory_id] != prior_inventory_bindings[inventory_id]:950 raise ProtocolError(f"ledger changed prior inventory {inventory_id}.")951 prior_evidence_digests = {952 prior_evidence_bindings[evidence_id]953 for evidence_id in prior_root["contract_evidence_ids"]954 }955 prior_inventory_digests = {956 prior_inventory_bindings[inventory_id]957 for inventory_id in prior_root["inventory_ids"]958 }959 content_new_evidence = {960 evidence_id961 for evidence_id in new_evidence962 if artifacts[evidence_id]["digest"] not in prior_evidence_digests963 }964 content_new_inventory = {965 inventory_id966 for inventory_id in new_inventory967 if inventory_bindings[inventory_id] not in prior_inventory_digests968 }969 if (970 prior_root["status"] == "closed"971 and current_root["status"] == "open"972 and not (content_new_inventory or content_new_evidence)973 ):974 raise ProtocolError(975 f"ledger reopened prior root {prior_id} without content-new evidence."976 )977 978 selected_dimensions = set(979 _strings(980 packet.get("selected_high_risk_dimensions"), "selected_high_risk_dimensions"981 )982 )983 assignments = _array(packet.get("reviewer_assignments"), "reviewer_assignments")984 if len(assignments) != 2:985 raise ProtocolError("reviewer_assignments must contain exactly two reviewers.")986 reviewer_ids: set[str] = set()987 assigned_inventory: set[str] = set()988 assigned_dimensions: set[str] = set()989 primary_specialty_owners: dict[str, str] = {}990 high_risk_specialty_owners: dict[str, str] = {}991 for index, raw_assignment in enumerate(assignments):992 assignment = _object(raw_assignment, f"reviewer_assignments[{index}]")993 reviewer_id = _text(994 assignment.get("reviewer_id"), f"reviewer_assignments[{index}].id"995 )996 if reviewer_id in reviewer_ids:997 raise ProtocolError(f"Duplicate reviewer ID: {reviewer_id}.")998 reviewer_ids.add(reviewer_id)999 reviewer_inventory = set(1000 _strings(1001 assignment.get("inventory_ids"), f"reviewer {reviewer_id} inventory"1002 )1003 )1004 primary_dimensions = _strings(1005 assignment.get("primary_dimensions"),1006 f"reviewer {reviewer_id} primary dimensions",1007 )1008 if not reviewer_inventory or not primary_dimensions:1009 raise ProtocolError(1010 f"Reviewer {reviewer_id} requires inventory and a primary specialty."1011 )1012 for dimension in primary_dimensions:1013 normalized = dimension.strip().casefold()1014 existing_reviewer = primary_specialty_owners.get(normalized)1015 if existing_reviewer is not None:1016 raise ProtocolError(1017 f"Reviewers {existing_reviewer} and {reviewer_id} have an overlapping "1018 f"primary specialty: {dimension!r}."1019 )1020 primary_specialty_owners[normalized] = reviewer_id1021 assigned_inventory.update(reviewer_inventory)1022 reviewer_dimensions = set(1023 _strings(1024 assignment.get("high_risk_dimensions"),1025 f"reviewer {reviewer_id} dimensions",1026 )1027 )1028 for dimension in reviewer_dimensions:1029 normalized = dimension.strip().casefold()1030 existing_reviewer = high_risk_specialty_owners.get(normalized)1031 if existing_reviewer is not None:1032 raise ProtocolError(1033 f"Reviewers {existing_reviewer} and {reviewer_id} have an overlapping "1034 f"high-risk specialty: {dimension!r}."1035 )1036 high_risk_specialty_owners[normalized] = reviewer_id1037 assigned_dimensions.update(reviewer_dimensions)1038 reviewer_components = set(1039 _strings(1040 assignment.get("expected_components"),1041 f"reviewer {reviewer_id} components",1042 )1043 )1044 reviewer_evidence = set(1045 _strings(assignment.get("evidence_ids"), f"reviewer {reviewer_id} evidence")1046 )1047 required_control_evidence = {1048 artifact_id1049 for artifact_id, artifact in artifacts.items()1050 if artifact["role"]1051 in {"complete-diff", "review-state", "repository-status"}1052 }1053 if (1054 reviewer_components != set(components)1055 or not required_control_evidence.issubset(reviewer_evidence)1056 or not reviewer_evidence.issubset(artifacts)1057 ):1058 raise ProtocolError(1059 f"Reviewer {reviewer_id} must receive every component and control artifact "1060 "without unknown evidence."1061 )1062 if (1063 assigned_inventory != inventory_ids1064 or assigned_dimensions != selected_dimensions1065 ):1066 raise ProtocolError(1067 "Reviewer assignments must cover the exact inventory and dimensions."1068 )1069 1070 verification = _object(packet.get("verification"), "verification")1071 preflight_commands: set[str] = set()1072 for index, result in enumerate(1073 _array(verification.get("preflight_results"), "verification.preflight_results")1074 ):1075 _command_result(result, f"verification.preflight_results[{index}]")1076 command = result["command"]1077 if command in preflight_commands:1078 raise ProtocolError(f"Duplicate preflight command: {command!r}.")1079 preflight_commands.add(command)1080 receipt_identities: set[FileIdentity] = set()1081 receipt_digests: set[str] = set()1082 receipt_digests_by_path: dict[str, str] = {}1083 receipt_commands: set[str] = set()1084 for index, raw_receipt in enumerate(1085 _array(verification.get("credited_receipts"), "verification.credited_receipts")1086 ):1087 receipt_path, receipt_data, receipt_digest, receipt_identity = _descriptor(1088 raw_receipt, f"verification.credited_receipts[{index}]"1089 )1090 if receipt_identity in receipt_identities:1091 raise ProtocolError(f"Duplicate credited receipt file identity: {receipt_path}.")1092 if receipt_digest in receipt_digests:1093 raise ProtocolError(f"Duplicate credited receipt digest: {receipt_digest}.")1094 receipt_identities.add(receipt_identity)1095 receipt_digests.add(receipt_digest)1096 receipt_digests_by_path[str(receipt_path)] = receipt_digest1097 receipt = _json_bytes(receipt_data, str(receipt_path))1098 validate_receipt_data(1099 receipt,1100 combined,1101 components,1102 state["repository_fingerprint"],1103 preflight_commands,1104 )1105 command = receipt["command"]1106 if command in receipt_commands:1107 raise ProtocolError(f"Duplicate credited receipt command: {command!r}.")1108 receipt_commands.add(command)1109 1110 _strings(packet.get("architecture_references"), "architecture_references")1111 return {1112 "packet_path": str(packet_path),1113 "packet_size_bytes": packet_size,1114 "packet_sha256": hashlib.sha256(packet_data).hexdigest(),1115 "ledger_sha256": hashlib.sha256(ledger_data).hexdigest(),1116 "review_state_path": str(artifacts[_at(packet, "review_state.evidence_id")]["path"]),1117 "combined_fingerprint": combined,1118 "components": components,1119 "repository_fingerprint": state["repository_fingerprint"],1120 "inventory_ids": sorted(inventory_ids),1121 "reviewer_ids": sorted(reviewer_ids),1122 "credited_receipt_digests": dict(sorted(receipt_digests_by_path.items())),1123 "credited_receipt_paths": sorted(receipt_digests_by_path),1124 }1125 1126 1127def _revalidate_control_files(1128 packet_path: Path,1129 expected_ledger_path: Path,1130 prior_ledger_path: Path | None,1131 prior_ledger_sha256: str | None,1132 summary: dict[str, Any],1133) -> tuple[dict[str, Any], bytes | None]:1134 packet_data = _read_unchanged(packet_path, summary["packet_sha256"], "Packet")1135 packet = _json_bytes(packet_data, str(packet_path.resolve()))1136 _read_unchanged(expected_ledger_path, summary["ledger_sha256"], "Current ledger")1137 if prior_ledger_path is None:1138 return packet, None1139 if prior_ledger_sha256 is None:1140 raise ProtocolError("Prior ledger path and SHA-256 must be supplied together.")1141 prior_ledger_data = _read_unchanged(1142 prior_ledger_path,1143 prior_ledger_sha256,1144 "Prior ledger",1145 )1146 return packet, prior_ledger_data1147 1148 1149def validate_reviewer_output(1150 packet_path: Path,1151 reviewer_id: str,1152 output_path: Path,1153 expected_task_id: str,1154 expected_ledger_path: Path,1155 prior_ledger_path: Path | None = None,1156 prior_ledger_sha256: str | None = None,1157) -> dict[str, Any]:1158 summary = validate_packet(1159 packet_path,1160 expected_task_id,1161 expected_ledger_path,1162 prior_ledger_path,1163 prior_ledger_sha256,1164 )1165 packet, prior_ledger_data = _revalidate_control_files(1166 packet_path,1167 expected_ledger_path,1168 prior_ledger_path,1169 prior_ledger_sha256,1170 summary,1171 )1172 output = _load_json(output_path)1173 _require_exact_fields(output, REVIEWER_OUTPUT_FIELDS, "Reviewer output")1174 if output["verdict"] not in {1175 "clean",1176 "findings require fixes",1177 "complexity reset required",1178 "incomplete packet",1179 }:1180 raise ProtocolError(f"Invalid reviewer verdict: {output['verdict']!r}.")1181 expected_fingerprints = {1182 "packet": summary["packet_sha256"],1183 "combined": summary["combined_fingerprint"],1184 "components": summary["components"],1185 "repository": summary["repository_fingerprint"],1186 }1187 if output["reviewed_fingerprints"] != expected_fingerprints:1188 raise ProtocolError("Reviewer packet digest or fingerprints do not match exactly.")1189 assignment = next(1190 (1191 item1192 for item in packet["reviewer_assignments"]1193 if item.get("reviewer_id") == reviewer_id1194 ),1195 None,1196 )1197 if assignment is None:1198 raise ProtocolError(f"Unknown reviewer ID: {reviewer_id}.")1199 1200 checked = set(_strings(output["checked_inventory_ids"], "checked_inventory_ids"))1201 unchecked: set[str] = set()1202 for index, raw_item in enumerate(1203 _array(output["unchecked_inventory_ids"], "unchecked_inventory_ids")1204 ):1205 item = _object(raw_item, f"unchecked_inventory_ids[{index}]")1206 _require_exact_fields(1207 item,1208 {"id", "reason"},1209 f"unchecked_inventory_ids[{index}]",1210 )1211 unchecked_id = _text(item.get("id"), f"unchecked_inventory_ids[{index}].id")1212 if unchecked_id in unchecked:1213 raise ProtocolError(f"Duplicate unchecked inventory ID: {unchecked_id}.")1214 unchecked.add(unchecked_id)1215 _text(1216 item.get("reason"),1217 f"unchecked_inventory_ids[{index}].reason",1218 concrete=True,1219 )1220 if checked & unchecked or checked | unchecked != set(assignment["inventory_ids"]):1221 raise ProtocolError(1222 "Reviewer inventory accounting differs from the assignment."1223 )1224 if set(1225 _strings(output["high_risk_dimensions_checked"], "high_risk_dimensions_checked")1226 ) != set(assignment["high_risk_dimensions"]):1227 raise ProtocolError(1228 "Reviewer high-risk dimension accounting differs from the assignment."1229 )1230 for index, probe in enumerate(_array(output["focused_probes"], "focused_probes")):1231 _command_result(probe, f"focused_probes[{index}]")1232 1233 canonical_roots = {root["id"]: root for root in packet["ledger"]["root_causes"]}1234 prior_canonical_roots: dict[str, dict[str, Any]] = {}1235 prior_evidence_bindings: dict[str, str] = {}1236 prior_inventory_bindings: dict[str, str] = {}1237 if prior_ledger_data is not None:1238 prior_ledger = _json_bytes(prior_ledger_data, str(prior_ledger_path))1239 prior_canonical_roots = {root["id"]: root for root in prior_ledger["root_causes"]}1240 prior_evidence_bindings = prior_ledger["contract_evidence_sha256"]1241 prior_inventory_bindings = prior_ledger["inventory_sha256"]1242 evidence_digests = {1243 artifact["id"]: artifact["sha256"] for artifact in packet["evidence_artifacts"]1244 }1245 inventory_digests = packet["ledger"]["inventory_sha256"]1246 indexed_evidence = set(evidence_digests)1247 indexed_inventory = {row["id"] for row in packet["inventory"]}1248 owned_evidence = {1249 evidence_id1250 for root in canonical_roots.values()1251 for evidence_id in root["contract_evidence_ids"]1252 }1253 owned_evidence_digests = {evidence_digests[evidence_id] for evidence_id in owned_evidence}1254 inventory_owners = {1255 inventory_id: root_id1256 for root_id, root in canonical_roots.items()1257 for inventory_id in root["inventory_ids"]1258 }1259 findings = _array(output["findings"], "findings")1260 proposed_roots: set[str] = set()1261 proposed_evidence_owners: dict[str, str] = {}1262 for index, raw_finding in enumerate(findings):1263 finding = _object(raw_finding, f"findings[{index}]")1264 _require_exact_fields(finding, FINDING_FIELDS, f"Finding {index}")1265 if finding["priority"] not in {"P0", "P1", "P2", "P3"}:1266 raise ProtocolError(f"Finding {index} has an invalid priority.")1267 for field in FINDING_FIELDS - {1268 "priority",1269 "root_cause_id",1270 "root_cause_evidence",1271 }:1272 _text(finding[field], f"findings[{index}].{field}")1273 root_id = _text(finding["root_cause_id"], f"findings[{index}].root_cause_id")1274 evidence = _object(finding["root_cause_evidence"], f"findings[{index}].root_cause_evidence")1275 _require_exact_fields(1276 evidence,1277 {"new_contract_evidence_ids", "new_inventory_ids"},1278 f"findings[{index}].root_cause_evidence",1279 )1280 new_evidence = set(1281 _strings(1282 evidence.get("new_contract_evidence_ids"), f"finding {index} evidence"1283 )1284 )1285 new_inventory = set(1286 _strings(evidence.get("new_inventory_ids"), f"finding {index} inventory")1287 )1288 if not new_evidence.issubset(indexed_evidence) or not new_inventory.issubset(1289 indexed_inventory1290 ):1291 raise ProtocolError(f"Finding {index} references unindexed root evidence.")1292 root = canonical_roots.get(root_id)1293 if root is not None:1294 foreign_inventory = sorted(1295 inventory_id1296 for inventory_id in new_inventory1297 if inventory_owners.get(inventory_id) != root_id1298 )1299 if foreign_inventory:1300 raise ProtocolError(1301 f"Finding {index} reassigns inventory owned by another canonical root: "1302 f"{foreign_inventory}."1303 )1304 prior_root = prior_canonical_roots.get(root_id)1305 prior_evidence = (1306 set(prior_root["contract_evidence_ids"]) if prior_root else set()1307 )1308 prior_inventory = set(prior_root["inventory_ids"]) if prior_root else set()1309 added_evidence = set(root["contract_evidence_ids"]) - prior_evidence1310 added_inventory = set(root["inventory_ids"]) - prior_inventory1311 if not new_evidence.issubset(added_evidence) or not new_inventory.issubset(1312 added_inventory1313 ):1314 raise ProtocolError(1315 f"Finding {index} root evidence must be new in the current ledger round."1316 )1317 prior_evidence_digests = {1318 prior_evidence_bindings[evidence_id] for evidence_id in prior_evidence1319 }1320 prior_inventory_digests = {1321 prior_inventory_bindings[inventory_id] for inventory_id in prior_inventory1322 }1323 content_new_evidence = {1324 evidence_id1325 for evidence_id in new_evidence1326 if evidence_digests[evidence_id] not in prior_evidence_digests1327 }1328 content_new_inventory = {1329 inventory_id1330 for inventory_id in new_inventory1331 if inventory_digests[inventory_id] not in prior_inventory_digests1332 }1333 new_for_root = bool(content_new_evidence or content_new_inventory)1334 if root["status"] == "closed" and not new_for_root:1335 raise ProtocolError(1336 f"Finding {index} reopens closed root {root_id} without content-new evidence."1337 )1338 elif not NEW_ROOT_CAUSE_ID.fullmatch(root_id):1339 raise ProtocolError(1340 f"Finding {index} must reuse a canonical root ID or propose NEW:<slug>."1341 )1342 else:1343 if new_inventory:1344 raise ProtocolError(1345 f"Finding {index} new root proposal cannot reuse canonical inventory: "1346 f"{sorted(new_inventory)}."1347 )1348 proposal_digests = {1349 evidence_digests[evidence_id]1350 for evidence_id in new_evidence1351 if evidence_digests[evidence_id] not in owned_evidence_digests1352 }1353 available_digests = {1354 digest1355 for digest in proposal_digests1356 if proposed_evidence_owners.get(digest) in {None, root_id}1357 }1358 if not available_digests:1359 existing_owners = sorted(1360 {1361 proposed_evidence_owners[digest]1362 for digest in proposal_digests1363 if digest in proposed_evidence_owners1364 }1365 )1366 if existing_owners:1367 raise ProtocolError(1368 f"Finding {index} reuses evidence owned by proposed root {existing_owners}."1369 )1370 raise ProtocolError(1371 f"Finding {index} proposes {root_id} without content-new evidence."1372 )1373 for digest in available_digests:1374 proposed_evidence_owners.setdefault(digest, root_id)1375 proposed_roots.add(root_id)1376 1377 uncertainty = _strings(output["remaining_uncertainty"], "remaining_uncertainty")1378 if (1379 output["verdict"] in {"findings require fixes", "complexity reset required"}1380 and not findings1381 ):1382 raise ProtocolError(1383 f"Verdict {output['verdict']!r} requires at least one finding."1384 )1385 if output["verdict"] == "clean" and (unchecked or uncertainty or findings):1386 raise ProtocolError(1387 "A clean verdict requires no unchecked IDs, uncertainty, or findings."1388 )1389 inspection_count = _integer(1390 output["inspection_call_count"], "inspection_call_count", minimum=01391 )1392 reason = _text(output["inspection_budget_reason"], "inspection_budget_reason")1393 if inspection_count > 12 and reason.strip().lower() in SENTINELS:1394 raise ProtocolError(1395 "Inspection counts above 12 require an inspection_budget_reason."1396 )1397 for index, raw_scan in enumerate(1398 _array(output["sibling_scenario_scan"], "sibling_scenario_scan")1399 ):1400 scan = _object(raw_scan, f"sibling_scenario_scan[{index}]")1401 _require_exact_fields(1402 scan,1403 {"root_cause_id", "inventory_ids", "result"},1404 f"sibling_scenario_scan[{index}]",1405 )1406 root_id = _text(scan.get("root_cause_id"), f"sibling_scenario_scan[{index}].root_cause_id")1407 if root_id not in canonical_roots and root_id not in proposed_roots:1408 raise ProtocolError(1409 f"sibling_scenario_scan[{index}] must reference a canonical or proposed root."1410 )1411 if not set(1412 _strings(1413 scan.get("inventory_ids"),1414 f"sibling_scenario_scan[{index}].inventory_ids",1415 )1416 ).issubset(indexed_inventory):1417 raise ProtocolError(1418 f"sibling_scenario_scan[{index}] references unknown inventory IDs."1419 )1420 _text(1421 scan.get("result"), f"sibling_scenario_scan[{index}].result", concrete=True1422 )1423 return {1424 "reviewer_id": reviewer_id,1425 "verdict": output["verdict"],1426 "combined_fingerprint": summary["combined_fingerprint"],1427 "finding_count": len(findings),1428 }1429 1430 1431def _validate_credited_receipt(1432 packet_path: Path,1433 receipt_path: Path,1434 expected_task_id: str,1435 expected_ledger_path: Path,1436 prior_ledger_path: Path | None = None,1437 prior_ledger_sha256: str | None = None,1438) -> dict[str, Any]:1439 summary = validate_packet(1440 packet_path,1441 expected_task_id,1442 expected_ledger_path,1443 prior_ledger_path,1444 prior_ledger_sha256,1445 )1446 _revalidate_control_files(1447 packet_path,1448 expected_ledger_path,1449 prior_ledger_path,1450 prior_ledger_sha256,1451 summary,1452 )1453 canonical_receipt_path = receipt_path.resolve()1454 expected_digest = summary["credited_receipt_digests"].get(str(canonical_receipt_path))1455 if expected_digest is None:1456 raise ProtocolError("The receipt path is not indexed by the validated packet.")1457 _read_unchanged(canonical_receipt_path, expected_digest, "Receipt")1458 return {1459 "receipt_path": str(canonical_receipt_path),1460 "receipt_sha256": expected_digest,1461 "combined_fingerprint": summary["combined_fingerprint"],1462 "reusable": True,1463 }1464 1465 1466def main() -> None:1467 parser = argparse.ArgumentParser()1468 commands = parser.add_subparsers(dest="command", required=True)1469 packet_parser = commands.add_parser("packet")1470 packet_parser.add_argument("--packet", type=Path, required=True)1471 output_parser = commands.add_parser("reviewer-output")1472 output_parser.add_argument("--packet", type=Path, required=True)1473 output_parser.add_argument("--reviewer", required=True)1474 output_parser.add_argument("--output", type=Path, required=True)1475 receipt_parser = commands.add_parser("receipt")1476 receipt_parser.add_argument("--packet", type=Path, required=True)1477 receipt_parser.add_argument("--receipt", type=Path, required=True)1478 for command_parser in (packet_parser, output_parser, receipt_parser):1479 command_parser.add_argument("--task-id", required=True)1480 command_parser.add_argument("--ledger", type=Path, required=True)1481 command_parser.add_argument("--prior-ledger", type=Path)1482 command_parser.add_argument("--prior-ledger-sha256")1483 args = parser.parse_args()1484 try:1485 if args.command == "packet":1486 result = validate_packet(1487 args.packet,1488 args.task_id,1489 args.ledger,1490 args.prior_ledger,1491 args.prior_ledger_sha256,1492 )1493 elif args.command == "reviewer-output":1494 result = validate_reviewer_output(1495 args.packet,1496 args.reviewer,1497 args.output,1498 args.task_id,1499 args.ledger,1500 args.prior_ledger,1501 args.prior_ledger_sha256,1502 )1503 else:1504 result = _validate_credited_receipt(1505 args.packet,1506 args.receipt,1507 args.task_id,1508 args.ledger,1509 args.prior_ledger,1510 args.prior_ledger_sha256,1511 )1512 except ProtocolError as error:1513 parser.error(str(error))1514 print(json.dumps(result, indent=2, sort_keys=True))1515 1516 1517if __name__ == "__main__":1518 main()1519