4"""Gate: register symbols in first-party code must exist in the HUM.
6``cite_check.py`` asks "is this citation well-formed, and does it name a real
7chapter and a page inside that chapter?". It cannot ask "does the cited text
8describe the register being touched?", and it cannot ask "does this register
9exist at all?". Three landed defects lived in that gap, every one of them
10carrying a citation naming a REAL chapter and a REAL page:
12* the ``ra8_rsip`` crypto family addressed registers HUM Ch 52 does not
13 publish -- that chapter is six pages long and describes no registers at all;
14* ``ra8_ptp_regs.h`` declared a thirteen-register window at ``0x403E_0100``,
15 a reserved hole in the GPTP aperture. The demo printed ``gptp: clock PASS``
16 because a reserved aperture echoed its own writes back (#498);
17* ``ra8_etha_regs.h`` declared ``EASCR`` at ``0x0580``, a symbol absent from
18 the whole of Ch 32, and ~26 ETHA citations pointed at real pages describing
19 other registers (#539).
21This gate closes that gap by connecting a register SYMBOL in our source to the
22symbol table the manual publishes. Four rules, all reported per file:
24``unknown-cite-symbol``
25 A citation names symbol ``S`` in chapter ``C`` and ``S`` appears nowhere
26 in chapter ``C``. This is the signal that actually worked by hand: a
27 register name that is absent from the cited chapter is almost always
28 invented. When ``S`` does exist in some other chapter the diagnostic says
29 so, because a mis-attributed real register is a much smaller mistake.
32 A citation names a real symbol but points outside the pages that symbol's
33 description occupies. A description routinely spills onto the following
34 page, so the comparison is against a page RANGE (heading page up to the
35 next section heading) rather than a single page -- a naive equality check
36 would shout about hundreds of correct citations.
38``unknown-window-symbol``
39 A register-window struct member, or an offset enumerator, in any
40 first-party header whose symbol appears in none of the chapters that
41 header names. This is the rule that would have caught #498, whose
42 citations named no symbol at all.
44``wrong-window-offset``
45 An offset enumerator whose value disagrees with the offset the manual
46 prints for that register.
48WHAT THIS DOES NOT CHECK
49------------------------
50Being precise about the edge is part of not crying wolf:
52* **Bit fields.** Only register-level symbols are cross-checked. A driver
53 writing the right register with the wrong field -- which is exactly what
54 #539's third defect was, ``EATASGL0`` receiving a gate state instead of an
55 entry address -- is invisible here and always will be. Nothing mechanical
56 substitutes for reading the field table.
57* **Citations that name no register.** ``/* HUM Ch 32.4 "Error Interrupt
58 Sources" p 1685 */`` carries no symbol, so there is nothing to resolve;
59 1665 of the tree's citations name a register and are checked, the rest are
60 skipped rather than guessed at.
61* **Headers that name no HUM chapter.** ``ra8_touch_gt911_regs.h`` describes
62 an external I2C touch controller that is not in this manual at all, so it
63 is unattributable by construction and is left alone.
64* **Absolute-address enumerators, entirely.** The window scan recognises
65 struct members and OFFSET enumerators (``k_ra8_etha_off_eatasgl0``). An
66 enumerator holding a whole address instead -- ``k_ra8_wdt_ofs0_addr =
67 0x03001E04UL`` -- matches neither shape and is not examined at all. Nor
68 would the symbol rule help if it were: ``OFS0`` and ``OFS3`` are REAL
69 registers (HUM Ch 7.2.1 p 280 and Ch 7.2.6 p 287), documented by absolute
70 address with no offset, so this gate has nothing to compare. The defect in
71 that case is that ``0x03001E04`` / ``0x03001E20`` fall inside a window both
72 manuals mark ``Reserved area`` -- a wrong ADDRESS, not an invented NAME.
73 That needs an address-versus-reserved-window guard, which is a different
74 rule and is being added separately under #545. Do not read this gate as
77WHAT MAKES THIS GATE ABLE TO FAIL
78---------------------------------
79Both acceptance properties the #190 audit distilled are designed in, because
80they are precisely what a gate like this gets wrong:
82* **No check compares a constant to itself.** The authority is always the
83 committed manual PDF, re-parsed on every run. The committed
84 ``HUM_REGISTERS.csv`` is a reviewable convenience and is byte-compared
85 against that fresh parse, so a CSV edited to make a violation disappear
86 fails the gate instead of hiding the defect.
87* **Every scan has a vacuity floor.** A PDF extraction that silently yielded
88 zero symbols would report every register in the tree clean, forever -- the
89 single most likely failure mode here. ``hum_regmap.assert_not_vacuous``
90 makes that a hard error, and this gate additionally floors the number of
91 source claims it found: a scan that stops matching our own headers is just
92 as dishonest as one that stops matching the manual.
96A full-tree sweep starts with a backlog these four rules did not create: they
97made it visible. Switching rules off, or running warn-only, is the "gate that
98silently does nothing" pattern this tree keeps finding, so instead the debt is
99COUNTED per (file, rule) in ``.github/hum-register-baseline.txt`` -- the same
100shape ``tidy_ratchet.py`` and ``misra_ratchet.py`` use. A new or increased
101count FAILS. Shrinkage passes with a notice to re-baseline, which locks the
102progress in. Closing the debt means the baseline reaching zero rows and being
103deleted, so ``--update`` refuses to grow a bucket.
106 check_hum_register_map.py --selftest # prove it fires AND stays quiet
107 check_hum_register_map.py # the gate
108 check_hum_register_map.py --update # re-baseline (shrink only)
111from __future__
import annotations
117from collections
import Counter
118from dataclasses
import dataclass
120sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
123import hum_regmap_scan
124from lint_targets
import first_party_paths
125from selftest_assert
import expect, report
127REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
128BASELINE_FILE = REPO_ROOT /
".github" /
"hum-register-baseline.txt"
130SOURCE_SUFFIXES = (
".c",
".h",
".cpp",
".hpp")
133 "unknown-cite-symbol",
135 "unknown-window-symbol",
136 "wrong-window-offset",
142MIN_FILES_SCANNED = 500
144MIN_WINDOW_CLAIMS = 400
147"""Cap on offending findings echoed before the report truncates."""
150"""Column count of one baseline row: file, rule, count."""
152ARRAY_STRIDE_RE = re.compile(
r"\+\s*(?P<stride>0[xX][0-9A-Fa-f]+)\s*\*")
153"""The stride of an indexed register's offset, e.g. the 0x4 of "0x0040 + 0x4 * q"."""
156@dataclass(frozen=True)
158 """One rule violation, ready to print and to bucket for the ratchet."""
166def _check_cite(claim: hum_regmap_scan.SymbolClaim, rmap: hum_regmap.RegisterMap) -> Finding |
None:
167 """Apply the two citation rules to one citation claim."""
168 if claim.chapter
is None or claim.page
is None or claim.page_end
is None:
170 rows = rmap.lookup(claim.chapter, claim.symbol)
172 elsewhere = sorted({row.chapter
for row
in rmap.find_anywhere(claim.symbol)})
173 where = f
"but Ch {elsewhere} describes it" if elsewhere
else "and no chapter describes it"
177 "unknown-cite-symbol",
178 f
"{claim.symbol} does not appear in HUM Ch {claim.chapter} -- {where}",
180 if any(row.covers(claim.page, claim.page_end)
for row
in rows):
182 spans =
", ".join(f
"p {row.page}-{row.page_end}" for row
in rows)
187 f
"{claim.symbol} cited at p {claim.page}; HUM describes it at {spans}",
192 symbol: str, chapters: set[int], rmap: hum_regmap.RegisterMap
193) -> list[hum_regmap.HumRegister]:
194 """Resolve ``EATMFSC0``-style names for the manual's ``EATMFSCq`` array.
196 A header names the base element of an indexed register with an explicit
197 ``0`` (``k_ra8_etha_off_eatmfsc0`` for what the manual calls
198 ``EATMFSCq``). That is a real alias, not an invented register, so it is
199 resolved rather than reported -- but only when the manual's own entry is
200 genuinely INDEXED, i.e. its offset expression carries a stride. Without
201 that condition the rule would also swallow ``EAEIS9``, and the point of
202 trailing digits staying significant is that ``EAEIS0`` and ``EAEIS1`` are
205 base = symbol.rstrip(
"0123456789")
206 if base == symbol
or not base:
208 rows = [row
for chapter
in sorted(chapters)
for row
in rmap.lookup(chapter, base)]
209 return [row
for row
in rows
if "*" in row.offset_expr]
212def _expected_offsets(row: hum_regmap.HumRegister, symbol: str) -> list[int]:
213 """Byte offsets the manual allows for a header symbol naming `row`.
215 For a plain register that is just the printed offset. For an INDEXED
216 register the header may name any element explicitly -- ``IPCSEM15`` for
217 the manual's ``IPCSEMn`` at ``0x000 + 0x004 * n`` -- so the element index
218 is read off the header's own symbol and the stride applied. Comparing
219 element 15 against the array's base offset was reporting sixteen correct
220 declarations as wrong.
224 base = int(row.offset, 16)
225 stride_match = ARRAY_STRIDE_RE.search(row.offset_expr)
226 if stride_match
is None:
228 digits = symbol[len(symbol.rstrip(
"0123456789")) :]
231 return [base, base + int(digits) * int(stride_match.group(
"stride"), 16)]
235 claim: hum_regmap_scan.SymbolClaim, chapters: set[int], rmap: hum_regmap.RegisterMap
237 """Apply the two register-window rules to one struct or offset claim."""
238 rows = [row
for chapter
in sorted(chapters)
for row
in rmap.lookup(chapter, claim.symbol)]
240 rows = _array_base_rows(claim.symbol, chapters, rmap)
242 elsewhere = sorted({row.chapter
for row
in rmap.find_anywhere(claim.symbol)})
243 where = f
"but Ch {elsewhere} describes it" if elsewhere
else "and no chapter describes it"
247 "unknown-window-symbol",
248 f
"{claim.symbol} does not appear in the chapters this file cites "
249 f
"({sorted(chapters)}) -- {where}",
251 if claim.offset
is None:
253 expected = sorted({addr
for row
in rows
for addr
in _expected_offsets(row, claim.symbol)})
254 if not expected
or claim.offset
in expected:
256 printed =
", ".join(f
"0x{addr:04X}" for addr
in expected)
260 "wrong-window-offset",
261 f
"{claim.symbol} declared at 0x{claim.offset:04X}; HUM puts it at {printed}",
267 """Everything one full-tree scan produced, findings and vacuity counters."""
269 findings: list[Finding]
275def scan_tree(rmap: hum_regmap.RegisterMap, paths: list[str] |
None =
None) -> ScanResult:
276 """Apply every rule to every first-party C file."""
280 else first_party_paths(SOURCE_SUFFIXES, respect_language_excludes=
True)
282 result = ScanResult([], 0, 0, 0)
285 text = (REPO_ROOT / rel).read_text(encoding=
"utf-8", errors=
"replace")
289 cites = hum_regmap_scan.cite_claims(rel, text)
290 result.cite_claims += len(cites)
292 finding = _check_cite(claim, rmap)
293 if finding
is not None:
294 result.findings.append(finding)
299 if not rel.endswith((
".h",
".hpp")):
301 windows = hum_regmap_scan.window_claims(rel, text)
302 result.window_claims += len(windows)
310 chapters = hum_regmap_scan.cited_chapters(text)
313 for claim
in windows:
314 finding = _check_window(claim, chapters, rmap)
315 if finding
is not None:
316 result.findings.append(finding)
320def assert_scan_not_vacuous(result: ScanResult) ->
None:
321 """Fail when the source-side scan found too little to have worked.
324 hum_regmap.HumExtractionError: a floor was not met.
327 if result.files < MIN_FILES_SCANNED:
328 problems.append(f
"{result.files} files scanned < floor {MIN_FILES_SCANNED}")
329 if result.cite_claims < MIN_CITE_CLAIMS:
330 problems.append(f
"{result.cite_claims} register citations < floor {MIN_CITE_CLAIMS}")
331 if result.window_claims < MIN_WINDOW_CLAIMS:
332 problems.append(f
"{result.window_claims} window symbols < floor {MIN_WINDOW_CLAIMS}")
334 raise hum_regmap.HumExtractionError(
335 "source scan is vacuous -- every rule would pass because nothing was "
336 "examined: " +
"; ".join(problems)
340def bucket(findings: list[Finding]) -> Counter[tuple[str, str]]:
341 """Reduce findings to the ``(file, rule) -> count`` the ratchet compares.
343 Line numbers churn on every unrelated edit above them, so they are not
344 part of the key: a count per file per rule is invariant under that and
345 still trips the moment a file gains another violation.
347 return Counter((finding.path, finding.rule)
for finding
in findings)
350def load_baseline() -> Counter[tuple[str, str]]:
351 """Read the committed per-(file, rule) debt, or an empty tally."""
352 counts: Counter[tuple[str, str]] = Counter()
353 if not BASELINE_FILE.is_file():
355 for raw
in BASELINE_FILE.read_text(encoding=
"utf-8").splitlines():
357 if not line
or line.startswith(
"#"):
359 parts = line.split(
"\t")
360 if len(parts) != BASELINE_COLUMNS:
362 counts[(parts[0], parts[1])] = int(parts[2])
366def write_baseline(counts: Counter[tuple[str, str]]) ->
None:
367 """Write the ratchet file in a stable, diffable order."""
369 "# HUM register-map debt, per (file, rule). Generated by",
370 "# scripts/checks/check_hum_register_map.py --update.",
372 "# A NEW or INCREASED count fails the gate. Shrinkage passes with a notice",
373 "# to re-baseline, which locks the progress in. Closing the debt means this",
374 "# file reaching zero rows and being deleted -- --update refuses to grow a",
375 "# bucket, so a genuine increase has to be a reviewable hand edit.",
377 for (path, rule), count
in sorted(counts.items()):
379 lines.append(f
"{path}\t{rule}\t{count}")
380 BASELINE_FILE.parent.mkdir(parents=
True, exist_ok=
True)
381 BASELINE_FILE.write_text(
"\n".join(lines) +
"\n", encoding=
"utf-8")
384def compare(actual: Counter[tuple[str, str]], baseline: Counter[tuple[str, str]]) -> list[str]:
385 """Return one message per bucket that grew above its baseline."""
387 for key, count
in sorted(actual.items()):
388 allowed = baseline.get(key, 0)
391 regressions.append(f
"{path}: {rule} {count} > baseline {allowed}")
395def freshness_error(rows: list[hum_regmap.HumRegister]) -> str |
None:
396 """Describe how the committed CSV differs from a fresh parse of the PDF."""
397 fresh = hum_regmap.to_csv(rows)
398 if not hum_regmap.HUM_CSV.is_file():
399 return f
"{hum_regmap.HUM_CSV} is missing"
400 committed = hum_regmap.HUM_CSV.read_text(encoding=
"utf-8")
401 if committed == fresh:
404 f
"{hum_regmap.HUM_CSV.relative_to(REPO_ROOT)} does not match a fresh parse of "
405 f
"{hum_regmap.HUM_PDF.name} ({len(committed)} vs {len(fresh)} bytes); "
406 "run scripts/gen/gen_hum_register_map.py"
410def _print_findings(findings: list[Finding]) ->
None:
411 """Echo up to ``MAX_DETAIL_LINES`` findings, worst rule first."""
412 order = {rule: index
for index, rule
in enumerate(RULES)}
413 ranked = sorted(findings, key=
lambda f: (order.get(f.rule, 99), f.path, f.line))
414 for finding
in ranked[:MAX_DETAIL_LINES]:
416 f
" {finding.path}:{finding.line}: [{finding.rule}] {finding.detail}", file=sys.stderr
418 if len(ranked) > MAX_DETAIL_LINES:
419 print(f
" ... and {len(ranked) - MAX_DETAIL_LINES} more", file=sys.stderr)
422def _verdict(actual: Counter[tuple[str, str]], findings: list[Finding]) -> int:
423 """Compare against the committed baseline and print the gate's verdict."""
424 baseline = load_baseline()
425 regressions = compare(actual, baseline)
428 f
"\nFAIL: {len(regressions)} (file, rule) bucket(s) above the committed baseline.",
431 for message
in regressions:
432 print(f
" {message}", file=sys.stderr)
433 grown = {key
for key, count
in actual.items()
if count > baseline.get(key, 0)}
434 print(
"\nOffending findings:", file=sys.stderr)
435 _print_findings([f
for f
in findings
if (f.path, f.rule)
in grown])
438 shrunk = sum(1
for key, count
in baseline.items()
if actual.get(key, 0) < count)
441 f
"\nNOTICE: {shrunk} bucket(s) shrank. Re-baseline with "
442 "`python3 scripts/checks/check_hum_register_map.py --update` to lock it in."
444 print(
"\nPASS: no register symbol regressed against the manual.")
448def run_gate(update: bool) -> int:
449 """Regenerate the map, scan the tree, and apply the ratchet."""
451 rows = hum_regmap.extract_from_pdf()
452 except hum_regmap.HumExtractionError
as exc:
453 print(f
"ERROR: {exc}", file=sys.stderr)
456 stale = freshness_error(rows)
457 if stale
is not None:
458 print(f
"ERROR: {stale}", file=sys.stderr)
461 rmap = hum_regmap.RegisterMap(rows)
462 result = scan_tree(rmap)
464 assert_scan_not_vacuous(result)
465 except hum_regmap.HumExtractionError
as exc:
466 print(f
"ERROR: {exc}", file=sys.stderr)
469 actual = bucket(result.findings)
471 f
"HUM register map: {len(rows)} registers over {len(rmap.chapters)} chapters; "
472 f
"scanned {result.files} files, {result.cite_claims} register citations, "
473 f
"{result.window_claims} window symbols"
476 total = sum(count
for (_, name), count
in actual.items()
if name == rule)
477 print(f
" {rule}: {total}")
480 baseline = load_baseline()
485 seeding =
not BASELINE_FILE.is_file()
486 grown = []
if seeding
else [k
for k, c
in actual.items()
if c > baseline.get(k, 0)]
488 print(f
"seeding {BASELINE_FILE.relative_to(REPO_ROOT)} for the first time")
490 print(
"ERROR: --update refuses to grow the baseline:", file=sys.stderr)
491 for path, rule
in sorted(grown):
492 print(f
" {path}: {rule}", file=sys.stderr)
494 write_baseline(actual)
495 print(f
"re-baselined {BASELINE_FILE.relative_to(REPO_ROOT)}")
498 return _verdict(actual, result.findings)
501def _selftest_map() -> hum_regmap.RegisterMap:
502 """A tiny hand-built register map standing in for the manual."""
503 return hum_regmap.RegisterMap(
505 hum_regmap.HumRegister(32,
"3.1.1",
"EAMC",
"0x0000",
"0x0000", 1630, 1631,
"Mode Cfg"),
506 hum_regmap.HumRegister(
507 32,
"3.2.6",
"EATMFSCq",
"0x0040",
"0x0040 + 0x4 * q", 1635, 1636,
"Max Frame"
513 hum_regmap.HumRegister(1,
"1", f
"REG{n:04d}",
"0x0000",
"0x0000", 100, 100,
"pad")
514 for n
in range(hum_regmap.MIN_TOTAL_ROWS)
517 hum_regmap.HumRegister(ch,
"1", f
"CH{ch}REG",
"0x0000",
"0x0000", 100, 100,
"pad")
518 for ch
in range(2, hum_regmap.MIN_CHAPTERS_WITH_REGISTERS + 2)
523def _selftest_cite_rules(failures: list[str]) ->
None:
524 """Assert the two citation rules fire and stay quiet as documented."""
525 rmap = _selftest_map()
528 '/* HUM Ch 32.3.1.1 "EAMC : Mode Cfg" p 1630 */\n'
529 '/* HUM Ch 32.3.1.1 "EAMC : Mode Cfg" p 1631 */\n'
530 '/* HUM Ch 32.3.2.6 "EATMFSCq : Max Frame" p 1635 */\n'
533 f
for c
in hum_regmap_scan.cite_claims(
"good.c", good)
if (f := _check_cite(c, rmap))
535 expect(
not findings,
"real symbol, real page, spilled page: no finding", failures)
537 invented =
'/* HUM Ch 32.3 "EASCR : Security Configuration" p 1697 */\n'
539 f
for c
in hum_regmap_scan.cite_claims(
"bad.c", invented)
if (f := _check_cite(c, rmap))
542 len(findings) == 1
and findings[0].rule ==
"unknown-cite-symbol",
543 "invented cited symbol fires unknown-cite-symbol",
547 misplaced =
'/* HUM Ch 32.3.1.1 "EAMC : Mode Cfg" p 1690 */\n'
549 f
for c
in hum_regmap_scan.cite_claims(
"bad.c", misplaced)
if (f := _check_cite(c, rmap))
552 len(findings) == 1
and findings[0].rule ==
"wrong-cite-page",
553 "real symbol on the wrong page fires wrong-cite-page",
557 prose =
'/* HUM Ch 32.4 "Error Interrupt Sources" p 1685 */\n'
559 not hum_regmap_scan.cite_claims(
"p.c", prose),
560 "a citation naming no register yields no claim (no false positive)",
565def _window_findings(source: str, rmap: hum_regmap.RegisterMap) -> list[Finding]:
566 """Run the register-window rules over a snippet, for the selftest."""
569 for claim
in hum_regmap_scan.window_claims(
"regs.h", source)
570 if (finding := _check_window(claim, {32}, rmap))
is not None
574def _selftest_window_rules(failures: list[str]) ->
None:
575 """Assert the two register-window rules fire and stay quiet as documented."""
576 rmap = _selftest_map()
579 " volatile uint32_t EAMC;\n"
580 " volatile uint32_t reserved08;\n"
581 " volatile uint32_t EATMFSC[8];\n"
582 " k_ra8_etha_off_eamc = 0x0000U,\n"
584 claims = hum_regmap_scan.window_claims(
"regs.h", window_ok)
585 findings = _window_findings(window_ok, rmap)
586 expect(
not findings,
"real window symbols and offsets: no finding", failures)
588 all(claim.symbol !=
"RESERVED08" for claim
in claims),
589 "reserved padding is not treated as a register",
593 window_bad =
" volatile uint32_t EASCR;\n"
594 findings = _window_findings(window_bad, rmap)
596 len(findings) == 1
and findings[0].rule ==
"unknown-window-symbol",
597 "invented struct member fires unknown-window-symbol",
602def _selftest_array_rules(failures: list[str]) ->
None:
603 """Assert indexed-register aliases resolve without weakening the rule."""
604 rmap = _selftest_map()
606 array_base =
" k_ra8_etha_off_eatmfsc0 = 0x0040U,\n"
607 findings = _window_findings(array_base, rmap)
610 "EATMFSC0 resolves to the manual's indexed EATMFSCq (no false positive)",
614 not_indexed =
" volatile uint32_t EAMC9;\n"
615 findings = _window_findings(not_indexed, rmap)
617 len(findings) == 1
and findings[0].rule ==
"unknown-window-symbol",
618 "a trailing digit on a NON-indexed register is still reported (EAMC9)",
622 element =
" k_ra8_etha_off_eatmfsc3 = 0x004CU,\n"
623 findings = _window_findings(element, rmap)
626 "an explicitly-named array element uses base + index * stride (EATMFSC3)",
630 element_bad =
" k_ra8_etha_off_eatmfsc3 = 0x0050U,\n"
631 findings = _window_findings(element_bad, rmap)
633 len(findings) == 1
and findings[0].rule ==
"wrong-window-offset",
634 "an array element at the wrong stride multiple still fires",
638 offset_bad =
" k_ra8_etha_off_eamc = 0x0580U,\n"
639 findings = _window_findings(offset_bad, rmap)
641 len(findings) == 1
and findings[0].rule ==
"wrong-window-offset",
642 "offset disagreeing with the manual fires wrong-window-offset",
647def _selftest_guards(failures: list[str]) ->
None:
648 """Assert the vacuity floors and the ratchet direction both hold."""
651 hum_regmap.assert_not_vacuous(empty)
653 except hum_regmap.HumExtractionError:
655 expect(fired,
"an empty manual extraction is rejected as vacuous", failures)
658 hum_regmap.assert_not_vacuous(_selftest_map().rows)
660 except hum_regmap.HumExtractionError:
662 expect(quiet,
"a full manual extraction passes the vacuity floor", failures)
665 assert_scan_not_vacuous(ScanResult([], 0, 0, 0))
667 except hum_regmap.HumExtractionError:
669 expect(fired,
"a source scan that examined nothing is rejected as vacuous", failures)
671 base = Counter({(
"a.c",
"unknown-cite-symbol"): 2})
673 not compare(Counter({(
"a.c",
"unknown-cite-symbol"): 2}), base),
674 "a bucket at its baseline passes",
678 not compare(Counter({(
"a.c",
"unknown-cite-symbol"): 1}), base),
679 "a bucket below its baseline passes",
683 len(compare(Counter({(
"a.c",
"unknown-cite-symbol"): 3}), base)) == 1,
684 "a bucket above its baseline fails",
688 len(compare(Counter({(
"b.c",
"unknown-cite-symbol"): 1}), base)) == 1,
689 "a file absent from the baseline fails on its first finding",
694 hum_regmap.canonical_symbol(
"EATMFSCq") == hum_regmap.canonical_symbol(
"EATMFSC"),
695 "array index letters normalise away (EATMFSCq == EATMFSC)",
699 hum_regmap.canonical_symbol(
"EAEIS0") != hum_regmap.canonical_symbol(
"EAEIS1"),
700 "trailing digits stay significant (EAEIS0 != EAEIS1)",
705def selftest() -> int:
706 """Prove the detector fires on broken input and stays quiet on good input."""
707 print(
"check_hum_register_map selftest:")
708 failures: list[str] = []
709 _selftest_cite_rules(failures)
710 _selftest_window_rules(failures)
711 _selftest_array_rules(failures)
712 _selftest_guards(failures)
713 return report(failures)
716def main(argv: list[str] |
None =
None) -> int:
718 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
719 parser.add_argument(
"--selftest", action=
"store_true", help=
"assert both directions and exit")
720 parser.add_argument(
"--update", action=
"store_true", help=
"re-baseline (shrink only)")
721 args = parser.parse_args(argv)
724 return run_gate(args.update)
727if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.