4"""Refresh the summary of the closed historical HAL completion record.
6This script keeps archived evidence internally consistent; it does not track
7current work. The Summary block sits between two HTML-comment markers that this
10 <!-- BEGIN SUMMARY -- DO NOT EDIT BY HAND -- managed by roadmap_stats.py -->
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.
24 --check -- exit non-zero if the file would change. CI / pre-commit.
25 (default) -- rewrite the file in place.
27The check mode is the gate the pre-commit hook uses: it ensures
28the summary you committed matches the checkbox state of the rest
32from __future__
import annotations
39REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
40ROADMAP_PATH = REPO_ROOT /
"docs" /
"ROADMAP.md"
42BEGIN_MARK =
"<!-- BEGIN SUMMARY -- DO NOT EDIT BY HAND -- managed by roadmap_stats.py -->"
43END_MARK =
"<!-- END SUMMARY -->"
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~!])\]")
50def parse_roadmap(text: str) -> tuple[dict[str, int], int, int]:
51 """Return ({status_counts}, total_boxes, ticked_boxes).
53 status_counts has keys 'DONE', 'WIP', 'BLOCKED', 'TODO'.
55 counts = {
"DONE": 0,
"WIP": 0,
"BLOCKED": 0,
"TODO": 0}
59 lines = text.splitlines()
65 m = DRIVER_HEADING_RE.match(line)
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])
75 status_mark = sm.group(
"mark")
77 if status_mark
is None:
81 if status_mark ==
"x":
83 elif status_mark ==
"~":
85 elif status_mark ==
"!":
86 counts[
"BLOCKED"] += 1
95 if lines[k].startswith(
"### ")
or lines[k].startswith(
"## "):
97 if lines[k].strip().startswith(
"```"):
99 while k < n
and not lines[k].strip().startswith(
"```"):
100 cb = CHECKBOX_RE.match(lines[k])
103 if cb.group(
"mark") ==
"x":
111 return counts, total_boxes, ticked_boxes
115 counts: dict[str, int],
119 """Render the summary block that sits between the ROADMAP.md markers.
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.
124 total_drivers = sum(counts.values())
125 pct = ticked_boxes / total_boxes * 100.0
if total_boxes
else 0.0
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}%)",
140def rewrite(text: str, summary: str) -> str:
141 """Replace the marked summary region of ROADMAP.md with ``summary``.
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.
147 Everything outside the markers is preserved byte-for-byte.
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
157def main(argv: list[str]) -> int:
158 """Recompute the roadmap summary, or with ``--check`` verify it is current.
160 ``--check`` exits 1 when the file would change and writes nothing, so CI
161 detects a stale summary instead of quietly regenerating it.
163 parser = argparse.ArgumentParser(description=__doc__)
167 help=
"check mode: exit 1 if the file would change",
171 default=str(ROADMAP_PATH),
172 help=f
"path to ROADMAP.md (default: {ROADMAP_PATH})",
174 args = parser.parse_args(argv)
176 path = pathlib.Path(args.roadmap)
177 if not path.exists():
178 print(f
"roadmap_stats.py: not found: {path}", file=sys.stderr)
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)
186 new_text = rewrite(text, summary)
187 except ValueError
as exc:
188 print(f
"roadmap_stats.py: {exc}", file=sys.stderr)
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})",
204 "roadmap_stats.py: ROADMAP.md summary is stale "
205 "(run `just docs::record_stats` to refresh)",
210 path.write_text(new_text, encoding=
"utf-8")
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})",
222if __name__ ==
"__main__":
223 raise SystemExit(
main(sys.argv[1:]))
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.