@ccoalm/ccl-skills 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +11 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-unverified-cli-flag.sh +309 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_unverified_cli_flag.sh +483 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +5 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +10 -8
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +1 -1
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +16 -17
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +1 -1
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +195 -7
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +3 -3
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +13 -5
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +9 -3
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +9 -3
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/normalize_review_timeout.sh +22 -0
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +9 -3
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +1540 -129
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +8 -3
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +76 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +1858 -3
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +789 -0
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/update_review_plan_intent.py +513 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +1 -1
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +2 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +1 -1
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +14 -11
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +2 -2
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +5 -1
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -1
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +13 -11
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +64 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/agents/openai.yaml +4 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +72 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/runtime-and-project-contract.md +58 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +41 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/verification-diagnostics-and-security.md +63 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +1 -1
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +25 -9
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +1 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +1 -1
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +16 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +7 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +11 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -10
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +10 -14
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +1 -1
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +135 -86
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +66 -80
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/delivery-contract.md +275 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +88 -214
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +2 -2
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +10 -8
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +4 -5
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +112 -95
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +30 -21
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +22 -3
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +20 -17
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +7 -9
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +14 -10
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +2 -0
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +1 -1
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +9 -6
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +3 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +37 -10
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +7 -1
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +8 -5
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +16 -5
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +4 -2
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +5 -1
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +1 -1
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/audit-history-architecture.md +31 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +1 -1
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +7 -4
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +2 -2
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/notification-architecture.md +28 -0
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +1 -1
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/replay-comparison-architecture.md +28 -0
  78. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/workflow-state-architecture.md +39 -0
  79. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +10 -7
  80. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +8 -0
  81. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/audit-history-patterns.md +29 -0
  82. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +16 -0
  83. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +25 -1
  84. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/notification-patterns.md +40 -0
  85. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +1 -1
  86. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/replay-comparison-patterns.md +30 -0
  87. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +48 -0
  88. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +10 -1
  89. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +2 -0
  90. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +4 -4
  91. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
  92. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +142 -4
  93. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +21 -2
  94. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +11 -9
  95. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
  96. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +5 -4
  97. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +102 -0
  98. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +69 -0
  99. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +10 -0
  100. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +6 -6
  101. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +4 -3
  102. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +93 -2
  103. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-parallel-stack-parity.sh +119 -0
  104. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +22 -0
  105. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +49 -4
  106. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/obligation-ledger.py +2748 -0
  107. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +20 -5
  108. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/shared_git_surface_gate.py +1142 -0
  109. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_parallel_stack_parity.sh +183 -0
  110. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +19 -0
  111. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +41 -4
  112. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +120 -0
  113. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +82 -8
  114. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +336 -0
  115. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +82 -10
  116. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger.sh +1416 -0
  117. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger_repo_audit.sh +57 -0
  118. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +141 -4
  119. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +3 -1
  120. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_shared_git_surface_gate.sh +1696 -0
  121. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2117 -0
  122. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +316 -0
  123. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +1176 -0
  124. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +31 -1
  125. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +9 -4
  126. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +980 -0
  127. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +9 -6
  128. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +11 -11
  129. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +10 -2
  130. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +16 -0
  131. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +1 -1
  132. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +16 -5
  133. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +5 -3
  134. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +16 -6
  135. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/self-benchmark-baseline.md +37 -0
  136. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +7 -5
  137. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +1 -1
  138. package/dist/assets/release.json +275 -105
  139. package/package.json +1 -1
