3"""Parse every authoritative ratchet baseline into suppression inventory rows."""
5from __future__
import annotations
10from dataclasses
import dataclass
11from pathlib
import Path, PurePosixPath
13from suppression_catalog
import ownership
14from suppression_model
import Finding, Suppression
18PATH_RULE_COUNT_COLUMNS = 3
19TREE_MEASURED_COLUMNS = 6
20TREE_UNMEASURED_COLUMNS = 3
22MIN_BASELINE_ROWS = 5343
23CEILING_LEDGER_PATH =
".github/suppression-debt-ceilings.tsv"
24CEILING_LEDGER_SHA256 =
"dcfbe6ffd0ecfe10e45646989ea32d1725d02e2c5585d3718440b27b267ce67b"
25CEILING_LEDGER_HEADER = (
26 "# Suppression debt per-key ceilings v1.",
27 "# Key digests bind the canonical baseline path and consumer-semantic key.",
28 "# Rows may disappear or decrease; a new key or weaker value is growth.",
29 "# baseline<TAB>sha256<TAB>ceiling",
31CEILING_LEDGER_COLUMNS = 3
34@dataclass(frozen=True)
36 """Grammar and provenance for one authoritative committed baseline."""
42 declared_pattern: str =
""
43 declared_is_sum: bool =
False
44 key_shape: str =
"path-rule"
45 ceiling_shape: str =
"count"
46 provenance_anchors: tuple[str, ...] = ()
49SPECS: dict[str, BaselineSpec] = {
50 ".github/agnostic-register-baseline.txt": BaselineSpec(
53 "scripts/checks/check_agnostic_registers.py",
54 "Frozen driver reach-in debt first measured by issue #698; counts may only shrink.",
56 "# Concrete RA8 driver reach-in debt, per (file, peripheral family).",
57 "# Consumed by scripts/checks/check_agnostic_registers.py --check",
60 ".github/cite-baseline.txt": BaselineSpec(
63 "scripts/checks/cite_ratchet.py",
64 "Uncited MMIO debt predating issue #534 enforcement; counts may only shrink.",
65 r"Total at this baseline:\s*(\d+)\s+uncited access",
69 "# HUM citation-COVERAGE ratchet baseline -- per-file uncited-access counts.",
70 "# Consumed by scripts/checks/cite_ratchet.py --check (CI gate: cite-check).",
73 ".github/emulator-matrix-baseline.txt": BaselineSpec(
76 "scripts/checks/matrix_ratchet.py",
77 "CI-runner emulator failures measured under issue #400 need an explained cause.",
79 key_shape=
"matrix-app",
80 ceiling_shape=
"matrix-verdict",
82 "# ra8_emulator example-matrix baseline -- see scripts/checks/matrix_ratchet.py",
83 "# MEASURE THIS ON THE CI RUNNER, NEVER ON A DEVELOPER BOX -- see #400.",
86 ".github/freestanding-runtime-baseline.json": BaselineSpec(
87 "freestanding-runtime",
89 "scripts/checks/check_freestanding_runtime.py",
90 "Zero-debt freestanding target ratchet from issue #847; per-app debt may only shrink.",
92 ceiling_shape=
"count",
94 ".github/hum-register-baseline.txt": BaselineSpec(
97 "scripts/checks/check_hum_register_map.py",
98 "Frozen register-map migration debt checked against the committed HUM.",
100 "# HUM register-map debt, per (file, rule). Generated by",
101 "# scripts/checks/check_hum_register_map.py --update.",
104 ".github/mcdc-baseline.txt": BaselineSpec(
107 "scripts/ci/gates/tests.sh",
108 "Aggregate MC/DC floor frozen from the pinned llvm-cov measurement.",
109 key_shape=
"repository",
110 ceiling_shape=
"percentage",
112 "# Aggregate reachable decision-complete MC/DC percentage ratchet floor.",
113 "# Consumed by scripts/ci/gates/tests.sh (CI gate: mcdc).",
116 ".github/mcdc-compound-baseline.txt": BaselineSpec(
117 "mcdc-compound-ratchet",
119 "scripts/checks/mcdc_compound_ratchet.py",
120 "Compound decisions predating issue #426 enforcement; counts may only shrink.",
121 r"Total at this baseline:\s*(\d+)\s+uncovered compound decision",
122 declared_is_sum=
True,
124 "# MC/DC compound-decision ratchet baseline -- per-file-per-function counts.",
125 "# Consumed by scripts/checks/mcdc_compound_ratchet.py --check",
128 ".github/misra-baseline.txt": BaselineSpec(
131 "scripts/checks/misra_ratchet.py",
132 "Cppcheck 2.13 debt with migration provenance in issues #786 and #790.",
133 r"total findings:\s*(\d+)",
134 declared_is_sum=
True,
136 "# MISRA-C 2012 ratchet baseline -- per-file-per-rule finding counts.",
137 "# Consumed by scripts/checks/misra_ratchet.py --check (CI job: misra).",
140 ".github/tidy-baseline.txt": BaselineSpec(
141 "clang-tidy-ratchet",
143 "scripts/checks/tidy_ratchet.py",
144 "Findings exposed by issue #369 and #370 scope expansion; counts may only shrink.",
146 "# clang-tidy ratchet baseline -- per-file-per-check finding counts.",
147 "# Consumed by scripts/checks/tidy_ratchet.py --check (CI gate: tidy).",
150 ".github/tree-coverage-baseline.txt": BaselineSpec(
153 "scripts/checks/check_tree_coverage.py",
154 "Single-tree debt from the pinned gcovr producer; coverage may only improve.",
157 ceiling_shape=
"tree-coverage",
159 "# ONE coverage baseline for every first-party translation unit.",
160 "# Emitted by `python3 scripts/checks/check_tree_coverage.py --update`.",
164BASELINE_CEILINGS: dict[str, tuple[int, int]] = {
165 ".github/agnostic-register-baseline.txt": (292, 672),
166 ".github/cite-baseline.txt": (246, 2831),
167 ".github/emulator-matrix-baseline.txt": (0, 0),
168 ".github/freestanding-runtime-baseline.json": (7, 0),
169 ".github/hum-register-baseline.txt": (46, 451),
170 ".github/mcdc-baseline.txt": (1, 1),
171 ".github/mcdc-compound-baseline.txt": (957, 1693),
172 ".github/misra-baseline.txt": (2710, 20014),
173 ".github/tidy-baseline.txt": (96, 121),
174 ".github/tree-coverage-baseline.txt": (1059, 9404),
176BASELINE_PERCENTAGE_FLOORS = {
".github/mcdc-baseline.txt": 89.72}
179KNOWN_MATRIX_VERDICTS = frozenset({
"FAULT",
"TRUNCATED",
"UNKNOWN",
"BUILD_FAIL",
"NO_ELF"})
180KNOWN_TREE_REASONS = frozenset(
182 "compiled-not-executed",
183 "firmware-composition",
184 "hosted-no-coverage-build",
185 "platform-cross-only",
188DebtRow = tuple[str, str, int, str]
189CeilingLedger = dict[str, tuple[str, str]]
190ValidationContext = tuple[
195 CeilingLedger |
None,
198SAFE_RULE_RE = re.compile(
r"[A-Za-z0-9_.+@()/-]+")
199PERCENT_RE = re.compile(
r"(?:100(?:\.0+)?|[0-9]{1,2}(?:\.[0-9]+)?)")
200SHA256_RE = re.compile(
r"[0-9a-f]{64}")
203def _safe_target(value: str) -> bool:
204 """Return whether a target is one canonical safe repository-relative path."""
205 path = PurePosixPath(value)
209 and "\\" not in value
210 and not path.is_absolute()
211 and ".." not in path.parts
212 and value == path.as_posix()
216def _semantic_key(spec: BaselineSpec, target: str, rule: str) -> str:
217 """Return the baseline consumer's canonical bucket identity."""
218 if spec.key_shape ==
"path-rule":
219 return f
"{target}\0{rule}"
220 if spec.key_shape ==
"path":
222 if spec.key_shape ==
"rule":
224 if spec.key_shape ==
"repository":
226 raise ValueError(spec.key_shape)
229def _ceiling_key(baseline: str, semantic_key: str) -> str:
230 """Hash one canonical consumer bucket together with its owning baseline."""
231 payload = f
"{baseline}\0{semantic_key}".encode(
"ascii")
232 return hashlib.sha256(payload).hexdigest()
235def _record_evidence(record: Suppression, prefix: str) -> str:
236 """Return one required evidence value from a suppression record."""
237 return next(item.removeprefix(prefix)
for item
in record.evidence
if item.startswith(prefix))
246 ceiling: tuple[str, str] = (
"",
""),
248 """Build one source-located ratchet inventory row."""
249 target, rule, count, reason = debt
250 semantic_key, ceiling_value = ceiling
251 key = semantic_key
or _semantic_key(spec, target, rule)
252 value = ceiling_value
or (
"present" if spec.ceiling_shape ==
"presence" else str(count))
260 "committed-baseline",
261 "repository" if not target
else f
"file:{target}",
262 reason
or spec.reason,
264 ownership(target)
if target
else "first-party",
267 f
"consumer:{spec.consumer}",
268 f
"target:{target or 'repository'}",
269 f
"frozen-count:{count}",
270 f
"ceiling-key:{_ceiling_key(baseline, key)}",
271 f
"ceiling-value:{value}",
274 recommendation=
"burn-down-only",
278ParseResult = tuple[Suppression |
None, str]
281def _positive_count(value: str) -> int |
None:
282 """Parse a strictly positive decimal baseline count."""
283 return int(value)
if re.fullmatch(
r"[1-9][0-9]*", value)
else None
286def _parse_path_rule_count(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
287 """Parse path, rule, and count fields."""
288 fields = raw.split(
"\t")
289 if len(fields) != PATH_RULE_COUNT_COLUMNS:
290 return None,
"expected path<TAB>rule<TAB>count"
291 target, rule, count_text = fields
292 count = _positive_count(count_text)
293 if not _safe_target(target)
or SAFE_RULE_RE.fullmatch(rule)
is None or count
is None:
294 return None,
"invalid path, rule, or positive count"
295 return _record(baseline, line, spec, (target, rule, count,
"")),
""
298def _parse_path_count(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
299 """Parse path and count fields."""
300 fields = raw.split(
"\t")
301 if len(fields) != PATH_COUNT_COLUMNS:
302 return None,
"expected path<TAB>count"
303 target, count_text = fields
304 count = _positive_count(count_text)
305 if not _safe_target(target)
or count
is None:
306 return None,
"invalid path or positive count"
307 return _record(baseline, line, spec, (target,
"uncited-mmio", count,
"")),
""
310def _parse_path(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
311 """Parse one repository-relative path."""
312 if "\t" in raw
or not _safe_target(raw):
313 return None,
"expected one repository-relative path"
314 return _record(baseline, line, spec, (raw,
"historical-gap", 1,
"")),
""
317def _parse_percentage(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
318 """Parse one aggregate percentage floor."""
319 if PERCENT_RE.fullmatch(raw)
is None:
320 return None,
"expected one percentage in the inclusive range 0..100"
321 reason = f
"Aggregate MC/DC may not fall below {raw}%."
327 (
"",
"aggregate-mcdc-floor", 1, reason),
334def _parse_matrix(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
335 """Parse one explained emulator debt row."""
336 fields = raw.split(
"\t")
337 if len(fields) != PATH_RULE_COUNT_COLUMNS:
338 return None,
"expected app<TAB>verdict<TAB>cause"
339 app, verdict, cause = fields
340 if not _safe_target(app)
or verdict
not in KNOWN_MATRIX_VERDICTS
or not cause.strip():
341 return None,
"unknown verdict or blank failure cause"
347 (
"", verdict, 1, cause.strip()),
348 ceiling=(app, verdict),
354def _parse_tree_coverage(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
355 """Parse gcovr measured and declared-unmeasured rows."""
356 fields = raw.split(
"\t")
357 if len(fields) == TREE_UNMEASURED_COLUMNS
and fields[1] ==
"UNMEASURED":
358 target, _kind, reason = fields
359 if not _safe_target(target)
or reason
not in KNOWN_TREE_REASONS:
360 return None,
"invalid UNMEASURED path or reason class"
361 debt = (target, f
"unmeasured:{reason}", 1, reason)
362 return _record(baseline, line, spec, debt, ceiling=(
"", f
"U:{reason}")),
""
363 if len(fields) == TREE_MEASURED_COLUMNS
and fields[1] ==
"MEASURED":
366 if not _safe_target(target)
or any(
not value.isdecimal()
for value
in counts):
367 return None,
"invalid MEASURED path or non-negative counts"
368 covered_line, total_line, covered_branch, total_branch = map(int, counts)
369 if covered_line > total_line
or covered_branch > total_branch:
370 return None,
"covered count exceeds total count"
371 debt = (total_line - covered_line) + (total_branch - covered_branch)
372 row = (target,
"measured-line-branch-floor", max(debt, 1),
"")
373 value = f
"M:{covered_line},{total_line},{covered_branch},{total_branch}"
374 return _record(baseline, line, spec, row, ceiling=(
"", value)),
""
375 return None,
"expected a tool-emitted MEASURED or UNMEASURED row"
378def _parse_opaque(baseline: str, line: int, raw: str, spec: BaselineSpec) -> ParseResult:
379 """Parse one nonempty checker-owned key."""
380 if not raw.strip()
or "\t" in raw:
381 return None,
"expected one nonempty checker-owned key"
382 return _record(baseline, line, spec, (
"", raw.strip(), 1,
"")),
""
385def _freestanding_archive_units(archives: object) -> int |
None:
386 """Count live-archive debt units, or None when malformed."""
387 if not isinstance(archives, dict):
390 for name, items
in archives.items():
391 if not isinstance(name, str)
or not isinstance(items, list):
393 if not all(isinstance(m, str)
for m
in items):
395 members += len(items)
399def _freestanding_units(entry: object) -> int |
None:
400 """Count debt units in one freestanding app entry, or None when malformed."""
401 if not isinstance(entry, dict):
403 symbols = entry.get(
"forbidden_symbols")
404 if not isinstance(symbols, list)
or not all(isinstance(s, str)
for s
in symbols):
406 members = _freestanding_archive_units(entry.get(
"forbidden_archives"))
407 provider = entry.get(
"sbrk_provider")
408 end = entry.get(
"end_symbol")
409 if members
is None or not isinstance(provider, str)
or not isinstance(end, bool):
411 return len(symbols) + members + (0
if provider ==
"none" else 1) + (1
if end
else 0)
414def _json_key_line(text: str, key: str) -> int:
415 """Return the 1-based line defining one JSON object key."""
416 match = re.search(
r'"' + re.escape(key) +
r'"\s*:', text)
419 return text.count(
"\n", 0, match.start()) + 1
422def _freestanding_rows(
424) -> tuple[list[tuple[DebtRow, int]], str]:
425 """Parse the freestanding JSON ratchet into located per-app debt rows."""
427 data = json.loads(text)
428 except json.JSONDecodeError:
429 return [],
"baseline is not valid JSON"
430 if not isinstance(data, dict):
431 return [],
"baseline root must be an object"
432 apps = data.get(
"apps")
433 exceptions = data.get(
"linker_script_exceptions")
434 if not isinstance(apps, dict)
or not isinstance(exceptions, list):
435 return [],
"baseline must define apps and linker_script_exceptions"
436 if not all(isinstance(e, str)
for e
in exceptions):
437 return [],
"linker_script_exceptions must list strings"
438 rows, error = _freestanding_app_rows(text, apps)
443 (
"",
"linker-script-exceptions", len(exceptions),
""),
444 _json_key_line(text,
"linker_script_exceptions"),
450def _freestanding_app_rows(
451 text: str, apps: dict[object, object]
452) -> tuple[list[tuple[DebtRow, int]], str]:
453 """Parse sorted per-app debt rows with source locations."""
454 names = sorted(app
for app
in apps
if isinstance(app, str))
455 if len(names) != len(apps):
456 return [],
"app keys must be strings"
457 rows: list[tuple[DebtRow, int]] = []
459 if SAFE_RULE_RE.fullmatch(app)
is None:
460 return [], f
"unsafe app key {app!r}"
461 units = _freestanding_units(apps[app])
463 return [], f
"malformed app entry {app!r}"
464 rows.append(((
"", app, units,
""), _json_key_line(text, app)))
469 "path-rule-count": _parse_path_rule_count,
470 "path-count": _parse_path_count,
472 "percentage": _parse_percentage,
473 "matrix": _parse_matrix,
474 "tree-coverage": _parse_tree_coverage,
475 "opaque": _parse_opaque,
480 root: Path, baseline: str, record: Suppression, findings: list[Finding]
482 """Reject stale paths and exempt-owner baseline rows."""
484 item.removeprefix(
"target:")
for item
in record.evidence
if item.startswith(
"target:")
486 if target ==
"repository":
488 if not (root / target).is_file():
489 findings.append(Finding(
"stale-baseline-path", target, baseline, record.line))
490 if record.owner !=
"first-party":
491 message = f
"{target} is {record.owner}"
492 findings.append(Finding(
"baseline-owner-mismatch", message, baseline, record.line))
495def _only_lost_code(now: tuple[int, int], base: tuple[int, int]) -> bool:
496 """Return whether a smaller metric lost no more covered than total items."""
498 base_covered, base_total = base
499 return (total < base_total)
and ((base_covered - covered) <= (base_total - total))
502def _metric_regressed(now: tuple[int, int], base: tuple[int, int]) -> bool:
503 """Return whether uncovered debt grew or a non-shrink ratio fell."""
505 base_covered, base_total = base
506 debt_grew = (total - covered) > (base_total - base_covered)
509 and covered * base_total < base_covered * total
510 and not _only_lost_code(now, base)
512 return debt_grew
or ratio_fell
515def _tree_value(value: str) -> tuple[str, tuple[int, ...] | str] |
None:
516 """Parse one compact tree-coverage ceiling value."""
517 if value.startswith(
"U:")
and value.removeprefix(
"U:")
in KNOWN_TREE_REASONS:
518 return "U", value.removeprefix(
"U:")
519 if value.startswith(
"M:"):
520 fields = value.removeprefix(
"M:").split(
",")
521 if len(fields) == TREE_METRIC_FIELDS
and all(field.isdecimal()
for field
in fields):
522 counts = tuple(int(field)
for field
in fields)
523 if counts[0] <= counts[1]
and counts[2] <= counts[3]:
528def _tree_regressed(candidate: str, audited: str) -> bool:
529 """Return whether a candidate tree row weakens its audited per-metric floor."""
530 now = _tree_value(candidate)
531 base = _tree_value(audited)
532 regressed = now
is None or base
is None
533 if now
is not None and base
is not None:
534 now_kind, now_value = now
535 base_kind, base_value = base
536 if base_kind ==
"U" and now_kind ==
"U":
537 regressed = now_value != base_value
538 elif base_kind ==
"U" and isinstance(now_value, tuple):
539 line_ok = now_value[1] == 0
or now_value[0] * 100 >= now_value[1] * 90
540 branch_ok = now_value[3] == 0
or now_value[2] * 100 >= now_value[3] * 80
541 regressed =
not (line_ok
and branch_ok)
545 and isinstance(now_value, tuple)
546 and isinstance(base_value, tuple)
548 regressed = _metric_regressed(now_value[:2], base_value[:2])
or _metric_regressed(
549 now_value[2:], base_value[2:]
556def _ceiling_regressed(spec: BaselineSpec, candidate: str, audited: str) -> bool:
557 """Compare one candidate value with its immutable per-key ceiling."""
558 if spec.ceiling_shape ==
"count":
559 return not candidate.isdecimal()
or not audited.isdecimal()
or int(candidate) > int(audited)
560 if spec.ceiling_shape ==
"presence":
561 return candidate !=
"present" or audited !=
"present"
562 if spec.ceiling_shape ==
"percentage":
563 return PERCENT_RE.fullmatch(candidate)
is None or float(candidate) < float(audited)
564 if spec.ceiling_shape ==
"matrix-verdict":
565 return candidate != audited
566 if spec.ceiling_shape ==
"tree-coverage":
567 return _tree_regressed(candidate, audited)
571def ceiling_snapshot(records: list[Suppression]) -> CeilingLedger:
572 """Return the immutable semantic-key ceiling map represented by records."""
574 _record_evidence(record,
"ceiling-key:"): (
576 _record_evidence(record,
"ceiling-value:"),
578 for record
in records
582def _parse_ceiling_ledger(text: str) -> tuple[CeilingLedger, list[str]]:
583 """Parse the exact committed ceiling-ledger grammar."""
584 lines = text.splitlines()
585 errors: list[str] = []
586 if tuple(lines[: len(CEILING_LEDGER_HEADER)]) != CEILING_LEDGER_HEADER:
587 errors.append(
"header does not match the authenticated v1 ledger header")
588 ledger: CeilingLedger = {}
589 for line_no, raw
in enumerate(lines[len(CEILING_LEDGER_HEADER) :], start=5):
592 fields = raw.split(
"\t")
593 if len(fields) != CEILING_LEDGER_COLUMNS:
594 errors.append(f
"line {line_no}: expected baseline<TAB>sha256<TAB>ceiling")
596 baseline, digest, value = fields
597 if baseline
not in SPECS
or SHA256_RE.fullmatch(digest)
is None or not value:
598 errors.append(f
"line {line_no}: invalid baseline, digest, or ceiling")
601 errors.append(f
"line {line_no}: duplicate digest {digest}")
603 ledger[digest] = (baseline, value)
604 return ledger, errors
607def _load_ceiling_ledger(root: Path) -> tuple[CeilingLedger, list[Finding]]:
608 """Load and authenticate the committed per-key ceiling ledger."""
609 path = root / CEILING_LEDGER_PATH
610 if not path.is_file():
611 return {}, [Finding(
"missing-baseline-ceilings", CEILING_LEDGER_PATH)]
612 raw = path.read_bytes()
613 digest = hashlib.sha256(raw).hexdigest()
614 findings: list[Finding] = []
615 if digest != CEILING_LEDGER_SHA256:
618 "baseline-ceiling-integrity",
619 f
"ledger sha256 {digest} does not match {CEILING_LEDGER_SHA256}",
624 text = raw.decode(
"ascii")
625 except UnicodeDecodeError
as error:
626 decode_finding = Finding(
"baseline-ceiling-integrity", str(error), CEILING_LEDGER_PATH)
627 return {}, [*findings, decode_finding]
628 ledger, errors = _parse_ceiling_ledger(text)
630 Finding(
"baseline-ceiling-integrity", message, CEILING_LEDGER_PATH)
for message
in errors
632 return ledger, findings
635def _validate_ceiling(
639 ledger: CeilingLedger,
640 findings: list[Finding],
642 """Reject a new semantic bucket or a weaker value for an audited bucket."""
643 digest = _record_evidence(record,
"ceiling-key:")
644 candidate = _record_evidence(record,
"ceiling-value:")
645 entry = ledger.get(digest)
648 Finding(
"baseline-growth",
"new consumer-semantic debt bucket", baseline, record.line)
651 owner, audited = entry
652 if owner != baseline
or _ceiling_regressed(spec, candidate, audited):
653 message = f
"bucket ceiling {audited!r} weakened to {candidate!r}"
654 findings.append(Finding(
"baseline-growth", message, baseline, record.line))
657def _accept_record(record: Suppression, context: ValidationContext) ->
None:
658 """Apply semantic duplicate, target, ceiling, and percentage validation."""
659 root, baseline, spec, keys, ledger, findings = context
660 key = _record_evidence(record,
"ceiling-key:")
661 prior = keys.get(key)
662 if prior
is not None:
663 message = f
"consumer-semantic key duplicates line {prior}"
664 findings.append(Finding(
"duplicate-baseline-row", message, baseline, record.line))
666 keys[key] = record.line
667 _validate_target(root, baseline, record, findings)
668 if ledger
is not None:
669 _validate_ceiling(baseline, spec, record, ledger, findings)
670 minimum = BASELINE_PERCENTAGE_FLOORS.get(baseline)
671 value = _record_evidence(record,
"ceiling-value:")
672 if minimum
is not None and float(value) < minimum:
673 message = f
"{value}% is below the audited {minimum:.2f}% floor"
674 findings.append(Finding(
"baseline-growth", message, baseline, record.line))
678 root: Path, baseline: str, spec: BaselineSpec, ledger: CeilingLedger |
None
679) -> tuple[list[Suppression], list[Finding]]:
680 """Parse and structurally validate one committed baseline."""
681 text = (root / baseline).read_text(encoding=
"ascii")
682 if spec.shape ==
"freestanding-apps":
683 return _parse_freestanding_file(root, baseline, spec, ledger)
684 records: list[Suppression] = []
685 findings: list[Finding] = []
686 keys: dict[str, int] = {}
687 parser = PARSERS[spec.shape]
688 exact_lines = frozenset(text.splitlines())
690 Finding(
"missing-baseline-provenance", repr(anchor), baseline)
691 for anchor
in spec.provenance_anchors
692 if anchor
not in exact_lines
694 for line_no, raw
in enumerate(text.splitlines(), start=1):
695 if not raw.strip()
or raw.lstrip().startswith(
"#"):
697 record, error = parser(baseline, line_no, raw, spec)
699 findings.append(Finding(
"malformed-baseline-row", error, baseline, line_no))
701 _accept_record(record, (root, baseline, spec, keys, ledger, findings))
702 records.append(record)
703 findings.extend(_check_file_totals(baseline, spec, records, text))
704 return records, findings
707def _check_file_totals(
708 baseline: str, spec: BaselineSpec, records: list[Suppression], text: str
710 """Enforce audited row/unit ceilings and declared totals for one baseline."""
711 findings: list[Finding] = []
712 row_ceiling, unit_ceiling = BASELINE_CEILINGS[baseline]
713 units = sum(item.match_count
for item
in records)
714 if len(records) > row_ceiling
or units > unit_ceiling:
716 f
"parsed {len(records)} row(s)/{units} debt unit(s); "
717 f
"audited ceilings are {row_ceiling}/{unit_ceiling}"
719 findings.append(Finding(
"baseline-growth", message, baseline))
720 if spec.declared_pattern:
721 match = re.search(spec.declared_pattern, text, re.IGNORECASE)
723 findings.append(Finding(
"missing-baseline-total", spec.declared_pattern, baseline))
725 declared = int(match.group(1))
727 sum(item.match_count
for item
in records)
if spec.declared_is_sum
else len(records)
729 if declared != actual:
730 message = f
"declares {declared}, parsed {actual}"
731 findings.append(Finding(
"stale-baseline-total", message, baseline))
735def _parse_freestanding_file(
736 root: Path, baseline: str, spec: BaselineSpec, ledger: CeilingLedger |
None
737) -> tuple[list[Suppression], list[Finding]]:
738 """Parse the freestanding JSON ratchet through the shared validation path."""
739 text = (root / baseline).read_text(encoding=
"ascii")
740 records: list[Suppression] = []
741 findings: list[Finding] = []
742 keys: dict[str, int] = {}
743 rows, error = _freestanding_rows(text)
745 return [], [Finding(
"malformed-baseline-row", error, baseline)]
746 for debt, line_no
in rows:
747 record = _record(baseline, line_no, spec, debt)
748 _accept_record(record, (root, baseline, spec, keys, ledger, findings))
749 records.append(record)
750 findings.extend(_check_file_totals(baseline, spec, records, text))
751 return records, findings
754def scan_baseline_repository(
758 enforce_floors: bool,
759 ceiling_ledger: CeilingLedger |
None =
None,
760) -> tuple[list[Suppression], list[Finding]]:
761 """Discover, parse, and inventory all authoritative ratchet baselines."""
762 authorities = sorted(
765 if rel.startswith(
".github/")
766 and re.search(
r"baseline|ratchet", PurePosixPath(rel).name, re.IGNORECASE)
768 discovered = [rel
for rel
in authorities
if rel
in SPECS]
769 records: list[Suppression] = []
771 Finding(
"unknown-baseline-file", authority, authority)
772 for authority
in authorities
773 if authority
not in SPECS
775 ledger = ceiling_ledger
776 if enforce_floors
and ledger
is None:
777 ledger, ledger_findings = _load_ceiling_ledger(root)
778 findings.extend(ledger_findings)
779 for baseline
in discovered:
780 spec = SPECS[baseline]
781 if not (root / spec.consumer).is_file():
782 findings.append(Finding(
"missing-baseline-consumer", spec.consumer, baseline))
783 parsed, errors = _parse_file(root, baseline, spec, ledger)
784 records.extend(parsed)
785 findings.extend(errors)
788 Finding(
"missing-baseline-file", baseline, baseline)
789 for baseline
in sorted(set(SPECS) - set(discovered))
791 if len(discovered) < MIN_BASELINE_FILES:
792 message = f
"only {len(discovered)} files; floor is {MIN_BASELINE_FILES}"
793 findings.append(Finding(
"vacuous-baseline-files", message))
794 if len(records) < MIN_BASELINE_ROWS:
795 message = f
"only {len(records)} rows; audited floor is {MIN_BASELINE_ROWS}"
796 findings.append(Finding(
"vacuous-baseline-rows", message))
797 return records, findings