Adds a per-section table of contents at the top of the doc, wrapped in <details> so it's collapsed by default (the spec body is visible as soon as the file opens; click "Contents" to navigate). Every H2 and H3 heading is linked, including the unnumbered §14 failure-mode categories. Also wraps §11.6 (NomadNet specifics) in <details> — it's already flagged "informational, not normative" and is the longest H3 sub-tree in the document. Readers implementing only the §11 wire layer can skim past it; readers implementing a NomadNet client one click away. tools/_gen_toc.py regenerates the ToC by re-extracting headings. Run it after adding/removing/renaming any H2 or H3. Picked this over the per-layer-file split previously listed in todo because the split would have broken ~37 cross-references (flow docs, verifier docstrings, agent.md, README) for marginal reader benefit at the document's current size (~3300 lines). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.1 KiB
Python
72 lines
2.1 KiB
Python
"""
|
|
One-shot helper that builds a navigation Table of Contents for SPEC.md
|
|
by extracting every H2 and H3 heading and computing the GitHub anchor
|
|
for each. The output is printed to stdout. To regenerate the ToC after
|
|
adding/removing/renaming headings:
|
|
|
|
python tools/_gen_toc.py > /tmp/toc.md
|
|
# paste contents into SPEC.md between the <!-- TOC --> markers
|
|
|
|
This is a maintenance helper, not a verifier; not listed in
|
|
`tools/README.md`. Delete if it stops being useful.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SPEC_PATH = os.path.join(REPO_ROOT, "SPEC.md")
|
|
|
|
|
|
def slugify(s: str) -> str:
|
|
s = s.lower()
|
|
s = s.replace("`", "")
|
|
# Drop everything except word chars (letters/digits/_), whitespace, and hyphen
|
|
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
|
|
s = re.sub(r"\s+", "-", s)
|
|
s = re.sub(r"-+", "-", s)
|
|
return s.strip("-")
|
|
|
|
|
|
def main() -> int:
|
|
with open(SPEC_PATH, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
|
|
in_fence = False
|
|
entries = []
|
|
for line in lines:
|
|
stripped = line.rstrip("\n")
|
|
if stripped.startswith("```"):
|
|
in_fence = not in_fence
|
|
continue
|
|
if in_fence:
|
|
continue
|
|
m2 = re.match(r"^## (.+?)\s*$", stripped)
|
|
m3 = re.match(r"^### (.+?)\s*$", stripped)
|
|
if m2:
|
|
title = m2.group(1)
|
|
entries.append(("h2", title, slugify(title)))
|
|
elif m3:
|
|
title = m3.group(1)
|
|
entries.append(("h3", title, slugify(title)))
|
|
|
|
out = sys.stdout
|
|
out.reconfigure(encoding="utf-8") if hasattr(out, "reconfigure") else None
|
|
|
|
out.write("<!-- TOC: regenerate via `python tools/_gen_toc.py` -->\n")
|
|
out.write("## Contents\n\n")
|
|
for kind, title, slug in entries:
|
|
if kind == "h2":
|
|
out.write(f"- [{title}](#{slug})\n")
|
|
else:
|
|
out.write(f" - [{title}](#{slug})\n")
|
|
out.write("<!-- /TOC -->\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|