@ccoalm/ccl-skills 0.16.0 → 0.18.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 (43) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +8 -7
  2. package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +22 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-review-covers-head.sh +128 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-untracked-background.sh +53 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_review_covers_head.sh +104 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_untracked_background.sh +75 -0
  7. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +8 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +6 -4
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/development-completion.md +13 -1
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +78 -126
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/wording-only-review.md +136 -0
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +521 -32
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +25 -2
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_order.sh +30 -15
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +439 -19
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +14 -7
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/SKILL.md +1 -1
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +1 -1
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +6 -6
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +1 -0
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +48 -119
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +9 -9
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +22 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +18 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +2 -2
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check_review_evidence_present.py +122 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +4 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +37 -8
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +102 -20
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ai_coding_implementation_gates.sh +16 -27
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +3 -5
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_review_evidence_present.sh +81 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +137 -279
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_resolution.sh +41 -1
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +49 -3
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +3 -2
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/closeout-reread.md +40 -0
  38. package/dist/assets/release.json +72 -52
  39. package/package.json +1 -1
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +0 -1242
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +0 -978
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +0 -1477
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +0 -1183
@@ -1,1183 +0,0 @@
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
- # The extraction wrapper fixes the autonomous lane at one review plus one
34
- # challenge; every numeric bound below derives from these two constants so a
35
- # future budget change lands in exactly one place.
36
- WRAPPER_CHALLENGE_BUDGET = 1
37
- WRAPPER_AUTONOMOUS_ROUNDS = WRAPPER_CHALLENGE_BUDGET + 1
38
- # A fix that edits the reviewed owner package ends its chain by construction, so
39
- # the post-fix candidate is challenged in ONE succeeding chain rather than inside
40
- # the ended one. The lane therefore spans at most two chains: the wrapper-budget
41
- # chain plus a single succession challenge bound to the landing candidate.
42
- LANE_MAX_CHAINS = 2
43
- LANE_MAX_ROUNDS = WRAPPER_AUTONOMOUS_ROUNDS + 1
44
- DISPOSITIONS = {
45
- "fixed",
46
- "source_refuted",
47
- "accepted_tradeoff",
48
- "pre_existing_out_of_scope",
49
- "needs_human_decision",
50
- "open",
51
- }
52
- RESOLVED_DISPOSITIONS = {
53
- "fixed",
54
- "source_refuted",
55
- "accepted_tradeoff",
56
- "pre_existing_out_of_scope",
57
- }
58
-
59
-
60
- class StateError(Exception):
61
- pass
62
-
63
-
64
- def fail(message: str) -> NoReturn:
65
- raise StateError(message)
66
-
67
-
68
- def bounded_text(value: object, field: str, maximum: int = 1000) -> str:
69
- if not isinstance(value, str) or value != value.strip() or not value:
70
- fail(f"{field} must be a non-empty normalized string")
71
- # Interior control characters would otherwise be echoed into stderr
72
- # diagnostics (terminal-escape injection on the error path). C1 controls
73
- # (NEL, CSI) and Unicode line/paragraph separators forge line breaks too.
74
- if any(
75
- ord(char) < 0x20 or 0x7F <= ord(char) <= 0x9F or char in "\u2028\u2029"
76
- for char in value
77
- ):
78
- fail(f"{field} must not contain control characters")
79
- # Default-ignorable format characters (ZWSP, word joiner, bidi controls)
80
- # let visually identical class keys or predicates register as distinct,
81
- # splitting one recurrence class below its sweep threshold.
82
- if any(unicodedata.category(char) == "Cf" for char in value):
83
- fail(f"{field} must not contain format characters")
84
- try:
85
- value.encode("utf-8")
86
- except UnicodeEncodeError:
87
- fail(f"{field} must be valid UTF-8 text")
88
- if len(value) > maximum:
89
- fail(f"{field} exceeds {maximum} characters")
90
- return value
91
-
92
-
93
- def object_id(value: object, field: str) -> str:
94
- text = bounded_text(value, field, 64)
95
- if OBJECT_ID_RE.fullmatch(text) is None:
96
- fail(f"{field} must be a lowercase 40- or 64-character hexadecimal object id")
97
- return text
98
-
99
-
100
- def sha256(value: object, field: str) -> str:
101
- text = bounded_text(value, field, 64)
102
- if SHA256_RE.fullmatch(text) is None:
103
- fail(f"{field} must be a lowercase 64-character SHA-256 digest")
104
- return text
105
-
106
-
107
- def exact_object(value: object, fields: set[str], label: str) -> dict[str, Any]:
108
- if not isinstance(value, dict) or set(value) != fields:
109
- fail(f"{label} must contain exactly: {', '.join(sorted(fields))}")
110
- return value
111
-
112
-
113
- def read_regular(path: Path, *, label: str, maximum: int) -> bytes:
114
- if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_NONBLOCK"):
115
- fail(f"platform cannot open {label} with no-follow and non-blocking safety")
116
- try:
117
- fd = os.open(
118
- path,
119
- os.O_RDONLY
120
- | os.O_NOFOLLOW
121
- | getattr(os, "O_CLOEXEC", 0)
122
- | os.O_NONBLOCK,
123
- )
124
- except (OSError, UnicodeError, ValueError) as exc:
125
- fail(f"{label} is unreadable: {exc}")
126
- try:
127
- try:
128
- info = os.fstat(fd)
129
- except OSError as exc:
130
- fail(f"cannot inspect {label}: {exc}")
131
- if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
132
- fail(f"{label} must be a singly linked regular file")
133
- if info.st_size > maximum:
134
- fail(f"{label} exceeds {maximum} bytes")
135
- chunks: list[bytes] = []
136
- remaining = maximum + 1
137
- while remaining:
138
- try:
139
- chunk = os.read(fd, remaining)
140
- except OSError as exc:
141
- fail(f"cannot read {label}: {exc}")
142
- if not chunk:
143
- break
144
- chunks.append(chunk)
145
- remaining -= len(chunk)
146
- raw = b"".join(chunks)
147
- finally:
148
- try:
149
- os.close(fd)
150
- except OSError:
151
- pass
152
- if len(raw) > maximum:
153
- fail(f"{label} exceeds {maximum} bytes")
154
- return raw
155
-
156
-
157
- def decode_json(raw: bytes, *, label: str) -> dict[str, Any]:
158
- def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
159
- value: dict[str, Any] = {}
160
- for key, item in pairs:
161
- if key in value:
162
- fail(f"{label} contains duplicate object key: {key}")
163
- value[key] = item
164
- return value
165
-
166
- def reject_constant(constant: str) -> NoReturn:
167
- fail(f"{label} contains a non-standard JSON constant: {constant}")
168
-
169
- try:
170
- value = json.loads(
171
- raw.decode("utf-8"),
172
- object_pairs_hook=unique_object,
173
- parse_constant=reject_constant,
174
- )
175
- except StateError:
176
- raise
177
- except (UnicodeError, json.JSONDecodeError) as exc:
178
- fail(f"{label} must be UTF-8 JSON: {exc}")
179
- if not isinstance(value, dict):
180
- fail(f"{label} must be a JSON object")
181
- return value
182
-
183
-
184
- def sibling_name(value: object, field: str) -> str:
185
- name = bounded_text(value, field, 255)
186
- if Path(name).is_absolute() or Path(name).name != name or "/" in name or "\\" in name:
187
- fail(f"{field} must name a file in the ledger directory")
188
- return name
189
-
190
-
191
- def load_sibling(
192
- ledger_dir: Path,
193
- *,
194
- file_value: object,
195
- digest_value: object,
196
- label: str,
197
- maximum: int,
198
- ) -> bytes:
199
- name = sibling_name(file_value, f"{label}.file")
200
- expected = sha256(digest_value, f"{label}.sha256")
201
- raw = read_regular(ledger_dir / name, label=label, maximum=maximum)
202
- actual = hashlib.sha256(raw).hexdigest()
203
- if actual != expected:
204
- fail(f"{label} digest does not match {name}")
205
- return raw
206
-
207
-
208
- def canonical_hash(value: object, label: str) -> str:
209
- try:
210
- encoded = json.dumps(
211
- value,
212
- ensure_ascii=False,
213
- sort_keys=True,
214
- separators=(",", ":"),
215
- ).encode()
216
- except (TypeError, ValueError, UnicodeError) as exc:
217
- fail(f"{label} is not canonical JSON: {exc}")
218
- return hashlib.sha256(encoded).hexdigest()
219
-
220
-
221
- def parse_rfc3339(value: object, field: str) -> datetime:
222
- text = bounded_text(value, field, 100)
223
- if RFC3339_RE.fullmatch(text) is None:
224
- fail(f"{field} must be a strict RFC3339 timestamp")
225
- try:
226
- parsed = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text)
227
- except ValueError as exc:
228
- fail(f"{field} must be a valid RFC3339 timestamp: {exc}")
229
- if parsed.utcoffset() is None:
230
- fail(f"{field} must include an RFC3339 UTC offset")
231
- return parsed
232
-
233
-
234
- def load(path: Path) -> tuple[dict[str, Any], Path]:
235
- raw = read_regular(path, label="ledger", maximum=MAX_LEDGER_BYTES)
236
- value = decode_json(raw, label="ledger")
237
- payload = exact_object(
238
- value,
239
- {
240
- "schema_version",
241
- "candidate_sha256",
242
- "controller_receipts",
243
- "completion_receipt",
244
- "base_attestations",
245
- "autonomous_round",
246
- "controller_review_state",
247
- "finding_classes",
248
- "unreviewed_delta",
249
- "closeout_state",
250
- } | ({"finding_dispositions"} if "finding_dispositions" in value else set()),
251
- "ledger",
252
- )
253
- return payload, path.absolute().parent
254
-
255
-
256
- def validate_scope(receipt: dict[str, Any], label: str) -> str:
257
- scope = exact_object(
258
- receipt.get("review_scope"),
259
- {
260
- "schema_version",
261
- "intent_sha256",
262
- "acceptance_sha256",
263
- "stage",
264
- "review_depth",
265
- "risk_tags",
266
- "challenge_budget",
267
- "wording_only_proof_sha256",
268
- "wording_only_scope_sha256",
269
- },
270
- f"{label}.review_scope",
271
- )
272
- if scope["schema_version"] != 3 or type(scope["schema_version"]) is not int:
273
- fail(f"{label}.review_scope.schema_version must be 3")
274
- sha256(scope["intent_sha256"], f"{label}.review_scope.intent_sha256")
275
- sha256(scope["acceptance_sha256"], f"{label}.review_scope.acceptance_sha256")
276
- stage = bounded_text(scope["stage"], f"{label}.review_scope.stage", 80)
277
- depth = bounded_text(scope["review_depth"], f"{label}.review_scope.review_depth", 80)
278
- risks = scope["risk_tags"]
279
- if not isinstance(risks, list):
280
- fail(f"{label}.review_scope.risk_tags must be an array")
281
- normalized_risks = [
282
- bounded_text(item, f"{label}.review_scope.risk_tags", 100) for item in risks
283
- ]
284
- if len(normalized_risks) != len(set(normalized_risks)):
285
- fail(f"{label}.review_scope.risk_tags contains duplicates")
286
- if scope["challenge_budget"] != WRAPPER_CHALLENGE_BUDGET or type(scope["challenge_budget"]) is not int:
287
- fail(f"{label}.review_scope.challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
288
- if (
289
- scope["wording_only_proof_sha256"] is not None
290
- or scope["wording_only_scope_sha256"] is not None
291
- or "wording_only_proof_sha256" not in receipt
292
- or receipt["wording_only_proof_sha256"] is not None
293
- or "wording_only_scope" not in receipt
294
- or receipt["wording_only_scope"] is not None
295
- ):
296
- fail(f"{label} must bind the non-wording extraction scope")
297
- recorded = sha256(receipt.get("review_scope_sha256"), f"{label}.review_scope_sha256")
298
- if canonical_hash(scope, f"{label}.review_scope") != recorded:
299
- fail(f"{label}.review_scope_sha256 does not reproduce review_scope")
300
- if (
301
- receipt.get("stage") != stage
302
- or receipt.get("review_depth") != depth
303
- or receipt.get("risk_tags") != normalized_risks
304
- ):
305
- fail(f"{label} top-level scope fields contradict review_scope")
306
- return recorded
307
-
308
-
309
- def validate_controller_receipts(
310
- payload: dict[str, Any], ledger_dir: Path
311
- ) -> tuple[list[dict[str, Any]], list[str], dict[str, list[str]], str, str]:
312
- refs = payload["controller_receipts"]
313
- if not isinstance(refs, list) or not 1 <= len(refs) <= LANE_MAX_ROUNDS:
314
- fail(
315
- f"controller_receipts must contain one to {LANE_MAX_ROUNDS} ordered Agent rounds"
316
- )
317
- if payload["autonomous_round"] != len(refs) or type(payload["autonomous_round"]) is not int:
318
- fail("autonomous_round must equal the ordered controller receipt count")
319
-
320
- receipts: list[dict[str, Any]] = []
321
- receipt_hashes: list[str] = []
322
- finding_hashes: dict[str, list[str]] = {}
323
- chain_id: str | None = None
324
- final_chain_id: str | None = None
325
- scope_hash: str | None = None
326
- candidates: list[str] = []
327
- succession_index: int | None = None
328
- ledger_candidate = sha256(
329
- payload["candidate_sha256"], "ledger.candidate_sha256"
330
- )
331
- for expected_index, value in enumerate(refs, start=1):
332
- # A caller supplying more receipts than the lane can mint would
333
- # otherwise claim a negative remaining count and reach completion
334
- # validation as a ready-state budget bypass. The wrapper bound still
335
- # governs each chain; the lane bound governs their sum.
336
- if expected_index > LANE_MAX_ROUNDS:
337
- fail("controller chain exceeds the lane budget")
338
- ref = exact_object(
339
- value, {"sequence", "file", "sha256"}, f"controller_receipts[{expected_index - 1}]"
340
- )
341
- if ref["sequence"] != expected_index or type(ref["sequence"]) is not int:
342
- fail("controller receipt sequence must be contiguous from 1")
343
- receipt_hash = sha256(ref["sha256"], f"controller_receipts[{expected_index - 1}].sha256")
344
- raw = load_sibling(
345
- ledger_dir,
346
- file_value=ref["file"],
347
- digest_value=receipt_hash,
348
- label=f"controller receipt {expected_index}",
349
- maximum=MAX_RESULT_BYTES,
350
- )
351
- receipt = decode_json(raw, label=f"controller receipt {expected_index}")
352
- if receipt.get("schema_version") != 3 or type(receipt.get("schema_version")) is not int:
353
- fail(f"controller receipt {expected_index} schema_version must be 3")
354
- # A succession round opens a second chain, so its own chain arithmetic
355
- # restarts at one while its lane position keeps counting. Classify first;
356
- # every expectation below reads the chain position, not the lane index.
357
- predecessor_chain = receipt.get("predecessor_chain_id")
358
- is_succession = predecessor_chain is not None
359
- if is_succession:
360
- if expected_index != len(refs):
361
- fail("a succession round must be the lane's final round")
362
- if expected_index != WRAPPER_AUTONOMOUS_ROUNDS + 1:
363
- fail("a succession round may only follow a spent wrapper chain")
364
- succession_index = expected_index
365
- chain_position = 1
366
- else:
367
- # Only a succession round may sit past the wrapper chain. Without this
368
- # the caller claims a negative remaining count and reaches completion
369
- # validation as a ready-state budget bypass.
370
- if expected_index > WRAPPER_AUTONOMOUS_ROUNDS:
371
- fail("controller chain exceeds the wrapper budget")
372
- chain_position = expected_index
373
- expected_mode = (
374
- "challenge" if is_succession else "review" if expected_index == 1 else "challenge"
375
- )
376
- if receipt.get("mode") != expected_mode:
377
- fail(f"controller receipt {expected_index} must have mode {expected_mode}")
378
- if receipt.get("status") not in {"passed", "findings"}:
379
- fail(f"controller receipt {expected_index} must have status passed or findings")
380
- if receipt.get("review_chain_tracked") is not True:
381
- fail(f"controller receipt {expected_index} must belong to a tracked chain")
382
- current_chain = bounded_text(
383
- receipt.get("review_chain_id"), f"controller receipt {expected_index}.review_chain_id", 120
384
- )
385
- if is_succession:
386
- if current_chain == chain_id:
387
- fail(
388
- f"controller receipt {expected_index} succeeds its own chain"
389
- )
390
- final_chain_id = current_chain
391
- elif chain_id is None:
392
- chain_id = current_chain
393
- final_chain_id = current_chain
394
- elif current_chain != chain_id:
395
- fail(f"controller receipt {expected_index} review_chain_id changed")
396
- if receipt.get("autonomous_review_index") != chain_position or type(
397
- receipt.get("autonomous_review_index")
398
- ) is not int:
399
- fail(f"controller receipt {expected_index} autonomous_review_index is not contiguous")
400
- expected_challenge_index = (
401
- 1 if is_succession else 0 if expected_index == 1 else expected_index - 1
402
- )
403
- if receipt.get("challenge_index") != expected_challenge_index or type(
404
- receipt.get("challenge_index")
405
- ) is not int:
406
- fail(f"controller receipt {expected_index} challenge_index is invalid")
407
- if receipt.get("challenge_budget") != WRAPPER_CHALLENGE_BUDGET or type(receipt.get("challenge_budget")) is not int:
408
- fail(f"controller receipt {expected_index} challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
409
- if receipt.get("autonomous_review_budget") != WRAPPER_AUTONOMOUS_ROUNDS or type(
410
- receipt.get("autonomous_review_budget")
411
- ) is not int:
412
- fail(f"controller receipt {expected_index} autonomous_review_budget must be {WRAPPER_AUTONOMOUS_ROUNDS}")
413
- expected_remaining = WRAPPER_AUTONOMOUS_ROUNDS - chain_position
414
- if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
415
- receipt.get("autonomous_reviews_remaining")
416
- ) is not int:
417
- fail(f"controller receipt {expected_index} autonomous_reviews_remaining is invalid")
418
- if receipt.get("autonomous_review_allowed") is not (expected_remaining > 0):
419
- fail(f"controller receipt {expected_index} autonomous_review_allowed is invalid")
420
- prior = receipt.get("prior_review_result_sha256")
421
- expected_prior = [] if is_succession else receipt_hashes
422
- if prior != expected_prior:
423
- fail(f"controller receipt {expected_index} prior_review_result_sha256 is not the complete ordered prefix")
424
- candidate = sha256(
425
- receipt.get("candidate_sha256"), f"controller receipt {expected_index}.candidate_sha256"
426
- )
427
- packet = sha256(
428
- receipt.get("packet_sha256"), f"controller receipt {expected_index}.packet_sha256"
429
- )
430
- if packet != candidate:
431
- fail(f"controller receipt {expected_index} packet_sha256 must equal candidate_sha256")
432
- # Which candidate a round must bind depends on its phase, and the phase
433
- # split is only known once every round is loaded, so this is settled
434
- # after the loop rather than here.
435
- candidates.append(candidate)
436
- current_scope_hash = validate_scope(receipt, f"controller receipt {expected_index}")
437
- if scope_hash is None:
438
- scope_hash = current_scope_hash
439
- elif current_scope_hash != scope_hash:
440
- fail(f"controller receipt {expected_index} review scope changed")
441
-
442
- findings = receipt.get("findings")
443
- if not isinstance(findings, list):
444
- fail(f"controller receipt {expected_index}.findings must be an array")
445
- current_findings: list[str] = []
446
- current_finding_set: set[str] = set()
447
- for finding_index, finding in enumerate(findings):
448
- if not isinstance(finding, dict):
449
- fail(f"controller receipt {expected_index}.findings[{finding_index}] must be an object")
450
- finding_hash = canonical_hash(
451
- finding, f"controller receipt {expected_index}.findings[{finding_index}]"
452
- )
453
- if finding_hash in current_finding_set:
454
- # Raw receipts stay intact; repeated canonical payloads share
455
- # this receipt's existing finding identity and first-seen order.
456
- continue
457
- current_finding_set.add(finding_hash)
458
- current_findings.append(finding_hash)
459
- if receipt["status"] == "passed" and findings:
460
- fail(f"controller receipt {expected_index} passed status cannot carry findings")
461
- if receipt["status"] == "findings" and not findings:
462
- fail(f"controller receipt {expected_index} findings status requires findings")
463
- state = bounded_text(
464
- receipt.get("review_state"), f"controller receipt {expected_index}.review_state", 80
465
- )
466
- if state not in EXTERNAL_REVIEW_STATES:
467
- fail(f"controller receipt {expected_index} has unknown controller review_state {state}")
468
- expected_state = (
469
- "post_review_budget"
470
- if receipt["status"] == "findings" and chain_position == WRAPPER_AUTONOMOUS_ROUNDS
471
- else "findings_pending"
472
- if receipt["status"] == "findings"
473
- else "reviewed"
474
- )
475
- if state != expected_state:
476
- fail(f"controller receipt {expected_index} review_state contradicts status and round")
477
- if receipt.get("human_decision_required") is not (state == "post_review_budget"):
478
- fail(f"controller receipt {expected_index} human_decision_required contradicts review_state")
479
-
480
- receipts.append(receipt)
481
- receipt_hashes.append(receipt_hash)
482
- finding_hashes[receipt_hash] = current_findings
483
-
484
- if chain_id is None or scope_hash is None or final_chain_id is None:
485
- fail("controller receipt chain is empty")
486
-
487
- # Phase binding. Without a succession every counted round must have inspected
488
- # the exact final candidate. With one, the wrapper chain reviewed the candidate
489
- # the fix batch then moved, and the succession round is the only round that can
490
- # bind what actually lands — which is the whole point of running it.
491
- if succession_index is None:
492
- for position, value in enumerate(candidates, start=1):
493
- if value != ledger_candidate:
494
- fail(f"controller receipt {position} does not bind the ledger candidate")
495
- else:
496
- succession = receipts[-1]
497
- succeeded_candidate = sha256(
498
- succession.get("predecessor_candidate_sha256"),
499
- "succession receipt.predecessor_candidate_sha256",
500
- )
501
- if candidates[-1] != ledger_candidate:
502
- fail("the succession round does not bind the ledger candidate")
503
- if succeeded_candidate == ledger_candidate:
504
- fail("a succession round must bind a candidate the wrapper chain never saw")
505
- for position, value in enumerate(candidates[:-1], start=1):
506
- if value != succeeded_candidate:
507
- fail(f"controller receipt {position} does not bind the succeeded candidate")
508
- if succession.get("predecessor_chain_id") != chain_id:
509
- fail("the succession round does not name the chain it succeeds")
510
- if sha256(
511
- succession.get("predecessor_result_sha256"),
512
- "succession receipt.predecessor_result_sha256",
513
- ) != receipt_hashes[-2]:
514
- fail("the succession round does not bind the succeeded chain's terminal receipt")
515
- return receipts, receipt_hashes, finding_hashes, final_chain_id, scope_hash
516
-
517
-
518
- def validate_completion_receipt(
519
- value: object,
520
- ledger_dir: Path,
521
- receipts: list[dict[str, Any]],
522
- receipt_hashes: list[str],
523
- chain_id: str,
524
- scope_hash: str,
525
- ) -> dict[str, Any] | None:
526
- if value is None:
527
- return None
528
- ref = exact_object(value, {"file", "sha256"}, "completion_receipt")
529
- raw = load_sibling(
530
- ledger_dir,
531
- file_value=ref["file"],
532
- digest_value=ref["sha256"],
533
- label="completion receipt",
534
- maximum=MAX_RESULT_BYTES,
535
- )
536
- receipt = decode_json(raw, label="completion receipt")
537
- final = receipts[-1]
538
- if (
539
- receipt.get("schema_version") != 3
540
- or type(receipt.get("schema_version")) is not int
541
- or receipt.get("mode") != "complete"
542
- or receipt.get("status") != "passed"
543
- or receipt.get("review_state") != "self_reviewed"
544
- or receipt.get("completion_gated") is not False
545
- or receipt.get("next_action") != "complete"
546
- or receipt.get("findings") != []
547
- ):
548
- fail("completion receipt must be a passed self_reviewed complete result")
549
- basis = receipt.get("completion_basis", "external_pass")
550
- if basis == "external_pass":
551
- if final.get("status") != "passed" or final.get("findings") != []:
552
- fail("completion receipt cannot close a final external receipt with findings")
553
- if receipt.get("finding_dispositions_sha256") is not None or receipt.get(
554
- "resolved_finding_occurrences", []
555
- ) != []:
556
- fail("external_pass completion cannot carry finding dispositions")
557
- elif basis == "source_refuted_findings":
558
- if (
559
- len(receipts) < 2
560
- or receipts[0].get("mode") != "review"
561
- or final.get("mode") != "challenge"
562
- or final.get("status") not in {"passed", "findings"}
563
- or any(item.get("predecessor_chain_id") is not None for item in receipts)
564
- or any(item.get("candidate_sha256") != final.get("candidate_sha256") for item in receipts)
565
- ):
566
- fail("source_refuted completion requires a same-candidate review and challenge without succession")
567
- else:
568
- fail("completion receipt has an unknown completion_basis")
569
- if receipt.get("review_chain_tracked") is not True or receipt.get("review_chain_id") != chain_id:
570
- fail("completion receipt review_chain_id does not match the controller chain")
571
- if receipt.get("challenge_budget") != WRAPPER_CHALLENGE_BUDGET or type(receipt.get("challenge_budget")) is not int:
572
- fail(f"completion receipt challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
573
- if receipt.get("autonomous_review_budget") != WRAPPER_AUTONOMOUS_ROUNDS or type(
574
- receipt.get("autonomous_review_budget")
575
- ) is not int:
576
- fail(f"completion receipt autonomous_review_budget must be {WRAPPER_AUTONOMOUS_ROUNDS}")
577
- # The completion checkpoint binds the final external round, so its chain
578
- # arithmetic is that round's — which is the succeeding chain's when a fix
579
- # batch ended the wrapper chain, not the lane's round count.
580
- final_index = final.get("autonomous_review_index")
581
- if receipt.get("autonomous_review_index") != final_index or type(
582
- receipt.get("autonomous_review_index")
583
- ) is not int:
584
- fail("completion receipt autonomous_review_index does not match the final round")
585
- expected_remaining = WRAPPER_AUTONOMOUS_ROUNDS - final_index
586
- if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
587
- receipt.get("autonomous_reviews_remaining")
588
- ) is not int:
589
- fail("completion receipt autonomous_reviews_remaining does not match the final round")
590
- if receipt.get("autonomous_review_allowed") is not False:
591
- fail("completion receipt must disable further autonomous review")
592
- if receipt.get("prior_review_result_sha256") != final.get(
593
- "prior_review_result_sha256"
594
- ):
595
- fail("completion receipt prior_review_result_sha256 does not match the final external receipt")
596
- if receipt.get("completion_review_result_sha256") != receipt_hashes[-1]:
597
- fail("completion receipt completion_review_result_sha256 does not identify the final external receipt")
598
- candidate = sha256(receipt.get("candidate_sha256"), "completion receipt.candidate_sha256")
599
- packet = sha256(receipt.get("packet_sha256"), "completion receipt.packet_sha256")
600
- if packet != candidate or candidate != final.get("candidate_sha256"):
601
- fail("completion receipt does not bind the exact final candidate")
602
- if validate_scope(receipt, "completion receipt") != scope_hash:
603
- fail("completion receipt review scope changed")
604
- return receipt
605
-
606
-
607
- def validate_completion_dispositions(
608
- payload: dict[str, Any],
609
- ledger_dir: Path,
610
- completion: dict[str, Any] | None,
611
- receipt_hashes: list[str],
612
- receipt_findings: dict[str, list[str]],
613
- candidate: str,
614
- ) -> None:
615
- if completion is None or completion.get("completion_basis") != "source_refuted_findings":
616
- if "finding_dispositions" in payload:
617
- fail("finding_dispositions requires a source_refuted_findings completion")
618
- return
619
- ref = exact_object(payload.get("finding_dispositions"), {"file", "sha256"}, "finding_dispositions")
620
- raw = load_sibling(
621
- ledger_dir,
622
- file_value=ref["file"],
623
- digest_value=ref["sha256"],
624
- label="finding dispositions",
625
- maximum=MAX_EVIDENCE_BYTES,
626
- )
627
- if completion.get("finding_dispositions_sha256") != ref["sha256"]:
628
- fail("completion disposition digest does not match the bound finding_dispositions file")
629
- document = exact_object(
630
- decode_json(raw, label="finding dispositions"),
631
- {"schema_version", "candidate_sha256", "review_result_sha256", "dispositions"},
632
- "finding dispositions",
633
- )
634
- if type(document["schema_version"]) is not int or document["schema_version"] != 1:
635
- fail("finding dispositions schema_version must be 1")
636
- if sha256(document["candidate_sha256"], "finding dispositions.candidate_sha256") != candidate:
637
- fail("finding dispositions are stale for the current candidate")
638
- if document["review_result_sha256"] != receipt_hashes:
639
- fail("finding dispositions must bind the full ordered controller receipts")
640
- expected = [
641
- {"receipt_sha256": receipt_hash, "finding_sha256": finding_hash}
642
- for receipt_hash in receipt_hashes
643
- for finding_hash in receipt_findings[receipt_hash]
644
- ]
645
- if not expected:
646
- fail("source_refuted completion requires at least one controller finding")
647
- if completion.get("resolved_finding_occurrences") != expected:
648
- fail("completion resolved_finding_occurrences must match the ordered controller findings")
649
- dispositions = document["dispositions"]
650
- if not isinstance(dispositions, list) or len(dispositions) != len(expected):
651
- fail("finding dispositions must cover the full ordered controller findings")
652
- class_occurrences = {
653
- (item["receipt_sha256"], item["finding_sha256"]): item
654
- for row in payload["finding_classes"]
655
- for item in row["occurrences"]
656
- }
657
- for index, (value, expected_pair) in enumerate(zip(dispositions, expected)):
658
- label = f"finding dispositions.dispositions[{index}]"
659
- row = exact_object(value, {"receipt_sha256", "finding_sha256", "disposition", "evidence"}, label)
660
- if any(row[key] != value for key, value in expected_pair.items()):
661
- fail("finding dispositions must cover the full ordered controller findings")
662
- occurrence = class_occurrences[(row["receipt_sha256"], row["finding_sha256"])]
663
- if row["disposition"] != "source_refuted" or occurrence["disposition"] != "source_refuted":
664
- fail("adjudicated completion permits only source_refuted finding occurrences")
665
- evidence = row["evidence"]
666
- if not isinstance(evidence, list) or not evidence:
667
- fail(f"{label} requires a non-empty evidence array")
668
- for evidence_index, item in enumerate(evidence):
669
- bounded_text(item, f"{label}.evidence[{evidence_index}]", 1000)
670
- if len(evidence) != len(set(evidence)):
671
- fail(f"{label}.evidence contains duplicates")
672
- # Class validation already binds this witness to the candidate, raw
673
- # finding, and disposition. Require the complete checkpoint to use the
674
- # same evidence; neither record authenticates its semantic truth.
675
- witness = decode_json(load_sibling(
676
- ledger_dir,
677
- file_value=occurrence["disposition_evidence_file"],
678
- digest_value=occurrence["disposition_evidence_sha256"],
679
- label=f"{label} class disposition evidence",
680
- maximum=MAX_EVIDENCE_BYTES,
681
- ), label=f"{label} class disposition evidence")
682
- if evidence != witness["evidence"]:
683
- fail(f"{label} does not match its class disposition evidence")
684
-
685
-
686
- def validate_base_attestations(
687
- payload: dict[str, Any],
688
- ledger_dir: Path,
689
- receipt_hashes: list[str],
690
- closeout: str,
691
- ) -> int:
692
- attestations = payload["base_attestations"]
693
- if not isinstance(attestations, list) or not attestations:
694
- fail("base_attestations must be a non-empty array")
695
- base_shas: list[str] = []
696
- mapped_receipts: list[str] = []
697
- mapped_receipt_bases: list[str] = []
698
- expected_remote: str | None = None
699
- expected_ref: str | None = None
700
- previous_time: datetime | None = None
701
- base_changes = 0
702
- second_drift_sequence: int | None = None
703
- for index, item in enumerate(attestations, start=1):
704
- row = exact_object(
705
- item,
706
- {
707
- "sequence",
708
- "remote",
709
- "ref",
710
- "sha",
711
- "confirmed_at",
712
- "controller_receipt_sha256",
713
- "evidence_file",
714
- "evidence_sha256",
715
- },
716
- f"base_attestations[{index - 1}]",
717
- )
718
- if row["sequence"] != index or type(row["sequence"]) is not int:
719
- fail("base attestation sequence must be contiguous from 1")
720
- remote = bounded_text(row["remote"], f"base_attestations[{index - 1}].remote", 200)
721
- ref = bounded_text(row["ref"], f"base_attestations[{index - 1}].ref", 300)
722
- if expected_remote is None:
723
- expected_remote, expected_ref = remote, ref
724
- elif remote != expected_remote or ref != expected_ref:
725
- fail("base attestations must use the same remote and ref")
726
- base_sha = object_id(row["sha"], f"base_attestations[{index - 1}].sha")
727
- if base_shas and base_sha != base_shas[-1]:
728
- base_changes += 1
729
- if base_changes == 2:
730
- second_drift_sequence = index
731
- confirmed = parse_rfc3339(
732
- row["confirmed_at"], f"base_attestations[{index - 1}].confirmed_at"
733
- )
734
- if previous_time is not None and confirmed <= previous_time:
735
- fail("base attestation confirmed_at values must strictly increase")
736
- previous_time = confirmed
737
- raw = load_sibling(
738
- ledger_dir,
739
- file_value=row["evidence_file"],
740
- digest_value=row["evidence_sha256"],
741
- label=f"base attestation {index} evidence",
742
- maximum=MAX_EVIDENCE_BYTES,
743
- )
744
- expected_raw = f"{base_sha}\t{ref}\n".encode()
745
- if raw != expected_raw:
746
- fail(f"base attestation {index} evidence does not contain canonical ls-remote output")
747
- receipt_hash_value = row["controller_receipt_sha256"]
748
- if receipt_hash_value is not None:
749
- if second_drift_sequence is not None:
750
- fail(
751
- "a controller receipt is mapped at or after the second base drift"
752
- )
753
- mapped_receipts.append(
754
- sha256(
755
- receipt_hash_value,
756
- f"base_attestations[{index - 1}].controller_receipt_sha256",
757
- )
758
- )
759
- mapped_receipt_bases.append(base_sha)
760
- base_shas.append(base_sha)
761
- if mapped_receipts != receipt_hashes:
762
- fail("base attestations must map every controller receipt exactly once in order")
763
- second_drift = base_changes >= 2
764
- if second_drift != (closeout == "baseline_race"):
765
- fail("the second base drift must terminate as baseline_race, and baseline_race requires two ordered base changes")
766
- if (
767
- closeout != "baseline_race"
768
- and mapped_receipt_bases[-1] != base_shas[-1]
769
- ):
770
- fail(
771
- "the final controller receipt does not consume the latest attested base"
772
- )
773
- return base_changes
774
-
775
-
776
- def validate_disposition_evidence(
777
- ledger_dir: Path,
778
- *,
779
- file_value: object,
780
- digest_value: object,
781
- candidate: str,
782
- receipt_hash: str,
783
- finding_hash: str,
784
- disposition: str,
785
- class_key: str,
786
- occurrence_index: int,
787
- class_pairs: list[tuple[str, str]],
788
- unresolved_pairs: set[tuple[str, str]],
789
- ) -> list[tuple[str, str]]:
790
- label = f"finding class {class_key} disposition evidence {occurrence_index + 1}"
791
- raw = load_sibling(
792
- ledger_dir,
793
- file_value=file_value,
794
- digest_value=digest_value,
795
- label=label,
796
- maximum=MAX_EVIDENCE_BYTES,
797
- )
798
- evidence = exact_object(
799
- decode_json(raw, label=label),
800
- {
801
- "schema_version",
802
- "candidate_sha256",
803
- "receipt_sha256",
804
- "finding_sha256",
805
- "disposition",
806
- "resolves_occurrences",
807
- "evidence",
808
- },
809
- label,
810
- )
811
- if evidence["schema_version"] != 1 or type(evidence["schema_version"]) is not int:
812
- fail(f"{label}.schema_version must be 1")
813
- if sha256(evidence["candidate_sha256"], f"{label}.candidate_sha256") != candidate:
814
- fail(f"{label} is stale for the current candidate")
815
- if sha256(evidence["receipt_sha256"], f"{label}.receipt_sha256") != receipt_hash:
816
- fail(f"{label} does not bind its controller receipt")
817
- if sha256(evidence["finding_sha256"], f"{label}.finding_sha256") != finding_hash:
818
- fail(f"{label} does not bind its controller finding")
819
- if bounded_text(evidence["disposition"], f"{label}.disposition", 80) != disposition:
820
- fail(f"{label} does not bind its disposition")
821
-
822
- refs = evidence["resolves_occurrences"]
823
- if not isinstance(refs, list) or not refs:
824
- fail(f"{label}.resolves_occurrences must be a non-empty array")
825
- resolved_pairs: list[tuple[str, str]] = []
826
- seen_resolved: set[tuple[str, str]] = set()
827
- for resolved_index, value in enumerate(refs):
828
- resolved = exact_object(
829
- value,
830
- {"receipt_sha256", "finding_sha256"},
831
- f"{label}.resolves_occurrences[{resolved_index}]",
832
- )
833
- pair = (
834
- sha256(
835
- resolved["receipt_sha256"],
836
- f"{label}.resolves_occurrences[{resolved_index}].receipt_sha256",
837
- ),
838
- sha256(
839
- resolved["finding_sha256"],
840
- f"{label}.resolves_occurrences[{resolved_index}].finding_sha256",
841
- ),
842
- )
843
- if pair in seen_resolved:
844
- fail(f"{label} repeats a resolved occurrence")
845
- if pair not in class_pairs:
846
- fail(f"{label} resolves an occurrence outside the current class prefix")
847
- if pair not in unresolved_pairs:
848
- fail(f"{label} resolves an occurrence that is not currently unresolved")
849
- seen_resolved.add(pair)
850
- resolved_pairs.append(pair)
851
- current_pair = (receipt_hash, finding_hash)
852
- if current_pair not in seen_resolved:
853
- fail(f"{label} must resolve its current occurrence")
854
- expected_order = [pair for pair in class_pairs if pair in seen_resolved]
855
- if resolved_pairs != expected_order:
856
- fail(f"{label}.resolves_occurrences must follow finding class order")
857
-
858
- evidence_items = evidence["evidence"]
859
- if not isinstance(evidence_items, list) or not evidence_items:
860
- fail(f"{label}.evidence must be a non-empty array")
861
- normalized_evidence = [
862
- bounded_text(value, f"{label}.evidence[{index}]", 1000)
863
- for index, value in enumerate(evidence_items)
864
- ]
865
- if len(normalized_evidence) != len(set(normalized_evidence)):
866
- fail(f"{label}.evidence contains duplicates")
867
- return resolved_pairs
868
-
869
-
870
- def validate_finding_classes(
871
- payload: dict[str, Any],
872
- ledger_dir: Path,
873
- receipt_findings: dict[str, list[str]],
874
- candidate: str,
875
- closeout: str,
876
- ) -> bool:
877
- classes = payload["finding_classes"]
878
- if not isinstance(classes, list):
879
- fail("finding_classes must be an array")
880
- expected_pairs = {
881
- (receipt_hash, finding_hash)
882
- for receipt_hash, findings in receipt_findings.items()
883
- for finding_hash in findings
884
- }
885
- seen_pairs: set[tuple[str, str]] = set()
886
- seen_keys: set[str] = set()
887
- seen_predicates: dict[str, str] = {}
888
- any_unresolved = False
889
- finding_order = {
890
- (receipt_hash, finding_hash): (receipt_index, finding_index)
891
- for receipt_index, (receipt_hash, findings) in enumerate(receipt_findings.items())
892
- for finding_index, finding_hash in enumerate(findings)
893
- }
894
- for class_index, item in enumerate(classes):
895
- row = exact_object(
896
- item,
897
- {"key", "root_cause_predicate", "affected_surface", "occurrences", "authoritative_sweep"},
898
- f"finding_classes[{class_index}]",
899
- )
900
- key = bounded_text(row["key"], f"finding_classes[{class_index}].key", 120)
901
- if key in seen_keys:
902
- fail(f"duplicate finding class key: {key}")
903
- seen_keys.add(key)
904
- predicate = bounded_text(
905
- row["root_cause_predicate"],
906
- f"finding_classes[{class_index}].root_cause_predicate",
907
- 2000,
908
- )
909
- normalized_predicate = " ".join(predicate.casefold().split())
910
- prior_key = seen_predicates.get(normalized_predicate)
911
- if prior_key is not None:
912
- fail(
913
- "duplicate normalized root_cause_predicate across finding classes: "
914
- f"{prior_key}, {key}"
915
- )
916
- seen_predicates[normalized_predicate] = key
917
- bounded_text(
918
- row["affected_surface"], f"finding_classes[{class_index}].affected_surface", 500
919
- )
920
- occurrences = row["occurrences"]
921
- if not isinstance(occurrences, list) or not occurrences:
922
- fail(f"finding_classes[{class_index}].occurrences must be non-empty")
923
- class_order: list[tuple[int, int]] = []
924
- class_pairs: list[tuple[str, str]] = []
925
- unresolved_pairs: set[tuple[str, str]] = set()
926
- human_decision_pairs: set[tuple[str, str]] = set()
927
- for occurrence_index, occurrence in enumerate(occurrences):
928
- occurrence_label = (
929
- f"finding_classes[{class_index}].occurrences[{occurrence_index}]"
930
- )
931
- if not isinstance(occurrence, dict):
932
- fail(f"{occurrence_label} must be an object")
933
- disposition = bounded_text(
934
- occurrence.get("disposition"),
935
- f"{occurrence_label}.disposition",
936
- 80,
937
- )
938
- if disposition not in DISPOSITIONS:
939
- fail(f"finding class {key} has an unknown disposition")
940
- occurrence_fields = {
941
- "receipt_sha256",
942
- "finding_sha256",
943
- "disposition",
944
- }
945
- if disposition in RESOLVED_DISPOSITIONS:
946
- occurrence_fields |= {
947
- "disposition_evidence_file",
948
- "disposition_evidence_sha256",
949
- }
950
- occurrence_row = exact_object(
951
- occurrence,
952
- occurrence_fields,
953
- occurrence_label,
954
- )
955
- receipt_hash = sha256(
956
- occurrence_row["receipt_sha256"],
957
- f"finding_classes[{class_index}].occurrences[{occurrence_index}].receipt_sha256",
958
- )
959
- finding_hash = sha256(
960
- occurrence_row["finding_sha256"],
961
- f"finding_classes[{class_index}].occurrences[{occurrence_index}].finding_sha256",
962
- )
963
- pair = (receipt_hash, finding_hash)
964
- if pair not in expected_pairs:
965
- fail(f"finding class {key} occurrence does not identify a finding in its controller receipt")
966
- if pair in seen_pairs:
967
- fail(f"controller finding {finding_hash} is classified more than once")
968
- seen_pairs.add(pair)
969
- class_order.append(finding_order[pair])
970
- class_pairs.append(pair)
971
- unresolved_pairs.add(pair)
972
- if disposition == "needs_human_decision":
973
- human_decision_pairs.add(pair)
974
- if disposition in RESOLVED_DISPOSITIONS:
975
- resolved_pairs = validate_disposition_evidence(
976
- ledger_dir,
977
- file_value=occurrence_row["disposition_evidence_file"],
978
- digest_value=occurrence_row["disposition_evidence_sha256"],
979
- candidate=candidate,
980
- receipt_hash=receipt_hash,
981
- finding_hash=finding_hash,
982
- disposition=disposition,
983
- class_key=key,
984
- occurrence_index=occurrence_index,
985
- class_pairs=class_pairs,
986
- unresolved_pairs=unresolved_pairs,
987
- )
988
- if human_decision_pairs.intersection(resolved_pairs):
989
- fail(
990
- f"finding class {key} local evidence cannot resolve a "
991
- "needs_human_decision occurrence"
992
- )
993
- unresolved_pairs.difference_update(resolved_pairs)
994
- if class_order != sorted(class_order):
995
- fail(f"finding class {key} occurrences are not in controller receipt order")
996
- any_unresolved |= bool(unresolved_pairs)
997
-
998
- sweep = row["authoritative_sweep"]
999
- if len(occurrences) >= 3:
1000
- sweep_row = exact_object(
1001
- sweep,
1002
- {
1003
- "candidate_sha256",
1004
- "manifest_file",
1005
- "manifest_sha256",
1006
- "searched_set",
1007
- "unmatched_instances",
1008
- },
1009
- f"finding_classes[{class_index}].authoritative_sweep",
1010
- )
1011
- if sha256(
1012
- sweep_row["candidate_sha256"],
1013
- f"finding_classes[{class_index}].authoritative_sweep.candidate_sha256",
1014
- ) != candidate:
1015
- fail(f"finding class {key} sweep is stale for the current candidate")
1016
- searched = sweep_row["searched_set"]
1017
- if not isinstance(searched, list) or not searched:
1018
- fail(f"finding class {key} third occurrence requires a non-empty searched_set")
1019
- normalized = [
1020
- bounded_text(value, f"finding class {key} searched_set", 500)
1021
- for value in searched
1022
- ]
1023
- if len(normalized) != len(set(normalized)):
1024
- fail(f"finding class {key} searched_set contains duplicates")
1025
- unmatched = sweep_row["unmatched_instances"]
1026
- if type(unmatched) is not int or unmatched < 0:
1027
- fail(f"finding class {key} unmatched_instances must be a non-negative integer")
1028
- manifest_raw = load_sibling(
1029
- ledger_dir,
1030
- file_value=sweep_row["manifest_file"],
1031
- digest_value=sweep_row["manifest_sha256"],
1032
- label=f"finding class {key} sweep manifest",
1033
- maximum=MAX_EVIDENCE_BYTES,
1034
- )
1035
- manifest = exact_object(
1036
- decode_json(manifest_raw, label=f"finding class {key} sweep manifest"),
1037
- {"schema_version", "candidate_sha256", "searched_set", "unmatched_instances"},
1038
- f"finding class {key} sweep manifest",
1039
- )
1040
- if manifest["schema_version"] != 1 or type(manifest["schema_version"]) is not int:
1041
- fail(f"finding class {key} sweep manifest schema_version must be 1")
1042
- if sha256(manifest["candidate_sha256"], f"finding class {key} sweep manifest candidate") != candidate:
1043
- fail(f"finding class {key} sweep manifest is stale")
1044
- if manifest["searched_set"] != normalized:
1045
- fail(f"finding class {key} searched_set does not match its sweep manifest")
1046
- unmatched_items = manifest["unmatched_instances"]
1047
- if not isinstance(unmatched_items, list):
1048
- fail(f"finding class {key} sweep manifest unmatched_instances must be an array")
1049
- normalized_unmatched = [
1050
- bounded_text(value, f"finding class {key} unmatched instance", 500)
1051
- for value in unmatched_items
1052
- ]
1053
- if len(normalized_unmatched) != len(set(normalized_unmatched)):
1054
- fail(f"finding class {key} sweep manifest repeats an unmatched instance")
1055
- if len(normalized_unmatched) != unmatched:
1056
- fail(f"finding class {key} unmatched_instances count does not match its sweep manifest")
1057
- if closeout == "ready_for_human_decision" and unmatched != 0:
1058
- fail(f"finding class {key} ready sweep must report zero unmatched instances")
1059
- elif sweep is not None:
1060
- fail(f"finding class {key} has a sweep before its third occurrence")
1061
- if seen_pairs != expected_pairs:
1062
- fail("finding_classes omits controller findings")
1063
- return any_unresolved
1064
-
1065
-
1066
- def validate(payload: dict[str, Any], ledger_dir: Path) -> tuple[str, int, int]:
1067
- if payload["schema_version"] != 3 or type(payload["schema_version"]) is not int:
1068
- fail("schema_version must be 3")
1069
- candidate = sha256(payload["candidate_sha256"], "candidate_sha256")
1070
- closeout = bounded_text(payload["closeout_state"], "closeout_state", 80)
1071
- if closeout not in TERMINAL_STATES:
1072
- fail("closeout_state is not an extraction terminal state")
1073
-
1074
- receipts, receipt_hashes, receipt_findings, chain_id, scope_hash = (
1075
- validate_controller_receipts(payload, ledger_dir)
1076
- )
1077
- if receipts[-1]["candidate_sha256"] != candidate:
1078
- fail("candidate_sha256 must equal the final controller receipt candidate")
1079
- completion = validate_completion_receipt(
1080
- payload["completion_receipt"],
1081
- ledger_dir,
1082
- receipts,
1083
- receipt_hashes,
1084
- chain_id,
1085
- scope_hash,
1086
- )
1087
- base_changes = validate_base_attestations(
1088
- payload, ledger_dir, receipt_hashes, closeout
1089
- )
1090
- any_unresolved = validate_finding_classes(
1091
- payload, ledger_dir, receipt_findings, candidate, closeout
1092
- )
1093
- validate_completion_dispositions(
1094
- payload, ledger_dir, completion, receipt_hashes, receipt_findings, candidate
1095
- )
1096
-
1097
- delta = payload["unreviewed_delta"]
1098
- if not isinstance(delta, list):
1099
- fail("unreviewed_delta must be an array")
1100
- for index, value in enumerate(delta):
1101
- bounded_text(value, f"unreviewed_delta[{index}]", 500)
1102
-
1103
- if closeout == "ready_for_human_decision" and completion is None:
1104
- fail("ready_for_human_decision requires a completion receipt")
1105
- if closeout == "continuation_authorization_required" and completion is not None:
1106
- fail("continuation_authorization_required cannot carry a completion receipt")
1107
- if closeout == "baseline_race" and completion is not None:
1108
- fail("baseline_race cannot carry a completion receipt")
1109
-
1110
- controller_state = bounded_text(
1111
- payload["controller_review_state"], "controller_review_state", 80
1112
- )
1113
- if controller_state not in KNOWN_REVIEW_STATES:
1114
- fail(f"unknown controller review_state {controller_state}")
1115
- derived_state = completion["review_state"] if completion is not None else receipts[-1]["review_state"]
1116
- if controller_state != derived_state:
1117
- fail("controller_review_state does not match the referenced final receipt")
1118
-
1119
- if closeout == "ready_for_human_decision":
1120
- if len(receipts) < 2:
1121
- fail("ready_for_human_decision ready requires at least one tracked challenge")
1122
- if completion is None:
1123
- fail("ready_for_human_decision requires a completion receipt")
1124
- if completion["candidate_sha256"] != candidate or receipts[-1]["candidate_sha256"] != candidate:
1125
- fail("ready_for_human_decision completion receipt must bind the exact final candidate")
1126
- if any_unresolved or delta:
1127
- fail("ready_for_human_decision requires no unresolved finding occurrence and no unreviewed delta")
1128
- elif closeout == "continuation_authorization_required":
1129
- final = receipts[-1]
1130
- # The lane is spent either at the wrapper chain's last round, or — when a
1131
- # fix batch moved the candidate — at the succession round that reviewed it.
1132
- # Only the closeout knows the lane is spent: the controller sizes
1133
- # human_decision_required from its own chain, where the succession round
1134
- # is round one of two.
1135
- succeeded = final.get("predecessor_chain_id") is not None
1136
- if succeeded:
1137
- spent = (
1138
- len(receipts) == LANE_MAX_ROUNDS and final.get("status") == "findings"
1139
- )
1140
- else:
1141
- spent = (
1142
- len(receipts) == WRAPPER_AUTONOMOUS_ROUNDS
1143
- and final.get("status") == "findings"
1144
- and final.get("review_state") == "post_review_budget"
1145
- and final.get("human_decision_required") is True
1146
- )
1147
- if not spent:
1148
- fail(
1149
- "continuation_authorization_required requires final-round "
1150
- f"(round {WRAPPER_AUTONOMOUS_ROUNDS}) findings in post_review_budget, "
1151
- f"or a succession round (round {LANE_MAX_ROUNDS}) carrying findings"
1152
- )
1153
- elif closeout == "baseline_race" and not delta:
1154
- fail("baseline_race requires a non-empty unreviewed_delta")
1155
-
1156
- return closeout, len(payload["finding_classes"]), base_changes
1157
-
1158
-
1159
- def main() -> int:
1160
- parser = argparse.ArgumentParser(description=__doc__)
1161
- parser.add_argument("state_file", type=Path)
1162
- args = parser.parse_args()
1163
- try:
1164
- payload, ledger_dir = load(args.state_file)
1165
- state, class_count, base_changes = validate(payload, ledger_dir)
1166
- except StateError as exc:
1167
- print(f"extraction_review_state_invalid: {exc}", file=sys.stderr)
1168
- return 1
1169
- except (OSError, UnicodeError, ValueError):
1170
- print(
1171
- "extraction_review_state_invalid: local evidence read or encoding failed",
1172
- file=sys.stderr,
1173
- )
1174
- return 1
1175
- print(
1176
- "extraction_review_state_ok: "
1177
- f"closeout_state={state} finding_classes={class_count} base_changes={base_changes}"
1178
- )
1179
- return 0
1180
-
1181
-
1182
- if __name__ == "__main__":
1183
- raise SystemExit(main())