ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
cite_ratchet.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""cite_ratchet.py -- HUM citation-COVERAGE ratchet (vs committed baseline).
5
6WHY THIS EXISTS
7---------------
8`CLAUDE.md`, `docs/STYLE_GUIDE.md` and `docs/CITATION_POLICY.md` all state the
9same MANDATORY rule: every register read/write or access must carry an external
10Hardware User's Manual citation immediately above it. The detector for that rule
11exists -- `cite_check.py --require-cites` -- and it ran in NO gate (#534).
12
13Both call sites (the `cite-check` gate and `scripts/git/pre-commit`) invoked
14`cite_check.py --strict`, which is the cite-VALIDATION pass: it checks that
15citations which ALREADY EXIST parse and point at a real chapter and page. An
16MMIO write with no citation at all is invisible to it. So the headline half of
17the policy -- does an access HAVE a cite? -- was enforced nowhere, and a
18reviewer running exactly the command `CLAUDE.md` prescribes would approve an
19entirely uncited new driver.
20
21Turning `--require-cites --strict` on wholesale was not available: the tree
22carries a measured backlog of 2884 uncited accesses across 254 files. Those
23need ACCURATE per-register HUM subsection and page citations, which cannot be
24machine-fabricated -- a wrong subsection would pass validation while being
25factually false, which is worse than no citation. Two bad options were rejected:
26
27* leave the coverage pass out of every gate -- the status quo #534 exists to
28 end, and the reason the backlog's size was unknown until now;
29* exclude the directories carrying most of it -- which converts a measured
30 debt into a permanent blind spot, the "gate that silently does nothing"
31 pattern this tree keeps finding.
32
33So the coverage pass is IN the gate, every finding is counted, and this ratchet
34holds the line. It is the same shape as `tidy_ratchet.py` and
35`mcdc_compound_ratchet.py`, which this tree already uses for exactly this
36situation:
37
38* NEW findings (any per-file count above the baseline, or any file absent from
39 the baseline that now has findings) FAIL.
40* Shrinkage PASSES with a notice to re-baseline, which locks the progress in so
41 the debt can never quietly grow back.
42* Reformatting, renaming a local, or moving an existing uncited access changes
43 no count, so it passes -- a ratchet, not a cliff.
44
45Closing the debt means the baseline reaching zero rows and being DELETED -- not
46being regenerated larger. `--update` refuses to grow a bucket for exactly that
47reason; a genuine increase has to be justified by a human editing the file,
48which leaves a reviewable diff.
49
50WHAT COUNTS AS AN ACCESS
51------------------------
52The measurement is `cite_check.find_uncited_accesses`, imported rather than
53re-parsed from console output. There is therefore exactly one definition of
54"this access lacks a citation", and no text seam between detector and gate that
55could silently stop matching.
56
57Note that `&reg->FIELD` -- taking the address of a register field -- counts.
58That is deliberate and is asserted in `cite_check.py --selftest`. In this tree
59address-of is overwhelmingly a register handed to a register-agnostic poll
60helper (`ra8_hw_wait_flag_set32(&reg->CFDGSTS, ...)`), an aliasing volatile
61pointer that is then read and written, or a DMA/DTC descriptor field naming the
62register the engine will write. In every one of those the load or store happens
63somewhere that does NOT name the register, so the address-of site is the only
64reviewable place a citation can live.
65
66BASELINE NORMALISATION -- per-file COUNTS, not raw finding lines.
67Raw findings carry line numbers, which churn on every unrelated edit above them,
68and a snippet of source, which embeds identifiers. A `file -> count` map is
69invariant under both, still trips the moment a file gains another uncited
70access, and stays small and diffable. Per-file rather than per-function because
71an uncited access is a property of a source location, not of a decision: many
72sit in register-map headers and in table-driven initialisation blocks that have
73no enclosing function at all.
74
75USAGE
76 python3 scripts/checks/cite_ratchet.py --selftest # assert it fires
77 python3 scripts/checks/cite_ratchet.py --check # the CI gate
78 python3 scripts/checks/cite_ratchet.py --update # re-baseline
79 python3 scripts/checks/cite_ratchet.py --list # burn-down list
80
81Copyright (c) 2026 Brighton Sikarskie
82SPDX-License-Identifier: MIT
83"""
84
85from __future__ import annotations
86
87import argparse
88import pathlib
89import sys
90import tempfile
91from collections import Counter
92
93sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
94
95from cite_check import SOURCE_SUFFIXES, iter_source_files, scan_access_coverage
96from lint_targets import first_party_paths
97
98REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
99BASELINE_FILE = REPO_ROOT / ".github" / "cite-baseline.txt"
100
101MAX_DETAIL_LINES = 10
102"""Cap on offending buckets echoed before the report truncates."""
103
104BASELINE_COLUMNS = 2
105"""Column count of one baseline row: file, count."""
106
107MIN_SCANNED_FILES = 1700
108"""Refuse to ratchet a scan that saw implausibly few source files.
109
110`cite_check`'s derived first-party C scope is 2638 files today (libs 947,
111tests 784, examples 458, tools 211, apps 143, port 94). A scan that finds a
112fraction of that is not looking at this repository -- a partial checkout, a
113`git ls-files` that came back short, an enumeration that stopped covering a
114top-level directory. It would report FEWER uncited accesses, which reads as a
115burn-down, and `--update` would freeze that as the accepted state. 1700 leaves
116room for genuine deletion while catching the loss of any whole top-level tree.
117"""
118
119MIN_ACCESS_LINES = 3000
120"""...and refuse a scan whose DETECTOR stopped matching register accesses.
121
122The file floor above cannot catch this: the right 2120 files get opened and
123`ACCESS_RE` simply matches nothing, so the backlog collapses to zero and
124`--check` passes cleanly forever. That is this repository's dominant defect
125class, so the count of MMIO access lines the detector found -- cited or not --
126is measured alongside the findings and floored too. The tree carries 3971 of
127them today; 3000 clears real churn while a detector that broke drops to
128near zero.
129"""
130
131
132def scan_files(files: list[pathlib.Path], root: pathlib.Path) -> tuple[Counter[str], int, int]:
133 """Measure uncited accesses over an explicit file list.
134
135 Args:
136 files: Source files to scan. Read as UTF-8 with replacement, matching
137 `cite_check.check_file`.
138 root: Directory the returned keys are made relative to.
139
140 Returns:
141 A ``(counts, scanned_file_count, access_line_count)`` triple. ``counts``
142 maps a root-relative path to the number of uncited accesses in it. The
143 two scalars accompany it so the caller can refuse a scan whose scope
144 never got established -- a count is only trustworthy once the thing
145 that produced it is known to have looked at the tree AND to still be
146 matching register accesses at all.
147 """
148 counts: Counter[str] = Counter()
149 access_lines = 0
150 for path in files:
151 text = path.read_text(encoding="utf-8", errors="replace")
152 findings, seen = scan_access_coverage(path, text)
153 access_lines += seen
154 if findings:
155 counts[str(path.relative_to(root))] = len(findings)
156 return counts, len(files), access_lines
157
158
159def tree_files() -> list[pathlib.Path]:
160 """Every first-party C file `cite_check` scans, as absolute paths.
161
162 Returns:
163 The same derived scope `cite_check.main` uses with no path arguments,
164 so the ratchet and the detector can never disagree about what is in
165 scope.
166 """
167 targets = [REPO_ROOT / rel for rel in first_party_paths(SOURCE_SUFFIXES)]
168 return list(iter_source_files(targets))
169
170
171def scan_tree() -> tuple[Counter[str], int, int]:
172 """Run `scan_files` over the whole repository.
173
174 Returns:
175 The ``(counts, scanned_file_count, access_line_count)`` triple for this
176 repository, keyed on repo-relative paths.
177 """
178 return scan_files(tree_files(), REPO_ROOT)
179
180
181def load_baseline() -> Counter[str]:
182 """Read the committed baseline into a Counter.
183
184 Returns:
185 A ``file -> count`` Counter. A missing baseline file means an empty
186 one, which is the end state this ratchet is driving toward.
187 """
188 counts: Counter[str] = Counter()
189 if not BASELINE_FILE.is_file():
190 return counts
191 for raw in BASELINE_FILE.read_text(encoding="ascii").splitlines():
192 line = raw.strip()
193 if not line or line.startswith("#"):
194 continue
195 parts = line.split("\t")
196 if len(parts) != BASELINE_COLUMNS:
197 continue
198 counts[parts[0]] = int(parts[1])
199 return counts
200
201
202def write_baseline(counts: Counter[str]) -> None:
203 """Write `counts` out in the committed, sorted, diffable form.
204
205 Args:
206 counts: The ``file -> count`` map to freeze. Zero-valued buckets are
207 dropped so a burned-down file leaves the file entirely.
208 """
209 total = sum(counts.values())
210 lines = [
211 "# HUM citation-COVERAGE ratchet baseline -- per-file uncited-access counts.",
212 "# Consumed by scripts/checks/cite_ratchet.py --check (CI gate: cite-check).",
213 "#",
214 "# Each row is a source file holding N direct MMIO register accesses with NO",
215 '# `/* HUM Ch X.Y "..." p NNNN */` citation above them. CLAUDE.md, the style',
216 "# guide and docs/CITATION_POLICY.md all call that citation MANDATORY, but the",
217 "# detector for it (cite_check.py --require-cites) ran in no gate at all, so",
218 "# the rule was aspirational and the debt went unmeasured (#534).",
219 "#",
220 f"# Total at this baseline: {total} uncited access(es)",
221 f"# across {len(counts)} file(s).",
222 "#",
223 "# The gate fails on any INCREASE, so the debt is frozen and can only be",
224 "# burned down; a newly-added uncited access raises a count and fails.",
225 "#",
226 "# Burn one down: read the register in the Hardware User's Manual",
227 "# (docs/reference/ra8d2-hardware-user-manual.pdf), then put the real chapter,",
228 "# subsection and page above the access:",
229 "#",
230 '# /* HUM Ch 25.2.3 "AGT Control Register" p 1194 */',
231 "# reg->AGTCR = k_ra8_agt_start;",
232 "#",
233 "# One citation covers the contiguous block of accesses beneath it. Do NOT",
234 "# guess a subsection: a wrong one passes cite_check --strict while being",
235 "# factually false, which is worse than the missing citation it replaced.",
236 "#",
237 "# Regenerate after burning findings down:",
238 "# python3 scripts/checks/cite_ratchet.py --update",
239 "#",
240 "# MOVING a file retires its row and creates a new one, which reads as growth",
241 "# -- so the gate fails and --update refuses, by design. Rename the row here",
242 "# BY HAND, keeping the count identical. That is a one-line reviewable diff,",
243 "# and it is deliberately not automated: rename detection that guessed wrong",
244 "# would silently absorb a genuinely new uncited access.",
245 "#",
246 "# Closing this out means this file reaching zero rows and being",
247 "# DELETED -- never regenerated larger.",
248 "#",
249 "# file<TAB>count",
250 ]
251 for path, n in sorted(counts.items()):
252 if n:
253 lines.append(f"{path}\t{n}")
254 BASELINE_FILE.write_text("\n".join(lines) + "\n", encoding="ascii")
255
256
257def scope_reason(files: int, access_lines: int) -> str | None:
258 """Return a refusal reason when the scan's scope is not credible, else None.
259
260 Guards BOTH directions of the ratchet: a scan that examined a fragment of
261 the tree, or whose detector stopped recognising register accesses, produces
262 a number that must not be compared against the baseline and must certainly
263 never be written as one.
264
265 Args:
266 files: How many source files the scan actually opened.
267 access_lines: How many MMIO access lines it recognised, cited or not.
268
269 Returns:
270 A human-readable reason to refuse, or None when both floors are met.
271 """
272 if files < MIN_SCANNED_FILES:
273 return (
274 f"only {files} first-party C file(s) scanned, "
275 f"below the {MIN_SCANNED_FILES} floor.\n"
276 " The scan is not looking at this repository (a partial checkout,\n"
277 " or a git ls-files enumeration that came back short). A partial\n"
278 " scan reports FEWER uncited accesses, which reads as a burn-down;\n"
279 " refusing it is the only way that cannot be mistaken for progress."
280 )
281 if access_lines < MIN_ACCESS_LINES:
282 return (
283 f"only {access_lines} MMIO access line(s) recognised, "
284 f"below the {MIN_ACCESS_LINES} floor.\n"
285 " The right files were opened and the DETECTOR matched almost\n"
286 " nothing in them, so every count is meaningless in both\n"
287 " directions. What changed is cite_check.ACCESS_RE, not the code.\n"
288 " Run `python3 scripts/checks/cite_check.py --selftest` first."
289 )
290 return None
291
292
293def report(current: Counter[str], baseline: Counter[str]) -> int:
294 """Compare a scan against the baseline and print the verdict.
295
296 Args:
297 current: Freshly measured ``file -> count`` map.
298 baseline: The committed ``file -> count`` map.
299
300 Returns:
301 0 when every count is at or below the baseline, 1 when one grew.
302 """
303 grown = []
304 for path, n in sorted(current.items()):
305 was = baseline.get(path, 0)
306 if n > was:
307 grown.append((path, was, n))
308
309 total_now = sum(current.values())
310 total_was = sum(baseline.values())
311
312 if grown:
313 print(file=sys.stderr)
314 print("cite ratchet: NEW uncited MMIO accesses above the baseline", file=sys.stderr)
315 print(file=sys.stderr)
316 for path, was, now in grown[:MAX_DETAIL_LINES]:
317 print(f" {path}: {was} -> {now}", file=sys.stderr)
318 if len(grown) > MAX_DETAIL_LINES:
319 print(f" ... and {len(grown) - MAX_DETAIL_LINES} more file(s)", file=sys.stderr)
320 print(file=sys.stderr)
321 print(
322 "Every direct register read/write or access needs a Hardware User's\n"
323 "Manual citation immediately above it:\n"
324 "\n"
325 ' /* HUM Ch 25.2.3 "AGT Control Register" p 1194 */\n'
326 " reg->AGTCR = k_ra8_agt_start;\n"
327 "\n"
328 "One citation covers the contiguous block of accesses beneath it. See\n"
329 "the offending lines with:\n"
330 " python3 scripts/checks/cite_check.py --require-cites <file>\n"
331 "\n"
332 "The baseline is a burn-down of debt that predates enforcement, not a\n"
333 "place to record new debt. Do not --update to make this pass.",
334 file=sys.stderr,
335 )
336 return 1
337
338 print(f"cite ratchet: {total_now} uncited access(es), baseline {total_was} -- no growth.")
339 if total_now < total_was:
340 burned = total_was - total_now
341 print(f" {burned} access(es) burned down. Re-baseline to lock it in:")
342 print(" python3 scripts/checks/cite_ratchet.py --update")
343 return 0
344
345
346# ---------------------------------------------------------------------------
347# Self-test
348# ---------------------------------------------------------------------------
349
350# One uncited MMIO write. The measurement must COUNT it.
351_ST_UNCITED_C = """\
352void drv_init(void)
353{
354 volatile r_agt_regs_t* reg = agt0();
355 reg->AGTCR = 0U;
356}
357"""
358
359# The same write, cited. The measurement must NOT count it.
360_ST_CITED_C = """\
361void drv_start(void)
362{
363 volatile r_agt_regs_t* reg = agt0();
364 /* HUM Ch 25.2.3 "AGT Control Register" p 1194 */
365 reg->AGTCR = 1U;
366}
367"""
368
369# A SECOND uncited access added to a file the baseline already knows about --
370# the growth shape a whole-tree pass/fail check would miss entirely.
371_ST_UNCITED_C_GROWN = """\
372void drv_init(void)
373{
374 volatile r_agt_regs_t* reg = agt0();
375 reg->AGTCR = 0U;
376 reg->AGTMR1 = 0U;
377}
378"""
379
380
381_ST_FIXTURE_FILES = 2
382"""Files the selftest fixture plants: one uncited, one cited."""
383
384_ST_GROWN_COUNT = 2
385"""Uncited accesses in the fixture after the growth edit."""
386
387
388def _st_write(root: pathlib.Path, rel: str, body: str) -> pathlib.Path:
389 """Write `body` to `rel` under `root`, creating parent directories.
390
391 Args:
392 root: Fixture tree root.
393 rel: Path relative to `root`.
394 body: ASCII file contents.
395
396 Returns:
397 The absolute path written.
398 """
399 dst = root / rel
400 dst.parent.mkdir(parents=True, exist_ok=True)
401 dst.write_text(body, encoding="ascii")
402 return dst
403
404
405def _selftest_scan(tmp: pathlib.Path) -> list[str]:
406 """Assert the REAL measurement counts the right accesses and no others.
407
408 Args:
409 tmp: A throwaway directory to build the fixture tree in.
410
411 Returns:
412 A list of failure descriptions; empty when every assertion held.
413 """
414 failures: list[str] = []
415 root = tmp / "tree"
416 uncited = _st_write(root, "libs/uncited.c", _ST_UNCITED_C)
417 cited = _st_write(root, "libs/cited.c", _ST_CITED_C)
418
419 counts, files, access_lines = scan_files([uncited, cited], root)
420 if counts.get("libs/uncited.c") != 1:
421 failures.append("scan_files() did not count the uncited MMIO write")
422 if "libs/cited.c" in counts:
423 failures.append("scan_files() counted an access that carries a HUM citation")
424 if files != _ST_FIXTURE_FILES:
425 failures.append(f"scan_files() reported {files} scanned file(s), expected 2")
426 if access_lines != _ST_FIXTURE_FILES:
427 failures.append(f"scan_files() saw {access_lines} access line(s), expected 2")
428
429 # The measurement must SEE the growth it is meant to gate on.
430 _st_write(root, "libs/uncited.c", _ST_UNCITED_C_GROWN)
431 grown, _files, _lines = scan_files([uncited, cited], root)
432 if grown.get("libs/uncited.c") != _ST_GROWN_COUNT:
433 failures.append("scan_files() did not see a second uncited access added to a known file")
434 return failures
435
436
437def _selftest_ratchet() -> list[str]:
438 """Assert the growth verdict and the baseline round-trip, both directions.
439
440 Returns:
441 A list of failure descriptions; empty when every assertion held.
442 """
443 failures: list[str] = []
444
445 base: Counter[str] = Counter({"libs/f.c": 1})
446 # FAILS when a known bucket grows ...
447 if report(Counter({"libs/f.c": 2}), base) == 0:
448 failures.append("report() passed a file whose count GREW above the baseline")
449 # ... and when a file the baseline has never seen appears.
450 if report(Counter({"libs/new.c": 1}), base) == 0:
451 failures.append("report() passed a finding in a file absent from the baseline")
452 # PASSES when unchanged or shrinking -- the ratchet must not be a cliff.
453 if report(Counter({"libs/f.c": 1}), base) != 0:
454 failures.append("report() failed an unchanged bucket")
455 if report(Counter(), base) != 0:
456 failures.append("report() failed a fully burned-down baseline")
457
458 # Round-trip: what is written must read back identically, or a re-baseline
459 # would silently reshape the debt it claims to be freezing.
460 original = BASELINE_FILE.read_text(encoding="ascii") if BASELINE_FILE.is_file() else None
461 try:
462 fixture: Counter[str] = Counter({"libs/a.c": 3, "tests/test_b.c": 1})
463 write_baseline(fixture)
464 if load_baseline() != fixture:
465 failures.append("write_baseline()/load_baseline() did not round-trip")
466 finally:
467 if original is None:
468 BASELINE_FILE.unlink(missing_ok=True)
469 else:
470 BASELINE_FILE.write_text(original, encoding="ascii")
471
472 return failures
473
474
475def _selftest_scope_guard() -> list[str]:
476 """Assert the scope guards refuse an implausible scan, in both directions.
477
478 Returns:
479 A list of failure descriptions; empty when every assertion held.
480 """
481 failures: list[str] = []
482 if scope_reason(MIN_SCANNED_FILES - 1, MIN_ACCESS_LINES) is None:
483 failures.append("scope_reason() accepted a scan that saw too few source files")
484 if scope_reason(MIN_SCANNED_FILES, MIN_ACCESS_LINES - 1) is None:
485 failures.append("scope_reason() accepted a scan whose detector matched almost nothing")
486 if scope_reason(MIN_SCANNED_FILES, MIN_ACCESS_LINES) is not None:
487 failures.append("scope_reason() rejected a scan that is exactly at both floors")
488 return failures
489
490
491def selftest() -> int:
492 """Assert the measurement and the ratchet fire, in BOTH directions.
493
494 Runs the REAL `find_uncited_accesses` against a throwaway fixture tree, so
495 a detector that quietly stopped matching cannot pass as clean: it must
496 COUNT the uncited write, must NOT count the cited one, and must see a
497 second uncited access appear in a file it already knew about.
498
499 Returns:
500 0 when every assertion held, 1 otherwise.
501 """
502 with tempfile.TemporaryDirectory() as td:
503 failures = _selftest_scan(pathlib.Path(td))
504 failures += _selftest_ratchet() + _selftest_scope_guard()
505
506 if failures:
507 print("SELFTEST FAILED:", file=sys.stderr)
508 for problem in failures:
509 print(f" - {problem}", file=sys.stderr)
510 return 1
511 print(
512 "selftest: HUM citation-coverage ratchet OK "
513 "(counts an uncited access; ignores a cited one; fails on growth and on "
514 "an unseen file, passes on equal and on shrinkage; refuses an "
515 "implausible scope)."
516 )
517 return 0
518
519
520# ---------------------------------------------------------------------------
521# Main
522# ---------------------------------------------------------------------------
523
524
525def _list_backlog() -> int:
526 """Print every uncited access, worst file first, for burn-down work.
527
528 Returns:
529 0 always -- listing is a report, not a verdict.
530 """
531 counts, _files, _lines = scan_tree()
532 for path, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])):
533 print(f"{n:5d} {path}")
534 print(f"total: {sum(counts.values())} uncited access(es) across {len(counts)} file(s)")
535 return 0
536
537
538def _do_update(current: Counter[str], files: int) -> int:
539 """Rewrite the baseline, refusing to grow any existing bucket.
540
541 Args:
542 current: The freshly measured ``file -> count`` map.
543 files: How many source files the scan opened, for the summary line.
544
545 Returns:
546 0 when the baseline was written, 1 when the update was refused.
547 """
548 baseline = load_baseline()
549 # Seeding the very first baseline necessarily "grows" every bucket from
550 # nothing, so the no-growth rule applies only once a baseline exists.
551 seeding = not BASELINE_FILE.is_file()
552 grew = [] if seeding else [p for p, n in current.items() if n > baseline.get(p, 0)]
553 if grew:
554 print(
555 f"refusing to --update: {len(grew)} file(s) would GROW. "
556 "The baseline is a burn-down; write the missing HUM citations instead.",
557 file=sys.stderr,
558 )
559 for path in grew[:MAX_DETAIL_LINES]:
560 print(f" {path}", file=sys.stderr)
561 return 1
562 write_baseline(current)
563 print(
564 f"baseline updated: {sum(current.values())} uncited access(es) recorded "
565 f"across {len(current)} file(s) ({files} file(s) scanned)."
566 )
567 return 0
568
569
570def main() -> int:
571 """Ratchet uncited MMIO accesses against the committed baseline.
572
573 A ratchet, not a floor: ``--check`` fails when a count RISES, and
574 ``--update`` lowers the baseline once citations are written. The baseline
575 can therefore only move downward, which is what stops a large legacy count
576 from being permanently accepted while still letting CI block a new one
577 today.
578
579 Returns:
580 0 when every count is at or below the baseline, 1 when one grew or the
581 scan's scope was not credible.
582 """
583 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
584 parser.add_argument("--check", action="store_true", help="gate against the baseline")
585 parser.add_argument("--update", action="store_true", help="rewrite the baseline")
586 parser.add_argument("--list", action="store_true", help="print the whole backlog")
587 parser.add_argument("--selftest", action="store_true", help="assert this gate still fires")
588 args = parser.parse_args()
589
590 if args.selftest:
591 return selftest()
592 if args.list:
593 return _list_backlog()
594 if not (args.check or args.update):
595 parser.error("one of --check / --update / --list / --selftest is required")
596
597 current, files, access_lines = scan_tree()
598
599 # Refuse to ratchet a scan whose scope never got established, in EITHER
600 # direction, before comparing or writing anything.
601 broken = scope_reason(files, access_lines)
602 if broken:
603 verb = "--update" if args.update else "--check"
604 print(f"refusing to {verb}: {broken}", file=sys.stderr)
605 return 1
606
607 if args.update:
608 return _do_update(current, files)
609
610 print(f"cite ratchet: scanned {files} file(s), {access_lines} MMIO access line(s).")
611 return report(current, load_baseline())
612
613
614if __name__ == "__main__":
615 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298