@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.
- package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +8 -7
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-review-covers-head.sh +128 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-untracked-background.sh +53 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_review_covers_head.sh +104 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_untracked_background.sh +75 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +6 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/development-completion.md +13 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +78 -126
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/wording-only-review.md +136 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +521 -32
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +25 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_order.sh +30 -15
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +439 -19
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +14 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +6 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +48 -119
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +9 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check_review_evidence_present.py +122 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +4 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +37 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +102 -20
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ai_coding_implementation_gates.sh +16 -27
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +3 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_review_evidence_present.sh +81 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +137 -279
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_resolution.sh +41 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +49 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +3 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/closeout-reread.md +40 -0
- package/dist/assets/release.json +72 -52
- package/package.json +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +0 -1242
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +0 -978
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +0 -1477
- 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())
|