epythet v2 — decision record and fleet migration plan (2026-09) #15
Replies: 1 comment
Maintainer decisions (2026-09-10)All eight recommendations accepted as written: (1) safety pin now; (2) fleet-wide look change on the default flip; (3) delete committed Two additionsA. Coverage and correctness sweep, coupled with the artifact repair. When the fleet sweep edits a docstring to fix rendering, it also improves it: coverage (every public callable documented, with an example), correctness (docstring matches signature and behaviour), completeness. A research track on what makes documentation good for agents and humans feeds the docstring-style skill and the sweep procedure. B. Single-document aggregates as part of the default rendering. Every site also publishes one flat document containing the whole documentation, at a stable direct URL: |
Uh oh!
There was an error while loading. Please reload this page.
epythet v2 — decision record and fleet migration plan (draft, 2026-09-10)
Status: draft for maintainer review. Written by the crowsnest session after five parallel research/audit tracks and a baseline render of real fleet packages. Every claim marked [measured] was reproduced locally today; claims marked [research] come from the linked reports and were spot-checked where cheap.
Source reports (local, not committed):
~/.local/share/epythet/docs-modernization-2026-09/—research_doc_systems.md(64 refs),research_themes.md(96 refs),research_validation.md(91 refs),fleet_impact.md,recall_briefing.md, plus the baseline artifact scan (scan_results.txt,warning_classes.txt). Sanitized copies land inmisc/docs/with the v2 branch.0. What epythet wants to be
A way to get beautiful, correct documentation from a Python package with no boilerplate in the package: point it at a repo, and conventions (README, docstrings, package layout, pyproject metadata) produce the site. Every rendering concern lives in epythet's layer, not in docstrings. Docstrings carry the minimum markup that both a human reader of source and an agent need, and epythet absorbs the rest at build time. The same source produces a human site and agent-readable twins.
1. Where we are (facts that constrain the design)
@master, none pinned@master)with:controlpip install epythet(unpinned) thenepythet quickstart . --ignore …quickstart,--ignore, output at./docsrc/_build/html/docsrc/conf.pyanyway [measured: live dol footer has no copyright line], so committed copies are dead weight[tool.epythet]users>>>linesdol/i2with current epythet [measured]problematicspans; 77 / 24 "Returns:" one-liners rendered as prose; 31 / 21 accidental definition lists; landing page is a flat 48-entry module list2. Decisions I would make
D1. Engine: stay on Sphinx 9, pinned
>=9,<10Not MkDocs, not Zensical, not pdoc. The MkDocs side is a fork war; Sphinx is a stall, and a stalled Sphinx still builds. Revisit in about 18 months when Zensical ships native API docs. Pin Sphinx, MyST, and the API generator with upper bounds; today's
Sphinx>=3.3.1unbounded already splits the fleet across Sphinx 8.1 and 9.0 depending on the action's Python version [research].D2. Markup: RST-compatible docstrings, Markdown pages, and a build-time normalizer
Pages and docstrings are parsed by different machinery: autodoc emits RST regardless of page format [research, verified]. So:
index.mdis a{include}of../README.mdwith relative-image fixups, GitHub alerts, badges, mermaid fences, and heading anchors all rendering [research, verified]. This is the landing page fix.autodoc-process-docstring) and fixes the artifacts agents and humans actually produce: blank line before>>>/bullets after prose;```fences to.. code-block::;## Headingto a bold rubric;[text](url)to an RST link;Returns: textone-liners to a proper section;*args/**kwargsescaped in prose.default_role = "code"makes single backticks render as code, which matches the Markdown habit. This is retroactive across 200 repos with zero docstring edits, and it also restores doctests that Sphinx currently silently drops (367 at-risk blocks across 83 packages [research, measured on the fleet]).Args:,Returns:,Raises:,Examples:); doctests as the examples; single or double backticks for code;**bold**; bullet lists preceded by a blank line; types in annotations, never in the docstring. No RST roles or directives needed. This is what the shipped skill teaches agents.Not doing: a blanket Markdown-to-RST conversion of docstrings (the dead
commonmarkhook in the template). It would destroy the fleet's RST field lists.D3. API pages: an off-the-shelf recursive generator replaces
autogen.pyTwo candidates, both verified to give a nested tree with zero per-project templates [research]:
sphinx-autoapi(static parsing, no import, one option) and recursiveautosummary(stdlib Sphinx, imports the package, stock template recurses since Sphinx 3.1). The seam is one keyword:api_generator = "autosummary" | "autoapi". My leaning for the default is autosummary, because it keeps autodoc in the loop so the normalizer hook fires and today's import-based behaviour is preserved; autoapi is the alternative when a package cannot be imported in CI. The v1 spike decides after checking whether the normalizer can be hooked under autoapi. Either wayautogen.pyand the RST half oftemplates.pyare deleted, and nesting appears automatically for the 117 repos with more than 10 modules. Facade__init__.pyre-exports must not double-document (dropimported-members).D4. Themes: furo by default, a curated seven, deterministic variety
Default
furo: 400 KB static payload versus 9.94 MB forsphinx_rtd_theme[research, measured], real light/dark, the de-facto choice for pip, pytest, attrs, black, mypy. Curated set exposed to the theme-choice skill:Excluded with evidence: sphinx-immaterial (broken on Sphinx 9), sphinx-material, sphinx-press-theme.
Config:
[tool.epythet] theme = "furo" | "auto" | <name>,accent = "#hex"(default: derived from the package name in OKLCH with fixed lightness, so contrast is WCAG AA by construction across all 274 fleet names [research, measured]),mode = "auto" | "light" | "dark", and[tool.epythet.theme_options]as a verbatim passthrough that always wins.theme = "auto"hashes the package name into the curated list, so the fleet is varied but every package is stable across rebuilds (never rotate on date or build number: it breaks snapshots). Dropsphinx-toggleprompt(its pixel offset is tuned to RTD geometry; copybutton already handles prompts).D5. Configuration becomes a single source of truth;
docsrc/becomes optionaldocsrc/conf.pyis generated at build time from[tool.epythet]plus conventions, and a committeddocsrc/conf.pybecomes a two-line shim (from epythet.sphinx_conf import *then optional overrides). This closes i2mint/epythet#13 structurally.parse_configkeeps its 5-tuple (wads and 106 frozen conf.py copies unpack it); theme and layout live in a new dataclass accessor. Fix the precedence bug wheresetup.cfgsilently wins overpyproject.tomlin the 10 dual-config repos. Config keys:display_name,copyright,theme,accent,mode,theme_options,ignore,api_generator,agent_outputs,docs_dir.D6. Agent-facing outputs by default
Every build also emits:
llms.txt(index) plus rendered.mdtwins of every page viasphinx-llmwith the full dump disabled,<link rel="alternate" type="text/markdown">relations injected (the plugin does not do this itself [research, verified]), and theobjects.invthat already ships. Cost: a second build pass (roughly doubles build time; builds are seconds today). The rendered site is justified by humans; the twins and index are the cheap agent surface, since GitHub Pages has no content negotiation. Later, and only if wanted: a generated fleet-wideintersphinx_mappingso cross-package references become live links (nobody else has 200 interlinked packages).D7. An "AI artifacts" section in every site
epythet discovers a repo's agent artifacts by convention (
{pkg}/data/skills/*/SKILL.md,.claude/skills,.claude/agents,CLAUDE.md,AGENTS.md,.codex/,.cursor/rules) and renders a "For AI agents" section from a default template: what skills exist, how to install them (gh skill install owner/repo …), what subagents exist, and thellms.txtpointer. A shipped skill tells an agent where to find these artifacts in any repo.D8.
epythet validate: five tiers, doctree-based detection, a growing ledgerThe decisive research result [research, measured on a 30-case specimen]: a strict
sphinx-build -W -nflags 9 of 21 artifact classes; the other 12 render silently and wrongly, and the errors it does raise mean content was silently deleted. Detection must therefore read the docutils doctree of each docstring, obtainable without a build at about 1,000 docstrings per second, catching 20 of 21 classes with one inherently ambiguous false positive.[type]suffixes-b textsnapshots, empty pages, unresolved xrefs, dangling anchors, imagesExit codes distinguish the failing level;
--format table|json|jsonl|sarif. One seam:backend=(Levels 0 and 0.5 are backend-independent; only 1 and 2 touch Sphinx).Ledger: one YAML file per rule (
id,title,detector: regex|doctree|build-warning|render|llm,pattern,example_bad,example_good,fix,severity,precision,autofixable,first_seen) with a sibling annotated.pyfixture that is also the regression test. Rules ship inside epythet and are public. Observations (occurrences with file paths and snippets from real repos) are append-only JSONL kept outside the repo under~/.local/share/epythet/ledger/, because they are derived data from repos that are not all public. Level 3 and human reviewers add rules inproposedstatus; a maintainer promotes them. Seed: the 21 specimen classes plus the baseline findings above (e.g. theReturns: textone-liner rule, 77 hits in dol alone).D9. Repair and migration tooling for the fleet sweep
epythet repair: applies the normalizer's safe rewrites to source (blank lines, fences, one-liner sections, heading rubrics) with a diff for review; unsafe cases (prose*args, unmatched backticks) stay diagnostics. Keepsdiagnose_doctest_code_blocksandrepair_packageimport paths stable because wads'wads-docstring-renderskill imports them (pins>=0.1.14).epythet migrate-style: optional RST-field-list to Google conversion viadocstring_parser.compose()plus LibCST for formatting-safe rewrites [research]. Opt-in per module; never a fleet-wide mass conversion by default.epythet sweep: runs validate at level 0.5 across the fleet (about two minutes total) to produce the real artifact frequency distribution that decides severities.D10. AI artifacts for epythet itself
Consumer skills shipped in
epythet/data/skills/(layout per theskill-package-setuppolicy;gh skill install i2mint/epythetin the README):epythet-setup(convention-over-config quickstart),epythet-docstring-style(the dialect),epythet-validate(levels, ledger workflow),epythet-repair-migrate(the sweep procedure),epythet-theme(choose and parametrize, with links to each theme's docs and sphinx-themes.org),epythet-ai-artifacts(finding and documenting agent artifacts),epythet-pages(the existing Pages diagnosis). Subagents:docs-reviewer(Level 3 packet review),docs-migrator(per-repo sweep). These are documented in the README and rendered in epythet's own "For AI agents" section.3. Back-compatibility and the fleet migration plan
The fleet's only coupling to epythet is the composite action (which installs epythet from PyPI unpinned) and the
quickstartCLI. That gives a one-line kill switch and a one-line rollout.pip install "epythet<0.2". One commit in epythet, zero fleet PRs; 220 repos are frozen on today's behaviour. Rollback of anything later is reverting this file.epythet quickstart <dir> --ignore …still works and still writes HTML to./docsrc/_build/html/.make_docsrc,make_autodocs,makeremain as thin compatibility entry points that route to the new pipeline.parse_configkeeps its signature. Python floor for epythet moves to 3.11 (Sphinx 9 requires it); the action's default Python moves to 3.12. The floor only affects the CI docs job, not the packages' ownrequires-python.-b textsnapshots of v1 versus v2 are reviewed for content loss. This is the first real use ofepythet validate.publish-github-pages@v2in the action directory (installingepythet>=0.2,<0.3); 5 to 10 direct-CI repos switch theiruses:ref (small PRs). Watch their published sites for a week.@master's install line at>=0.2,<0.3. Rollout is naturally gradual: each repo's site rebuilds on its next push to its default branch. Also pin the wads stub and templates to a tagged action ref so future flips are deliberate.validate→repair→ review → fix correctness and completeness → set theme if the default is wrong for the package → delete the committeddocsrc/(CI regenerates it; keep only where hand-written pages exist) → land. Ledger observations accumulate; new rules get proposed.Out of scope for the flip: the 24 repos on the legacy
epythet make . githubpath, which keep working via the compatibility entry points and are migrated during the sweep.4. Decisions that are yours
epythet<0.2in the action immediately (reversible, no fleet PRs). Say no if you prefer every fleet site to track PyPI latest.[tool.epythet], which costs 200 PRs.docsrc/in 107 repos: delete during the sweep (recommended: CI regenerates it; add to.gitignore), or refresh to the thin shim and keep committing it for local builds?llms.txtplus.mdtwins, second build pass), or opt-in?theme = "auto"fleet-wide (deterministic variety), or have the sweep agent pick explicitly per package using the theme skill? Both are supported; this is about what the default does when nobody chooses.epythet reviewcommand?5. Work packages (one session each, spawned after your answers)
conf.py, MyST index with README include, recursive API generator, theme registry and OKLCH accent, normalizer, agent outputs, compatibility entry points, tests, smoke test on 3 fleet reposepythet validatelevels 0, 0.5, 1 and the ledger with seed rules and fixturesrepair,migrate-style,sweepRelated open issues folded in by design: #13 (conf.py SSOT), #9, #10, #8 (Markdown vs RST), #2 (doctest prompt toggle), #11 (doctest detection), #7 (version and date on the page). The wiki's "Documentation problems ledger" page becomes the first ledger entries and the wiki page is retired.
All reactions