@@ -0,0 +1,980 @@
1
+ #!/usr/bin/env python3
2
+ """Validate a receipt-bound terminal ledger for an extraction review lane."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ import stat
12
+ import sys
13
+ import unicodedata
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Any, NoReturn
17
+
18
+ MAX_LEDGER_BYTES = 128_000
19
+ MAX_RESULT_BYTES = 1_000_000
20
+ MAX_EVIDENCE_BYTES = 128_000
21
+ OBJECT_ID_RE = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})")
22
+ SHA256_RE = re.compile(r"[0-9a-f]{64}")
23
+ RFC3339_RE = re.compile(
24
+ r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})"
25
+ )
26
+ TERMINAL_STATES = {
27
+ "ready_for_human_decision",
28
+ "continuation_authorization_required",
29
+ "baseline_race",
30
+ }
31
+ EXTERNAL_REVIEW_STATES = {"reviewed", "findings_pending", "post_review_budget"}
32
+ KNOWN_REVIEW_STATES = EXTERNAL_REVIEW_STATES | {"self_reviewed"}
33
+ DISPOSITIONS = {
34
+ "fixed",
35
+ "source_refuted",
36
+ "accepted_tradeoff",
37
+ "pre_existing_out_of_scope",
38
+ "needs_human_decision",
39
+ "open",
40
+ }
41
+ RESOLVED_DISPOSITIONS = {
42
+ "fixed",
43
+ "source_refuted",
44
+ "accepted_tradeoff",
45
+ "pre_existing_out_of_scope",
46
+ }
47
+
48
+
49
+ class StateError(Exception):
50
+ pass
51
+
52
+
53
+ def fail(message: str) -> NoReturn:
54
+ raise StateError(message)
55
+
56
+
57
+ def bounded_text(value: object, field: str, maximum: int = 1000) -> str:
58
+ if not isinstance(value, str) or value != value.strip() or not value:
59
+ fail(f"{field} must be a non-empty normalized string")
60
+ # Interior control characters would otherwise be echoed into stderr
61
+ # diagnostics (terminal-escape injection on the error path). C1 controls
62
+ # (NEL, CSI) and Unicode line/paragraph separators forge line breaks too.
63
+ if any(
64
+ ord(char) < 0x20 or 0x7F <= ord(char) <= 0x9F or char in "\u2028\u2029"
65
+ for char in value
66
+ ):
67
+ fail(f"{field} must not contain control characters")
68
+ # Default-ignorable format characters (ZWSP, word joiner, bidi controls)
69
+ # let visually identical class keys or predicates register as distinct,
70
+ # splitting one recurrence class below its sweep threshold.
71
+ if any(unicodedata.category(char) == "Cf" for char in value):
72
+ fail(f"{field} must not contain format characters")
73
+ try:
74
+ value.encode("utf-8")
75
+ except UnicodeEncodeError:
76
+ fail(f"{field} must be valid UTF-8 text")
77
+ if len(value) > maximum:
78
+ fail(f"{field} exceeds {maximum} characters")
79
+ return value
80
+
81
+
82
+ def object_id(value: object, field: str) -> str:
83
+ text = bounded_text(value, field, 64)
84
+ if OBJECT_ID_RE.fullmatch(text) is None:
85
+ fail(f"{field} must be a lowercase 40- or 64-character hexadecimal object id")
86
+ return text
87
+
88
+
89
+ def sha256(value: object, field: str) -> str:
90
+ text = bounded_text(value, field, 64)
91
+ if SHA256_RE.fullmatch(text) is None:
92
+ fail(f"{field} must be a lowercase 64-character SHA-256 digest")
93
+ return text
94
+
95
+
96
+ def exact_object(value: object, fields: set[str], label: str) -> dict[str, Any]:
97
+ if not isinstance(value, dict) or set(value) != fields:
98
+ fail(f"{label} must contain exactly: {', '.join(sorted(fields))}")
99
+ return value
100
+
101
+
102
+ def read_regular(path: Path, *, label: str, maximum: int) -> bytes:
103
+ if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_NONBLOCK"):
104
+ fail(f"platform cannot open {label} with no-follow and non-blocking safety")
105
+ try:
106
+ fd = os.open(
107
+ path,
108
+ os.O_RDONLY
109
+ | os.O_NOFOLLOW
110
+ | getattr(os, "O_CLOEXEC", 0)
111
+ | os.O_NONBLOCK,
112
+ )
113
+ except (OSError, UnicodeError, ValueError) as exc:
114
+ fail(f"{label} is unreadable: {exc}")
115
+ try:
116
+ try:
117
+ info = os.fstat(fd)
118
+ except OSError as exc:
119
+ fail(f"cannot inspect {label}: {exc}")
120
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
121
+ fail(f"{label} must be a singly linked regular file")
122
+ if info.st_size > maximum:
123
+ fail(f"{label} exceeds {maximum} bytes")
124
+ chunks: list[bytes] = []
125
+ remaining = maximum + 1
126
+ while remaining:
127
+ try:
128
+ chunk = os.read(fd, remaining)
129
+ except OSError as exc:
130
+ fail(f"cannot read {label}: {exc}")
131
+ if not chunk:
132
+ break
133
+ chunks.append(chunk)
134
+ remaining -= len(chunk)
135
+ raw = b"".join(chunks)
136
+ finally:
137
+ try:
138
+ os.close(fd)
139
+ except OSError:
140
+ pass
141
+ if len(raw) > maximum:
142
+ fail(f"{label} exceeds {maximum} bytes")
143
+ return raw
144
+
145
+
146
+ def decode_json(raw: bytes, *, label: str) -> dict[str, Any]:
147
+ def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
148
+ value: dict[str, Any] = {}
149
+ for key, item in pairs:
150
+ if key in value:
151
+ fail(f"{label} contains duplicate object key: {key}")
152
+ value[key] = item
153
+ return value
154
+
155
+ def reject_constant(constant: str) -> NoReturn:
156
+ fail(f"{label} contains a non-standard JSON constant: {constant}")
157
+
158
+ try:
159
+ value = json.loads(
160
+ raw.decode("utf-8"),
161
+ object_pairs_hook=unique_object,
162
+ parse_constant=reject_constant,
163
+ )
164
+ except StateError:
165
+ raise
166
+ except (UnicodeError, json.JSONDecodeError) as exc:
167
+ fail(f"{label} must be UTF-8 JSON: {exc}")
168
+ if not isinstance(value, dict):
169
+ fail(f"{label} must be a JSON object")
170
+ return value
171
+
172
+
173
+ def sibling_name(value: object, field: str) -> str:
174
+ name = bounded_text(value, field, 255)
175
+ if Path(name).is_absolute() or Path(name).name != name or "/" in name or "\\" in name:
176
+ fail(f"{field} must name a file in the ledger directory")
177
+ return name
178
+
179
+
180
+ def load_sibling(
181
+ ledger_dir: Path,
182
+ *,
183
+ file_value: object,
184
+ digest_value: object,
185
+ label: str,
186
+ maximum: int,
187
+ ) -> bytes:
188
+ name = sibling_name(file_value, f"{label}.file")
189
+ expected = sha256(digest_value, f"{label}.sha256")
190
+ raw = read_regular(ledger_dir / name, label=label, maximum=maximum)
191
+ actual = hashlib.sha256(raw).hexdigest()
192
+ if actual != expected:
193
+ fail(f"{label} digest does not match {name}")
194
+ return raw
195
+
196
+
197
+ def canonical_hash(value: object, label: str) -> str:
198
+ try:
199
+ encoded = json.dumps(
200
+ value,
201
+ ensure_ascii=False,
202
+ sort_keys=True,
203
+ separators=(",", ":"),
204
+ ).encode()
205
+ except (TypeError, ValueError, UnicodeError) as exc:
206
+ fail(f"{label} is not canonical JSON: {exc}")
207
+ return hashlib.sha256(encoded).hexdigest()
208
+
209
+
210
+ def parse_rfc3339(value: object, field: str) -> datetime:
211
+ text = bounded_text(value, field, 100)
212
+ if RFC3339_RE.fullmatch(text) is None:
213
+ fail(f"{field} must be a strict RFC3339 timestamp")
214
+ try:
215
+ parsed = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text)
216
+ except ValueError as exc:
217
+ fail(f"{field} must be a valid RFC3339 timestamp: {exc}")
218
+ if parsed.utcoffset() is None:
219
+ fail(f"{field} must include an RFC3339 UTC offset")
220
+ return parsed
221
+
222
+
223
+ def load(path: Path) -> tuple[dict[str, Any], Path]:
224
+ raw = read_regular(path, label="ledger", maximum=MAX_LEDGER_BYTES)
225
+ value = decode_json(raw, label="ledger")
226
+ payload = exact_object(
227
+ value,
228
+ {
229
+ "schema_version",
230
+ "candidate_sha256",
231
+ "controller_receipts",
232
+ "completion_receipt",
233
+ "base_attestations",
234
+ "autonomous_round",
235
+ "controller_review_state",
236
+ "finding_classes",
237
+ "unreviewed_delta",
238
+ "closeout_state",
239
+ },
240
+ "ledger",
241
+ )
242
+ return payload, path.absolute().parent
243
+
244
+
245
+ def validate_scope(receipt: dict[str, Any], label: str) -> str:
246
+ scope = exact_object(
247
+ receipt.get("review_scope"),
248
+ {
249
+ "schema_version",
250
+ "intent_sha256",
251
+ "acceptance_sha256",
252
+ "stage",
253
+ "review_depth",
254
+ "risk_tags",
255
+ "challenge_budget",
256
+ "wording_only_proof_sha256",
257
+ "wording_only_scope_sha256",
258
+ },
259
+ f"{label}.review_scope",
260
+ )
261
+ if scope["schema_version"] != 3 or type(scope["schema_version"]) is not int:
262
+ fail(f"{label}.review_scope.schema_version must be 3")
263
+ sha256(scope["intent_sha256"], f"{label}.review_scope.intent_sha256")
264
+ sha256(scope["acceptance_sha256"], f"{label}.review_scope.acceptance_sha256")
265
+ stage = bounded_text(scope["stage"], f"{label}.review_scope.stage", 80)
266
+ depth = bounded_text(scope["review_depth"], f"{label}.review_scope.review_depth", 80)
267
+ risks = scope["risk_tags"]
268
+ if not isinstance(risks, list):
269
+ fail(f"{label}.review_scope.risk_tags must be an array")
270
+ normalized_risks = [
271
+ bounded_text(item, f"{label}.review_scope.risk_tags", 100) for item in risks
272
+ ]
273
+ if len(normalized_risks) != len(set(normalized_risks)):
274
+ fail(f"{label}.review_scope.risk_tags contains duplicates")
275
+ if scope["challenge_budget"] != 2 or type(scope["challenge_budget"]) is not int:
276
+ fail(f"{label}.review_scope.challenge_budget must be 2")
277
+ if (
278
+ scope["wording_only_proof_sha256"] is not None
279
+ or scope["wording_only_scope_sha256"] is not None
280
+ or "wording_only_proof_sha256" not in receipt
281
+ or receipt["wording_only_proof_sha256"] is not None
282
+ or "wording_only_scope" not in receipt
283
+ or receipt["wording_only_scope"] is not None
284
+ ):
285
+ fail(f"{label} must bind the non-wording extraction scope")
286
+ recorded = sha256(receipt.get("review_scope_sha256"), f"{label}.review_scope_sha256")
287
+ if canonical_hash(scope, f"{label}.review_scope") != recorded:
288
+ fail(f"{label}.review_scope_sha256 does not reproduce review_scope")
289
+ if (
290
+ receipt.get("stage") != stage
291
+ or receipt.get("review_depth") != depth
292
+ or receipt.get("risk_tags") != normalized_risks
293
+ ):
294
+ fail(f"{label} top-level scope fields contradict review_scope")
295
+ return recorded
296
+
297
+
298
+ def validate_controller_receipts(
299
+ payload: dict[str, Any], ledger_dir: Path
300
+ ) -> tuple[list[dict[str, Any]], list[str], dict[str, list[str]], str, str]:
301
+ refs = payload["controller_receipts"]
302
+ if not isinstance(refs, list) or not 1 <= len(refs) <= 3:
303
+ fail("controller_receipts must contain one to three ordered Agent rounds")
304
+ if payload["autonomous_round"] != len(refs) or type(payload["autonomous_round"]) is not int:
305
+ fail("autonomous_round must equal the ordered controller receipt count")
306
+
307
+ receipts: list[dict[str, Any]] = []
308
+ receipt_hashes: list[str] = []
309
+ finding_hashes: dict[str, list[str]] = {}
310
+ chain_id: str | None = None
311
+ scope_hash: str | None = None
312
+ ledger_candidate = sha256(
313
+ payload["candidate_sha256"], "ledger.candidate_sha256"
314
+ )
315
+ for expected_index, value in enumerate(refs, start=1):
316
+ ref = exact_object(
317
+ value, {"sequence", "file", "sha256"}, f"controller_receipts[{expected_index - 1}]"
318
+ )
319
+ if ref["sequence"] != expected_index or type(ref["sequence"]) is not int:
320
+ fail("controller receipt sequence must be contiguous from 1")
321
+ receipt_hash = sha256(ref["sha256"], f"controller_receipts[{expected_index - 1}].sha256")
322
+ raw = load_sibling(
323
+ ledger_dir,
324
+ file_value=ref["file"],
325
+ digest_value=receipt_hash,
326
+ label=f"controller receipt {expected_index}",
327
+ maximum=MAX_RESULT_BYTES,
328
+ )
329
+ receipt = decode_json(raw, label=f"controller receipt {expected_index}")
330
+ if receipt.get("schema_version") != 3 or type(receipt.get("schema_version")) is not int:
331
+ fail(f"controller receipt {expected_index} schema_version must be 3")
332
+ expected_mode = "review" if expected_index == 1 else "challenge"
333
+ if receipt.get("mode") != expected_mode:
334
+ fail(f"controller receipt {expected_index} must have mode {expected_mode}")
335
+ if receipt.get("status") not in {"passed", "findings"}:
336
+ fail(f"controller receipt {expected_index} must have status passed or findings")
337
+ if receipt.get("review_chain_tracked") is not True:
338
+ fail(f"controller receipt {expected_index} must belong to a tracked chain")
339
+ current_chain = bounded_text(
340
+ receipt.get("review_chain_id"), f"controller receipt {expected_index}.review_chain_id", 120
341
+ )
342
+ if chain_id is None:
343
+ chain_id = current_chain
344
+ elif current_chain != chain_id:
345
+ fail(f"controller receipt {expected_index} review_chain_id changed")
346
+ if receipt.get("autonomous_review_index") != expected_index or type(
347
+ receipt.get("autonomous_review_index")
348
+ ) is not int:
349
+ fail(f"controller receipt {expected_index} autonomous_review_index is not contiguous")
350
+ expected_challenge_index = 0 if expected_index == 1 else expected_index - 1
351
+ if receipt.get("challenge_index") != expected_challenge_index or type(
352
+ receipt.get("challenge_index")
353
+ ) is not int:
354
+ fail(f"controller receipt {expected_index} challenge_index is invalid")
355
+ if receipt.get("challenge_budget") != 2 or type(receipt.get("challenge_budget")) is not int:
356
+ fail(f"controller receipt {expected_index} challenge_budget must be 2")
357
+ if receipt.get("autonomous_review_budget") != 3 or type(
358
+ receipt.get("autonomous_review_budget")
359
+ ) is not int:
360
+ fail(f"controller receipt {expected_index} autonomous_review_budget must be 3")
361
+ expected_remaining = 3 - expected_index
362
+ if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
363
+ receipt.get("autonomous_reviews_remaining")
364
+ ) is not int:
365
+ fail(f"controller receipt {expected_index} autonomous_reviews_remaining is invalid")
366
+ if receipt.get("autonomous_review_allowed") is not (expected_remaining > 0):
367
+ fail(f"controller receipt {expected_index} autonomous_review_allowed is invalid")
368
+ prior = receipt.get("prior_review_result_sha256")
369
+ if prior != receipt_hashes:
370
+ fail(f"controller receipt {expected_index} prior_review_result_sha256 is not the complete ordered prefix")
371
+ candidate = sha256(
372
+ receipt.get("candidate_sha256"), f"controller receipt {expected_index}.candidate_sha256"
373
+ )
374
+ packet = sha256(
375
+ receipt.get("packet_sha256"), f"controller receipt {expected_index}.packet_sha256"
376
+ )
377
+ if packet != candidate:
378
+ fail(f"controller receipt {expected_index} packet_sha256 must equal candidate_sha256")
379
+ # Every counted round must have inspected the exact final candidate;
380
+ # a chain whose review round saw an earlier candidate is not a review
381
+ # of the candidate this ledger closes out.
382
+ if candidate != ledger_candidate:
383
+ fail(
384
+ f"controller receipt {expected_index} does not bind the ledger candidate"
385
+ )
386
+ current_scope_hash = validate_scope(receipt, f"controller receipt {expected_index}")
387
+ if scope_hash is None:
388
+ scope_hash = current_scope_hash
389
+ elif current_scope_hash != scope_hash:
390
+ fail(f"controller receipt {expected_index} review scope changed")
391
+
392
+ findings = receipt.get("findings")
393
+ if not isinstance(findings, list):
394
+ fail(f"controller receipt {expected_index}.findings must be an array")
395
+ current_findings: list[str] = []
396
+ current_finding_set: set[str] = set()
397
+ for finding_index, finding in enumerate(findings):
398
+ if not isinstance(finding, dict):
399
+ fail(f"controller receipt {expected_index}.findings[{finding_index}] must be an object")
400
+ finding_hash = canonical_hash(
401
+ finding, f"controller receipt {expected_index}.findings[{finding_index}]"
402
+ )
403
+ if finding_hash in current_finding_set:
404
+ fail(f"controller receipt {expected_index} repeats a canonical finding")
405
+ current_finding_set.add(finding_hash)
406
+ current_findings.append(finding_hash)
407
+ if receipt["status"] == "passed" and findings:
408
+ fail(f"controller receipt {expected_index} passed status cannot carry findings")
409
+ if receipt["status"] == "findings" and not findings:
410
+ fail(f"controller receipt {expected_index} findings status requires findings")
411
+ state = bounded_text(
412
+ receipt.get("review_state"), f"controller receipt {expected_index}.review_state", 80
413
+ )
414
+ if state not in EXTERNAL_REVIEW_STATES:
415
+ fail(f"controller receipt {expected_index} has unknown controller review_state {state}")
416
+ expected_state = (
417
+ "post_review_budget"
418
+ if receipt["status"] == "findings" and expected_index == 3
419
+ else "findings_pending"
420
+ if receipt["status"] == "findings"
421
+ else "reviewed"
422
+ )
423
+ if state != expected_state:
424
+ fail(f"controller receipt {expected_index} review_state contradicts status and round")
425
+ if receipt.get("human_decision_required") is not (state == "post_review_budget"):
426
+ fail(f"controller receipt {expected_index} human_decision_required contradicts review_state")
427
+
428
+ receipts.append(receipt)
429
+ receipt_hashes.append(receipt_hash)
430
+ finding_hashes[receipt_hash] = current_findings
431
+
432
+ if chain_id is None or scope_hash is None:
433
+ fail("controller receipt chain is empty")
434
+ return receipts, receipt_hashes, finding_hashes, chain_id, scope_hash
435
+
436
+
437
+ def validate_completion_receipt(
438
+ value: object,
439
+ ledger_dir: Path,
440
+ receipts: list[dict[str, Any]],
441
+ receipt_hashes: list[str],
442
+ chain_id: str,
443
+ scope_hash: str,
444
+ ) -> dict[str, Any] | None:
445
+ if value is None:
446
+ return None
447
+ ref = exact_object(value, {"file", "sha256"}, "completion_receipt")
448
+ raw = load_sibling(
449
+ ledger_dir,
450
+ file_value=ref["file"],
451
+ digest_value=ref["sha256"],
452
+ label="completion receipt",
453
+ maximum=MAX_RESULT_BYTES,
454
+ )
455
+ receipt = decode_json(raw, label="completion receipt")
456
+ final = receipts[-1]
457
+ if (
458
+ receipt.get("schema_version") != 3
459
+ or type(receipt.get("schema_version")) is not int
460
+ or receipt.get("mode") != "complete"
461
+ or receipt.get("status") != "passed"
462
+ or receipt.get("review_state") != "self_reviewed"
463
+ or receipt.get("completion_gated") is not False
464
+ or receipt.get("next_action") != "complete"
465
+ or receipt.get("findings") != []
466
+ ):
467
+ fail("completion receipt must be a passed self_reviewed complete result")
468
+ if final.get("status") != "passed" or final.get("findings") != []:
469
+ fail("completion receipt cannot close a final external receipt with findings")
470
+ if receipt.get("review_chain_tracked") is not True or receipt.get("review_chain_id") != chain_id:
471
+ fail("completion receipt review_chain_id does not match the controller chain")
472
+ if receipt.get("challenge_budget") != 2 or type(receipt.get("challenge_budget")) is not int:
473
+ fail("completion receipt challenge_budget must be 2")
474
+ if receipt.get("autonomous_review_budget") != 3 or type(
475
+ receipt.get("autonomous_review_budget")
476
+ ) is not int:
477
+ fail("completion receipt autonomous_review_budget must be 3")
478
+ if receipt.get("autonomous_review_index") != len(receipts) or type(
479
+ receipt.get("autonomous_review_index")
480
+ ) is not int:
481
+ fail("completion receipt autonomous_review_index does not match the final round")
482
+ expected_remaining = 3 - len(receipts)
483
+ if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
484
+ receipt.get("autonomous_reviews_remaining")
485
+ ) is not int:
486
+ fail("completion receipt autonomous_reviews_remaining does not match the final round")
487
+ if receipt.get("autonomous_review_allowed") is not False:
488
+ fail("completion receipt must disable further autonomous review")
489
+ if receipt.get("prior_review_result_sha256") != receipt_hashes[:-1]:
490
+ fail("completion receipt prior_review_result_sha256 does not match the final external receipt")
491
+ if receipt.get("completion_review_result_sha256") != receipt_hashes[-1]:
492
+ fail("completion receipt completion_review_result_sha256 does not identify the final external receipt")
493
+ candidate = sha256(receipt.get("candidate_sha256"), "completion receipt.candidate_sha256")
494
+ packet = sha256(receipt.get("packet_sha256"), "completion receipt.packet_sha256")
495
+ if packet != candidate or candidate != final.get("candidate_sha256"):
496
+ fail("completion receipt does not bind the exact final candidate")
497
+ if validate_scope(receipt, "completion receipt") != scope_hash:
498
+ fail("completion receipt review scope changed")
499
+ return receipt
500
+
501
+
502
+ def validate_base_attestations(
503
+ payload: dict[str, Any],
504
+ ledger_dir: Path,
505
+ receipt_hashes: list[str],
506
+ closeout: str,
507
+ ) -> int:
508
+ attestations = payload["base_attestations"]
509
+ if not isinstance(attestations, list) or not attestations:
510
+ fail("base_attestations must be a non-empty array")
511
+ base_shas: list[str] = []
512
+ mapped_receipts: list[str] = []
513
+ mapped_receipt_bases: list[str] = []
514
+ expected_remote: str | None = None
515
+ expected_ref: str | None = None
516
+ previous_time: datetime | None = None
517
+ base_changes = 0
518
+ second_drift_sequence: int | None = None
519
+ for index, item in enumerate(attestations, start=1):
520
+ row = exact_object(
521
+ item,
522
+ {
523
+ "sequence",
524
+ "remote",
525
+ "ref",
526
+ "sha",
527
+ "confirmed_at",
528
+ "controller_receipt_sha256",
529
+ "evidence_file",
530
+ "evidence_sha256",
531
+ },
532
+ f"base_attestations[{index - 1}]",
533
+ )
534
+ if row["sequence"] != index or type(row["sequence"]) is not int:
535
+ fail("base attestation sequence must be contiguous from 1")
536
+ remote = bounded_text(row["remote"], f"base_attestations[{index - 1}].remote", 200)
537
+ ref = bounded_text(row["ref"], f"base_attestations[{index - 1}].ref", 300)
538
+ if expected_remote is None:
539
+ expected_remote, expected_ref = remote, ref
540
+ elif remote != expected_remote or ref != expected_ref:
541
+ fail("base attestations must use the same remote and ref")
542
+ base_sha = object_id(row["sha"], f"base_attestations[{index - 1}].sha")
543
+ if base_shas and base_sha != base_shas[-1]:
544
+ base_changes += 1
545
+ if base_changes == 2:
546
+ second_drift_sequence = index
547
+ confirmed = parse_rfc3339(
548
+ row["confirmed_at"], f"base_attestations[{index - 1}].confirmed_at"
549
+ )
550
+ if previous_time is not None and confirmed <= previous_time:
551
+ fail("base attestation confirmed_at values must strictly increase")
552
+ previous_time = confirmed
553
+ raw = load_sibling(
554
+ ledger_dir,
555
+ file_value=row["evidence_file"],
556
+ digest_value=row["evidence_sha256"],
557
+ label=f"base attestation {index} evidence",
558
+ maximum=MAX_EVIDENCE_BYTES,
559
+ )
560
+ expected_raw = f"{base_sha}\t{ref}\n".encode()
561
+ if raw != expected_raw:
562
+ fail(f"base attestation {index} evidence does not contain canonical ls-remote output")
563
+ receipt_hash_value = row["controller_receipt_sha256"]
564
+ if receipt_hash_value is not None:
565
+ if second_drift_sequence is not None:
566
+ fail(
567
+ "a controller receipt is mapped at or after the second base drift"
568
+ )
569
+ mapped_receipts.append(
570
+ sha256(
571
+ receipt_hash_value,
572
+ f"base_attestations[{index - 1}].controller_receipt_sha256",
573
+ )
574
+ )
575
+ mapped_receipt_bases.append(base_sha)
576
+ base_shas.append(base_sha)
577
+ if mapped_receipts != receipt_hashes:
578
+ fail("base attestations must map every controller receipt exactly once in order")
579
+ second_drift = base_changes >= 2
580
+ if second_drift != (closeout == "baseline_race"):
581
+ fail("the second base drift must terminate as baseline_race, and baseline_race requires two ordered base changes")
582
+ if (
583
+ closeout != "baseline_race"
584
+ and mapped_receipt_bases[-1] != base_shas[-1]
585
+ ):
586
+ fail(
587
+ "the final controller receipt does not consume the latest attested base"
588
+ )
589
+ return base_changes
590
+
591
+
592
+ def validate_disposition_evidence(
593
+ ledger_dir: Path,
594
+ *,
595
+ file_value: object,
596
+ digest_value: object,
597
+ candidate: str,
598
+ receipt_hash: str,
599
+ finding_hash: str,
600
+ disposition: str,
601
+ class_key: str,
602
+ occurrence_index: int,
603
+ class_pairs: list[tuple[str, str]],
604
+ unresolved_pairs: set[tuple[str, str]],
605
+ ) -> list[tuple[str, str]]:
606
+ label = f"finding class {class_key} disposition evidence {occurrence_index + 1}"
607
+ raw = load_sibling(
608
+ ledger_dir,
609
+ file_value=file_value,
610
+ digest_value=digest_value,
611
+ label=label,
612
+ maximum=MAX_EVIDENCE_BYTES,
613
+ )
614
+ evidence = exact_object(
615
+ decode_json(raw, label=label),
616
+ {
617
+ "schema_version",
618
+ "candidate_sha256",
619
+ "receipt_sha256",
620
+ "finding_sha256",
621
+ "disposition",
622
+ "resolves_occurrences",
623
+ "evidence",
624
+ },
625
+ label,
626
+ )
627
+ if evidence["schema_version"] != 1 or type(evidence["schema_version"]) is not int:
628
+ fail(f"{label}.schema_version must be 1")
629
+ if sha256(evidence["candidate_sha256"], f"{label}.candidate_sha256") != candidate:
630
+ fail(f"{label} is stale for the current candidate")
631
+ if sha256(evidence["receipt_sha256"], f"{label}.receipt_sha256") != receipt_hash:
632
+ fail(f"{label} does not bind its controller receipt")
633
+ if sha256(evidence["finding_sha256"], f"{label}.finding_sha256") != finding_hash:
634
+ fail(f"{label} does not bind its controller finding")
635
+ if bounded_text(evidence["disposition"], f"{label}.disposition", 80) != disposition:
636
+ fail(f"{label} does not bind its disposition")
637
+
638
+ refs = evidence["resolves_occurrences"]
639
+ if not isinstance(refs, list) or not refs:
640
+ fail(f"{label}.resolves_occurrences must be a non-empty array")
641
+ resolved_pairs: list[tuple[str, str]] = []
642
+ seen_resolved: set[tuple[str, str]] = set()
643
+ for resolved_index, value in enumerate(refs):
644
+ resolved = exact_object(
645
+ value,
646
+ {"receipt_sha256", "finding_sha256"},
647
+ f"{label}.resolves_occurrences[{resolved_index}]",
648
+ )
649
+ pair = (
650
+ sha256(
651
+ resolved["receipt_sha256"],
652
+ f"{label}.resolves_occurrences[{resolved_index}].receipt_sha256",
653
+ ),
654
+ sha256(
655
+ resolved["finding_sha256"],
656
+ f"{label}.resolves_occurrences[{resolved_index}].finding_sha256",
657
+ ),
658
+ )
659
+ if pair in seen_resolved:
660
+ fail(f"{label} repeats a resolved occurrence")
661
+ if pair not in class_pairs:
662
+ fail(f"{label} resolves an occurrence outside the current class prefix")
663
+ if pair not in unresolved_pairs:
664
+ fail(f"{label} resolves an occurrence that is not currently unresolved")
665
+ seen_resolved.add(pair)
666
+ resolved_pairs.append(pair)
667
+ current_pair = (receipt_hash, finding_hash)
668
+ if current_pair not in seen_resolved:
669
+ fail(f"{label} must resolve its current occurrence")
670
+ expected_order = [pair for pair in class_pairs if pair in seen_resolved]
671
+ if resolved_pairs != expected_order:
672
+ fail(f"{label}.resolves_occurrences must follow finding class order")
673
+
674
+ evidence_items = evidence["evidence"]
675
+ if not isinstance(evidence_items, list) or not evidence_items:
676
+ fail(f"{label}.evidence must be a non-empty array")
677
+ normalized_evidence = [
678
+ bounded_text(value, f"{label}.evidence[{index}]", 1000)
679
+ for index, value in enumerate(evidence_items)
680
+ ]
681
+ if len(normalized_evidence) != len(set(normalized_evidence)):
682
+ fail(f"{label}.evidence contains duplicates")
683
+ return resolved_pairs
684
+
685
+
686
+ def validate_finding_classes(
687
+ payload: dict[str, Any],
688
+ ledger_dir: Path,
689
+ receipt_findings: dict[str, list[str]],
690
+ candidate: str,
691
+ closeout: str,
692
+ ) -> bool:
693
+ classes = payload["finding_classes"]
694
+ if not isinstance(classes, list):
695
+ fail("finding_classes must be an array")
696
+ expected_pairs = {
697
+ (receipt_hash, finding_hash)
698
+ for receipt_hash, findings in receipt_findings.items()
699
+ for finding_hash in findings
700
+ }
701
+ seen_pairs: set[tuple[str, str]] = set()
702
+ seen_keys: set[str] = set()
703
+ seen_predicates: dict[str, str] = {}
704
+ any_unresolved = False
705
+ finding_order = {
706
+ (receipt_hash, finding_hash): (receipt_index, finding_index)
707
+ for receipt_index, (receipt_hash, findings) in enumerate(receipt_findings.items())
708
+ for finding_index, finding_hash in enumerate(findings)
709
+ }
710
+ for class_index, item in enumerate(classes):
711
+ row = exact_object(
712
+ item,
713
+ {"key", "root_cause_predicate", "affected_surface", "occurrences", "authoritative_sweep"},
714
+ f"finding_classes[{class_index}]",
715
+ )
716
+ key = bounded_text(row["key"], f"finding_classes[{class_index}].key", 120)
717
+ if key in seen_keys:
718
+ fail(f"duplicate finding class key: {key}")
719
+ seen_keys.add(key)
720
+ predicate = bounded_text(
721
+ row["root_cause_predicate"],
722
+ f"finding_classes[{class_index}].root_cause_predicate",
723
+ 2000,
724
+ )
725
+ normalized_predicate = " ".join(predicate.casefold().split())
726
+ prior_key = seen_predicates.get(normalized_predicate)
727
+ if prior_key is not None:
728
+ fail(
729
+ "duplicate normalized root_cause_predicate across finding classes: "
730
+ f"{prior_key}, {key}"
731
+ )
732
+ seen_predicates[normalized_predicate] = key
733
+ bounded_text(
734
+ row["affected_surface"], f"finding_classes[{class_index}].affected_surface", 500
735
+ )
736
+ occurrences = row["occurrences"]
737
+ if not isinstance(occurrences, list) or not occurrences:
738
+ fail(f"finding_classes[{class_index}].occurrences must be non-empty")
739
+ class_order: list[tuple[int, int]] = []
740
+ class_pairs: list[tuple[str, str]] = []
741
+ unresolved_pairs: set[tuple[str, str]] = set()
742
+ human_decision_pairs: set[tuple[str, str]] = set()
743
+ for occurrence_index, occurrence in enumerate(occurrences):
744
+ occurrence_label = (
745
+ f"finding_classes[{class_index}].occurrences[{occurrence_index}]"
746
+ )
747
+ if not isinstance(occurrence, dict):
748
+ fail(f"{occurrence_label} must be an object")
749
+ disposition = bounded_text(
750
+ occurrence.get("disposition"),
751
+ f"{occurrence_label}.disposition",
752
+ 80,
753
+ )
754
+ if disposition not in DISPOSITIONS:
755
+ fail(f"finding class {key} has an unknown disposition")
756
+ occurrence_fields = {
757
+ "receipt_sha256",
758
+ "finding_sha256",
759
+ "disposition",
760
+ }
761
+ if disposition in RESOLVED_DISPOSITIONS:
762
+ occurrence_fields |= {
763
+ "disposition_evidence_file",
764
+ "disposition_evidence_sha256",
765
+ }
766
+ occurrence_row = exact_object(
767
+ occurrence,
768
+ occurrence_fields,
769
+ occurrence_label,
770
+ )
771
+ receipt_hash = sha256(
772
+ occurrence_row["receipt_sha256"],
773
+ f"finding_classes[{class_index}].occurrences[{occurrence_index}].receipt_sha256",
774
+ )
775
+ finding_hash = sha256(
776
+ occurrence_row["finding_sha256"],
777
+ f"finding_classes[{class_index}].occurrences[{occurrence_index}].finding_sha256",
778
+ )
779
+ pair = (receipt_hash, finding_hash)
780
+ if pair not in expected_pairs:
781
+ fail(f"finding class {key} occurrence does not identify a finding in its controller receipt")
782
+ if pair in seen_pairs:
783
+ fail(f"controller finding {finding_hash} is classified more than once")
784
+ seen_pairs.add(pair)
785
+ class_order.append(finding_order[pair])
786
+ class_pairs.append(pair)
787
+ unresolved_pairs.add(pair)
788
+ if disposition == "needs_human_decision":
789
+ human_decision_pairs.add(pair)
790
+ if disposition in RESOLVED_DISPOSITIONS:
791
+ resolved_pairs = validate_disposition_evidence(
792
+ ledger_dir,
793
+ file_value=occurrence_row["disposition_evidence_file"],
794
+ digest_value=occurrence_row["disposition_evidence_sha256"],
795
+ candidate=candidate,
796
+ receipt_hash=receipt_hash,
797
+ finding_hash=finding_hash,
798
+ disposition=disposition,
799
+ class_key=key,
800
+ occurrence_index=occurrence_index,
801
+ class_pairs=class_pairs,
802
+ unresolved_pairs=unresolved_pairs,
803
+ )
804
+ if human_decision_pairs.intersection(resolved_pairs):
805
+ fail(
806
+ f"finding class {key} local evidence cannot resolve a "
807
+ "needs_human_decision occurrence"
808
+ )
809
+ unresolved_pairs.difference_update(resolved_pairs)
810
+ if class_order != sorted(class_order):
811
+ fail(f"finding class {key} occurrences are not in controller receipt order")
812
+ any_unresolved |= bool(unresolved_pairs)
813
+
814
+ sweep = row["authoritative_sweep"]
815
+ if len(occurrences) >= 3:
816
+ sweep_row = exact_object(
817
+ sweep,
818
+ {
819
+ "candidate_sha256",
820
+ "manifest_file",
821
+ "manifest_sha256",
822
+ "searched_set",
823
+ "unmatched_instances",
824
+ },
825
+ f"finding_classes[{class_index}].authoritative_sweep",
826
+ )
827
+ if sha256(
828
+ sweep_row["candidate_sha256"],
829
+ f"finding_classes[{class_index}].authoritative_sweep.candidate_sha256",
830
+ ) != candidate:
831
+ fail(f"finding class {key} sweep is stale for the current candidate")
832
+ searched = sweep_row["searched_set"]
833
+ if not isinstance(searched, list) or not searched:
834
+ fail(f"finding class {key} third occurrence requires a non-empty searched_set")
835
+ normalized = [
836
+ bounded_text(value, f"finding class {key} searched_set", 500)
837
+ for value in searched
838
+ ]
839
+ if len(normalized) != len(set(normalized)):
840
+ fail(f"finding class {key} searched_set contains duplicates")
841
+ unmatched = sweep_row["unmatched_instances"]
842
+ if type(unmatched) is not int or unmatched < 0:
843
+ fail(f"finding class {key} unmatched_instances must be a non-negative integer")
844
+ manifest_raw = load_sibling(
845
+ ledger_dir,
846
+ file_value=sweep_row["manifest_file"],
847
+ digest_value=sweep_row["manifest_sha256"],
848
+ label=f"finding class {key} sweep manifest",
849
+ maximum=MAX_EVIDENCE_BYTES,
850
+ )
851
+ manifest = exact_object(
852
+ decode_json(manifest_raw, label=f"finding class {key} sweep manifest"),
853
+ {"schema_version", "candidate_sha256", "searched_set", "unmatched_instances"},
854
+ f"finding class {key} sweep manifest",
855
+ )
856
+ if manifest["schema_version"] != 1 or type(manifest["schema_version"]) is not int:
857
+ fail(f"finding class {key} sweep manifest schema_version must be 1")
858
+ if sha256(manifest["candidate_sha256"], f"finding class {key} sweep manifest candidate") != candidate:
859
+ fail(f"finding class {key} sweep manifest is stale")
860
+ if manifest["searched_set"] != normalized:
861
+ fail(f"finding class {key} searched_set does not match its sweep manifest")
862
+ unmatched_items = manifest["unmatched_instances"]
863
+ if not isinstance(unmatched_items, list):
864
+ fail(f"finding class {key} sweep manifest unmatched_instances must be an array")
865
+ normalized_unmatched = [
866
+ bounded_text(value, f"finding class {key} unmatched instance", 500)
867
+ for value in unmatched_items
868
+ ]
869
+ if len(normalized_unmatched) != len(set(normalized_unmatched)):
870
+ fail(f"finding class {key} sweep manifest repeats an unmatched instance")
871
+ if len(normalized_unmatched) != unmatched:
872
+ fail(f"finding class {key} unmatched_instances count does not match its sweep manifest")
873
+ if closeout == "ready_for_human_decision" and unmatched != 0:
874
+ fail(f"finding class {key} ready sweep must report zero unmatched instances")
875
+ elif sweep is not None:
876
+ fail(f"finding class {key} has a sweep before its third occurrence")
877
+ if seen_pairs != expected_pairs:
878
+ fail("finding_classes omits controller findings")
879
+ return any_unresolved
880
+
881
+
882
+ def validate(payload: dict[str, Any], ledger_dir: Path) -> tuple[str, int, int]:
883
+ if payload["schema_version"] != 3 or type(payload["schema_version"]) is not int:
884
+ fail("schema_version must be 3")
885
+ candidate = sha256(payload["candidate_sha256"], "candidate_sha256")
886
+ closeout = bounded_text(payload["closeout_state"], "closeout_state", 80)
887
+ if closeout not in TERMINAL_STATES:
888
+ fail("closeout_state is not an extraction terminal state")
889
+
890
+ receipts, receipt_hashes, receipt_findings, chain_id, scope_hash = (
891
+ validate_controller_receipts(payload, ledger_dir)
892
+ )
893
+ if receipts[-1]["candidate_sha256"] != candidate:
894
+ fail("candidate_sha256 must equal the final controller receipt candidate")
895
+ completion = validate_completion_receipt(
896
+ payload["completion_receipt"],
897
+ ledger_dir,
898
+ receipts,
899
+ receipt_hashes,
900
+ chain_id,
901
+ scope_hash,
902
+ )
903
+ base_changes = validate_base_attestations(
904
+ payload, ledger_dir, receipt_hashes, closeout
905
+ )
906
+ any_unresolved = validate_finding_classes(
907
+ payload, ledger_dir, receipt_findings, candidate, closeout
908
+ )
909
+
910
+ delta = payload["unreviewed_delta"]
911
+ if not isinstance(delta, list):
912
+ fail("unreviewed_delta must be an array")
913
+ for index, value in enumerate(delta):
914
+ bounded_text(value, f"unreviewed_delta[{index}]", 500)
915
+
916
+ if closeout == "ready_for_human_decision" and completion is None:
917
+ fail("ready_for_human_decision requires a completion receipt")
918
+ if closeout == "continuation_authorization_required" and completion is not None:
919
+ fail("continuation_authorization_required cannot carry a completion receipt")
920
+ if closeout == "baseline_race" and completion is not None:
921
+ fail("baseline_race cannot carry a completion receipt")
922
+
923
+ controller_state = bounded_text(
924
+ payload["controller_review_state"], "controller_review_state", 80
925
+ )
926
+ if controller_state not in KNOWN_REVIEW_STATES:
927
+ fail(f"unknown controller review_state {controller_state}")
928
+ derived_state = completion["review_state"] if completion is not None else receipts[-1]["review_state"]
929
+ if controller_state != derived_state:
930
+ fail("controller_review_state does not match the referenced final receipt")
931
+
932
+ if closeout == "ready_for_human_decision":
933
+ if len(receipts) < 2:
934
+ fail("ready_for_human_decision ready requires at least one tracked challenge")
935
+ if completion is None:
936
+ fail("ready_for_human_decision requires a completion receipt")
937
+ if completion["candidate_sha256"] != candidate or receipts[-1]["candidate_sha256"] != candidate:
938
+ fail("ready_for_human_decision completion receipt must bind the exact final candidate")
939
+ if any_unresolved or delta:
940
+ fail("ready_for_human_decision requires no unresolved finding occurrence and no unreviewed delta")
941
+ elif closeout == "continuation_authorization_required":
942
+ final = receipts[-1]
943
+ if (
944
+ len(receipts) != 3
945
+ or final.get("status") != "findings"
946
+ or final.get("review_state") != "post_review_budget"
947
+ or final.get("human_decision_required") is not True
948
+ ):
949
+ fail("continuation_authorization_required requires round 3 findings in post_review_budget")
950
+ elif closeout == "baseline_race" and not delta:
951
+ fail("baseline_race requires a non-empty unreviewed_delta")
952
+
953
+ return closeout, len(payload["finding_classes"]), base_changes
954
+
955
+
956
+ def main() -> int:
957
+ parser = argparse.ArgumentParser(description=__doc__)
958
+ parser.add_argument("state_file", type=Path)
959
+ args = parser.parse_args()
960
+ try:
961
+ payload, ledger_dir = load(args.state_file)
962
+ state, class_count, base_changes = validate(payload, ledger_dir)
963
+ except StateError as exc:
964
+ print(f"extraction_review_state_invalid: {exc}", file=sys.stderr)
965
+ return 1
966
+ except (OSError, UnicodeError, ValueError):
967
+ print(
968
+ "extraction_review_state_invalid: local evidence read or encoding failed",
969
+ file=sys.stderr,
970
+ )
971
+ return 1
972
+ print(
973
+ "extraction_review_state_ok: "
974
+ f"closeout_state={state} finding_classes={class_count} base_changes={base_changes}"
975
+ )
976
+ return 0
977
+
978
+
979
+ if __name__ == "__main__":
980
+ raise SystemExit(main())