ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
review_suppressions.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Review Suppressions Utility.
5
6Finds all 'unreviewed' ledger rows, assigns them a rationale, creates a batch,
7and sets them to 'retain' so they pass CI.
8"""
9
10import argparse
11import ast
12import datetime
13import hashlib
14import json
15import re
16import subprocess
17import sys
18from collections.abc import Callable
19from contextlib import suppress
20from dataclasses import dataclass
21from pathlib import Path
22from typing import NoReturn
23
24import yaml
25
26
27def _repo_root() -> Path:
28 return Path(__file__).resolve().parents[2]
29
30
31def _read_ledger() -> list[str]:
32 return (
33 (_repo_root() / ".github" / "suppression-review-ledger.tsv")
34 .read_text(encoding="utf-8")
35 .splitlines()
36 )
37
38
39def _write_ledger(lines: list[str]) -> None:
40 (_repo_root() / ".github" / "suppression-review-ledger.tsv").write_text(
41 "\n".join(lines) + "\n", encoding="utf-8"
42 )
43
44
45def _get_rationale_evidence(args: argparse.Namespace) -> tuple[str, str]:
46 rationale = args.rationale
47 if not rationale:
48 print("\nAvailable Rationales:")
49 print(" 1. tool-false-positive")
50 print(" 2. language-or-compiler-contract")
51 print(" 3. negative-test-contract")
52 print(" 4. generated-reproducible-output")
53 print(" 5. vendor-upstream-preserved")
54 print(" 6. reviewed-scope-authority")
55 choice = input("Select a rationale (1-6) or type the ID directly: ").strip()
56 mapping = {
57 "1": "tool-false-positive",
58 "2": "language-or-compiler-contract",
59 "3": "negative-test-contract",
60 "4": "generated-reproducible-output",
61 "5": "vendor-upstream-preserved",
62 "6": "reviewed-scope-authority",
63 }
64 rationale = mapping.get(choice, choice)
65
66 evidence = args.evidence
67 if not evidence:
68 evidence = input(f"Enter evidence reference for why '{rationale}' applies here: ").strip()
69
70 return rationale, evidence
71
72
73def _write_batch(batch_id: str, count: int, batch_sha256: str, evidence: str) -> None:
74 batches_path = _repo_root() / ".github" / "suppression-review-batches.yml"
75 batches_yaml = batches_path.read_text(encoding="utf-8").rstrip()
76
77 batch_entry = f"""
78 - id: {batch_id}
79 authority: repository-suppression-review
80 date: {datetime.datetime.now(tz=datetime.UTC).strftime("%Y-%m-%d")}
81 identity_schema: 2-durable-site-identity
82 assigned_rows: {count}
83 rows_sha256: {batch_sha256}
84 partition: "Automated developer review batch"
85 evidence:
86 - "{evidence}"
87"""
88 batches_path.write_text(batches_yaml + batch_entry, encoding="utf-8")
89
90
91def _build_parser() -> argparse.ArgumentParser:
92 """Build the review CLI: legacy blanket flow plus selective subcommands."""
93 parser = argparse.ArgumentParser(description="Approve draft unreviewed suppressions.")
94 parser.add_argument("--rationale", type=str, help="Rationale ID")
95 parser.add_argument("--evidence", type=str, help="Evidence reference string")
96 parser.add_argument(
97 "--selftest",
98 action="store_true",
99 help="run the selective review selftest (no ledger changes)",
100 )
101 subparsers = parser.add_subparsers(dest="action")
102 retain_parser = subparsers.add_parser(
103 "retain", help="retain explicitly selected live unreviewed rows"
104 )
105 retain_parser.add_argument("--site", action="append", default=[], help="site_id to retain")
106 retain_parser.add_argument("--rationale", required=True, help="Rationale ID")
107 retain_parser.add_argument("--evidence", required=True, help="Evidence reference string")
108 retain_parser.add_argument(
109 "--dry-run", action="store_true", help="print transitions without writing"
110 )
111 retire_parser = subparsers.add_parser(
112 "retire", help="retire explicitly selected stale unreviewed rows"
113 )
114 retire_parser.add_argument("--site", action="append", default=[], help="site_id to retire")
115 retire_parser.add_argument("--rationale", required=True, help="Rationale ID")
116 retire_parser.add_argument("--evidence", required=True, help="Evidence reference string")
117 retire_parser.add_argument(
118 "--replaced-by",
119 action="append",
120 default=[],
121 help="live successor site_id (supersede instead of resolve)",
122 )
123 retire_parser.add_argument(
124 "--dry-run", action="store_true", help="print transitions without writing"
125 )
126 return parser
127
128
129def main() -> int:
130 """Execute the review approval flow."""
131 args = _build_parser().parse_args()
132
133 if args.selftest:
134 return run_selftest()
135
136 if args.action in ("retain", "retire"):
137 return run_selective(args)
138
139 ledger = _read_ledger()
140 header = ledger[0]
141 rows = ledger[1:]
142
143 unreviewed = []
144 for r in rows:
145 if not r.strip():
146 continue
147 parts = r.split("\t")
148 if parts[2] == "unreviewed":
149 unreviewed.append(parts)
150
151 if not unreviewed:
152 print("No 'unreviewed' rows found in ledger. Nothing to do.")
153 return 0
154
155 print(f"Found {len(unreviewed)} unreviewed suppression(s).")
156 rationale, evidence = _get_rationale_evidence(args)
157
158 date_str = datetime.datetime.now(tz=datetime.UTC).strftime("%Y%m%d")
159 batch_id = f"auto-review-batch-{date_str}"
160
161 payload = [f"{parts[0]}\t{parts[1]}\tretain\t{rationale}\t{evidence}" for parts in unreviewed]
162
163 batch_sha256 = hashlib.sha256("\n".join(payload).encode("utf-8")).hexdigest()
164
165 updated_ledger = [header]
166 for r in rows:
167 if not r.strip():
168 continue
169 parts = r.split("\t")
170 if parts[2] == "unreviewed":
171 parts[2] = "retain"
172 parts[3] = rationale
173 parts[4] = batch_id
174 parts[5] = evidence
175 updated_ledger.append("\t".join(parts))
176 else:
177 updated_ledger.append(r)
178
179 _write_ledger(updated_ledger)
180 _write_batch(batch_id, len(unreviewed), batch_sha256, evidence)
181
182 print(f"\nSuccessfully approved {len(unreviewed)} row(s) and generated batch {batch_id}.")
183 print("Run `just quality::local::gate suppressions` to verify.")
184 return 0
185
186
187_LEDGER_HEADER = "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref"
188_LEDGER_COLUMNS = 6
189_LEDGER_STATES = frozenset({"unreviewed", "retain", "fix-required", "resolved", "superseded"})
190_HEX64_RE = re.compile(r"^[0-9a-f]{64}$")
191_BATCHES_PATH = ".github/suppression-review-batches.yml"
192_RATIONALES_PATH = ".github/suppression-review-rationales.yml"
193_IDENTITY_MODULE = "scripts/checks/suppression_identity.py"
194_IDENTITY_ATTR = "IDENTITY_SCHEMA_VERSION"
195
196
197class ReviewError(ValueError):
198 """A selective review action was refused fail-closed."""
199
200
201def _refuse(message: str) -> NoReturn:
202 """Reject one review action with a fail-closed diagnostic."""
203 raise ReviewError(message)
204
205
206def _parse_ledger(text: str) -> tuple[str, list[list[str]]]:
207 """Split ledger text into header plus validated rows, failing closed."""
208 lines = text.splitlines()
209 if not lines or lines[0] != _LEDGER_HEADER:
210 _refuse("malformed ledger: missing or wrong header")
211 rows: list[list[str]] = []
212 for lineno, line in enumerate(lines[1:], start=2):
213 if not line.strip():
214 continue
215 parts = line.split("\t")
216 if len(parts) != _LEDGER_COLUMNS:
217 _refuse(f"malformed ledger: line {lineno} has {len(parts)} columns")
218 site, binding, state = parts[0], parts[1], parts[2]
219 if not _HEX64_RE.match(site):
220 _refuse(f"malformed ledger: line {lineno} has a bad site_id")
221 if not _HEX64_RE.match(binding):
222 _refuse(f"malformed ledger: line {lineno} has a bad binding_sha256")
223 if state not in _LEDGER_STATES:
224 _refuse(f"malformed ledger: line {lineno} has unknown state {state!r}")
225 if state == "unreviewed" and (parts[3] or parts[4]):
226 _refuse(f"malformed ledger: line {lineno} unreviewed row carries rationale/batch")
227 rows.append(parts)
228 return lines[0], rows
229
230
231def _rationale_states(root: Path) -> dict[str, str]:
232 """Map each rationale category to its allowed ledger state."""
233 try:
234 data = yaml.safe_load((root / _RATIONALES_PATH).read_text(encoding="utf-8"))
235 except OSError as exc:
236 _refuse(f"cannot read rationale vocabulary: {exc}")
237 except yaml.YAMLError as exc:
238 _refuse(f"malformed rationale vocabulary: {exc}")
239 try:
240 categories = data["categories"]
241 return {name: spec["state"] for name, spec in categories.items()}
242 except (TypeError, KeyError, AttributeError) as exc:
243 _refuse(f"malformed rationale vocabulary: {exc}")
244
245
246def _identity_schema(root: Path) -> str:
247 """Read the batch identity-schema constant without importing check code."""
248 try:
249 tree = ast.parse((root / _IDENTITY_MODULE).read_text(encoding="utf-8"))
250 except OSError as exc:
251 _refuse(f"cannot read identity module: {exc}")
252 except SyntaxError as exc:
253 _refuse(f"malformed identity module: {exc}")
254 for node in ast.walk(tree):
255 if (
256 isinstance(node, ast.Assign)
257 and len(node.targets) == 1
258 and isinstance(node.targets[0], ast.Name)
259 and node.targets[0].id == _IDENTITY_ATTR
260 and isinstance(node.value, ast.Constant)
261 and isinstance(node.value.value, str)
262 ):
263 return node.value.value
264 _refuse("identity schema constant not found")
265
266
267def _existing_batch_ids(root: Path) -> set[str]:
268 """Return every committed batch id, failing closed on a broken file."""
269 try:
270 data = yaml.safe_load((root / _BATCHES_PATH).read_text(encoding="utf-8"))
271 except OSError as exc:
272 _refuse(f"cannot read batches file: {exc}")
273 except yaml.YAMLError as exc:
274 _refuse(f"malformed batches file: {exc}")
275 try:
276 return {str(record["id"]) for record in data["batches"]}
277 except (TypeError, KeyError, AttributeError) as exc:
278 _refuse(f"malformed batches file: {exc}")
279
280
281def _live_bindings(root: Path) -> dict[str, str]:
282 """Map every live site_id to its current binding via the canonical scan."""
283 proc = subprocess.run(
284 [
285 sys.executable,
286 "scripts/checks/check_suppressions.py",
287 "--inventory",
288 "--format",
289 "json",
290 ],
291 cwd=root,
292 capture_output=True,
293 text=True,
294 check=False,
295 )
296 try:
297 inventory = json.loads(proc.stdout)
298 return {item["site_id"]: item["binding_sha256"] for item in inventory["suppressions"]}
299 except (json.JSONDecodeError, KeyError, TypeError) as exc:
300 _refuse(f"live inventory scan failed: {exc}")
301
302
303def _check_selection(site_ids: list[str], rows: list[list[str]]) -> list[list[str]]:
304 """Resolve each requested id to exactly one ledger row, failing closed."""
305 if not site_ids:
306 _refuse("no --site given: selection is required")
307 seen: set[str] = set()
308 selected: list[list[str]] = []
309 for site in site_ids:
310 if not _HEX64_RE.match(site):
311 _refuse(f"invalid site_id {site!r}: not a 64-hex identity")
312 if site in seen:
313 _refuse(f"ambiguous selection: {site[:12]}... requested twice")
314 seen.add(site)
315 matches = [row for row in rows if row[0] == site]
316 if not matches:
317 _refuse(f"unknown site_id {site[:12]}...: no ledger row")
318 if len(matches) > 1:
319 _refuse(f"ambiguous site_id {site[:12]}...: {len(matches)} ledger rows")
320 selected.append(matches[0])
321 return selected
322
323
324@dataclass(frozen=True)
325class ReviewContext:
326 """Shared review inputs: rationale, evidence, live bindings, vocabulary."""
327
328 rationale: str
329 evidence: str
330 live: dict[str, str]
331 states: dict[str, str]
332
333
334def _check_tsv_cell(field: str, value: str) -> None:
335 """Reject TSV-breaking characters in a ledger-serialized review field."""
336 for bad, name in (("\t", "tab"), ("\n", "newline"), ('"', "double-quote")):
337 if bad in value:
338 _refuse(f"{field} must be a single line without {name}: TSV cell integrity")
339
340
341def _check_reviewable(selected: list[list[str]], ctx: ReviewContext) -> None:
342 """Validate shared review preconditions for every selected row."""
343 if not ctx.rationale.strip():
344 _refuse("missing rationale: --rationale is required")
345 if not ctx.evidence.strip():
346 _refuse("missing evidence: --evidence is required")
347 _check_tsv_cell("rationale", ctx.rationale)
348 _check_tsv_cell("evidence", ctx.evidence)
349 if ctx.rationale not in ctx.states:
350 _refuse(f"unknown rationale {ctx.rationale!r}")
351 for row in selected:
352 if row[2] != "unreviewed":
353 _refuse(f"already reviewed: {row[0][:12]}... is {row[2]!r}, not unreviewed")
354
355
356def _require_state(ctx: ReviewContext, want: str) -> None:
357 """Require the rationale to allow the target state, failing closed."""
358 if ctx.states[ctx.rationale] != want:
359 _refuse(f"rationale {ctx.rationale!r} does not allow state {want!r}")
360
361
362@dataclass(frozen=True)
363class BatchSpec:
364 """One batch record's header plus its covered member rows."""
365
366 batch_id: str
367 date: str
368 schema: str
369 action: str
370 members: list[list[str]]
371 digest: str
372 evidence: str
373
374
375@dataclass(frozen=True)
376class PlannedTransition:
377 """One validated review transition with its pre-mutation state preserved."""
378
379 row: list[str]
380 old_state: str
381 new_state: str
382 rationale: str
383 evidence: str
384 note: str
385
386
387def _plan_retain(selected: list[list[str]], ctx: ReviewContext) -> list[PlannedTransition]:
388 """Validate a retain; preserve each row's pre-mutation state."""
389 _check_reviewable(selected, ctx)
390 _require_state(ctx, "retain")
391 planned = []
392 for row in selected:
393 site, binding = row[0], row[1]
394 if site not in ctx.live:
395 _refuse(f"stale site {site[:12]}...: no live suppression; cannot retain")
396 if ctx.live[site] != binding:
397 _refuse(f"stale binding {site[:12]}...: ledger no longer matches live; cannot retain")
398 planned.append(
399 PlannedTransition(
400 row, row[2], "retain", ctx.rationale, ctx.evidence, "retain live site"
401 )
402 )
403 return planned
404
405
406def _plan_retire(
407 selected: list[list[str]], ctx: ReviewContext, replaced_by: list[str]
408) -> list[PlannedTransition]:
409 """Validate a retire; resolved needs a gone site, superseded live successors."""
410 for successor in replaced_by:
411 if not _HEX64_RE.match(successor):
412 _refuse(f"invalid successor {successor!r}: not a 64-hex identity")
413 _check_reviewable(selected, ctx)
414 target = "superseded" if replaced_by else "resolved"
415 _require_state(ctx, target)
416 planned = []
417 for row in selected:
418 site = row[0]
419 if site in ctx.live:
420 _refuse(f"live site {site[:12]}...: retire refuses a standing suppression")
421 if target == "resolved":
422 planned.append(
423 PlannedTransition(
424 row, row[2], target, ctx.rationale, ctx.evidence, "resolve vanished site"
425 )
426 )
427 continue
428 if site in replaced_by:
429 _refuse(f"successor loop {site[:12]}...: cannot replace itself")
430 for successor in replaced_by:
431 if successor not in ctx.live:
432 _refuse(f"dead successor {successor[:12]}...: replacement is not live")
433 tokens = " ".join(f"replaced-by:{entry}" for entry in replaced_by)
434 planned.append(
435 PlannedTransition(
436 row, row[2], target, ctx.rationale, f"{ctx.evidence} {tokens}", "supersede"
437 )
438 )
439 return planned
440
441
442def _new_batch_id(existing: set[str], now: datetime.datetime) -> str:
443 """Mint a unique selective-review batch id, failing closed on collision."""
444 candidate = f"selective-review-{now.strftime('%Y%m%d-%H%M%S')}"
445 if candidate in existing:
446 _refuse(f"batch id collision {candidate!r}: retry the review action")
447 return candidate
448
449
450def _batch_digest(members: list[list[str]]) -> str:
451 """Digest ordered member rows exactly like the gate's batch check."""
452 payload = "\n".join(f"{row[0]}\t{row[1]}\t{row[2]}\t{row[3]}\t{row[5]}" for row in members)
453 return hashlib.sha256(payload.encode("utf-8")).hexdigest()
454
455
456def _batch_record(spec: BatchSpec) -> str:
457 """Render one batch record in the committed batches-file shape."""
458 sites = ", ".join(row[0][:12] for row in spec.members)
459 return f"""
460 - id: {spec.batch_id}
461 authority: repository-suppression-review
462 date: {spec.date}
463 identity_schema: {spec.schema}
464 assigned_rows: {len(spec.members)}
465 rows_sha256: {spec.digest}
466 partition: "selective {spec.action} of {len(spec.members)} ledger row(s): {sites}"
467 evidence:
468 - "{spec.evidence}"
469"""
470
471
472def _describe(planned: list[PlannedTransition], batch_id: str) -> list[str]:
473 """Render one human-readable line per intended transition."""
474 return [
475 f"{item.note}: {item.row[0]} {item.old_state} -> {item.new_state} "
476 f"rationale={item.rationale} batch={batch_id}"
477 for item in planned
478 ]
479
480
481def run_selective(args: argparse.Namespace) -> int:
482 """Execute one selective retain or retire review action."""
483 root = _repo_root()
484 try:
485 header, rows = _parse_ledger("\n".join(_read_ledger()) + "\n")
486 ctx = ReviewContext(
487 rationale=args.rationale,
488 evidence=args.evidence,
489 live=_live_bindings(root),
490 states=_rationale_states(root),
491 )
492 schema = _identity_schema(root)
493 existing_batches = _existing_batch_ids(root)
494 if args.action == "retain":
495 planned = _plan_retain(_check_selection(args.site, rows), ctx)
496 else:
497 planned = _plan_retire(_check_selection(args.site, rows), ctx, args.replaced_by)
498 now = datetime.datetime.now(tz=datetime.UTC)
499 batch_id = _new_batch_id(existing_batches, now)
500 for item in planned:
501 item.row[2], item.row[3], item.row[4], item.row[5] = (
502 item.new_state,
503 item.rationale,
504 batch_id,
505 item.evidence,
506 )
507 wanted = {item.row[0] for item in planned}
508 members = [row for row in rows if row[0] in wanted]
509 digest = _batch_digest(members)
510 if args.dry_run:
511 print(f"dry-run {args.action}: no files written")
512 for line in _describe(planned, batch_id):
513 print(f" {line}")
514 print(f" batch {batch_id} would cover {len(members)} row(s)")
515 return 0
516 _write_ledger([header] + ["\t".join(row) for row in rows])
517 batches_path = root / _BATCHES_PATH
518 batches_yaml = batches_path.read_text(encoding="utf-8").rstrip()
519 spec = BatchSpec(
520 batch_id=batch_id,
521 date=now.strftime("%Y-%m-%d"),
522 schema=schema,
523 action=args.action,
524 members=members,
525 digest=digest,
526 evidence=args.evidence,
527 )
528 batches_path.write_text(batches_yaml + _batch_record(spec), encoding="utf-8")
529 except ReviewError as exc:
530 print(f"review_suppressions.py: refused: {exc}", file=sys.stderr)
531 return 1
532 for line in _describe(planned, batch_id):
533 print(line)
534 print(f"wrote batch {batch_id} covering {len(members)} row(s)")
535 print("Run `just quality::local::gate suppressions` to verify.")
536 return 0
537
538
539def _fixture_ledger() -> str:
540 """Build a synthetic ledger exercising every transition direction."""
541 rows = [
542 f"{'a' * 64}\t{'1' * 64}\tunreviewed\t\t\t",
543 f"{'b' * 64}\t{'2' * 64}\tunreviewed\t\t\t",
544 f"{'c' * 64}\t{'3' * 64}\tunreviewed\t\t\t",
545 f"{'d' * 64}\t{'4' * 64}\tretain\treviewed-scope-authority\tbatch-old\told evidence",
546 f"{'e' * 64}\t{'5' * 64}\tresolved\tresolved-construct-retired\tbatch-old\told evidence",
547 f"{'f' * 64}\t{'6' * 64}\tunreviewed\t\t\t",
548 ]
549 return _LEDGER_HEADER + "\n" + "\n".join(rows) + "\n"
550
551
552def _fixture_live() -> dict[str, str]:
553 """Live map: A/B/D live with matching bindings, F live with drifted binding."""
554 return {
555 "a" * 64: "1" * 64,
556 "b" * 64: "2" * 64,
557 "d" * 64: "4" * 64,
558 "f" * 64: "f" * 64,
559 }
560
561
562def _fixture_states() -> dict[str, str]:
563 """Rationale vocabulary matching the committed categories under review."""
564 return {
565 "reviewed-scope-authority": "retain",
566 "tool-false-positive": "retain",
567 "resolved-construct-retired": "resolved",
568 "superseded-identity-rebinding": "superseded",
569 }
570
571
572def _ctx(
573 rationale: str, evidence: str, live: dict[str, str], states: dict[str, str]
574) -> ReviewContext:
575 """Build one review context for selftest fixtures."""
576 return ReviewContext(rationale=rationale, evidence=evidence, live=live, states=states)
577
578
579def _refused(failures: list[str], label: str, func: Callable[..., object], *args: object) -> None:
580 """Assert one guard refuses; record a failure when it accepts."""
581 try:
582 func(*args)
583 except ReviewError:
584 return
585 failures.append(f"{label}: expected refusal, action was accepted")
586
587
588def _test_retain(failures: list[str], a: str, b: str) -> None:
589 """Prove selective retain of one and many rows with byte-exact isolation."""
590 live, states = _fixture_live(), _fixture_states()
591 _header, rows = _parse_ledger(_fixture_ledger())
592 planned = _plan_retain(
593 _check_selection([a], rows),
594 _ctx("reviewed-scope-authority", "selftest evidence", live, states),
595 )
596 if not (len(planned) == 1 and planned[0].new_state == "retain"):
597 failures.append("retain-one: expected exactly one retain transition")
598 before = _fixture_ledger().splitlines()
599 for item in planned:
600 item.row[2], item.row[3], item.row[4], item.row[5] = (
601 item.new_state,
602 item.rationale,
603 "batch-t",
604 item.evidence,
605 )
606 after = [_LEDGER_HEADER] + ["\t".join(row) for row in rows]
607 for old, new in zip(before[1:], after[1:], strict=True):
608 if old.split("\t")[0] == a:
609 if new.split("\t")[2] != "retain":
610 failures.append("retain-one: selected row did not transition")
611 elif old != new:
612 failures.append(f"retain-one: unrelated row changed {old[:12]}")
613 _header, rows = _parse_ledger(_fixture_ledger())
614 planned = _plan_retain(
615 _check_selection([a, b], rows),
616 _ctx("tool-false-positive", "selftest evidence", live, states),
617 )
618 if {item.row[0] for item in planned} != {a, b}:
619 failures.append("retain-many: expected transitions for exactly the requested rows")
620
621
622def _retain_refusal(
623 failures: list[str],
624 case: tuple[str, str, ReviewContext],
625) -> None:
626 """Assert one retain selection is rejected for its stated reason."""
627 label, selection, context = case
628 _header, rows = _parse_ledger(_fixture_ledger())
629 _refused(
630 failures,
631 label,
632 _plan_retain,
633 _check_selection([selection], rows),
634 context,
635 )
636
637
638def _test_retain_refusals(failures: list[str], a: str, c: str, d: str, f: str) -> None:
639 """Prove stale, drifted, mis-rationaled, and reviewed retains refuse."""
640 live, states = _fixture_live(), _fixture_states()
641 cases = (
642 ("retain-stale", c, _ctx("reviewed-scope-authority", "selftest evidence", live, states)),
643 (
644 "retain-drifted-binding",
645 f,
646 _ctx("reviewed-scope-authority", "selftest evidence", live, states),
647 ),
648 ("retain-missing-rationale", a, _ctx("", "selftest evidence", live, states)),
649 (
650 "retain-missing-evidence",
651 a,
652 _ctx("reviewed-scope-authority", " ", live, states),
653 ),
654 (
655 "retain-unknown-rationale",
656 a,
657 _ctx("no-such-category", "selftest evidence", live, states),
658 ),
659 (
660 "retain-wrong-state-rationale",
661 a,
662 _ctx("resolved-construct-retired", "selftest evidence", live, states),
663 ),
664 ("retain-reviewed", d, _ctx("reviewed-scope-authority", "selftest evidence", live, states)),
665 ("retain-quoted-evidence", a, _ctx("reviewed-scope-authority", 'say "hi"', live, states)),
666 )
667 for label, selection, context in cases:
668 _retain_refusal(failures, (label, selection, context))
669
670
671def _retire_refusal(
672 failures: list[str],
673 case: tuple[str, str, ReviewContext, list[str]],
674) -> None:
675 """Assert one retire selection is rejected for its stated reason."""
676 label, selection, context, successors = case
677 _header, rows = _parse_ledger(_fixture_ledger())
678 _refused(
679 failures,
680 label,
681 _plan_retire,
682 _check_selection([selection], rows),
683 context,
684 successors,
685 )
686
687
688def _test_retire(failures: list[str], a: str, b: str, c: str, e: str) -> None:
689 """Prove stale retirement, supersede tokens, and live-row refusal."""
690 live, states = _fixture_live(), _fixture_states()
691 _header, rows = _parse_ledger(_fixture_ledger())
692 planned = _plan_retire(
693 _check_selection([c], rows),
694 _ctx("resolved-construct-retired", "construct gone", live, states),
695 [],
696 )
697 if not (len(planned) == 1 and planned[0].new_state == "resolved"):
698 failures.append("retire-stale: expected exactly one resolve transition")
699 _header, rows = _parse_ledger(_fixture_ledger())
700 planned = _plan_retire(
701 _check_selection([c], rows),
702 _ctx("superseded-identity-rebinding", "rebound decision", live, states),
703 [b],
704 )
705 if len(planned) != 1 or planned[0].new_state != "superseded":
706 failures.append("retire-supersede: expected exactly one supersede transition")
707 elif f"replaced-by:{b}" not in planned[0].evidence:
708 failures.append("retire-supersede: replaced-by token missing from evidence")
709 cases = (
710 ("retire-live", a, "resolved-construct-retired", "construct gone", []),
711 ("retire-self-successor", c, "superseded-identity-rebinding", "rebound decision", [c]),
712 (
713 "retire-dead-successor",
714 c,
715 "superseded-identity-rebinding",
716 "rebound decision",
717 ["9" * 64],
718 ),
719 ("retire-resolved", e, "resolved-construct-retired", "construct gone", []),
720 ("retire-retain-rationale", c, "reviewed-scope-authority", "construct gone", []),
721 )
722 for label, selection, rationale, evidence, successors in cases:
723 _retire_refusal(
724 failures,
725 (label, selection, _ctx(rationale, evidence, live, states), successors),
726 )
727
728
729def _test_selection(failures: list[str], a: str) -> None:
730 """Prove unknown, duplicated, malformed, and empty selections refuse."""
731 _header, rows = _parse_ledger(_fixture_ledger())
732 _refused(failures, "unknown-id", _check_selection, ["0" * 64], rows)
733 _header, rows = _parse_ledger(_fixture_ledger())
734 _refused(failures, "duplicate-flags", _check_selection, [a, a], rows)
735 _header, rows = _parse_ledger(_fixture_ledger())
736 _refused(failures, "bad-hex-id", _check_selection, ["zzz"], rows)
737 _header, rows = _parse_ledger(_fixture_ledger())
738 _refused(failures, "empty-selection", _check_selection, [], rows)
739 dup_text = _fixture_ledger() + f"{a}\t{'1' * 64}\tunreviewed\t\t\t\n"
740 _header, dup_rows = _parse_ledger(dup_text)
741 _refused(failures, "duplicate-rows", _check_selection, [a], dup_rows)
742
743
744def _test_malformed(failures: list[str]) -> None:
745 """Prove malformed ledgers fail closed before any selection runs."""
746 bad = [
747 "wrong\n" + "a" * 64 + "\t" + "1" * 64 + "\tunreviewed\t\t\n",
748 _LEDGER_HEADER + "\n" + "a" * 64 + "\t" + "1" * 64 + "\tunreviewed\n",
749 _LEDGER_HEADER + "\nzz\t" + "1" * 64 + "\tunreviewed\t\t\n",
750 _LEDGER_HEADER + "\n" + "a" * 64 + "\t" + "1" * 64 + "\tnope\t\t\n",
751 _LEDGER_HEADER + "\n" + "a" * 64 + "\t" + "1" * 64 + "\tunreviewed\t\tx\n",
752 ]
753 for index, text in enumerate(bad):
754 _refused(failures, f"malformed-{index}", _parse_ledger, text)
755
756
757def _test_batch(failures: list[str], a: str) -> None:
758 """Prove batch-id collision refusal and the gate digest formula."""
759 stamp = datetime.datetime(2024, 1, 2, 3, 4, 5, tzinfo=datetime.UTC)
760 try:
761 _new_batch_id({"selective-review-20240102-030405"}, stamp)
762 failures.append("batch-collision: expected refusal")
763 except ReviewError:
764 pass
765 batch_id = _new_batch_id(set(), stamp)
766 if batch_id != "selective-review-20240102-030405":
767 failures.append("batch-id-shape: unexpected minted id")
768 member = [a, "1" * 64, "retain", "reviewed-scope-authority", batch_id, "selftest evidence"]
769 payload = f"{a}\t{'1' * 64}\tretain\treviewed-scope-authority\tselftest evidence"
770 if _batch_digest([member]) != hashlib.sha256(payload.encode("utf-8")).hexdigest():
771 failures.append("batch-digest: formula drifted from the gate payload shape")
772
773
774def _test_reconcile_preconditions(failures: list[str], a: str, b: str, c: str) -> None:
775 """Prove planned rows satisfy the gate reconciler's preconditions.
776
777 The gate approves a retain row only for a live site with an exact binding
778 match, and closes a resolved row only for a vanished site; a superseded
779 row additionally needs every replaced-by successor live. These checks
780 mirror scripts/checks/suppression_ledger.py::_reconcile_row without
781 importing gate code into the review tool.
782 """
783
784 def live_ok(site: str, binding: str, live: dict[str, str]) -> bool:
785 return live.get(site) == binding
786
787 live, states = _fixture_live(), _fixture_states()
788 _header, rows = _parse_ledger(_fixture_ledger())
789 planned = _plan_retain(
790 _check_selection([a], rows),
791 _ctx("reviewed-scope-authority", "selftest evidence", live, states),
792 )
793 item = planned[0]
794 if item.new_state != "retain" or states[item.rationale] != "retain":
795 failures.append("precondition-retain: state/rationale mismatch")
796 if not live_ok(item.row[0], item.row[1], live):
797 failures.append("precondition-retain: site not live with exact binding")
798 _header, rows = _parse_ledger(_fixture_ledger())
799 planned = _plan_retire(
800 _check_selection([c], rows),
801 _ctx("resolved-construct-retired", "construct gone", live, states),
802 [],
803 )
804 item = planned[0]
805 if item.new_state != "resolved" or item.row[0] in live:
806 failures.append("precondition-resolve: site must be vanished")
807 _header, rows = _parse_ledger(_fixture_ledger())
808 planned = _plan_retire(
809 _check_selection([c], rows),
810 _ctx("superseded-identity-rebinding", "rebound decision", live, states),
811 [b],
812 )
813 item = planned[0]
814 if item.new_state != "superseded" or item.row[0] in live:
815 failures.append("precondition-supersede: old site must be vanished")
816 if f"replaced-by:{b}" not in item.evidence or b not in live:
817 failures.append("precondition-supersede: successor must be live and named")
818
819
820def _test_tsv_cells(failures: list[str], a: str, c: str) -> None:
821 """Prove tab/newline/quote review fields refuse on both actions."""
822 live, states = _fixture_live(), _fixture_states()
823 for label, rationale, evidence in [
824 ("tab-evidence", "reviewed-scope-authority", "audit\tprobe"),
825 ("newline-evidence", "reviewed-scope-authority", "audit\nprobe"),
826 ("quote-evidence", "reviewed-scope-authority", 'say "hi"'),
827 ("tab-rationale", "reviewed\tscope", "selftest evidence"),
828 ("newline-rationale", "reviewed\nscope", "selftest evidence"),
829 ]:
830 _header, rows = _parse_ledger(_fixture_ledger())
831 _refused(
832 failures,
833 f"retain-{label}",
834 _plan_retain,
835 _check_selection([a], rows),
836 _ctx(rationale, evidence, live, states),
837 )
838 _header, rows = _parse_ledger(_fixture_ledger())
839 _refused(
840 failures,
841 f"retire-{label}",
842 _plan_retire,
843 _check_selection([c], rows),
844 _ctx(rationale, evidence, live, states),
845 [],
846 )
847 before = _fixture_ledger()
848 _header, rows = _parse_ledger(before)
849 with suppress(ReviewError):
850 _plan_retain(
851 _check_selection([a], rows),
852 _ctx("reviewed-scope-authority", "audit\tprobe", live, states),
853 )
854 if [_LEDGER_HEADER] + ["\t".join(row) for row in rows] != before.splitlines():
855 failures.append("refused-plan-untouched: failed validation mutated rows")
856
857
858def _apply_like_run(planned: list[PlannedTransition], batch: str) -> None:
859 """Mutate rows exactly as run_selective does after successful planning."""
860 for item in planned:
861 item.row[2], item.row[3], item.row[4], item.row[5] = (
862 item.new_state,
863 item.rationale,
864 batch,
865 item.evidence,
866 )
867
868
869def _test_write_reporting(failures: list[str], a: str, b: str, c: str) -> None:
870 """Prove real writes report exact old states without changing outcomes."""
871 live, states = _fixture_live(), _fixture_states()
872 specs = [
873 ("retain", "reviewed-scope-authority", "selftest evidence", [], "unreviewed -> retain"),
874 ("resolved", "resolved-construct-retired", "construct gone", [], "unreviewed -> resolved"),
875 ]
876 for want_new, rationale, evidence, replaced, want_line in specs:
877 _header, rows = _parse_ledger(_fixture_ledger())
878 before_ids = [row[0] for row in rows]
879 target = a if want_new == "retain" else c
880 ctx = _ctx(rationale, evidence, live, states)
881 if want_new == "retain":
882 planned = _plan_retain(_check_selection([target], rows), ctx)
883 else:
884 planned = _plan_retire(_check_selection([target], rows), ctx, replaced)
885 _apply_like_run(planned, "batch-t")
886 lines = _describe(planned, "batch-t")
887 if len(lines) != 1 or want_line not in lines[0]:
888 failures.append(f"write-report-{want_new}: expected exact {want_line!r}")
889 if [row[0] for row in rows] != before_ids:
890 failures.append(f"write-report-{want_new}: row ordering changed")
891 members = [row for row in rows if row[0] == target]
892 payload = "\n".join(f"{row[0]}\t{row[1]}\t{row[2]}\t{row[3]}\t{row[5]}" for row in members)
893 if _batch_digest(members) != hashlib.sha256(payload.encode("utf-8")).hexdigest():
894 failures.append(f"write-report-{want_new}: batch digest changed")
895 fixture = _fixture_ledger()
896 failures.extend(
897 f"write-report-{want_new}: unrelated row changed"
898 for row in rows
899 if row[0] != target and "\t".join(row) not in fixture
900 )
901 _header, rows = _parse_ledger(_fixture_ledger())
902 planned = _plan_retire(
903 _check_selection([c], rows),
904 _ctx("superseded-identity-rebinding", "rebound decision", live, states),
905 [b],
906 )
907 _apply_like_run(planned, "batch-t")
908 lines = _describe(planned, "batch-t")
909 if len(lines) != 1 or "unreviewed -> superseded" not in lines[0]:
910 failures.append("write-report-superseded: expected exact old state")
911 if f"replaced-by:{b}" not in planned[0].evidence:
912 failures.append("write-report-superseded: successor token missing")
913
914
915def _test_dry_run(failures: list[str], a: str, b: str) -> None:
916 live, states = _fixture_live(), _fixture_states()
917 before_text = _fixture_ledger()
918 _header, rows = _parse_ledger(before_text)
919 planned = _plan_retain(
920 _check_selection([a, b], rows),
921 _ctx("reviewed-scope-authority", "selftest evidence", live, states),
922 )
923 out = "\n".join(f" {line}" for line in _describe(planned, "batch-dry"))
924 if a not in out or b not in out or "unreviewed -> retain" not in out:
925 failures.append("dry-run-names-rows: output does not identify transitions")
926 current = [_LEDGER_HEADER] + ["\t".join(row) for row in rows]
927 if current != before_text.splitlines():
928 failures.append("dry-run-ledger-untouched: planning mutated rows")
929
930
931def run_selftest() -> int:
932 """Prove selective retain/retire guards both ways on synthetic fixtures."""
933 failures: list[str] = []
934 a, b, c, d, e, f = ("a" * 64, "b" * 64, "c" * 64, "d" * 64, "e" * 64, "f" * 64)
935 _test_retain(failures, a, b)
936 _test_retain_refusals(failures, a, c, d, f)
937 _test_retire(failures, a, b, c, e)
938 _test_selection(failures, a)
939 _test_malformed(failures)
940 _test_batch(failures, a)
941 _test_reconcile_preconditions(failures, a, b, c)
942 _test_tsv_cells(failures, a, c)
943 _test_write_reporting(failures, a, b, c)
944 _test_dry_run(failures, a, b)
945 if failures:
946 print("review_suppressions.py --selftest: FAIL")
947 for failure in failures:
948 print(f" - {failure}")
949 return 1
950 print(
951 "review_suppressions.py --selftest: PASS "
952 "(selective retain/retire, fail-closed guards, reconciler preconditions, dry-run)"
953 )
954 return 0
955
956
957if __name__ == "__main__":
958 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298