3"""Fail-closed review ledger binding every suppression site to a decision.
5Three committed authorities drive the ledger:
7* ``.github/suppression-review-rationales.yml`` -- the closed rationale
8 vocabulary. Each category names its allowed ledger state, concrete
9 applicability criteria, required evidence kinds, and an optional
10 revalidation trigger. Categories are vocabulary, never approval.
11* ``.github/suppression-review-ledger.tsv`` -- exactly one row per live
12 occurrence: ``site_id binding_sha256 state rationale_id batch_id
13 evidence_ref``. States: ``unreviewed`` (bootstrap only), ``retain``,
14 ``fix-required``, ``resolved``, ``superseded``.
15* ``.github/suppression-review-batches.yml`` -- one record per review batch:
16 authority, date, identity schema, assigned row count, and the digest of
17 the batch's ordered ledger rows.
19The gate rejects live sites missing from the ledger, ledger rows whose site
20is gone, binding drift under ``retain``, duplicate identities, unknown
21categories/states/batches, unsorted rows, batch count or digest mismatches,
22blank rationale or evidence on reviewed rows, active ``unreviewed`` or
23``fix-required`` rows, and identity-schema drift. Only a ``retain`` row with
24an exact binding match marks a row approved; nothing here generates
28from __future__
import annotations
32from collections.abc
import Hashable
33from dataclasses
import dataclass, replace
34from pathlib
import Path
37from suppression_identity
import IDENTITY_SCHEMA_VERSION
38from suppression_model
import Finding, Inventory
40RATIONALES_PATH =
".github/suppression-review-rationales.yml"
41LEDGER_PATH =
".github/suppression-review-ledger.tsv"
42BATCHES_PATH =
".github/suppression-review-batches.yml"
44LEDGER_STATES = frozenset({
"unreviewed",
"retain",
"fix-required",
"resolved",
"superseded"})
45ACTIVE_STATES = frozenset({
"unreviewed",
"retain",
"fix-required"})
46_HEX64_RE = re.compile(
r"^[0-9a-f]{64}$")
50@dataclass(frozen=True)
52 """One reviewed occurrence decision."""
63class DuplicateKeySafeLoader(yaml.SafeLoader):
64 """Safe YAML loader that rejects a repeated mapping key instead of dropping it.
66 PyYAML's mapping construction keeps the LAST value bound to a repeated
67 key and reports nothing, so a second ``id:`` inside a batch record, or a
68 second top-level ``batches:`` block, silently replaces the reviewed
69 authority before any application validation runs. Rejecting the document
70 at parse time is the only place that decision is still visible.
73 def construct_mapping(self, node: yaml.MappingNode, deep: bool =
False) -> dict:
74 """Construct one mapping, rejecting any explicitly repeated key.
76 Merge keys are skipped, so YAML's documented ``<<`` override semantics
77 keep working: only keys written twice in the same mapping are refused.
79 seen: set[object] = set()
80 for key_node, _value_node
in node.value:
81 if key_node.tag ==
"tag:yaml.org,2002:merge":
83 key = self.construct_object(key_node, deep=deep)
84 if not isinstance(key, Hashable):
87 context =
"while constructing a mapping"
88 problem = f
"duplicate key {key!r}"
89 raise yaml.constructor.ConstructorError(
90 context, node.start_mark, problem, key_node.start_mark
93 return super().construct_mapping(node, deep=deep)
96def _load_document(text: str) -> object:
97 """Parse one YAML document with duplicate keys rejected at parse time."""
98 loader = DuplicateKeySafeLoader(text)
100 return loader.get_single_data()
105def _load_yaml(root: Path, rel: str) -> tuple[dict |
None, Finding |
None]:
106 """Load one committed YAML authority, failing closed on any defect."""
108 data = _load_document((root / rel).read_text(encoding=
"utf-8"))
110 return None, Finding(
"missing-review-ledger", f
"{rel} is absent", rel)
111 except yaml.YAMLError
as exc:
112 return None, Finding(
"malformed-review-ledger", str(exc), rel)
113 if not isinstance(data, dict):
114 return None, Finding(
"malformed-review-ledger",
"document is not a mapping", rel)
118def load_rationales(root: Path) -> tuple[dict[str, dict], list[Finding]]:
119 """Parse the rationale vocabulary and validate every category contract."""
120 data, problem = _load_yaml(root, RATIONALES_PATH)
121 if problem
is not None:
123 findings: list[Finding] = []
124 categories = data.get(
"categories")
125 if not isinstance(categories, dict)
or not categories:
126 return {}, [Finding(
"malformed-review-ledger",
"no categories", RATIONALES_PATH)]
127 result: dict[str, dict] = {}
128 for name, spec
in categories.items():
129 if not isinstance(spec, dict):
132 "malformed-review-ledger", f
"category {name} is not a mapping", RATIONALES_PATH
136 state = spec.get(
"state")
137 applicability = spec.get(
"applicability")
138 evidence = spec.get(
"evidence")
140 state
not in LEDGER_STATES - {
"unreviewed"}
141 or not isinstance(applicability, str)
142 or not applicability.strip()
143 or not isinstance(evidence, list)
148 "malformed-review-ledger",
149 f
"category {name} needs a reviewed state, applicability, and evidence kinds",
155 return result, findings
158def load_ledger(root: Path) -> tuple[list[LedgerRow], list[Finding]]:
159 """Parse the ledger rows, rejecting malformed shape or ordering."""
161 text = (root / LEDGER_PATH).read_text(encoding=
"utf-8")
163 return [], [Finding(
"missing-review-ledger", f
"{LEDGER_PATH} is absent", LEDGER_PATH)]
164 findings: list[Finding] = []
165 rows: list[LedgerRow] = []
166 lines = text.splitlines()
167 expected_header =
"site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref"
168 if not lines
or lines[0] != expected_header:
169 findings.append(Finding(
"malformed-review-ledger",
"missing header row", LEDGER_PATH, 1))
171 for line_no, raw
in enumerate(lines[1:], start=2):
172 parts = raw.split(
"\t")
173 if len(parts) != LEDGER_COLUMNS:
175 Finding(
"malformed-review-ledger",
"wrong column count", LEDGER_PATH, line_no)
178 row = LedgerRow(line_no, *parts)
179 if not _HEX64_RE.match(row.site_id)
or not _HEX64_RE.match(row.binding_sha256):
181 Finding(
"malformed-review-ledger",
"identity is not 64 hex", LEDGER_PATH, line_no)
184 if row.state
not in LEDGER_STATES:
187 "malformed-review-ledger", f
"unknown state {row.state}", LEDGER_PATH, line_no
192 ordered = [row.site_id
for row
in rows]
193 if ordered != sorted(ordered):
195 Finding(
"malformed-review-ledger",
"rows are not sorted by site_id", LEDGER_PATH)
197 seen: set[str] = set()
199 if row.site_id
in seen:
202 "ledger-duplicate-site", f
"duplicate site {row.site_id}", LEDGER_PATH, row.line
205 seen.add(row.site_id)
206 return rows, findings
209def load_batches(root: Path) -> tuple[dict[str, dict], list[Finding]]:
210 """Parse the batch records and validate their schema fields."""
211 data, problem = _load_yaml(root, BATCHES_PATH)
212 if problem
is not None:
214 findings: list[Finding] = []
215 batches = data.get(
"batches")
216 if not isinstance(batches, list):
217 return {}, [Finding(
"malformed-review-ledger",
"no batches list", BATCHES_PATH)]
218 result: dict[str, dict] = {}
219 required = (
"id",
"authority",
"date",
"identity_schema",
"assigned_rows",
"rows_sha256")
220 for record
in batches:
221 if not isinstance(record, dict)
or any(key
not in record
for key
in required):
223 Finding(
"malformed-review-ledger",
"batch record missing fields", BATCHES_PATH)
226 if record[
"identity_schema"] != IDENTITY_SCHEMA_VERSION:
229 "ledger-schema-mismatch",
230 f
"batch {record['id']} reviewed under {record['identity_schema']}; "
231 f
"live schema is {IDENTITY_SCHEMA_VERSION}",
235 identity = str(record[
"id"])
236 if identity
in result:
239 "ledger-duplicate-batch",
240 f
"batch identity {identity} is recorded more than once",
245 result[identity] = record
246 return result, findings
249def _sample(values: list[str]) -> str:
250 """Render a bounded sample list for one aggregate finding."""
251 shown =
", ".join(values[:_SAMPLE_LIMIT])
252 more = len(values) -
min(len(values), _SAMPLE_LIMIT)
253 return shown + (f
" (+{more} more)" if more > 0
else "")
256def _batch_findings(rows: list[LedgerRow], batches: dict[str, dict]) -> list[Finding]:
257 """Verify per-batch row counts and ordered-row digests."""
258 findings: list[Finding] = []
259 by_batch: dict[str, list[LedgerRow]] = {}
261 if row.state ==
"unreviewed":
263 if row.batch_id
not in batches:
266 "ledger-unknown-reference",
267 f
"unknown batch {row.batch_id or '(blank)'}",
273 by_batch.setdefault(row.batch_id, []).append(row)
274 for batch_id, record
in batches.items():
275 members = by_batch.get(batch_id, [])
276 if len(members) != record[
"assigned_rows"]:
279 "ledger-batch-mismatch",
280 f
"batch {batch_id} has {len(members)} rows; record says "
281 f
"{record['assigned_rows']}",
286 f
"{row.site_id}\t{row.binding_sha256}\t{row.state}"
287 f
"\t{row.rationale_id}\t{row.evidence_ref}"
290 digest = hashlib.sha256(payload).hexdigest()
291 if digest != record[
"rows_sha256"]:
294 "ledger-batch-mismatch",
295 f
"batch {batch_id} rows digest {digest} != recorded {record['rows_sha256']}",
302def _portable_evidence_values(row: LedgerRow) -> tuple[dict[str, str], Finding |
None]:
303 """Parse the three required portable-test evidence fields."""
304 required = (
"test-name",
"passing-counterpart",
"registered-gate")
305 values: dict[str, str] = {}
306 duplicates: set[str] = set()
307 for part
in row.evidence_ref.split():
308 key, separator, value = part.partition(
":")
309 if key
not in required:
313 if separator
and value:
315 missing = [key
for key
in required
if not values.get(key)]
316 if missing
or duplicates:
319 detail.append(f
"missing {', '.join(missing)}")
321 detail.append(f
"duplicate {', '.join(sorted(duplicates))}")
323 "malformed-review-ledger",
324 "portable prerequisite evidence is incomplete: " +
"; ".join(detail),
331def _test_reference_is_live(root: Path, reference: str) -> bool:
332 """Return whether ``path::function`` identifies a current test function."""
333 target, separator, symbol = reference.partition(
"::")
335 text = (root / target).read_text(encoding=
"utf-8")
340 and re.fullmatch(
r"[A-Za-z_][A-Za-z0-9_]*", symbol)
341 and re.search(rf
"\bdef\s+{re.escape(symbol)}\s*\(", text)
345def _portable_gate_fails_closed(root: Path) -> bool:
346 """Return whether the work-harness gate forbids prerequisite skips."""
348 gate_text = (root /
"scripts/ci/gates/tests.sh").read_text(encoding=
"utf-8")
349 helper_text = (root /
"scripts/dev/work/tests/fixtures/work_testlib.py").read_text(
354 gate_match = re.search(
r"gate_work_harness\(\) \((.*?)^\)", gate_text, re.MULTILINE | re.DOTALL)
355 gate_body = gate_match.group(1)
if gate_match
is not None else ""
357 "require_cmd bash" in gate_body
358 and "require_cmd sh" in gate_body
359 and "RA8_WORK_HARNESS_REGISTERED_GATE=1" in gate_body
360 and "scripts/dev/work/src/work.py --selftest" in gate_body
363 'os.environ.get(REGISTERED_GATE_ENV) == "1"' in helper_text
364 and "raise RuntimeError(message)" in helper_text
366 return gate_contract
and helper_contract
369def _portable_test_evidence_findings(row: LedgerRow, root: Path) -> list[Finding]:
370 """Validate the fail-closed evidence contract for a portable test skip."""
371 values, malformed = _portable_evidence_values(row)
372 if malformed
is not None:
377 "ledger-unknown-reference",
378 f
"portable prerequisite {key} does not name a live test: {values[key]}",
382 for key
in (
"test-name",
"passing-counterpart")
383 if not _test_reference_is_live(root, values[key])
386 gate = values[
"registered-gate"]
387 if gate !=
"work-harness":
390 "ledger-unknown-reference",
391 f
"portable prerequisite names unknown registered gate {gate}",
397 if not _portable_gate_fails_closed(root):
400 "ledger-invalid-gate-contract",
401 "work-harness gate can skip a portable prerequisite instead of failing closed",
409def _reviewed_row_findings(
410 row: LedgerRow, rationales: dict[str, dict], root: Path
412 """Validate rationale, state compatibility, and evidence on one row."""
413 findings: list[Finding] = []
414 if row.state ==
"unreviewed":
415 if row.rationale_id
or row.batch_id:
418 "malformed-review-ledger",
419 "unreviewed rows carry no rationale or batch",
425 spec = rationales.get(row.rationale_id)
429 "ledger-unknown-reference",
430 f
"unknown rationale {row.rationale_id or '(blank)'}",
436 completed = row.state
in {
"resolved",
"superseded"}
and spec[
"state"] ==
"fix-required"
437 if spec[
"state"] != row.state
and not completed:
440 "ledger-state-conflict",
441 f
"rationale {row.rationale_id} allows state {spec['state']}, row says {row.state}",
446 if not row.evidence_ref.strip():
449 "malformed-review-ledger",
"reviewed row has blank evidence", LEDGER_PATH, row.line
452 if row.rationale_id ==
"portable-test-prerequisite-boundary":
453 findings.extend(_portable_test_evidence_findings(row, root))
457def _reconcile_row(inventory: Inventory, live: dict[str, int], row: LedgerRow) -> list[Finding]:
458 """Reconcile one ledger row against the live inventory, fail closed."""
459 findings: list[Finding] = []
460 index = live.get(row.site_id)
461 if row.state
in ACTIVE_STATES:
466 f
"{row.state} site no longer exists: {row.site_id}",
471 elif row.state ==
"retain":
472 item = inventory.suppressions[index]
473 if item.binding_sha256 != row.binding_sha256:
476 "ledger-binding-mismatch",
477 f
"retained site {row.site_id} content changed "
478 f
"({item.path}: reviewed {row.binding_sha256[:12]}, "
479 f
"live {item.binding_sha256[:12]})",
485 inventory.suppressions[index] = replace(item, disposition=
"approved")
486 elif index
is not None:
489 "ledger-resolved-still-present",
490 f
"{row.state} site is still present: {row.site_id}",
495 if row.state ==
"superseded":
497 part.removeprefix(
"replaced-by:")
498 for part
in row.evidence_ref.split()
499 if part.startswith(
"replaced-by:")
501 if not replacements
or any(rep
not in live
for rep
in replacements):
504 "ledger-unknown-reference",
505 f
"superseded site {row.site_id} names no live replacement",
513def apply_ledger(inventory: Inventory, root: Path) ->
None:
514 """Reconcile the committed ledger against the live inventory, fail closed."""
515 rationales, findings = load_rationales(root)
516 rows, row_findings = load_ledger(root)
517 batches, batch_findings = load_batches(root)
518 findings.extend(row_findings)
519 findings.extend(batch_findings)
520 if any(item.code ==
"missing-review-ledger" for item
in findings):
521 inventory.findings.extend(findings)
523 findings.extend(_batch_findings(rows, batches))
525 findings.extend(_reviewed_row_findings(row, rationales, root))
526 live = {item.site_id: index
for index, item
in enumerate(inventory.suppressions)}
527 by_site = {row.site_id: row
for row
in rows}
528 missing = sorted(site
for site
in live
if site
not in by_site)
532 "ledger-missing-site",
533 f
"{len(missing)} live suppression(s) absent from the ledger: {_sample(missing)}",
536 unreviewed = [row
for row
in rows
if row.state ==
"unreviewed"]
541 f
"{len(unreviewed)} ledger row(s) await review: "
542 f
"{_sample([row.site_id for row in unreviewed])}",
545 fix_required = [row
for row
in rows
if row.state ==
"fix-required"]
549 "ledger-fix-required",
550 f
"{len(fix_required)} site(s) carry an unremediated fix decision: "
551 f
"{_sample([row.site_id for row in fix_required])}",
555 findings.extend(_reconcile_row(inventory, live, row))
556 inventory.findings.extend(findings)
559def candidate_rows(inventory: Inventory) -> list[str]:
560 """Emit bootstrap candidates for live sites the ledger does not know.
562 Candidates are always ``unreviewed``: generation can never approve.
565 f
"{item.site_id}\t{item.binding_sha256}\tunreviewed\t\t\t"
566 for item
in sorted(inventory.suppressions, key=
lambda row: row.site_id)
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.