4"""check_obsolete_standards.py -- Reject references to obsolete safety standards.
6Per the project's IEC 61508 SIL 3 / DO-178C target (CLAUDE.md):
8- DO-178B was superseded by DO-178C in December 2011 and is no longer
9 recognized by FAA / EASA / Transport Canada. Any new doc, test
10 comment, or commit message that says "DO-178B" must be corrected to
11 DO-178C (or to IEC 61508 SIL 3 if industry-neutral).
13Two scan modes, and NEITHER is the default -- a bare invocation is an error.
15 check_obsolete_standards.py --all # the whole tracked tree (CI)
16 check_obsolete_standards.py --staged # the git index (the commit hook)
17 check_obsolete_standards.py --selftest # prove the detector both ways
19That is deliberate. This checker read ``git diff --cached`` unconditionally,
20and the ``pre-commit-checks`` CI gate invoked it with no arguments -- where
21nothing is ever staged. It therefore scanned ZERO files on every CI run and
22printed ``0 findings``, for as long as it had been wired there. The same
23defect was found and fixed in ``check_mcdc_block.py`` (#325) and
24``check_new_compound_has_mcdc.py`` (#355); this is the third instance, so the
25remedy is the one those adopted: make the mode explicit so a caller cannot
26silently get the vacuous one, and refuse an empty tree-wide scan outright.
28Whitelists the project's own self-documentation (CLAUDE.md note, this script,
29the migration memo) where the tokens appear in *explanatory* context.
32from __future__
import annotations
38from pathlib
import Path
40sys.path.insert(0, str(Path(__file__).resolve().parent))
42from lint_targets
import first_party_paths
43from selftest_assert
import expect, report
48 re.compile(
r"\bDO-178B\b"),
49 re.compile(
r"\bDO178B\b"),
57 "scripts/checks/check_obsolete_standards.py",
58 "scripts/git/pre-commit",
70SCAN_SUFFIXES: tuple[str, ...] = (
89def staged_files() -> list[str]:
90 """Return the list of files staged for the in-progress commit."""
92 [
"git",
"diff",
"--cached",
"--name-only",
"--diff-filter=ACMR"],
97 return [line
for line
in out.splitlines()
if line]
100def tracked_files() -> list[str]:
101 """Return every tracked first-party path this checker scans tree-wide.
103 Enumeration goes through ``lint_targets.first_party_paths`` -- the shared
104 derived-scope primitive -- rather than a hand-written directory list, so a
105 new top-level directory is covered the day it lands and cannot quietly
106 fall out of scope the way a hardcoded tuple does.
108 named = [
"justfile",
"Justfile",
"CMakeLists.txt"]
109 paths = list(first_party_paths(SCAN_SUFFIXES))
110 paths.extend(path
for path
in first_party_paths(tuple(named))
if Path(path).name
in set(named))
111 return sorted(set(paths))
114def scannable(path: Path) -> bool:
115 """Decide whether a staged path is subject to the obsolete-standard scan.
117 Three subtractions, in order of cost: a path that no longer exists (staged
118 as a deletion, so there is nothing to read), an explicitly whitelisted
119 file, and anything under a ``third_party`` directory -- vendored code may
120 cite whatever standard it was written against and is not ours to correct.
122 Suffix is matched case-insensitively, so a ``.MD`` or ``.YAML`` cannot
123 duck the scan on spelling alone.
125 if not path.is_file():
127 if str(path)
in WHITELIST:
129 if any(part ==
"third_party" for part
in path.parts):
131 suffix = path.suffix.lower()
147 return path.name
in {
"justfile",
"Justfile",
"CMakeLists.txt"}
150def scan_text(text: str) -> list[tuple[int, str]]:
151 """Return ``(lineno, line)`` for each line citing an obsolete standard.
153 Only the first matching pattern per line is recorded, so a line naming two
154 obsolete standards is reported once -- the fix is to rewrite the line, and
155 a second finding on it would just be noise.
157 hits: list[tuple[int, str]] = []
158 for lineno, line
in enumerate(text.splitlines(), start=1):
159 for pat
in FORBIDDEN_PATTERNS:
161 hits.append((lineno, line.rstrip()))
166def scan_paths(names: list[str]) -> list[tuple[str, int, str]]:
167 """Scan the given paths, returning every ``(path, lineno, line)`` finding."""
168 findings: list[tuple[str, int, str]] = []
171 if not scannable(path):
174 text = path.read_text(encoding=
"utf-8")
175 except (UnicodeDecodeError, OSError):
177 findings.extend((name, lineno, line)
for lineno, line
in scan_text(text))
181def selftest() -> int:
182 """Prove the detector fires on a citation and stays quiet without one.
184 Also asserts the tree-wide enumeration is not empty. That last assertion
185 is the point of this selftest: the scan was silently reduced to zero files
186 for its whole life in CI, and a detector nobody has watched fire on a real
187 file list is indistinguishable from one that has stopped looking.
190 0 when every assertion held in both directions, 1 otherwise.
192 failures: list[str] = []
193 for label, text, must_fire
in (
194 (
"a DO-178B citation",
"/* Written to DO-178B Level B. */",
True),
195 (
"the hyphen-less DO178B spelling",
"# targets DO178B objectives",
True),
196 (
"the current DO-178C citation",
"/* Written to DO-178C Level B. */",
False),
197 (
"an unrelated line naming no standard",
"int x = 178;",
False),
199 fired = bool(scan_text(text))
200 expectation =
"must fire" if must_fire
else "must stay quiet"
201 expect(fired == must_fire, f
"{label} ({expectation})", failures)
203 tracked = tracked_files()
205 len(tracked) >= TREE_FLOOR,
206 f
"tree-wide enumeration sees {len(tracked)} file(s) (floor {TREE_FLOOR})",
209 return report(failures)
213 """Fail any file citing a superseded safety standard (e.g. DO-178B).
215 The scan mode is mandatory rather than defaulted. ``--all`` sweeps the
216 tracked tree and is what CI runs; ``--staged`` reads the git index and is
217 what the commit hook runs. A bare invocation used to mean "staged", which
218 made the CI gate scan nothing at all and report a clean tree forever.
221 0 when the selected set is clean, 1 with each offending line quoted,
222 and 2 when no mode was given or a tree-wide sweep found too few files
225 parser = argparse.ArgumentParser(description=
"ban references to superseded safety standards")
226 mode = parser.add_mutually_exclusive_group()
227 mode.add_argument(
"--all", action=
"store_true", help=
"scan every tracked first-party file")
228 mode.add_argument(
"--staged", action=
"store_true", help=
"scan the git index (commit hook)")
229 parser.add_argument(
"--selftest", action=
"store_true", help=
"prove the detector both ways")
230 args = parser.parse_args()
235 if not (args.all
or args.staged):
237 "check_obsolete_standards.py: pass --all (CI, the whole tracked tree) or\n"
238 " --staged (the pre-commit hook, the git index). There is no default:\n"
239 " this checker silently defaulted to --staged and so scanned nothing at\n"
240 " all in CI, where the index is always empty.\n"
244 names = tracked_files()
if args.all
else staged_files()
245 if args.all
and len(names) < TREE_FLOOR:
247 f
"check_obsolete_standards.py: FATAL -- the tree-wide sweep enumerated only\n"
248 f
" {len(names)} file(s), below the floor of {TREE_FLOOR}. An empty or\n"
249 f
" collapsed file list reports success because it saw nothing.\n"
253 findings = scan_paths(names)
255 print(
"[FAIL] Obsolete standard reference detected (DO-178B was")
256 print(
" superseded by DO-178C in December 2011). Use")
257 print(
" DO-178C, IEC 61508 SIL 3, or ISO 26262 ASIL C/D")
258 print(
" per CLAUDE.md. Offending lines:")
259 for name, lineno, line
in findings:
260 print(f
" {name}:{lineno}: {line}")
263 print(f
"check_obsolete_standards.py: 0 findings across {len(names)} file(s).")
267if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.