Docs Conventions — what's generated, what's hand-owned, and what not to edit¶
What's generated vs. hand-owned¶
zensical reads mkdocs.yml natively (make docs); the C API pages are
prebuilt separately with real mkdocs against a second nav file
(make gen-c-api runs mkdocs build -f mkdocs-capi.yml). Most of docs/
is hand-written prose, but several pieces are generated and must never be
hand-edited — check this table before editing anything that looks
suspiciously uniform or mechanical:
| Path | Generated by | Regenerate with |
|---|---|---|
docs/c-api/** (except index.md) |
mkdoxy, from native/inc/ Doxygen comments |
make gen-c-api |
docs/c-api/index.md |
(hand-written — restored via git checkout after gen-c-api wipes it) |
— |
docs/api/*.md — page prose |
(hand-written) | — |
docs/api/*.md — doppler.x.Y directive output |
mkdocstrings, from the Python docstring, at build time | edit the docstring, not the page |
docs/api/*.md — ## Related pages block |
scripts/gen_related_pages.py |
make docs-relink |
README.md — entire body below the badges |
scripts/gen_readme.py, from docs/index.md |
make docs-relink |
tests/install/build-*-deps.sh (rendered into install docs) |
scripts/gen_install_scripts.py, from jb.toml's [dev.*] lists |
make docs-relink |
docs/design/index.md, docs/dev/index.md, docs/gallery/index.md |
(hand-written — completeness CI-enforced) | add a bullet by hand; gated by check_nav_index.py |
docs/benchmarks.md |
make bench-docs |
make bench-docs |
docs/specan/frames.json |
make record-demo |
make record-demo |
docs/assets/*.png (gallery figures) |
make gallery |
make gallery |
everything else under docs/ |
(hand-written) | — |
The general rule: if a path above says "generated by," never hand-edit
it — a hand edit is either silently discarded on the next regen (Related
pages, c-api/) or just drifts further from reality until someone notices
(benchmarks, specan frames). Edit the source the generator reads from
instead (a Doxygen comment, a Python docstring, a benchmark run) and
re-run the command in the table.
The CI-enforced completeness gates¶
The checks in CI's docs job keep the docs tree from rotting the way
design/index.md and dev/index.md already had, twice, before the
nav-index coverage gate below existed: a page gets added, gets forgotten in
its section's hand-curated index, and nobody notices until someone manually
audits the tree. All follow the same
house idiom used by Doc Examples and check_api_docs.py
— discovered, not registered — so a new page is covered the moment it
exists, with no opt-in list to remember.
| Check | Script | What it needs from you |
|---|---|---|
| API docs coverage | scripts/check_api_docs.py |
Every public symbol named somewhere under docs/api/ |
| Nav-index coverage | scripts/check_nav_index.py |
Every page linked from its section's index.md |
| Related-pages generation | scripts/gen_related_pages.py |
Nothing — see below |
| README sync | scripts/gen_readme.py |
Nothing — see below |
| Install-script sync | scripts/gen_install_scripts.py |
Nothing — edit jb.toml, run make docs-relink |
| Version strings | scripts/check_version_strings.py |
Never hand-type the current release version in prose |
| Site internal links | scripts/check_site_links.py |
Internal links/anchors resolve in the built site |
| Strict build | zensical build --strict |
Zero build warnings (bad refs, includes) |
| Init-param optionality | scripts/check_init_param_optionality.py |
A published constructor signature is callable |
(The three fence gates and the example gate live in the python-tests
job — see Doc Examples for the whole testing story.)
Init-param optionality (check_init_param_optionality.py)¶
A constructor argument must be optional on both faces or neither. jm can render
a stub parameter as x: T = ... — omittable — while the generated
PyArg_ParseTupleAndKeywords format string places it before the | and
requires it. Nothing else catches that: the stub parses, the extension
compiles, the tests pass because they pass every argument, and the only symptom
is that following the published signature raises TypeError. A type checker
blesses the failing call, because the stub is all it can see.
The check parses the committed .pyi and the committed _ext_*.c — no import
and no construction, so classes whose constructors need live resources are
covered like the rest. It matches on names via the kwlist, never on
position: stub order and binding order can differ (jm reports that separately
as kwargs-drift), and comparing positionally names the wrong parameter.
Accepted divergences live in scripts/.init-param-optionality-ignore as
Class.param, with the reason. Both faces are jm-generated, so the entries
there are upstream-blocked rather than deferred work — and the check reports an
entry that has stopped diverging, so the list cannot outlive its reason.
Run it locally: python scripts/check_init_param_optionality.py.
Nav-index coverage (check_nav_index.py)¶
For docs/design/, docs/dev/, and docs/gallery/, this unions every
on-disk *.md file (excluding archive/) with every mkdocs.yml nav entry
under that section, and fails if any of them has no bullet in that section's
index.md.
What to do: when you add a new page to one of these three sections, add
a real one-line bullet for it to the section's index.md in the same PR —
- [Title](file.md) — a real one-line description, matching the existing
entries' style. Don't write a generic stub description; these are the only
hand-curated lines left in this system, so they carry actual editorial
context a bare file listing can't.
What NOT to do: don't add the page only to mkdocs.yml's nav (or only
create the file) without touching index.md — that's exactly the drift
this gate exists to catch, and CI will fail with a clear "add a bullet for
X" message telling you which file and which page.
Run it locally: python scripts/check_nav_index.py.
Related pages (gen_related_pages.py)¶
Every docs/api/*.md page gets a generated ## Related pages section
listing every gallery, guide, design, and dev page that mentions one of its
documented classes/functions — grouped by family, with a marked region
(<!-- related-pages:start --> … <!-- related-pages:end -->) so the
generator can find and replace its own output on every run.
How to be picked up: the generator only counts a symbol mention inside
a backtick span or a markdown link's text — `SymbolSync`,
[`track.SymbolSync`](../api/python-track.md), or
[SymbolSync](...). A bare, unformatted prose mention does not count.
This is deliberate, not a bug: several doppler class names double as common
English words (Plan, Segment, Reader, Writer, Push, Pull, Timeline, Corr,
Composer, spelled without backticks right here on purpose, so this
paragraph doesn't itself show up as a related page for all nine of them) —
a bare word-boundary match would produce constant false positives from
ordinary prose. If you want your new gallery,
guide, or design page to show up on a class's API page, just name the class
in backticks somewhere on the page — nothing else to configure.
What NOT to do:
- Never hand-edit the block between the
related-pages:start/:endmarkers. It is fully regenerated on every--writeand any hand edit is silently discarded; CI's--checkstep also fails if the committed block doesn't match a fresh regeneration, so a hand edit shows up as permanent, unresolvable drift. - Don't add your own
## Related pagesheading anywhere else — the heading name is reserved for this generator. Use## See also(already hand-written on a few pages) for anything you want to write by hand; the generator always appends after it and never touches it. - Don't reach for the manual escape hatch first. For a purely
conceptual reference with no matching class name,
docs/api/.related-pages-manual.ymlexists — but prefer naming the class in backticks on the source page instead whenever you can; the escape hatch is meant to stay small.
Regenerating locally: make docs-relink (wraps --write), or
python scripts/gen_related_pages.py --check to preview drift without
writing anything. If you add/edit a gallery, guide, design, or dev page and
CI's related-pages check fails, run make docs-relink and commit the
result — you don't need to hand-author anything.
A page only qualifies for scanning once it satisfies check_api_docs.py
(its symbol is documented somewhere under docs/api/, generally via a
doppler.<module>.<Symbol> mkdocstrings directive, or a
## `Symbol` heading for the rare hand-written page). Nothing else is
required to add a new docs/api/*.md page — the next --write picks it up
automatically.
README sync (gen_readme.py)¶
docs/index.md and README.md deliberately show the identical landing
content, but the two pages render on different engines —
mkdocs-material admonitions (!!! tip) on the docs site, GitHub's native
alert syntax (> [!TIP]) on GitHub — so they can never be byte-identical.
Keeping them in sync by hand rotted repeatedly (a live tagline edit, a
missing git clone step, a stale quickstart link — and later a
Performance/Licensing drift in exactly the sections a first,
quickstart-only version of the generator didn't cover), so
docs/index.md is the single source of truth: gen_readme.py extracts
everything after its <!-- readme-sync:source-start --> marker (tagline,
navigation, Why, Performance, Quick start, Build, Docs, Licensing —
through end of file), rewrites admonitions into GitHub's alert syntax,
rewrites relative doc links to their docs/-prefixed form (README.md
lives at the repo root, one level shallower than docs/index.md), and
writes the result into README.md between
<!-- readme-sync:start --> … <!-- readme-sync:end --> markers — same
idiom as gen_related_pages.py's block. Only the wordmark + badge header
above the markers stays hand-owned in each file (the two heads genuinely
differ: image sizing, glightbox classes, absolute-vs-relative badge
targets).
What NOT to do: never hand-edit the block between the
readme-sync:start/:end markers in README.md. Edit docs/index.md
below its readme-sync:source-start marker instead, then run
make docs-relink (or python scripts/gen_readme.py --write) and commit
the result.
Regenerating locally: make docs-relink, or
python scripts/gen_readme.py --check to preview drift without
writing anything.
Install-script sync (gen_install_scripts.py)¶
jb.toml's [dev.*] package lists are the single source of truth for
doppler's system dependencies — make install-deps, CI, and both
Dockerfiles read them via just-bashit. The per-distro
tests/install/build-*-deps.sh scripts that the install docs render via
--8<-- includes are generated projections of those lists (minus
patchelf and rust, which only the wheel-repair and Rust-test paths
need — see the script's docstring). Never edit the scripts: change
jb.toml and run make docs-relink; CI fails on any drift.