ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_ledger_selftest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Both-direction fixtures for the fail-closed review ledger."""
4
5from __future__ import annotations
6
7import hashlib
8from pathlib import Path
9
10from selftest_assert import expect
11from suppression_identity import IDENTITY_SCHEMA_VERSION
12from suppression_ledger import (
13 BATCHES_PATH,
14 LEDGER_PATH,
15 RATIONALES_PATH,
16 apply_ledger,
17 candidate_rows,
18 load_batches,
19 load_rationales,
20)
21from suppression_model import Inventory, Suppression
22from suppression_scan import scan_paths
23
24_RATIONALES = """schema: suppression-review-rationales-1
25categories:
26 bounded-cleanup-status-mask:
27 state: retain
28 applicability: enumerated benign failure with an immediate consumer
29 evidence: [consumer-or-assert]
30 revalidate: when the consumer changes
31 portable-test-prerequisite-boundary:
32 state: retain
33 applicability: direct portable skip, registered gate failure
34 evidence: [test-name, passing-counterpart, registered-gate]
35 revalidate: when the test or gate changes
36 vendor-upstream-preserved:
37 state: retain
38 applicability: vendored SOUP bound to the exact blob
39 evidence: [upstream-pin]
40 revalidate: on blob change
41 fix-remove:
42 state: fix-required
43 applicability: delete the marker
44 evidence: [review-decision]
45 revalidate: never
46 superseded-identity-rebinding:
47 state: superseded
48 applicability: the decision moves to the live replacement site named here
49 evidence: [replacement-site]
50 revalidate: never
51 resolved-construct-retired:
52 state: resolved
53 applicability: the construct is gone and nothing live replaces it
54 evidence: [removal-evidence]
55 revalidate: never
56"""
57_FILES = {
58 "tool.sh": "#!/bin/sh\nprobe || true # enumerated benign probe\n",
59 "libs/third_party/vendor/legacy.txt": "",
60}
61
62
63def _batch_digest(rows: list[str]) -> str:
64 """Recompute the recorded digest for one batch's ordered rows."""
65 payload = "\n".join(
66 "\t".join(
67 (
68 line.split("\t")[0],
69 line.split("\t")[1],
70 line.split("\t")[2],
71 line.split("\t")[3],
72 line.split("\t")[5],
73 )
74 )
75 for line in rows
76 ).encode("utf-8")
77 return hashlib.sha256(payload).hexdigest()
78
79
80def _write_ledger(root: Path, rows: list[str], *, schema: str = IDENTITY_SCHEMA_VERSION) -> None:
81 """Write the three committed ledger authorities for one fixture run."""
82 (root / ".github").mkdir(exist_ok=True)
83 (root / RATIONALES_PATH).write_text(_RATIONALES, encoding="ascii")
84 header = "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref"
85 (root / LEDGER_PATH).write_text("\n".join([header, *rows]) + "\n", encoding="ascii")
86 reviewed = [row for row in rows if row.split("\t")[2] != "unreviewed"]
87 (root / BATCHES_PATH).write_text(
88 "schema: suppression-review-batches-1\n"
89 "batches:\n"
90 " - id: batch-fixture\n"
91 " authority: repository-suppression-review\n"
92 " date: 2026-08-24\n"
93 f" identity_schema: {schema}\n"
94 f" assigned_rows: {len(reviewed)}\n"
95 f" rows_sha256: {_batch_digest(reviewed)}\n",
96 encoding="ascii",
97 )
98
99
100def _scan(root: Path, files: dict[str, str]) -> Inventory:
101 """Materialize fixture files and scan them without repository floors."""
102 for rel, text in files.items():
103 target = root / rel
104 target.parent.mkdir(parents=True, exist_ok=True)
105 if rel.endswith("legacy.txt"):
106 target.write_bytes(b"vendored-\xff-text\n")
107 else:
108 target.write_text(text, encoding="ascii")
109 return scan_paths(root, sorted(files))
110
111
112def _codes(inventory: Inventory) -> set[str]:
113 """Return the ledger-relevant finding codes of one reconciliation."""
114 return {item.code for item in inventory.findings}
115
116
117def _shell_row(inventory: Inventory) -> Suppression:
118 """Return the shell-status fixture row."""
119 return next(item for item in inventory.suppressions if item.family == "shell-status")
120
121
122def _vendor_row(inventory: Inventory) -> Suppression:
123 """Return the vendored encoding-exemption fixture row."""
124 return next(item for item in inventory.suppressions if item.family == "encoding-exemption")
125
126
127def _retain(row: Suppression, rationale: str = "bounded-cleanup-status-mask") -> str:
128 """Render one retained ledger row for a live inventory row."""
129 return (
130 f"{row.site_id}\t{row.binding_sha256}\tretain\t{rationale}\tbatch-fixture\tevidence:fixture"
131 )
132
133
134def _retain_rows(inventory: Inventory) -> list[str]:
135 """Render a retain row for every live suppression the fixture scanned."""
136 return [
137 _retain(item, "vendor-upstream-preserved")
138 if item.family == "encoding-exemption"
139 else _retain(item)
140 for item in inventory.suppressions
141 ]
142
143
144def _closed(site: str, state: str, rationale: str, evidence: str) -> str:
145 """Render one closed ledger row for a site the inventory no longer carries."""
146 return f"{site}\t{'1' * 64}\t{state}\t{rationale}\tbatch-fixture\t{evidence}"
147
148
149_PORTABLE_TEST = "scripts/dev/work/tests/test_portable.py" # PATHREF-OK: synthetic fixture path
150
151
152def _write_portable_contract(root: Path, *, fail_closed: bool) -> None:
153 """Write the exact test and gate evidence used by portable-skip fixtures."""
154 test = root / _PORTABLE_TEST
155 test.parent.mkdir(parents=True, exist_ok=True)
156 test.write_text(
157 "def test_direct_skip():\n pass\n\ndef test_registered_failure():\n pass\n",
158 encoding="ascii",
159 )
160 helper = root / "scripts/dev/work/tests/fixtures/work_testlib.py"
161 helper.parent.mkdir(parents=True, exist_ok=True)
162 helper.write_text(
163 'import os\nREGISTERED_GATE_ENV = "RA8_WORK_HARNESS_REGISTERED_GATE"\n'
164 'if os.environ.get(REGISTERED_GATE_ENV) == "1":\n'
165 ' message = "missing"\n raise RuntimeError(message)\n',
166 encoding="ascii",
167 )
168 gate = root / "scripts/ci/gates/tests.sh"
169 gate.parent.mkdir(parents=True, exist_ok=True)
170 registration = (
171 " RA8_WORK_HARNESS_REGISTERED_GATE=1 python3 -I scripts/dev/work/src/work.py --selftest\n"
172 if fail_closed
173 else " python3 -I scripts/dev/work/src/work.py --selftest\n"
174 )
175 gate.write_text(
176 "gate_work_harness() (\n require_cmd bash\n require_cmd sh\n" + registration + ")\n",
177 encoding="ascii",
178 )
179
180
181def _portable_evidence(gate: str = "work-harness") -> str:
182 """Return a complete portable-prerequisite evidence reference."""
183 return (
184 f"test-name:{_PORTABLE_TEST}::test_direct_skip "
185 f"passing-counterpart:{_PORTABLE_TEST}::test_registered_failure "
186 f"registered-gate:{gate}"
187 )
188
189
190def _closing_codes() -> set[str]:
191 """Return every reconciliation code a well-formed closed row must avoid."""
192 return {
193 "ledger-unknown-reference",
194 "ledger-missing-site",
195 "ledger-stale-site",
196 "ledger-state-conflict",
197 "ledger-resolved-still-present",
198 }
199
200
201def assert_ledger_gate(base: Path, failures: list[str]) -> None:
202 """Assert every ledger rejection fires and a complete ledger passes."""
203 root = base / "ledger-repo"
204 root.mkdir()
205 inventory = _scan(root, _FILES)
206 shell = _shell_row(inventory)
207 vendor = _vendor_row(inventory)
208 others = [
209 item
210 for item in inventory.suppressions
211 if item.site_id not in {shell.site_id, vendor.site_id}
212 ]
213 complete = sorted(
214 [_retain(shell), _retain(vendor, "vendor-upstream-preserved")]
215 + [_retain(item) for item in others]
216 )
217 _write_ledger(root, complete)
218 apply_ledger(inventory, root)
219 approved = sum(item.disposition == "approved" for item in inventory.suppressions)
220 expect(
221 approved == len(inventory.suppressions)
222 and not (_codes(inventory) & {"ledger-missing-site", "ledger-binding-mismatch"}),
223 "quiet: a complete matching ledger approves every retained site",
224 failures,
225 )
226
227 fresh = _scan(base / "ledger-missing", _FILES)
228 ledger_without_shell = sorted(
229 [_retain(vendor, "vendor-upstream-preserved")] + [_retain(item) for item in others]
230 )
231 _write_ledger(base / "ledger-missing", ledger_without_shell)
232 apply_ledger(fresh, base / "ledger-missing")
233 expect(
234 "ledger-missing-site" in _codes(fresh),
235 "must fire: a live suppression absent from the ledger fails",
236 failures,
237 )
238 _assert_ledger_binding_drift(base, complete, failures)
239 stale_root = base / "ledger-stale"
240 stale = _scan(stale_root, {"tool.sh": _FILES["tool.sh"]})
241 stale_rows = sorted(
242 [_retain(item) for item in stale.suppressions]
243 + [_retain(vendor, "vendor-upstream-preserved")]
244 )
245 _write_ledger(stale_root, stale_rows)
246 apply_ledger(stale, stale_root)
247 expect(
248 "ledger-stale-site" in _codes(stale),
249 "must fire: a retained ledger row without a live site fails",
250 failures,
251 )
252
253 _assert_ledger_resolution(base, failures)
254 _assert_ledger_supersession(base, failures)
255 _assert_portable_prerequisite_evidence(base, failures)
256 _assert_committed_vocabulary(base, failures)
257 _assert_ledger_shape_rules(base, failures)
258
259
260def _portable_case(base: Path, name: str, evidence: str, *, fail_closed: bool = True) -> set[str]:
261 """Apply one portable-prerequisite ledger fixture and return finding codes."""
262 root = base / f"ledger-portable-{name}"
263 inventory = _scan(root, _FILES)
264 _write_portable_contract(root, fail_closed=fail_closed)
265 shell = _shell_row(inventory)
266 rows = [
267 _retain(item, "vendor-upstream-preserved")
268 if item.family == "encoding-exemption"
269 else (
270 f"{item.site_id}\t{item.binding_sha256}\tretain\t"
271 "portable-test-prerequisite-boundary\tbatch-fixture\t"
272 f"{evidence}"
273 if item.site_id == shell.site_id
274 else _retain(item)
275 )
276 for item in inventory.suppressions
277 ]
278 _write_ledger(root, sorted(rows))
279 apply_ledger(inventory, root)
280 return _codes(inventory)
281
282
283def _assert_portable_prerequisite_evidence(base: Path, failures: list[str]) -> None:
284 """Assert portable skips require exact live tests and a fail-closed gate."""
285 accepted = _portable_case(base, "accepted", _portable_evidence())
286 expect(
287 not (
288 accepted
289 & {
290 "malformed-review-ledger",
291 "ledger-unknown-reference",
292 "ledger-invalid-gate-contract",
293 }
294 ),
295 "quiet: exact tests plus a fail-closed registered gate justify a portable skip",
296 failures,
297 )
298 missing = _portable_case(
299 base,
300 "missing",
301 f"test-name:{_PORTABLE_TEST}::test_direct_skip registered-gate:work-harness",
302 )
303 expect(
304 "malformed-review-ledger" in missing,
305 "must fire: portable evidence missing a required field is rejected",
306 failures,
307 )
308 unknown_test = _portable_case(
309 base,
310 "unknown-test",
311 f"test-name:{_PORTABLE_TEST}::test_absent "
312 f"passing-counterpart:{_PORTABLE_TEST}::test_registered_failure "
313 "registered-gate:work-harness",
314 )
315 expect(
316 "ledger-unknown-reference" in unknown_test,
317 "must fire: portable evidence naming an unknown test is rejected",
318 failures,
319 )
320 unknown_gate = _portable_case(base, "unknown-gate", _portable_evidence("ghost-gate"))
321 expect(
322 "ledger-unknown-reference" in unknown_gate,
323 "must fire: portable evidence naming an unknown gate is rejected",
324 failures,
325 )
326 skippable = _portable_case(base, "skippable-gate", _portable_evidence(), fail_closed=False)
327 expect(
328 "ledger-invalid-gate-contract" in skippable,
329 "must fire: a registered gate that can skip its prerequisite is rejected",
330 failures,
331 )
332
333
334def _assert_ledger_supersession(base: Path, failures: list[str]) -> None:
335 """Assert supersession closes a row only via a live, ledgered successor."""
336 probe = _scan(base / "ledger-superseded", _FILES)
337 expect(
338 {"shell-status", "encoding-exemption"} <= {item.family for item in probe.suppressions},
339 "quiet: the supersession fixture scans a non-empty inventory",
340 failures,
341 )
342 gone = "0" * 64
343 for rationale in ("fix-remove", "superseded-identity-rebinding"):
344 root = base / f"ledger-sup-{rationale}"
345 inventory = _scan(root, _FILES)
346 successor = _shell_row(inventory)
347 rows = sorted(
348 [
349 *_retain_rows(inventory),
350 _closed(gone, "superseded", rationale, f"replaced-by:{successor.site_id}"),
351 ]
352 )
353 _write_ledger(root, rows)
354 apply_ledger(inventory, root)
355 expect(
356 not (_codes(inventory) & _closing_codes()),
357 f"quiet: a superseded row under {rationale} naming a live successor passes",
358 failures,
359 )
360 _assert_supersession_references(base, failures)
361
362
363def _assert_supersession_references(base: Path, failures: list[str]) -> None:
364 """Assert every unusable ``replaced-by:`` reference fails closed."""
365 gone = "0" * 64
366 cases = {
367 "no successor named": "evidence:removed",
368 "malformed successor token": "replaced-by:not-a-site",
369 "successor that is not live": f"replaced-by:{'2' * 64}",
370 }
371 for label, evidence in cases.items():
372 root = base / f"ledger-sup-{label.split()[0]}"
373 inventory = _scan(root, _FILES)
374 rows = sorted(
375 [*_retain_rows(inventory), _closed(gone, "superseded", "fix-remove", evidence)]
376 )
377 _write_ledger(root, rows)
378 apply_ledger(inventory, root)
379 expect(
380 "ledger-unknown-reference" in _codes(inventory),
381 f"must fire: a superseded row with {label} is rejected",
382 failures,
383 )
384 _assert_supersession_boundaries(base, failures)
385
386
387def _assert_supersession_boundaries(base: Path, failures: list[str]) -> None:
388 """Assert an unledgered successor and a still-live site both fail closed."""
389 gone = "0" * 64
390 missing_root = base / "ledger-sup-unledgered"
391 missing = _scan(missing_root, _FILES)
392 successor = _shell_row(missing)
393 _write_ledger(
394 missing_root,
395 [_closed(gone, "superseded", "fix-remove", f"replaced-by:{successor.site_id}")],
396 )
397 apply_ledger(missing, missing_root)
398 expect(
399 "ledger-missing-site" in _codes(missing),
400 "must fire: a successor site carrying no ledger row of its own is rejected",
401 failures,
402 )
403
404 present_root = base / "ledger-sup-present"
405 present = _scan(present_root, _FILES)
406 shell = _shell_row(present)
407 vendor = _vendor_row(present)
408 still_live = (
409 f"{shell.site_id}\t{shell.binding_sha256}\tsuperseded\tfix-remove"
410 f"\tbatch-fixture\treplaced-by:{vendor.site_id}"
411 )
412 _write_ledger(present_root, sorted([still_live, _retain(vendor, "vendor-upstream-preserved")]))
413 apply_ledger(present, present_root)
414 expect(
415 "ledger-resolved-still-present" in _codes(present),
416 "must fire: a superseded row whose own site is still live is rejected",
417 failures,
418 )
419 _assert_supersession_vocabulary(base, failures)
420
421
422def _assert_supersession_vocabulary(base: Path, failures: list[str]) -> None:
423 """Assert the retain / closing vocabulary boundary holds in both directions."""
424 root = base / "ledger-sup-conflict"
425 inventory = _scan(root, _FILES)
426 successor = _shell_row(inventory)
427 named = f"replaced-by:{successor.site_id}"
428 rows = sorted(
429 [
430 *_retain_rows(inventory),
431 _closed("0" * 64, "superseded", "bounded-cleanup-status-mask", named),
432 ]
433 )
434 _write_ledger(root, rows)
435 apply_ledger(inventory, root)
436 expect(
437 "ledger-state-conflict" in _codes(inventory),
438 "must fire: a retain-vocabulary rationale in state superseded is rejected",
439 failures,
440 )
441
442 retired_root = base / "ledger-sup-retired"
443 retired = _scan(retired_root, _FILES)
444 retired_rows = sorted(
445 [
446 *_retain_rows(retired),
447 _closed("0" * 64, "resolved", "resolved-construct-retired", "evidence:removed"),
448 ]
449 )
450 _write_ledger(retired_root, retired_rows)
451 apply_ledger(retired, retired_root)
452 expect(
453 not (_codes(retired) & _closing_codes()),
454 "quiet: a resolved row under resolved-construct-retired needs no successor",
455 failures,
456 )
457
458
459def _assert_committed_vocabulary(base: Path, failures: list[str]) -> None:
460 """Assert the committed vocabulary parses and offers both closing states."""
461 categories, findings = load_rationales(Path(__file__).resolve().parents[2])
462 states = {spec["state"] for spec in categories.values()}
463 expect(
464 not findings and {"superseded", "resolved"} <= states,
465 "quiet: the committed vocabulary parses and offers both closing states",
466 failures,
467 )
468 root = base / "ledger-vocabulary"
469 (root / ".github").mkdir(parents=True)
470 (root / RATIONALES_PATH).write_text(
471 "schema: suppression-review-rationales-1\n"
472 "categories:\n"
473 " outside-the-vocabulary:\n"
474 " state: approved\n"
475 " applicability: a state no ledger row may carry\n"
476 " evidence: [review-decision]\n",
477 encoding="ascii",
478 )
479 _, rejected = load_rationales(root)
480 expect(
481 any(item.code == "malformed-review-ledger" for item in rejected),
482 "must fire: a category naming a state outside the ledger vocabulary is rejected",
483 failures,
484 )
485
486
487def _assert_ledger_binding_drift(base: Path, complete: list[str], failures: list[str]) -> None:
488 """Assert reason and vendored-blob edits reopen review."""
489 drift_root = base / "ledger-drift"
490 drift = _scan(
491 drift_root,
492 {**_FILES, "tool.sh": "#!/bin/sh\nprobe || true # a different rationale\n"},
493 )
494 _write_ledger(drift_root, complete)
495 apply_ledger(drift, drift_root)
496 drift_codes = _codes(drift)
497 expect(
498 "ledger-binding-mismatch" in drift_codes
499 and all(
500 item.disposition != "approved"
501 for item in drift.suppressions
502 if item.family == "shell-status"
503 ),
504 "must fire: a reason edit preserves the site but invalidates its binding",
505 failures,
506 )
507
508 vendor_root = base / "ledger-vendor"
509 vendor_changed = _scan(vendor_root, _FILES)
510 (vendor_root / "libs/third_party/vendor/legacy.txt").write_bytes(b"vendored-\xfe-changed\n")
511 vendor_changed = scan_paths(vendor_root, sorted(_FILES))
512 _write_ledger(vendor_root, complete)
513 apply_ledger(vendor_changed, vendor_root)
514 expect(
515 "ledger-binding-mismatch" in _codes(vendor_changed),
516 "must fire: a vendored blob change invalidates its encoding review",
517 failures,
518 )
519
520
521def _assert_ledger_resolution(base: Path, failures: list[str]) -> None:
522 """Assert resolved rows demand absence and re-appearance fails."""
523 resolved_root = base / "ledger-resolved"
524 resolved = _scan(resolved_root, {"tool.sh": _FILES["tool.sh"]})
525 gone = f"{'0' * 64}\t{'1' * 64}\tresolved\tfix-remove\tbatch-fixture\tevidence:removed"
526 resolved_rows = sorted([_retain(item) for item in resolved.suppressions] + [gone])
527 _write_ledger(resolved_root, resolved_rows)
528 apply_ledger(resolved, resolved_root)
529 expect(
530 "ledger-resolved-still-present" not in _codes(resolved)
531 and "ledger-stale-site" not in _codes(resolved),
532 "quiet: a resolved row whose site is gone is the completed shape",
533 failures,
534 )
535 still_root = base / "ledger-still-present"
536 still = _scan(still_root, {"tool.sh": _FILES["tool.sh"]})
537 live = next(iter(still.suppressions))
538 present = (
539 f"{live.site_id}\t{live.binding_sha256}\tresolved"
540 "\tfix-remove\tbatch-fixture\tevidence:removed"
541 )
542 still_rows = sorted(
543 [present] + [_retain(item) for item in still.suppressions if item.site_id != live.site_id]
544 )
545 _write_ledger(still_root, still_rows)
546 apply_ledger(still, still_root)
547 expect(
548 "ledger-resolved-still-present" in _codes(still),
549 "must fire: a resolved site that still exists fails",
550 failures,
551 )
552 conflict_root = base / "ledger-resolved-conflict"
553 conflict = _scan(conflict_root, {"tool.sh": _FILES["tool.sh"]})
554 conflict_rows = sorted(
555 [
556 *(_retain(item) for item in conflict.suppressions),
557 _closed("0" * 64, "resolved", "bounded-cleanup-status-mask", "evidence:removed"),
558 ]
559 )
560 _write_ledger(conflict_root, conflict_rows)
561 apply_ledger(conflict, conflict_root)
562 expect(
563 "ledger-state-conflict" in _codes(conflict),
564 "must fire: a retain-vocabulary rationale in state resolved is rejected",
565 failures,
566 )
567
568
569def _assert_ledger_shape_rules(base: Path, failures: list[str]) -> None:
570 """Assert structural, batch, and state rules fail closed."""
571 root = base / "ledger-shape"
572 inventory = _scan(root, {"tool.sh": _FILES["tool.sh"]})
573 row = next(iter(inventory.suppressions))
574
575 fix_rows = [
576 f"{row.site_id}\t{row.binding_sha256}\tfix-required\tfix-remove\tbatch-fixture\tevidence:fixture"
577 ]
578 _write_ledger(root, fix_rows)
579 apply_ledger(inventory, root)
580 expect(
581 "ledger-fix-required" in _codes(inventory)
582 and all(item.disposition != "approved" for item in inventory.suppressions),
583 "must fire: an active fix-required row keeps the gate red",
584 failures,
585 )
586
587 cases = {
588 "unknown rationale": (
589 f"{row.site_id}\t{row.binding_sha256}\tretain\tno-such-category\tbatch-fixture\tevidence:x",
590 "ledger-unknown-reference",
591 ),
592 "state conflict": (
593 f"{row.site_id}\t{row.binding_sha256}\tretain\tfix-remove\tbatch-fixture\tevidence:x",
594 "ledger-state-conflict",
595 ),
596 "blank evidence": (
597 f"{row.site_id}\t{row.binding_sha256}\tretain\tbounded-cleanup-status-mask\tbatch-fixture\t",
598 "malformed-review-ledger",
599 ),
600 "unknown state": (
601 f"{row.site_id}\t{row.binding_sha256}\tapproved\tbounded-cleanup-status-mask\tbatch-fixture\tevidence:x",
602 "malformed-review-ledger",
603 ),
604 "unknown batch": (
605 f"{row.site_id}\t{row.binding_sha256}\tretain\tbounded-cleanup-status-mask\tbatch-ghost\tevidence:x",
606 "ledger-unknown-reference",
607 ),
608 }
609 for label, (ledger_row, code) in cases.items():
610 fresh = _scan(
611 base / f"ledger-{code}-{label[:7].replace(' ', '')}", {"tool.sh": _FILES["tool.sh"]}
612 )
613 fixture_root = base / f"ledger-{code}-{label[:7].replace(' ', '')}"
614 _write_ledger(fixture_root, [ledger_row])
615 apply_ledger(fresh, fixture_root)
616 expect(code in _codes(fresh), f"must fire: {label} is rejected", failures)
617 _assert_ledger_duplicate_rules(base, failures)
618
619
620def _assert_ledger_duplicate_rules(base: Path, failures: list[str]) -> None:
621 """Assert duplicate and unsorted ledger rows fail closed."""
622 dup_root = base / "ledger-dup"
623 dup = _scan(dup_root, {"tool.sh": _FILES["tool.sh"]})
624 dup_row = _retain(next(iter(dup.suppressions)))
625 _write_ledger(dup_root, [dup_row, dup_row])
626 apply_ledger(dup, dup_root)
627 expect("ledger-duplicate-site" in _codes(dup), "must fire: duplicate site ids fail", failures)
628
629 unsorted_root = base / "ledger-unsorted"
630 unsorted = _scan(unsorted_root, _FILES)
631 shell = _shell_row(unsorted)
632 vendor = _vendor_row(unsorted)
633 pair = sorted([_retain(shell), _retain(vendor, "vendor-upstream-preserved")], reverse=True)
634 if pair[0].split("\t")[0] > pair[1].split("\t")[0]:
635 _write_ledger(unsorted_root, pair)
636 apply_ledger(unsorted, unsorted_root)
637 expect(
638 "malformed-review-ledger" in _codes(unsorted),
639 "must fire: unsorted ledger rows fail",
640 failures,
641 )
642
643 _assert_ledger_batch_rules(base, failures)
644
645
646def _assert_ledger_batch_rules(base: Path, failures: list[str]) -> None:
647 """Assert schema, batch, and bootstrap-candidate rules fail closed."""
648 schema_root = base / "ledger-schema"
649 schema = _scan(schema_root, {"tool.sh": _FILES["tool.sh"]})
650 schema_rows = [_retain(item) for item in schema.suppressions]
651 _write_ledger(schema_root, sorted(schema_rows), schema="1-old-schema")
652 apply_ledger(schema, schema_root)
653 expect(
654 "ledger-schema-mismatch" in _codes(schema),
655 "must fire: an identity-schema change fails closed",
656 failures,
657 )
658
659 batch_root = base / "ledger-batchcount"
660 batch = _scan(batch_root, {"tool.sh": _FILES["tool.sh"]})
661 batch_rows = sorted(_retain(item) for item in batch.suppressions)
662 _write_ledger(batch_root, batch_rows)
663 text = (batch_root / BATCHES_PATH).read_text(encoding="ascii")
664 (batch_root / BATCHES_PATH).write_text(
665 text.replace(f"assigned_rows: {len(batch_rows)}", f"assigned_rows: {len(batch_rows) + 1}"),
666 encoding="ascii",
667 )
668 apply_ledger(batch, batch_root)
669 expect(
670 "ledger-batch-mismatch" in _codes(batch),
671 "must fire: a batch row-count mismatch fails",
672 failures,
673 )
674
675 candidates = candidate_rows(batch)
676 expect(
677 bool(candidates) and all("\tunreviewed\t" in line for line in candidates),
678 "quiet: generated candidates are always unreviewed, never approvals",
679 failures,
680 )
681 unrev_root = base / "ledger-unrev"
682 unrev = _scan(unrev_root, {"tool.sh": _FILES["tool.sh"]})
683 unrev_rows = sorted(candidate_rows(unrev))
684 _write_ledger(unrev_root, unrev_rows)
685 apply_ledger(unrev, unrev_root)
686 expect(
687 "ledger-unreviewed" in _codes(unrev)
688 and all(item.disposition != "approved" for item in unrev.suppressions),
689 "must fire: bootstrap candidates keep the gate red until reviewed",
690 failures,
691 )
692 _assert_batch_identity_rules(base, failures)
693
694
695def _batch_documents() -> tuple[dict[str, tuple[str, str]], str, str]:
696 """Build the rejected, valid, and merge-key batch authorities as YAML text."""
697 header = "schema: suppression-review-batches-1\nbatches:\n"
698 tail = (
699 " authority: repository-suppression-review\n"
700 " date: 2026-08-24\n"
701 f" identity_schema: {IDENTITY_SCHEMA_VERSION}\n"
702 " assigned_rows: 0\n"
703 f" rows_sha256: {'0' * 64}\n"
704 )
705 rejected = {
706 "duplicate batch identity": (
707 header + " - id: batch-one\n" + tail + " - id: batch-one\n" + tail,
708 "ledger-duplicate-batch",
709 ),
710 "duplicate key inside a record": (
711 header + " - id: batch-one\n id: batch-two\n" + tail,
712 "malformed-review-ledger",
713 ),
714 "duplicate key at document top level": (
715 header + " - id: batch-one\n" + tail + "batches:\n - id: batch-two\n" + tail,
716 "malformed-review-ledger",
717 ),
718 }
719 valid = header + " - id: batch-one\n" + tail + " - id: batch-two\n" + tail
720 merged = (
721 "schema: suppression-review-batches-1\n"
722 "shared: &shared\n"
723 " authority: inherited-authority\n"
724 " date: 2026-08-24\n"
725 "batches:\n"
726 " - <<: *shared\n"
727 " authority: repository-suppression-review\n"
728 " id: batch-merge\n"
729 f" identity_schema: {IDENTITY_SCHEMA_VERSION}\n"
730 " assigned_rows: 0\n"
731 f" rows_sha256: {'0' * 64}\n"
732 )
733 return rejected, valid, merged
734
735
736def _load_batches_document(root: Path, document: str) -> tuple[dict[str, dict], set[str]]:
737 """Write one batches authority into a fixture root and load it."""
738 (root / ".github").mkdir(parents=True, exist_ok=True)
739 (root / BATCHES_PATH).write_text(document, encoding="ascii")
740 batches, findings = load_batches(root)
741 return batches, {item.code for item in findings}
742
743
744def _assert_batch_identity_rules(base: Path, failures: list[str]) -> None:
745 """Assert repeated batch identities fail and valid YAML still parses.
746
747 PyYAML keeps the LAST value bound to a repeated mapping key and reports
748 nothing, so a second ``id:`` inside a record, or a second ``batches:``
749 block, silently replaced a reviewed authority before any application
750 validation ran. Both directions are asserted here: every repeat is
751 refused, and anchors, merge keys, and distinct identities still load
752 exactly as written.
753 """
754 rejected, valid, merged = _batch_documents()
755 for index, (label, (document, code)) in enumerate(rejected.items()):
756 root = base / f"ledger-batchid-{index}"
757 root.mkdir(parents=True, exist_ok=True)
758 _batches, codes = _load_batches_document(root, document)
759 expect(code in codes, f"must fire: {label} is rejected", failures)
760
761 valid_root = base / "ledger-batchid-valid"
762 valid_root.mkdir(parents=True, exist_ok=True)
763 batches, codes = _load_batches_document(valid_root, valid)
764 expect(
765 not codes and sorted(batches) == ["batch-one", "batch-two"],
766 "quiet: two distinct batch identities load unchanged",
767 failures,
768 )
769
770 merge_root = base / "ledger-batchid-merge"
771 merge_root.mkdir(parents=True, exist_ok=True)
772 records, codes = _load_batches_document(merge_root, merged)
773 expect(
774 not codes
775 and sorted(records) == ["batch-merge"]
776 and records["batch-merge"]["authority"] == "repository-suppression-review"
777 and str(records["batch-merge"]["date"]) == "2026-08-24",
778 "quiet: a merge key and its explicit override keep documented YAML semantics",
779 failures,
780 )