4"""Prove that EVERY code file in this repository is linted and formatted.
8"Is everything linted?" was, until this gate, answerable only by opening each
9checker and reading its scan list by hand. That audit was performed five times
10and was wrong five times -- #296, #332, #358, #359, #360. Every one of those
11was the same defect in a different checker: a hardcoded root list that stopped
12matching the tree, so the checker reported a clean run over a subset while
13files outside it sat unchecked for months. A gate that scans nothing reports
14success, and success is indistinguishable from having done the work.
16This gate answers the question mechanically:
18 1. Enumerate every file from ``git ls-files`` -- never a directory list.
19 A hardcoded directory list is the exact defect being killed here, so this
20 gate must not contain one.
21 2. Classify each file by exact name, then extension, then shebang.
22 3. Ask each checker, in its own "list what you would scan" mode, which files
23 it claims. The gate does NOT restate any checker's scope: a second copy of
24 the coverage map is a new instance of the original bug.
25 4. Assert every CODE file is claimed by at least one linter and at least one
27 5. FAIL on any file whose type has no classification rule at all.
29Point 5 is the one that earns the gate its keep. The day someone commits a
30``.rs``, a ``.ts`` or a ``.proto``, this goes red and somebody has to decide
31how that language is checked -- rather than it entering the tree silently and
32being discovered by the sixth hand audit.
34LAYOUT AGNOSTICISM IS A DESIGN REQUIREMENT
35------------------------------------------
36Checkers are located by BASENAME through ``git ls-files``, not by a hardcoded
37path. ``scripts/`` was reorganised into subdirectories in #359 and every
38checker this gate resolves changed directory; the basename lookup carried that
39move without a single edit. A gate that hardcoded full paths would have broken
40on it and -- far worse -- could then have been "fixed" by dropping the
41provider, silently shrinking coverage. By name, a MOVE is invisible and a
46 check_lint_coverage.py --selftest # assert the gate itself still fires
47 check_lint_coverage.py # the real check
48 check_lint_coverage.py --matrix # print the coverage matrix and exit 0
51from __future__
import annotations
58from dataclasses
import dataclass
59from pathlib
import Path
61sys.path.insert(0, str(Path(__file__).resolve().parent))
63from lint_coverage_rules
import (
76from selftest_assert
import expect, report
80 [
"git",
"rev-parse",
"--show-toplevel"],
100EXPECTED_FIXTURE_EXEMPT = 3
109@dataclass(frozen=True)
111 """A checker, the roles it fills, and how to ask it what it scans.
113 ``script`` is a BASENAME resolved through git at run time -- see the module
114 docstring on layout agnosticism.
118 roles: tuple[str, ...]
119 classes: tuple[str, ...]
121 args: tuple[str, ...]
122 runner: str =
"python3"
125PROVIDERS: tuple[Provider, ...] = (
126 Provider(
"clang-tidy", (LINT,), (
"c-family",),
"clang_tidy.sh", (
"--list-files",),
"bash"),
127 Provider(
"clang-format", (FORMAT,), (
"c-family",),
"format_code.sh", (
"--list-files",),
"bash"),
128 Provider(
"ruff", (LINT,), (
"python",),
"check_ruff.py", (
"--list-files",)),
130 "ruff-format", (FORMAT,), (
"python",),
"format_tree.sh", (
"--list-files",
"python"),
"bash"
132 Provider(
"vet+staticcheck", (LINT,), (
"golang",),
"check_go.py", (
"--list-files",)),
133 Provider(
"gofmt", (FORMAT,), (
"golang",),
"format_tree.sh", (
"--list-files",
"go"),
"bash"),
134 Provider(
"shellcheck", (LINT,), (
"shell",),
"check_shell.py", (
"--list-files",)),
135 Provider(
"shfmt", (FORMAT,), (
"shell",),
"format_tree.sh", (
"--list-files",
"shell"),
"bash"),
136 Provider(
"cmake-lint", (LINT,), (
"cmake",),
"lint_targets.py", (
"cmake",)),
138 "cmake-format", (FORMAT,), (
"cmake",),
"format_tree.sh", (
"--list-files",
"cmake"),
"bash"
140 Provider(
"check_justfiles", (LINT,), (
"just",),
"check_justfiles.py", (
"--list-files",)),
141 Provider(
"just-fmt", (FORMAT,), (
"just",),
"format_tree.sh", (
"--list-files",
"just"),
"bash"),
142 Provider(
"yamllint+actionlint", (LINT, FORMAT), (
"yaml",),
"lint_targets.py", (
"yaml",)),
144 "check_linker_scripts",
147 "check_linker_scripts.py",
150 Provider(
"check_asm", (LINT, FORMAT), (
"asm",),
"check_asm.py", (
"--list-files",)),
154 (
"dockerfile",
"zsh"),
155 "check_devcontainer.py",
159 "fleet-ansible-template",
161 (
"ansible-systemd-template",),
162 "check_fleet_declaration.py",
171def git_files() -> list[str]:
172 """Every tracked or untracked-but-not-ignored path, repo-relative."""
173 proc = subprocess.run(
180 "--exclude-standard",
187 if proc.returncode != 0:
188 sys.stderr.write(proc.stderr)
189 sys.stderr.write(
"check_lint_coverage.py: FATAL -- `git ls-files` failed\n")
191 return sorted(_present_worktree_files(proc.stdout.split(
"\0")))
194def _present_worktree_files(paths: list[str], root: Path = REPO_ROOT) -> list[str]:
195 """Retain live candidate files and drop deleted paths left in the index."""
196 return [path
for path
in paths
if path
and (root / path).is_file()]
199def read_shebang(rel: str) -> str:
200 """First line of `rel` if it is a shebang, else the empty string."""
202 with (REPO_ROOT / rel).open(
"rb")
as handle:
203 first = handle.readline(200)
206 if not first.startswith(b
"#!"):
208 return first.decode(
"utf-8", errors=
"replace").strip()
211def classify(rel: str) -> str |
None:
212 """Return the class name for `rel`, or None when nothing claims it.
214 Order is exact path, exact name, extension, then shebang. Exact path keeps
215 one reproducible generated file from exempting every future file with the
216 same extension. Name beats extension so ``CMakeLists.txt`` is cmake rather
217 than text; shebang comes last so it only rescues files the tables genuinely
218 miss -- which is how an extensionless ``scripts/git/pre-commit`` is
221 if rel
in PATH_CLASS:
222 return PATH_CLASS[rel]
223 name = rel.rsplit(
"/", 1)[-1]
224 if name
in NAME_CLASS:
225 return NAME_CLASS[name]
228 suffix =
"." + name.rsplit(
".", 1)[-1]
229 if suffix.lower()
in EXT_CLASS:
230 return EXT_CLASS[suffix.lower()]
231 line = read_shebang(rel)
233 for token, cls
in SHEBANG_CLASS:
242def resolve_script(basename: str, tracked: list[str]) -> str |
None:
243 """Locate a checker by basename anywhere in the tree. None if absent."""
244 hits = [p
for p
in tracked
if p.rsplit(
"/", 1)[-1] == basename]
245 if len(hits) != EXACTLY_ONE:
250def provider_files(prov: Provider, tracked: list[str]) -> tuple[set[str], str |
None]:
251 """Run `prov` in list mode. Returns (files, error). Never swallows failure."""
252 path = resolve_script(prov.script, tracked)
254 return set(), f
"cannot locate {prov.script} (moved, deleted or ambiguous)"
255 runner = shutil.which(prov.runner)
or prov.runner
256 proc = subprocess.run(
257 [runner, path, *prov.args],
263 if proc.returncode != 0:
264 detail = proc.stderr.strip().splitlines()
265 tail = detail[-1]
if detail
else f
"exit {proc.returncode}"
266 return set(), f
"{prov.script} --list-files failed: {tail}"
267 files = {ln.strip()
for ln
in proc.stdout.splitlines()
if ln.strip()}
268 foreign_code = sorted(
271 if (cls := classify(rel))
is not None
272 and CLASSES[cls].kind ==
"code"
273 and cls
not in prov.classes
277 f
"{prov.script} --list-files claimed {foreign_code[0]!r} outside "
278 f
"its declared classes {prov.classes!r}"
287 """Outcome of one evaluation."""
289 def __init__(self) -> None:
290 """Start an empty report with every failure bucket distinct.
292 The buckets are kept separate rather than merged into one findings
293 list because they fail for different reasons and carry different
294 remedies -- an unclassified file type needs a rule, an uncovered file
295 needs a checker, and gap growth needs the gap closed.
297 self.unclassified: list[str] = []
298 self.uncovered: list[tuple[str, str, str]] = []
299 self.gap_growth: list[str] = []
300 self.gap_sizes: dict[str, int] = {}
301 self.counts: dict[str, int] = {}
305 def ok(self) -> bool:
306 """Whether the report is clean across every failing bucket.
308 Note ``gap_sizes`` and ``exempt`` are deliberately NOT consulted: a
309 recorded gap of unchanged size is the accepted state, and only its
312 return not (self.unclassified
or self.uncovered
or self.gap_growth)
315def _read_text(rel: str) -> str:
316 """File contents for a gap predicate, empty when unreadable or binary."""
318 return (REPO_ROOT / rel).read_text(errors=
"replace")
323def _bucket_gaps(raw: list[tuple[str, str, str]], report: Report) -> list[tuple[str, str, str]]:
324 """Split uncovered pairs into recorded gaps and genuine violations.
326 Also runs the ratchet: a gap that has grown past its recorded count is a
327 failure, because "carried deliberately while it is closed" and "quietly
328 becoming permanent" must not look the same.
330 hits: dict[str, set[str]] = {gap.name: set()
for gap
in KNOWN_GAPS}
331 violations: list[tuple[str, str, str]] = []
332 text_cache: dict[str, str] = {}
333 for rel, cls, role
in raw:
334 if rel
not in text_cache:
335 text_cache[rel] = _read_text(rel)
336 ctx = GapCtx(rel=rel, cls=cls, text=text_cache[rel])
337 for gap
in KNOWN_GAPS:
339 hits[gap.name].add(rel)
342 violations.append((rel, cls, role))
344 for gap
in KNOWN_GAPS:
345 got = len(hits[gap.name])
346 report.gap_sizes[gap.name] = got
348 report.gap_growth.append(
349 f
"known gap {gap.name!r} ({gap.issue}) grew from {gap.count} to {got} "
350 "file(s). Close it -- raising the recorded count needs a stated reason."
355def _provider_claims_class(prov: Provider, rel: str) -> bool:
356 """Whether a provider may satisfy coverage for this path's code class."""
358 return cls
is not None and cls
in prov.classes
361def evaluate(files: list[str], claimed: dict[str, set[str]]) -> Report:
362 """Decide coverage for `files` given each provider's claimed set.
364 `claimed` maps provider name -> the set of paths that provider scans. The
365 real run fills it from the checkers themselves; --selftest fills it by
366 hand, which is what makes both directions assertable without touching the
370 lint_by: dict[str, set[str]] = {}
371 fmt_by: dict[str, set[str]] = {}
372 for prov
in PROVIDERS:
375 got = {rel
for rel
in claimed.get(prov.name, set())
if _provider_claims_class(prov, rel)}
376 if LINT
in prov.roles:
377 lint_by[prov.name] = got
378 if FORMAT
in prov.roles:
379 fmt_by[prov.name] = got
381 all_lint = set().union(*lint_by.values())
if lint_by
else set()
382 all_fmt = set().union(*fmt_by.values())
if fmt_by
else set()
384 raw: list[tuple[str, str, str]] = []
386 if exemption_reason(rel)
is not None:
391 report.unclassified.append(rel)
393 report.counts[cls] = report.counts.get(cls, 0) + 1
394 if CLASSES[cls].kind !=
"code":
396 for role, pool
in ((LINT, all_lint), (FORMAT, all_fmt)):
398 raw.append((rel, cls, role))
400 report.uncovered = _bucket_gaps(raw, report)
407def print_matrix(report: Report, claimed: dict[str, set[str]]) ->
None:
408 """Print the class-by-provider coverage matrix.
410 The human-readable answer to "which checker claims this file type?", which
411 is the question that goes unasked until a whole language turns out to have
412 had no checker at all.
414 by_class: dict[str, list[str]] = {}
415 for prov
in PROVIDERS:
416 for cls
in prov.classes:
417 by_class.setdefault(cls, []).append(prov.name)
418 print(f
"{'CLASS':<18}{'COUNT':>7} {'KIND':<6} PROVIDERS")
420 for cls
in sorted(report.counts):
422 provs =
", ".join(by_class.get(cls, []))
or "-- none --"
423 n = report.counts[cls]
424 print(f
"{cls:<18}{n:>7} {spec.kind:<6} {provs}")
426 total = sum(report.counts.values())
427 print(f
"{'total classified':<18}{total:>7}")
428 print(f
"{'exempt':<18}{report.exempt:>7}")
429 for prov
in PROVIDERS:
430 print(f
" scanned by {prov.name:<22} {len(claimed.get(prov.name, set())):>6} file(s)")
432 print(
"\nRECORDED GAPS -- code with no checker, held flat by the ratchet:")
433 for gap
in KNOWN_GAPS:
434 got = report.gap_sizes.get(gap.name, 0)
435 print(f
" {gap.name:<28}{got:>4}/{gap.count:<4} {gap.issue} {gap.reason[:60]}")
438def print_failures(report: Report) ->
None:
439 """Print each failing bucket to stderr, capped per bucket.
441 Capped because a newly-added file type can produce hundreds of identical
442 findings, and the first few plus a count communicate the same thing
443 without burying the other buckets.
445 if report.unclassified:
446 print(
"\nUNCLASSIFIED FILE TYPES -- no rule says how these are checked:", file=sys.stderr)
447 for rel
in report.unclassified[:MAX_SHOWN]:
448 print(f
" {rel}", file=sys.stderr)
449 extra = len(report.unclassified) - MAX_SHOWN
451 print(f
" ... and {extra} more", file=sys.stderr)
453 " Add the type to PATH_CLASS/EXT_CLASS/NAME_CLASS in lint_coverage_rules.py and,\n"
454 " if it is code, wire a linter and a formatter for it.",
459 f
"\nUNCOVERED CODE FILES -- {len(report.uncovered)} file/role pair(s) "
460 "that no checker claims:",
463 noun = {LINT:
"linter", FORMAT:
"formatter"}
464 for rel, cls, role
in report.uncovered[:MAX_SHOWN]:
465 print(f
" {rel} [{cls}] has no {noun[role]}", file=sys.stderr)
466 extra = len(report.uncovered) - MAX_SHOWN
468 print(f
" ... and {extra} more", file=sys.stderr)
469 for msg
in report.gap_growth:
470 print(f
"\nGAP RATCHET: {msg}", file=sys.stderr)
476def _fixture() -> tuple[list[str], dict[str, set[str]]]:
477 """A miniature repo that is fully covered, used as the quiet baseline."""
479 "libs/ra8_core/src/ra8_err.c",
480 "libs/ra8_core/inc/ra8_err.h",
481 "scripts/checks/check_thing.py",
482 "scripts/git/pre-commit",
484 "examples/app/linker_script.ld",
485 "examples/app/boot.S",
486 ".devcontainer/Dockerfile",
487 ".devcontainer/zshrc",
488 ".github/workflows/firmware.yml",
489 "infra/ansible/roles/dev_box/templates/ra8-hil-runner.service.j2",
491 "apps/shared_libs/third_party/miniz/miniz.c",
492 "apps/board/stand_alone/ereader/content/library/book.epub",
493 "docs/reference/ra8d2-datasheet.pdf",
496 "clang-tidy": {
"libs/ra8_core/src/ra8_err.c",
"libs/ra8_core/inc/ra8_err.h"},
497 "clang-format": {
"libs/ra8_core/src/ra8_err.c",
"libs/ra8_core/inc/ra8_err.h"},
498 "ruff": {
"scripts/checks/check_thing.py"},
499 "ruff-format": {
"scripts/checks/check_thing.py"},
500 "shellcheck": {
"scripts/git/pre-commit"},
501 "shfmt": {
"scripts/git/pre-commit"},
502 "cmake-lint": {
"CMakeLists.txt"},
503 "cmake-format": {
"CMakeLists.txt"},
504 "yamllint+actionlint": {
".github/workflows/firmware.yml"},
505 "check_linker_scripts": {
"examples/app/linker_script.ld"},
506 "check_asm": {
"examples/app/boot.S"},
507 "hadolint+zsh": {
".devcontainer/Dockerfile",
".devcontainer/zshrc"},
508 "fleet-ansible-template": {
509 "infra/ansible/roles/dev_box/templates/ra8-hil-runner.service.j2"
512 return files, claimed
515def _assert_quiet(files: list[str], claimed: dict[str, set[str]], failures: list[str]) ->
None:
516 """Assert the model stays silent on trees that are genuinely covered.
518 Split out along the QUIET / MUST-FIRE boundary this suite already drew in
519 comments: the two directions share only the fixture, and a reader checking
520 "does a covered tree pass?" should not have to step over the fires cases.
522 base = evaluate(files, claimed)
523 expect(base.ok,
"a fully-covered tree passes", failures)
525 base.exempt == EXPECTED_FIXTURE_EXEMPT,
526 f
"exempt paths counted, not flagged (got {base.exempt})",
530 plus = [*files,
"libs/ra8_core/src/ra8_new.c"]
531 claimed2 = {k: set(v)
for k, v
in claimed.items()}
532 claimed2[
"clang-tidy"].add(
"libs/ra8_core/src/ra8_new.c")
533 claimed2[
"clang-format"].add(
"libs/ra8_core/src/ra8_new.c")
535 evaluate(plus, claimed2).ok,
536 "a new file of a covered type in a covered dir stays quiet",
541def _assert_fires(files: list[str], claimed: dict[str, set[str]], failures: list[str]) ->
None:
542 """Assert the model fires on each distinct way coverage can be lost."""
543 rust = evaluate([*files,
"tools/agent/src/main.rs"], claimed)
545 rust.unclassified == [
"tools/agent/src/main.rs"],
546 "an unclassified file type (.rs) fires",
550 orphan = evaluate([*files,
"newdir/thing.c"], claimed)
552 sorted({r
for r, _, _
in orphan.uncovered}) == [
"newdir/thing.c"]
553 and len(orphan.uncovered) == BOTH_ROLES,
554 "a code file in a directory no checker enumerates fires (lint AND format)",
558 narrowed = {k: set(v)
for k, v
in claimed.items()}
559 narrowed[
"clang-format"].discard(
"libs/ra8_core/inc/ra8_err.h")
560 drop = evaluate(files, narrowed)
562 drop.uncovered == [(
"libs/ra8_core/inc/ra8_err.h",
"c-family", FORMAT)],
563 "narrowing a checker's scan list fires on the file that dropped out",
567 missing_py = {k: set(v)
for k, v
in claimed.items()}
568 missing_py[
"ruff"] = set()
569 missing_py[
"ruff-format"] = set()
571 len(evaluate(files, missing_py).uncovered) == BOTH_ROLES,
572 "losing python lint and format ownership fires both roles",
575 missing_template = {k: set(v)
for k, v
in claimed.items()}
576 missing_template[
"fleet-ansible-template"] = set()
578 len(evaluate(files, missing_template).uncovered) == BOTH_ROLES,
579 "the HIL systemd template needs both semantic lint and format ownership",
582 leaked_census = {k: set(v)
for k, v
in claimed.items()}
583 leaked_census[
"ruff"] = set()
584 leaked_census[
"ruff-format"] = set()
585 leaked_census[
"fleet-ansible-template"].add(
586 "scripts/checks/check_thing.py"
589 len(evaluate(files, leaked_census).uncovered) == BOTH_ROLES,
590 "ownership-census files cannot inflate another class's lint/format coverage",
595def _assert_exact_classifications(failures: list[str]) ->
None:
596 """Prove reviewed generated inputs do not exempt future files by suffix."""
598 classify(
"coprocessor/esp32c6/patches/0001-custom-rpc-sync-response-hook.patch")
599 ==
"validated-input",
600 "the pinned ESP32-C6 patch is an exact validated input",
604 classify(
"scripts/checks/patches/cppcheck-2.13/misra_9-c23-empty-initializer.patch")
605 ==
"validated-input",
606 "the pinned cppcheck MISRA patch is an exact selftested input",
610 classify(
"docs/sbom/patches/stb/0001-harden-font-parser-bounds.patch") ==
"validated-input"
611 and classify(
"docs/sbom/patches/stb/series") ==
"validated-input",
612 "the reviewed SOUP patch and series are exact replay-gated inputs",
616 classify(
"libs/ra8_c6link/proto/ra8_media_download.proto") ==
"validated-input",
617 "the pinned protobuf schema is an exact validated input",
621 classify(
"libs/ra8_c6link/src/ra8_media_download.pb-c.c") ==
"generated-source",
622 "the pinned protobuf-C output is exact generated source",
626 classify(
"infra/ansible/roles/dev_box/templates/ra8-hil-runner.service.j2")
627 ==
"ansible-systemd-template",
628 "the exact managed HIL systemd template has semantic ownership",
632 classify(
"infra/ansible/roles/dev_box/templates/ra8-hil-privileged-policy.json.j2")
634 and classify(
"scripts/hil/lib/ra8-hil-privileged.sha256") ==
"validated-input",
635 "the privilege checker owns its exact policy template and identity manifest",
638 _assert_future_classifications(failures)
641def _assert_future_classifications(failures: list[str]) ->
None:
642 """Prove lookalike paths cannot inherit an exact reviewed classification."""
644 classify(
"coprocessor/esp32c6/patches/another.patch")
is None,
645 "a future upstream patch remains unclassified",
650 "scripts/checks/patches/cppcheck-2.13/future.patch"
653 "a future cppcheck patch remains unclassified",
658 classify(
"infra/ansible/roles/other/templates/sudoers.j2")
is None,
659 "a future Jinja template remains unclassified",
663 classify(
"docs/sbom/patches/future/series")
is None,
664 "a future patch series remains unclassified",
668 classify(
"libs/new/proto/another.proto")
is None,
669 "a future protobuf schema remains unclassified",
673 classify(
"libs/new/src/another.pb-c.c") ==
"c-family",
674 "a future generated-looking C file remains first-party C",
678 classify(
"infra/ansible/roles/dev_box/templates/future.service.j2")
is None,
679 "a future Jinja template remains unclassified until it has a validator",
683 classify(
"infra/ansible/roles/dev_box/templates/future-policy.json.j2")
is None
685 "scripts/hil/lib/future.sha256"
688 "future policy and digest inputs remain unclassified without an exact checker",
693def _assert_ratchet(files: list[str], claimed: dict[str, set[str]], failures: list[str]) ->
None:
694 """Assert the recorded-gap ratchet holds, and that closed gaps really closed.
696 Separate from the plain must-fire cases because these test a different
697 mechanism: not "is this file covered?" but "has a gap we agreed to tolerate
698 grown, and did the gaps we claim to have closed actually close?".
702 many = [f
"tools/ra8_x/src/v{n}.m" for n
in range(3)]
703 grew = evaluate([*files, *many], claimed)
704 expect(bool(grew.gap_growth),
"a known gap that grows fires the ratchet", failures)
706 not evaluate([*files, many[0]], claimed).gap_growth,
707 "a known gap at or under its recorded count stays quiet",
714 orphan_cxx = evaluate([*files,
"libs/ra8_x/src/orphan.cpp"], claimed)
716 sorted({r
for r, _, _
in orphan_cxx.uncovered}) == [
"libs/ra8_x/src/orphan.cpp"]
717 and not orphan_cxx.gap_growth,
718 "an unclaimed .cpp is a violation now, not a recorded gap",
724 orphan_asm = evaluate([*files,
"newdir/boot.S"], claimed)
726 sorted({r
for r, _, _
in orphan_asm.uncovered}) == [
"newdir/boot.S"]
727 and not orphan_asm.gap_growth,
728 "an unclaimed .S is a violation now, not a recorded gap",
733def selftest() -> int:
734 """Prove the coverage model both fires and stays quiet, against fixtures.
736 Validates the classification tables first: a rule keyed on a class no
737 provider claims, or a provider claiming a class that does not exist,
738 makes every later answer meaningless.
740 Returns 0 when both directions hold, 1 otherwise.
742 print(
"check_lint_coverage.py --selftest")
743 failures: list[str] = []
745 problems = validate_tables()
746 expect(
not problems, f
"classification tables self-consistent ({problems})", failures)
748 with tempfile.TemporaryDirectory()
as tmp:
750 (root /
"present.py").touch()
752 _present_worktree_files([
"present.py",
"deleted.py"], root) == [
"present.py"],
753 "candidate inventory keeps live files and drops deleted index paths",
757 files, claimed = _fixture()
758 _assert_exact_classifications(failures)
759 _assert_quiet(files, claimed, failures)
760 _assert_fires(files, claimed, failures)
761 _assert_ratchet(files, claimed, failures)
763 return report(failures)
766def run_check(show_matrix: bool) -> int:
767 """Verify every tracked code file is claimed by at least one checker.
769 Enforces a FILE FLOOR before anything else and exits 2 below it. That is
770 the load-bearing part: if the enumeration collapses, every file is
771 trivially covered and the gate reports perfect coverage precisely because
772 it saw nothing -- the exact failure mode it exists to detect in others.
774 Returns 0 when every file is covered, 1 on a coverage failure, 2 when the
775 enumeration is too small to trust.
777 tracked = git_files()
778 if len(tracked) < FILE_FLOOR:
780 f
"check_lint_coverage.py: FATAL -- only {len(tracked)} file(s) enumerated, "
781 f
"floor is {FILE_FLOOR}.\n"
782 " A collapsed enumeration reports full coverage because it saw nothing.\n"
786 claimed: dict[str, set[str]] = {}
787 errors: list[str] = []
788 for prov
in PROVIDERS:
789 got, err = provider_files(prov, tracked)
791 errors.append(f
"{prov.name}: {err}")
792 claimed[prov.name] = got
794 sys.stderr.write(
"check_lint_coverage.py: FATAL -- provider enumeration failed.\n")
796 sys.stderr.write(f
" {err}\n")
798 " A provider that cannot report its scope leaves coverage unknown;\n"
799 " unknown is a failure, never a pass.\n"
803 report = evaluate(tracked, claimed)
804 if show_matrix
or report.ok:
805 print_matrix(report, claimed)
807 held = sum(report.gap_sizes.values())
810 f
"\ncheck_lint_coverage.py: every code file is linted and formatted, "
811 f
"except {held} file(s) in the recorded gaps above -- each tracked by "
812 "an issue and held flat by the ratchet."
815 print(
"\ncheck_lint_coverage.py: every code file is linted and formatted.")
817 print_failures(report)
821def main(argv: list[str]) -> int:
822 """Run the lint-coverage gate, its selftest, or print the coverage matrix."""
823 ap = argparse.ArgumentParser(description=__doc__)
824 ap.add_argument(
"--selftest", action=
"store_true", help=
"assert both directions")
825 ap.add_argument(
"--matrix", action=
"store_true", help=
"print the coverage matrix")
826 args = ap.parse_args(argv[1:])
829 return run_check(args.matrix)
832if __name__ ==
"__main__":
833 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.