"""Load generate-docs.py by path: its hyphenated filename is not a valid
Root cause. DF-002 edited the markdown sources under docs/ (dropping the nonexistent doctor / spec validate / spec info / spec convert commands) but never re-ran the HTML build, so the tracked docs/_build/ pages kept shipping dead commands to anyone browsing the built docs. The trap: docs/scripts/generate-docs.py rglobs all docs/*.md, so a naive full re-run also emits ~20 new untracked HTML files for runbooks/dogfood/examples — polluting the PR with unrelated files.
Fix. A targeted regen script that derives the exact tracked set from git itself (git ls-files docs/_build/*.html → maps each back to its markdown source) and re-renders only those pages, reusing generate-docs.py's renderer so the targeted and full builds can never diverge. New file docs/scripts/regenerate-html.py:
#!/usr/bin/env python3
"""Regenerate ONLY the Git-tracked docs HTML.
generate-docs.py rglobs every docs/*.md and thus emits ~20 NEW untracked
HTML pages (runbooks/dogfood/examples) on a full run. For a docs-content
fix that is scope creep, so this script derives the exact tracked set from
`git ls-files docs/_build/*.html` and re-renders just those pages from
their markdown sources, using the same renderer as the full build.
"""
import importlib.util
import subprocess
import sys
from pathlib import Path
# exec_module() would write generate-docs-*.pyc into docs/scripts/__pycache__/
# and pollute the tree; the build must only touch the HTML files it owns.
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
SRC = ROOT / "docs"
OUT = ROOT / "docs" / "_build"
def _load_generate_docs():
"""Load generate-docs.py by path: its hyphenated filename is not a valid
import name, so plain `from generate_docs import render` cannot work."""
gen_path = Path(__file__).resolve().parent / "generate-docs.py"
spec = importlib.util.spec_from_file_location("generate_docs", gen_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load shared renderer: {gen_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
render = _load_generate_docs().render # single source of truth
def tracked_html() -> list[Path]:
"""Exact set of HTML files Git tracks under docs/_build/."""
proc = subprocess.run(["git", "ls-files", "docs/_build"], cwd=ROOT,
capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError("not a git checkout. This script must run inside the repo.")
prefix = "docs/_build/"
return [OUT / rel[len(prefix):] for rel in proc.stdout.splitlines()
if rel.startswith(prefix) and rel.endswith(".html")]
def main() -> int:
html_pages = tracked_html()
if not html_pages:
print("ERROR: no tracked HTML pages found under docs/_build/", file=sys.stderr)
return 1
# Preflight all sources before writing anything (no half-rebuilds).
pairs = []
for html in sorted(html_pages):
md = SRC / html.relative_to(OUT).with_suffix(".md")
if not md.exists():
print(f"ERROR: {html.relative_to(ROOT)} has no source {md.relative_to(ROOT)}",
file=sys.stderr)
return 1
pairs.append((md, html))
print(f"regenerating {len(pairs)} tracked pages:")
for md, html in pairs:
print(f" {md.relative_to(ROOT)} -> {html.relative_to(ROOT)}")
render(md, html)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run it and verify:
python3 docs/scripts/regenerate-html.py # rebuilds exactly the 15 tracked pages
grep -rE 'doctor|spec validate|spec info|spec convert' docs/_build/ # → 0 matches
(Use -E: the bare grep -r 'doctor|spec validate|...' treats | literally and trivially matches nothing; -E makes the alternation a real check.)
CONTRIBUTING.md — replace the docs checklist's full-build step so future edits can't go stale again:
- [ ] Edit the **markdown source** (`docs/*.md`); never hand-edit `docs/_build/`.
- [ ] Regenerate the tracked HTML build so `docs/_build/` never goes stale:
`python3 docs/scripts/regenerate-html.py`
(rebuilds only the Git-tracked pages — run `generate-docs.py` only for a
deliberate full build, and clean up the untracked runbooks/dogfood/examples
output it emits).
- [ ] Verify no dead commands leaked into the build:
`grep -rE 'doctor|spec validate|spec info|spec convert' docs/_build/`
must return zero matches.
- [ ] Commit the markdown **and** the regenerated `docs/_build/` HTML in the
same PR, or the build goes stale exactly like it did after DF-002.
No repo exists in this environment, so I built a faithful simulation at `/tmp/godocs-fix-demo` (git repo; `docs/` with index + 14 command pages, runbooks/dogfood/examples pages, rglob `generate-docs.py`, stale `_build/`) and reproduced the exact scenario: initial commit → DF-002 markdown fix commit → stale tracked HTML with `git status` clean. Results: | # | Test | Result | |---|------|--------| | 1 | Stale build detected: `grep -rE 'doctor\|spec validate\|spec info\|spec convert' docs/_build/` before fix | 6 matches (dead commands present) | | 2 | Naive full run (`generate-docs.py`) | 2 tracked HTML modified **+ 20 new untracked HTML** (runbooks 6, dogfood 7, examples 7) — the scope creep | | 3 | Targeted `regenerate-html.py` | regenerates exactly **15 tracked pages** (14 content + index); `git status`: 0 untracked, only the 2 genuinely-changed pages differ (13 others byte-identical) | | 4 | Dead-command grep after fix (both `-r` literal and `-rE`) | **0 matches** | | 5 | Fixed content present | committed `spec.html` contains `gdoc spec check` + `Exit codes`; no validate/info/convert | | 6 | Idempotency | re-run produces byte-identical `sha256sum` output | | 7 | Missing source md (moved `index.md`) | preflight error `docs/_build/index.html has no source docs/index.md`, exit 1, **nothing written** | | 8 | Not a git checkout | clear `RuntimeError: not a git checkout...`, exit 1 | | 9 | Tracked asset (`docs/_build/assets/docs.css`) | untouched by the script (filters `.html`) | | 10 | CONTRIBUTING.md checklist | diff applied; fix commit contains only: `CONTRIBUTING.md`, `regenerate-html.py`, `index.html`, `spec.html` | One real bug surfaced during verification: `generate-docs.py`'s hyphenated filename can't be imported (`from generate_docs import ...` → `ModuleNotFoundError`), fixed with `importlib.util.spec_from_file_location`; and `exec_module` polluted the tree with `__pycache__/`, fixed with `sys.dont_write_bytecode = True`. Final commit `1aa1e07` has a clean 4-file diff and a fully clean working tree.
{"model": "deepseek-v4-flash", "problem_class": "go-docs-html-build-stale", "result": "passed", "tests": 10}