ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
roadmap_stats.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"""Refresh the summary of the closed historical HAL completion record.
5
6This script keeps archived evidence internally consistent; it does not track
7current work. The Summary block sits between two HTML-comment markers that this
8script owns:
9
10 <!-- BEGIN SUMMARY -- DO NOT EDIT BY HAND -- managed by roadmap_stats.py -->
11 ...
12 <!-- END SUMMARY -->
13
14This script:
15
16 1. Parses every archived "###" driver section in ROADMAP.md.
17 2. Detects each driver's historical status from its `Status: ...` line.
18 3. Counts the checkboxes inside the section's fenced code
19 block (`[ ]` / `[~]` / `[x]` / `[!]`).
20 4. Rewrites the summary block with deterministic counts.
21
22Modes:
23
24 --check -- exit non-zero if the file would change. CI / pre-commit.
25 (default) -- rewrite the file in place.
26
27The check mode is the gate the pre-commit hook uses: it ensures
28the summary you committed matches the checkbox state of the rest
29of the file.
30"""
31
32from __future__ import annotations
33
34import argparse
35import pathlib
36import re
37import sys
38
39REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
40ROADMAP_PATH = REPO_ROOT / "docs" / "ROADMAP.md"
41
42BEGIN_MARK = "<!-- BEGIN SUMMARY -- DO NOT EDIT BY HAND -- managed by roadmap_stats.py -->"
43END_MARK = "<!-- END SUMMARY -->"
44
45DRIVER_HEADING_RE = re.compile(r"^###\s+(.+?)\s*$")
46STATUS_LINE_RE = re.compile(r"`\‍[(?P<mark>[ x~!])\‍]`\s*Status:")
47CHECKBOX_RE = re.compile(r"^\s*\‍[(?P<mark>[ x~!])\‍]")
48
49
50def parse_roadmap(text: str) -> tuple[dict[str, int], int, int]: # noqa: PLR0912 # parser/gate dispatch, splitting hurts readability
51 """Return ({status_counts}, total_boxes, ticked_boxes).
52
53 status_counts has keys 'DONE', 'WIP', 'BLOCKED', 'TODO'.
54 """
55 counts = {"DONE": 0, "WIP": 0, "BLOCKED": 0, "TODO": 0}
56 total_boxes = 0
57 ticked_boxes = 0
58
59 lines = text.splitlines()
60 i = 0
61 n = len(lines)
62
63 while i < n:
64 line = lines[i]
65 m = DRIVER_HEADING_RE.match(line)
66 if not m:
67 i += 1
68 continue
69
70 # Look for the Status line within the next ~6 lines.
71 status_mark: str | None = None
72 for j in range(i + 1, min(i + 8, n)):
73 sm = STATUS_LINE_RE.search(lines[j])
74 if sm:
75 status_mark = sm.group("mark")
76 break
77 if status_mark is None:
78 i += 1
79 continue
80
81 if status_mark == "x":
82 counts["DONE"] += 1
83 elif status_mark == "~":
84 counts["WIP"] += 1
85 elif status_mark == "!":
86 counts["BLOCKED"] += 1
87 else:
88 counts["TODO"] += 1
89
90 # Walk forward to the section's checkbox block, which sits
91 # inside the next fenced ``` ... ``` block. Stop at the
92 # next "###" or "##".
93 k = i + 1
94 while k < n:
95 if lines[k].startswith("### ") or lines[k].startswith("## "):
96 break
97 if lines[k].strip().startswith("```"):
98 k += 1
99 while k < n and not lines[k].strip().startswith("```"):
100 cb = CHECKBOX_RE.match(lines[k])
101 if cb:
102 total_boxes += 1
103 if cb.group("mark") == "x":
104 ticked_boxes += 1
105 k += 1
106 break
107 k += 1
108
109 i = max(k, i + 1)
110
111 return counts, total_boxes, ticked_boxes
112
113
114def render_summary(
115 counts: dict[str, int],
116 total_boxes: int,
117 ticked_boxes: int,
118) -> str:
119 """Render the summary block that sits between the ROADMAP.md markers.
120
121 Emits the BEGIN/END markers as part of the block, so the result can be
122 substituted wholesale and the markers can never be lost by a rewrite.
123 """
124 total_drivers = sum(counts.values())
125 pct = ticked_boxes / total_boxes * 100.0 if total_boxes else 0.0
126 return "\n".join(
127 [
128 BEGIN_MARK,
129 f"- Total drivers tracked: {total_drivers}",
130 f"- DONE: {counts['DONE']}",
131 f"- WIP: {counts['WIP']}",
132 f"- BLOCKED: {counts['BLOCKED']}",
133 f"- TODO: {counts['TODO']}",
134 f"- Checklist coverage: {ticked_boxes}/{total_boxes} boxes ticked ({pct:.1f}%)",
135 END_MARK,
136 ]
137 )
138
139
140def rewrite(text: str, summary: str) -> str:
141 """Replace the marked summary region of ROADMAP.md with ``summary``.
142
143 Raises when either marker is missing rather than appending: without them
144 there is no way to know which part of the file is generated, and guessing
145 would eventually overwrite hand-written prose.
146
147 Everything outside the markers is preserved byte-for-byte.
148 """
149 if BEGIN_MARK not in text or END_MARK not in text:
150 msg = "ROADMAP.md is missing the BEGIN/END SUMMARY markers"
151 raise ValueError(msg)
152 pre, _, rest = text.partition(BEGIN_MARK)
153 _, _, post = rest.partition(END_MARK)
154 return pre + summary + post
155
156
157def main(argv: list[str]) -> int:
158 """Recompute the roadmap summary, or with ``--check`` verify it is current.
159
160 ``--check`` exits 1 when the file would change and writes nothing, so CI
161 detects a stale summary instead of quietly regenerating it.
162 """
163 parser = argparse.ArgumentParser(description=__doc__)
164 parser.add_argument(
165 "--check",
166 action="store_true",
167 help="check mode: exit 1 if the file would change",
168 )
169 parser.add_argument(
170 "--roadmap",
171 default=str(ROADMAP_PATH),
172 help=f"path to ROADMAP.md (default: {ROADMAP_PATH})",
173 )
174 args = parser.parse_args(argv)
175
176 path = pathlib.Path(args.roadmap)
177 if not path.exists():
178 print(f"roadmap_stats.py: not found: {path}", file=sys.stderr)
179 return 2
180
181 text = path.read_text(encoding="utf-8")
182 counts, total_boxes, ticked_boxes = parse_roadmap(text)
183 summary = render_summary(counts, total_boxes, ticked_boxes)
184
185 try:
186 new_text = rewrite(text, summary)
187 except ValueError as exc:
188 print(f"roadmap_stats.py: {exc}", file=sys.stderr)
189 return 2
190
191 if new_text == text:
192 print(
193 f"roadmap_stats.py: unchanged "
194 f"(drivers={sum(counts.values())} "
195 f"DONE={counts['DONE']} WIP={counts['WIP']} "
196 f"BLOCKED={counts['BLOCKED']} TODO={counts['TODO']} "
197 f"boxes={ticked_boxes}/{total_boxes})",
198 file=sys.stderr,
199 )
200 return 0
201
202 if args.check:
203 print(
204 "roadmap_stats.py: ROADMAP.md summary is stale "
205 "(run `just docs::record_stats` to refresh)",
206 file=sys.stderr,
207 )
208 return 1
209
210 path.write_text(new_text, encoding="utf-8")
211 print(
212 f"roadmap_stats.py: rewrote summary "
213 f"(drivers={sum(counts.values())} "
214 f"DONE={counts['DONE']} WIP={counts['WIP']} "
215 f"BLOCKED={counts['BLOCKED']} TODO={counts['TODO']} "
216 f"boxes={ticked_boxes}/{total_boxes})",
217 file=sys.stderr,
218 )
219 return 0
220
221
222if __name__ == "__main__":
223 raise SystemExit(main(sys.argv[1:]))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157