3"""The audit-only report: docs/DOXYGEN_GAPS.csv and docs/DOXYGEN_GAPS.md.
5Separate from the gate because it answers a different question. The gate asks
6"is the tree clean" and exits non-zero; this asks "where is the remaining work
7and how much of it is there", writes two artefacts, and always exits 0.
9It walks the same files the gate does, through
10:func:`doxy_scope.function_files`, so the report can never describe a
11different set of files than the gate enforces over -- the two used to carry
12the same walk inline, twice. That accessor also carries the shared vacuity
13floor: "always exits 0" is about findings, not about infrastructure, and a
14report sized from a collapsed walk would understate the remaining work as
18from __future__
import annotations
20from collections
import Counter
22from doxy_functions
import audit_file
23from doxy_scope
import MODULE_PATH_MIN_DEPTH, function_files, repo_root
27MD_REPORT_LINE_CAP = 200
34def collect_rows() -> list:
35 """Audit every in-scope file, sorted by path then line."""
37 for path
in function_files():
38 rows.extend(audit_file(path))
39 rows.sort(key=
lambda r: (r[0], r[1]))
43def _write_csv(gap_rows: list) ->
None:
44 """Write the full row-per-function gap table."""
45 csv_path = repo_root() /
"docs" /
"DOXYGEN_GAPS.csv"
46 with csv_path.open(
"w", encoding=
"ascii")
as f:
47 f.write(
"source_file,line,function_name,missing_tags,severity\n")
48 for src, line, name, missing, sev
in gap_rows:
49 f.write(f
"{src},{line},{name},{';'.join(missing)},{sev}\n")
52def _tag_frequency(gap_rows: list) -> Counter:
53 """Count missing tags, collapsing per-instance spellings into their family.
55 ``@param[buf]`` and ``@param[len]`` are two instances of one missing tag,
56 and ``@pre(<2:1)`` records a count rather than a distinct tag; without the
57 collapse the frequency table lists every parameter name in the tree.
59 counter: Counter = Counter()
62 if t.startswith(
"@param["):
63 counter[
"@param"] += 1
64 elif t.startswith(
"@pre"):
66 elif t.startswith(
"@post"):
73def _module_of(path: str) -> str:
74 """Two-segment module label for a repo-relative path (e.g. "libs/ra8_hal")."""
75 parts = path.split(
"/")
76 if len(parts) >= MODULE_PATH_MIN_DEPTH:
77 return f
"{parts[0]}/{parts[1]}"
81def _table(title: str, header: str, sep: str, rows: list[tuple[str, int]]) -> list[str]:
82 """One Markdown section: a heading and a two-column count table.
84 ``sep`` is passed rather than generated so the emitted report stays
85 byte-identical to the committed docs/DOXYGEN_GAPS.md -- the dash widths
86 are cosmetic to a renderer but a spurious diff to a reviewer.
88 out = [f
"## {title}",
"", header, sep]
89 out.extend(f
"| `{label}` | {count} |" for label, count
in rows)
94def _render_markdown(rows: list, gap_rows: list) -> str:
95 """Build the human-readable summary report."""
96 total_funcs = len(rows)
97 total_gaps = len(gap_rows)
98 total_missing_tags = sum(len(r[3])
for r
in gap_rows)
101 "# Doxygen Documentation Gap Report",
103 "Audit-only report generated by `scripts/checks/doxy_audit.py` against the",
104 "Doxygen Documentation Requirements in `CLAUDE.md`. Scope: `libs/`,",
105 "`port/` and all of `tools/` (excluding vendored and generated",
110 f
"- Total functions audited: {total_funcs}",
111 f
"- Functions with gaps: {total_gaps}",
112 f
"- Total missing-tag instances: {total_missing_tags}",
116 "Most-frequently-missing tags",
119 _tag_frequency(gap_rows).most_common(),
122 f
"Worst {TOP_MODULES} modules by gap count",
123 "| Module | Functions with gaps |",
124 "|--------|---------------------|",
125 Counter(_module_of(r[0])
for r
in gap_rows).most_common(TOP_MODULES),
128 f
"Top {TOP_FILES} files by gap count",
129 "| File | Functions with gaps |",
130 "|------|---------------------|",
131 Counter(r[0]
for r
in gap_rows).most_common(TOP_FILES),
134 "## Severity legend",
136 "- `high`: missing `@brief` or any `@param[...]`",
137 "- `medium`: missing `@return` / `@retval` / `@pre` / `@post`",
138 "- `low`: only optional / informational tags missing (`@note`, `@since`, ...)",
140 "See `docs/DOXYGEN_GAPS.csv` for the full row-by-row data.",
144 "| Date | Functions with gaps | Missing-tag instances |",
145 "|------|---------------------|-----------------------|",
146 "| 2026-05-02 (original) | 2557 | 20328 |",
147 "| 2026-05-02 (refresh) | 663 | 4935 |",
148 f
"| 2026-05-02 (auditor false-pos fix) | {total_gaps} | {total_missing_tags} |",
152 return "\n".join(lines[:MD_REPORT_LINE_CAP])
155def run_report() -> int:
156 """Write both artefacts and print the headline counts. Always exits 0."""
157 rows = collect_rows()
158 gap_rows = [r
for r
in rows
if r[3]]
161 md_path = repo_root() /
"docs" /
"DOXYGEN_GAPS.md"
162 md_path.write_text(_render_markdown(rows, gap_rows), encoding=
"ascii")
164 total_missing_tags = sum(len(r[3])
for r
in gap_rows)
165 print(f
"functions={len(rows)} gaps={len(gap_rows)} missing_tags={total_missing_tags}")