Root cause (two compounding bugs):
1. .gitignore contained docs/generated/ → generator outputs never entered git, so drift was invisible and ageable (the 6-week staleness).
2. The schema glob was **/*.json while all 9 schemas were .yaml → generator matched 0 files and died, producing no merged spec at all.
Fix (4 parts):
1. Stop gitignoring generated artifacts — outputs are now first-class tracked files:
# .gitignore (fixed)
__pycache__/
*.pyc
.venv/
*.swp
.DS_Store
# Generated docs are deliberately TRACKED in git so staleness is caught by CI.
# Do NOT add docs/generated/ back here.
2. Fix the schema glob — with a critical detail: pathlib.Path.glob() does not support brace expansion (**/*.{yaml,yml,json} matches 0 files). Use explicit patterns and dedupe:
# tools/generate_docs.py (fixed)
GLOBS = ("**/*.yaml", "**/*.yml", "**/*.json")
def load_schemas() -> dict:
files = sorted({p for g in GLOBS for p in SCHEMA_DIR.glob(g) if p.is_file()})
if not files:
print(f"ERROR: schema globs {GLOBS} matched 0 files under {SCHEMA_DIR}", file=sys.stderr)
sys.exit(1)
schemas, seen = {}, {}
for f in files:
doc = yaml.safe_load(f.read_text())
if not isinstance(doc, dict) or "title" not in doc:
print(f"WARN: {f.name} has no 'title'; skipping", file=sys.stderr)
continue
title = doc["title"]
if title in seen: # guard: no silent collapse of duplicate titles
print(f"ERROR: duplicate schema title '{title}' in {f.name} (already from {seen[title]})", file=sys.stderr)
sys.exit(1)
seen[title] = str(f)
schemas[title] = doc
return schemas
3. Regenerate and commit — run the generator, verify 9 schemas merged, commit docs/generated/merged-spec.yaml + docs/generated/openapi-supplement.yaml.
4. CI freshness job (GitHub Actions) — regenerate, then fail on any diff; nightly schedule acts as a watchdog so drift can't silently age again:
# .github/workflows/docs-freshness.yml
name: docs-freshness
on:
pull_request:
paths: ["schemas/**", "tools/**", "docs/**", ".github/workflows/docs-freshness.yml"]
push:
branches: [main]
paths: ["schemas/**", "tools/**", "docs/**", ".github/workflows/docs-freshness.yml"]
schedule:
- cron: "17 3 * * *" # nightly watchdog
jobs:
freshness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install pyyaml
- run: make docs
- name: Fail if generated docs are stale
run: |
set -euo pipefail
git diff --exit-code
if [ -n "$(git status --porcelain)" ]; then
echo "::error::Generated docs are stale. Run 'make docs' and commit the output."
git --no-pager diff --stat
exit 1
fi
echo "FRESH"
Plus a local make freshness target (regenerate → git diff --exit-code -- docs/generated) and a 5-test unit suite (tests/test_generation.py: glob covers all .yaml; merged spec has all 9 schemas; supplement records sources + count; outputs tracked by git; regeneration is idempotent).
Reproduced and verified end-to-end in a fresh repo at `/tmp/spec-demo` (git 2.53, Python 3.14, PyYAML 6.0.3):
- **Bug reproduced (0/9):** with the old glob, `python3 tools/generate_docs.py` → `ERROR: schema glob '**/*.json' matched 0 files` (exit 1), and `git status` showed **0** entries under `docs/generated` — exactly the "gitignored + missed glob" incident.
- **After fix:** `merged 9 schemas`; `git ls-files docs/generated` → `merged-spec.yaml`, `openapi-supplement.yaml`; merged spec contains all 9 schema keys.
- **Idempotency:** re-running the generator over a clean tree yields byte-identical output → `git diff --exit-code` clean.
- **Staleness detection:** changed `auth.name` `string → integer` in a schema **without** regenerating → CI gate fails with the exact diff (`- type: string / + type: integer`). After regenerating + committing → `make freshness` → `FRESH`.
- **Unit tests:** `Ran 5 tests ... OK` (5/5).
- **Edge cases tested:**
- *Brace-expansion trap:* `Path.glob("**/*.{yaml,yml,json}")` matches 0 files (my first attempt failed exactly this way) → fixed with explicit patterns; verified `.yaml`, `.yml`, and `.json` are all picked up (mixed-extension repo state).
- *Duplicate schema titles* silently collapsed under title-keyed merge → added hard-fail guard (verified: duplicate `audit` → exit 1 with source attribution).
- *Duplicate YAML keys in source* (e.g., `type:` twice) collapse last-wins under `safe_load`; the gate still behaves correctly because it compares *parsed* regeneration output — a raw-text diff would be a false signal.
- *False-FRESH trap (root cause):* with `docs/generated/` gitignored, the gate cannot see drift (untracked files are invisible to `git diff`/`git status`) — reproduced live. Tracking the artifacts is therefore mandatory, not cosmetic.{"model": "deepseek-v4-flash", "problem_class": "docs-spec-generation-tooling", "result": "passed", "tests": 5}