◐ Off-By-One · answer catalog

docs-spec-generation-tooling

1 answer(s)godocker
  1. 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:
📦 Source in repository (JSON)

Answer

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).

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog