> built 2026-09-15 12:35 UTC from 7f52d70 (master) · epythet 0.2.12. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# epythet

Beautiful, correct documentation from a Python package, with no boilerplate in the package.
Less humdrum, more automation, earlier at the pub.

[Full documentation here](https://i2mint.github.io/epythet/), generated by epythet.

```bash
pip install epythet
epythet quickstart /path/to/project --ignore tests/ scrap/ examples/
```

Open `/path/to/project/docsrc/_build/html/index.html`. You get:

- a landing page that **is your README** (badges, images, GitHub alerts and mermaid fences intact),
- a **nested API tree** built from your package layout, one page per module,
- **RST field lists and Google/NumPy sections rendered side by side**, types linked from annotations,
- a modern theme with light/dark mode and an accent colour derived from your package name,
- **agent-facing twins**: `llms.txt`, a `.md` twin of every page, a flat `<package>.md`, and `objects.inv`.

Every site also says where it came from. A small line at the bottom of the landing page reads `built <UTC time> from <commit> (<branch>) · <package> <version> · about this build`, so a reader can tell whether the docs match the repository and the installed package, and a maintainer can see whether the latest push has been published. The `about-this-build` page behind the link holds the full diagnosis (commit, tags, dirty flag, CI run, tool versions, resolved configuration, latest PyPI release and whether it matches, how to reproduce the build), and `build_info.json` at the site root holds the same for machines; `epythet build-info DIR` prints it. `[tool.epythet] provenance = false` turns it off, `"minimal"` keeps the line and the JSON without the page, and `provenance_template` points at your own page template.

Nothing has to be added to the package. Everything is read from `pyproject.toml` (or `setup.cfg`), the README and the docstrings.

<!-- epythet:agentic-readme:start -->

# For AI agents

`epythet` ships tooling for coding agents. If you are one, start here.

**Skills** ([Agent Skills](https://agentskills.io) format), for any agent host. Install one with `gh skill`:

```bash
gh skill install i2mint/epythet epythet-setup --agent claude-code   # or copilot, cursor, codex, gemini
```

| Skill                     | Use it to                                                                                                                             |
|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
| `epythet-agentic-readme`  | make sure a repository’s README documents its agentic aspects                                                                         |
| `epythet-ai-artifacts`    | find, install and document a repository’s AI agent artifacts                                                                          |
| `epythet-docstring-style` | write and improve Python docstrings that render correctly in epythet/Sphinx and that help both humans and AI agents                   |
| `epythet-pages`           | diagnose and fix GitHub Pages publishing for Python documentation built with epythet (or any Sphinx site pushed to a gh-pages branch) |
| `epythet-repair-migrate`  | the per-repository documentation sweep for packages documented with epythet                                                           |
| `epythet-setup`           | set up documentation for a Python package with epythet                                                                                |
| `epythet-theme`           | choose and parametrize the Sphinx theme of an epythet documentation site                                                              |
| `epythet-validate`        | check a Python package’s docstrings for rendering artifacts and build problems with `epythet validate`                                |

The same skills are inside the wheel, under `epythet/data/skills/`.

**Subagents**: `docs-migrator` (runs the epythet documentation sweep on one repository end to end), `docs-reviewer` (reviews the rendered documentation of a Python package), in `epythet/data/agents/`. Copy one into your project’s `.claude/agents/` (or your host’s equivalent).

**Instruction files**: `.claude/CLAUDE.md` (Claude Code).

**The documentation, machine-readable**: [`llms.txt`](https://i2mint.github.io/epythet/llms.txt) indexes every page; [`epythet.md`](https://i2mint.github.io/epythet/epythet.md) is the whole documentation in one file; every page has a `.md` twin; [`objects.inv`](https://i2mint.github.io/epythet/objects.inv) maps symbols to URLs. The full list, with install lines, is on the site’s [For AI agents](https://i2mint.github.io/epythet/ai-agents.html) page.

If you would rather understand than delegate, the rest of this README is written for you, starting at [What it fixes without touching your docstrings]().

<!-- epythet:agentic-readme:end -->

# What it fixes without touching your docstrings

Docstrings in real packages mix reStructuredText, Google sections and Markdown habits, and a few recurring slips render wrongly, often silently. epythet rewrites those at build time (the *normalizer*), so the rendered site is right even when the source is not:

| you wrote                                       | what happened before                                               | what epythet renders     |
|-------------------------------------------------|--------------------------------------------------------------------|--------------------------|
| a `>>>` block right after a sentence            | a paragraph starting with `>>>`; never run by `sphinx.ext.doctest` | a doctest block          |
| ````python` fences                              | the backticks printed literally                                    | a highlighted code block |
| `Returns: the answer` on one line               | a sentence                                                         | a *Returns* section      |
| `## Heading`                                    | a literal `##`                                                     | a heading                |
| `*args` / `**kwargs` in prose                   | an “emphasis start-string without end-string” error                | escaped, as written      |
| `[text](url)`                                   | printed literally                                                  | a link                   |
| a wrapped list item at the bullet’s indentation | “bullet list ends without a blank line”                            | a list item              |
| `Examples:` followed by an unindented doctest   | a stray “Examples:” paragraph                                      | an *Examples* rubric     |

Single backticks render as code (`default_role = "code"`), matching the Markdown habit. Code inside doctests, literal blocks and fences is never touched. The rules are pure functions in `epythet.normalizer`; you can see what one docstring becomes with `epythet.normalize_text(docstring)`.

On the `dol` package (48 modules, 23,000 lines of doctests) this took the build from 91 Sphinx warnings and 266 detected rendering artifacts to 25 and 55, with every doctest that autodoc documented still documented.

# Configuration: `[tool.epythet]`

All optional. Omit the section and you get the defaults below.

```toml
[tool.epythet]
display_name = "Dol"            # site title; default: the project name
copyright = "2024, Jane Doe"    # footer; default: no copyright line at all
theme = "auto"                  # "auto" | "furo" | "shibuya" | "pydata" | "sphinxawesome" | "book" | "alabaster" | "rtd" | any installed theme
accent = "#3661ac"              # default: derived from the package name (OKLCH, WCAG AA on white by construction)
mode = "auto"                   # "auto" | "light" | "dark"  (where the theme supports forcing it)
ignore = ["tests/", "scrap/", "examples/"]   # path substrings to skip; `--ignore` on the CLI overrides
api_generator = "auto"          # "auto" (autosummary if the package imports, else autoapi) | "autosummary" | "autoapi"
agent_outputs = true            # llms.txt, .md twins, <link rel="alternate"> relations
aggregates = ["md"]             # flat single-document twins at the site root: "md", "pdf"
ai_artifacts = true             # "For AI agents" page when the repo has skills, agents or CLAUDE.md
ai_artifacts_template = ""      # project-relative file overriding that page's template
package_dir = "src/dol"         # default: found by convention (<name>/ or src/<name>/)
docs_dir = "docsrc"             # where the Sphinx sources are generated

[tool.epythet.theme_options]    # verbatim passthrough into Sphinx's html_theme_options; always wins
announcement = "v2 is in beta"

[tool.epythet.readme]           # pins the generated "For AI agents" README section (see "For agents"); wins over ~/.config/epythet
humor = true
agentic_first = true
```

`setup.cfg` projects put the same keys under `[metadata]` (`display_name`, `copyright`) or a `[tool.epythet]` section. When both files exist, `pyproject.toml` wins.

**Themes.** `theme = "auto"` (the default) hashes the package name into a curated pool (furo, shibuya, pydata-sphinx-theme, sphinxawesome-theme) so a fleet of packages gets variety while every package keeps the same look across rebuilds. The pool’s themes are installed with epythet; `sphinx-book-theme` and `sphinx_rtd_theme` come with `pip install "epythet[themes]"`. The accent is one hue per package, at a fixed perceptual lightness, so every possible colour clears WCAG AA against white and AAA on a dark background; an explicit `accent` is used as given in light mode and lifted to the same dark-mode lightness for dark mode.

**API generator.** `autosummary` (Sphinx built-in) imports your package, so aliases, `functools.partial` objects and other assigned names keep the docstring of what they point to. `autoapi` parses statically and needs no import. The default `auto` probes the import once and picks `autosummary` when it succeeds, `autoapi` otherwise (a missing optional dependency in CI then costs you the aliases, not the whole API section). Both give the nested tree; both run the normalizer; both skip `__main__`. Under `autosummary`, `ignore` keeps the ignored modules out of the tree, but Python still imports them once while discovering the package.

**PDF aggregate.** `aggregates = ["md", "pdf"]` renders `<package>.pdf` from the Markdown aggregate with Playwright (`pip install "epythet[pdf]" && playwright install chromium`) or WeasyPrint, whichever is installed. No LaTeX.

# The generated `docsrc/`

`epythet quickstart` (or `epythet make-docsrc`) writes a `docsrc/` directory holding a two-line `conf.py`:

```python
from epythet.sphinx_conf import *  # noqa: F401,F403
```

and an `index.md` that includes your README and a hidden toctree for the API pages. That is the whole scaffold; the API pages and the agent outputs are generated at build time. You do not need to commit `docsrc/` (CI regenerates it), but if you do, the shim is the single source of truth: put project-specific Sphinx overrides below the import and they win over the generated values. A hand-written `conf.py` without the import is never overwritten.

`epythet make PROJECT_DIR [html|doctest|markdown|github|clean]` runs `sphinx-build` with the current interpreter; there is no Makefile. `github` builds HTML and copies it into `PROJECT_DIR/docs`. `doctest` is Sphinx’s doctest builder, which runs examples without the module’s namespace; for docstring doctests use `pytest --doctest-modules`.

# For agents

Every site also serves, next to the HTML:

- `llms.txt`: an index of every page with a one-line description,
- `<page>.html.md`: a fully rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`,
- `<package>.md`: the whole documentation as one Markdown file, linked from the landing page (and `<package>.pdf` when enabled),
- `objects.inv`: the Sphinx inventory, a machine-readable symbol-to-URL index (`sphobjinv convert plain objects.inv -`).

Set `agent_outputs = false` to skip the second (Markdown) build pass.

**The README.** `epythet ai-readme-check PROJECT_DIR` reports which of these a project has (skills, subagents, instruction files, the outputs above) and whether its README mentions each; `--format json` for machines, `--fail-on warn` for CI (the exit code is 0 otherwise). `--draft` prints a “For AI agents” README section rendered from text snippets; `--write` adds it between marker comments and updates it in place on later runs, wherever you moved it (the section at the top of this README is one). What to do about a missing section is a user-level policy in `~/.config/epythet/config.toml`: `[readme] agentic_aspects = "warn"` (the packaged default) or `"add"`, `humor = true` to draw the “for humans” line from a pool, `agentic_first = true` to put the section before every other; a project pins the keys that shape its committed text in `[tool.epythet.readme]`, which wins. The wording is yours too: `epythet snippets list | show NAME | init | diff` resolve a snippet from `~/.config/epythet/snippets/` over the packaged default; `init` copies the defaults out once, with a header recording the epythet version, and never overwrites; `diff` shows how your copy differs from the current default after an upgrade. The `epythet-agentic-readme` skill walks an agent through the whole thing.

**The “For AI agents” page.** When the repository ships anything for agents, epythet adds an `ai-agents` page to the site listing it: skills (`<pkg>/data/skills/*/SKILL.md`, `skills/*/SKILL.md`, `.claude/skills/*/SKILL.md`) with their `gh skill install` lines and source folders, subagents (`<pkg>/data/agents/*.md`, `.claude/agents/*.md`), instruction files (`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, `.cursor/rules`, `.codex`), and the outputs above with their URLs. Symlinks are followed and duplicates removed. `epythet ai-artifacts PROJECT_DIR` prints the same inventory (`--format json` for machines). Turn the page off with `ai_artifacts = false` (or, for a whole CI fleet, the environment variable `EPYTHET_AI_ARTIFACTS=0`), or replace its template with `ai_artifacts_template = "path/to/template.md"` (a `str.format` template; see `epythet.ai_artifacts`). A hand-written `docsrc/ai-agents.md` is left alone. A malformed `SKILL.md` never fails the build: the skill is listed by folder name.

# Python API

```python
from epythet import (
    quickstart,
    make_docsrc,
    make,
    load_config,
    sphinx_settings,
    normalize_text,
)

quickstart(
    "/path/to/project", ignore=["tests/"]
)  # scaffold + build; returns the html dir
cfg = load_config("/path/to/project")  # the resolved DocsConfig
sphinx_settings(cfg)  # the conf.py namespace as a dict
```

Diagnosis and repair of docstring formatting in *source* files (missing blank lines before doctests) is unchanged: `epythet.diagnose_doctest_code_blocks`, `epythet.repair_package`.

# Publishing to GitHub Pages with GitHub Actions

## Step 1: Add the CI workflow

Add a workflow such as [.github/workflows/publish-docs.yml](https://github.com/i2mint/epythet/blob/master/.github/workflows/publish-docs.yml) to your repo and adjust the trigger. The example below runs after the “Continuous Integration” workflow completes.

```yaml
name: GitHub Pages

on:
  workflow_run:
    workflows: ["Continuous Integration"]
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: i2mint/epythet/actions/publish-github-pages@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          ignore: "tests/,scrap/,examples/"
          python-version: "3.12"
```

The action installs epythet, installs your project, runs `epythet quickstart . --ignore ...` and pushes `./docsrc/_build/html/` to the `gh-pages` branch.

## Step 2: Enable GitHub Pages

After the CI runs and creates the `gh-pages` branch, you need to tell GitHub to actually serve it. There are two ways to do this:

### The clicky way (for those who enjoy navigating settings menus)

Go to your repo’s **Settings > Pages**, set the source branch to `gh-pages` and the folder to `/ (root)`, then click Save.

![image](https://user-images.githubusercontent.com/22692594/212193474-80b287e2-211c-470d-aa7c-9f779bdd3866.png)

### The fast way (for those who value their time)

If you have the [`gh` CLI](https://cli.github.com/) installed:

```bash
# Check if Pages is set up correctly
epythet check-pages owner/repo

# Enable or fix Pages configuration
epythet configure-pages owner/repo
```

Or from Python:

```python
from epythet import check_pages_setup, enable_pages

# Diagnose
check_pages_setup("owner/repo")

# Fix
enable_pages("owner/repo")
```

You can also point these at a local git checkout instead of `owner/repo`:

```bash
epythet check-pages .
epythet configure-pages /path/to/my/project
```

These tools work with either the `gh` CLI (recommended) or a `GITHUB_TOKEN`
environment variable.

Under the hood, `configure-pages` is just the GitHub Pages REST API — the direct
`gh` equivalent of *Settings > Pages → Branch `gh-pages`, folder `/ (root)` →
Save* is:

```bash
# POST creates the Pages site (when Pages is not yet enabled — GitHub's default);
# use -X PUT instead to change an already-enabled Pages config.
gh api repos/owner/repo/pages -X POST -f 'source[branch]=gh-pages' -f 'source[path]=/'
```

See [CI epythet troubleshooting](https://github.com/i2mint/epythet/wiki/CI-epythet-troubleshooting).

# Upgrading from epythet 0.1.x

epythet 0.2 keeps the contract the fleet depends on and changes what is behind it:

- `epythet quickstart DIR --ignore ...` still writes HTML to `DIR/docsrc/_build/html/`; the `--ignore` flag with no values still means “use the default”.
- `make_docsrc`, `make_autodocs`, `make` and `quickstart` are still importable from `epythet` (and from `epythet.setup_docsrc` / `epythet.call_make`); `make_autodocs` is now a no-op alias of `make_docsrc`, since API pages are generated at build time.
- `epythet.config_parser.parse_config` keeps its 5-tuple `(name, copyright, author, version, display_name)`, so a committed 0.1.x `docsrc/conf.py` keeps working. It now resolves the project directory whatever path it is given, so `pyproject.toml` wins over a stale `setup.cfg` (0.1.x silently preferred `setup.cfg`).
- A committed 0.1.x `docsrc/` (template `conf.py`, `index.rst`, `table_of_contents.rst`, `module_docs/`, Makefile) is recognised and replaced by the new scaffold on the next `quickstart`.
- Dropped: the `sphinx_rtd_theme` default (furo-class themes replace it), `sphinx-toggleprompt` (copybutton already strips prompts), `commonmark`, and the `Makefile`. Requires Python 3.11+, Sphinx 9, myst-parser 5.1; a project that pins `sphinx<9` or `docutils<0.22` in its own dependencies will conflict with epythet 0.2 in the same environment.
- URLs of API pages changed (`module_docs/<pkg>/<mod>.html` is now `_autosummary/<pkg>.<mod>.html`); `objects.inv` keeps every symbol resolvable across sites.

The publish action pins `epythet<0.2` until v2 is validated across the fleet; see the [v2 decision record](https://github.com/i2mint/epythet/discussions/15) and the [tracking issue](https://github.com/i2mint/epythet/issues/16).

# Validation and repair

`epythet validate` checks a package’s documentation in tiers, `epythet repair` fixes the markup slips it finds in the source, and `epythet sweep` runs the checks across many packages. Install the extras you need: `pip install 'epythet[validate]'` (ruff, pydoclint, PyYAML), `'epythet[repair]'` (LibCST, optional), `'epythet[migrate]'` (docstring-parser), `'epythet[review]'` (Playwright screenshots).

```bash
epythet validate .                  # lint + parse every docstring (no build), exit 0/10/11
epythet validate . --level 3        # + Sphinx build + rendered-output checks, exit 12/13
epythet repair . && epythet repair . --write   # diff first, then apply
epythet sweep pkg1 pkg2 --manifest my_packages.pth   # read-only, many packages
```

## `validate`: five levels, one exit code per level

| `--level`   | Runs     | What it reads                                                                                                                                                                                                                            | Exit code                        |
|-------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------|
| 0           | lint     | the docstring text: ruff `D` rules, pydoclint, and the coverage detectors (public objects without a docstring, entry points without an example, summaries that only restate the name, parameter descriptions that only restate the type) | 10                               |
| 1 (default) | + parse  | the docutils doctree of every docstring, in isolation, against the artifact ledger (a `:param` line glued to the summary, a doctest rendered as prose, a Markdown fence, `*args` opening an emphasis, …)                                 | 11                               |
| 2           | + build  | the Sphinx warning stream                                                                                                                                                                                                                | 12                               |
| 3           | + render | the built pages: `-b xml` for objects described with nothing and cross-references that rendered as plain code, `-b html` for dangling `#idN` anchors and missing images, `-b text` for snapshots                                         | 13                               |
| 4           | + review | writes a review *packet* (rendered text of the changed or sampled pages, the rubric, a strict JSON schema) for an in-session agent; never calls a model and never gates                                                                  | 14, only with `--fail-on-review` |

Levels 0 to 3 gate on `--fail-on error` (default), `warning` or `info`; exit 20 means the ledger itself is broken. `--format json` and `--format jsonl` give the machine-readable report.

Text snapshots are opt-in: `--update-snapshots` writes the `-b text` render of every page under `docsrc/_snapshots/text`, and `--snapshot` diffs against it (a changed page is a level-2 error; the snapshot diff is the proof that a migration changed nothing). Level 4 writes its packet under the user data dir (`~/.local/share/epythet/review/<package>/<run>/`, `EPYTHET_DATA_DIR` overrides); a reviewer answers with a `review.json` that `--review-reply` ingests, and `epythet ledger propose review.json` turns its proposed rules into `status: proposed` ledger rules in an overlay (`--ledger DIR` uses them) for a human to promote.

Every finding names a rule from the ledger (`epythet/ledger/rules/`, one YAML per rule with a fixture that is also its regression test). Findings are appended, as observations, to `~/.local/share/epythet/ledger/observations.jsonl`, never inside the repository.

## `repair`: the normalizer’s fixes, written back to the source

The docs build already normalizes docstrings on the fly. `epythet repair` applies the same source-safe rewrites to the files: a blank line before a doctest, list or field list; a Markdown fence to a `.. code-block::` (or a `::` literal block with `--fence-style literal`); `Returns: text` to a real section; `## Heading` to a rubric; `[text](url)` to an RST link; a short title underline padded.

```bash
epythet repair path/to/pkg            # dry run: a unified diff, and what needs a hand
epythet repair path/to/pkg --write    # apply, after verifying
```

Only the docstring literals change; the rest of the file is copied byte for byte and the module’s AST outside its docstrings must be identical or nothing is written. Doctest sources are never altered. Each rewritten docstring is re-validated at level 1 (a rewrite that would introduce a finding is dropped), and with `--write` the doctests of every touched file are run before and after, and a file whose failures went up is restored. What no rule can fix safely (a prose `*args`, unmatched backticks, a backslash in a non-raw docstring) is listed under “needs a hand”. `repair_package` from `epythet.tools` keeps working and delegates here.

`epythet migrate-style path --to google` (or `numpy`) is the opt-in cousin: it rewrites an RST field list (`:param x:`, `:returns:`, `:raises:`) as a Google or NumPy section via `docstring_parser`, with the same guarantees, and leaves alone any docstring whose fields would not round-trip.

## `sweep`: the fleet distribution and the work queue

`epythet sweep DIR... [--manifest FILE]` validates every package at levels 0 and 0.5 without writing into any of them, prints how often each rule fires (findings, packages affected, rate per hundred public objects) and a queue of packages ranked by the work they hold, entry points first. It is what decides the severities in the ledger, and the shortest path to “which package should I document next”.

<p class="epythet-aggregates">This documentation as a single file: <a href="epythet.md">epythet.md</a> (Markdown, for agents).</p>


# _autosummary/epythet.agent_outputs.html.md

# epythet.agent_outputs

Agent-facing outputs: `llms.txt`, Markdown twins, link relations, aggregates.

Humans get the rendered HTML. Agents get, from the same build:

- `llms.txt`: an index of every page with a one-line description, and
  `<page>.md` twins of every page, both produced by the `sphinx_llm.txt`
  extension (which drives a second, Markdown build).
- `<link rel="alternate" type="text/markdown">` and
  `<link rel="describedby" href="llms.txt">` in every HTML page’s `<head>`,
  the llms.txt discovery contract. The plugin does not add these; the
  `html-page-context` hook here does.
- `objects.inv`: the Sphinx inventory, already a machine-readable symbol index.
- Aggregates: one flat document with the whole site, at a stable URL at the
  site root: `<package>.md` (from the Markdown build’s concatenation) and,
  when a renderer is available, `<package>.pdf`. See [`write_aggregates()`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.write_aggregates).

The `aggregates` seam is the `aggregates` key of the configuration:
`["md"]` by default, `["md", "pdf"]` when a PDF is wanted.

### Module Attributes

| [`LLMS_FULL_TXT`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.LLMS_FULL_TXT)   | File the Markdown build writes when `llms_txt_full_build` is on.   |
|------------------------------------------------------------------|--------------------------------------------------------------------|

### Functions

| [`inject_link_relations`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.inject_link_relations)(app, pagename, ...)    | `html-page-context` hook: advertise the Markdown twin and `llms.txt`.           |
|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`inject_link_relations_into_site`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.inject_link_relations_into_site)(html_dir)    | Add the link relations to every built page that has a Markdown twin but no tag. |
| [`link_relation_tags`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.link_relation_tags)(pagename)                 | The two `<link>` tags for a page, with hrefs relative to that page.             |
| [`markdown_to_pdf`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.markdown_to_pdf)(md_path, pdf_path)           | Render a Markdown file to PDF, without LaTeX.                                   |
| [`sphinx_settings`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.sphinx_settings)(\*[, description])           | The Sphinx configuration values for the agent outputs.                          |
| [`write_aggregates`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.write_aggregates)(html_dir, \*, package_name) | Produce the flat single-document twins at the root of a built site.             |

### epythet.agent_outputs.LLMS_FULL_TXT *= 'llms-full.txt'*

File the Markdown build writes when `llms_txt_full_build` is on.

### epythet.agent_outputs.inject_link_relations(app, pagename, templatename, context, doctree)

`html-page-context` hook: advertise the Markdown twin and `llms.txt`.

### epythet.agent_outputs.inject_link_relations_into_site(html_dir)

Add the link relations to every built page that has a Markdown twin but no tag.

Themes that do not render Sphinx’s `metatags` block (shibuya) get nothing
from the `html-page-context` hook; this post-build pass covers them.
Returns the number of files changed.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### epythet.agent_outputs.link_relation_tags(pagename)

The two `<link>` tags for a page, with hrefs relative to that page.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(link_relation_tags("_autosummary/pkg.mod"))

<link rel="alternate" type="text/markdown" href="pkg.mod.html.md">
<link rel="describedby" href="../llms.txt" type="text/markdown">
```

### epythet.agent_outputs.markdown_to_pdf(md_path, pdf_path)

Render a Markdown file to PDF, without LaTeX.

Tries, in order: Playwright (Chromium print-to-PDF, best fidelity) and
WeasyPrint. Returns `None`, with a printed notice, when neither is installed.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.agent_outputs.sphinx_settings(, description='')

The Sphinx configuration values for the agent outputs.

`llms_txt_full_build` stays on because its concatenation is the `<pkg>.md`
aggregate; [`write_aggregates()`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.write_aggregates) renames it after the build.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.agent_outputs.write_aggregates(html_dir, , package_name, aggregates=('md',))

Produce the flat single-document twins at the root of a built site.

* **Parameters:**
  * **html_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – the built site (`docsrc/_build/html`)
  * **package_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – names the files: `<package_name>.md` / `.pdf`
  * **aggregates** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – which of `"md"`, `"pdf"` to produce
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]
* **Returns:**
  the files written, keyed by kind

The `.md` aggregate is the Markdown build’s `llms-full.txt` renamed, and
`llms.txt` is rewritten to point at it. The `.pdf` aggregate is rendered
from the `.md` one by [`markdown_to_pdf()`](_autosummary/epythet.agent_outputs.html.md#epythet.agent_outputs.markdown_to_pdf), which needs an optional
renderer; when none is installed the PDF is skipped with a notice.


# _autosummary/epythet.agentic_readme.html.md

# epythet.agentic_readme

Check that a README documents a project’s agentic aspects; render and place the section.

A project that ships skills, subagents or instruction files, and whose site
publishes agent-readable documentation (`llms.txt`, `<package>.md`), should
say so in its README: that is where an agent arriving at the repository looks
first. [`check_readme()`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.check_readme) reuses [`epythet.ai_artifacts.discover_artifacts()`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.discover_artifacts)
to learn what exists and reads the README to see whether each kind is mentioned
(a `gh skill install` line, a skill or agent name, `CLAUDE.md`, `llms.txt`,
a heading about agents). The result is a [`ReadmeReport`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.ReadmeReport): one
[`KindCheck`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.KindCheck) per kind, each `ok`, `warn` or `n/a`. The check is a
heuristic: a mention counts whatever the sentence around it says.

[`render_section()`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.render_section) produces the README section from the effective snippets
([`epythet.userconfig`](_autosummary/epythet.userconfig.html.md#module-epythet.userconfig): the user’s `agentic-readme-section.md` and
`agentic-readme-humor.md` over the packaged defaults) and the effective
[`ReadmePolicy`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.ReadmePolicy) (the user’s `config.toml`, with a
project’s `[tool.epythet.readme]` keys on top so a committed README does not
depend on who ran the tool). [`place_section()`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.place_section) puts it between two marker
comments so a later run updates it in place: before the first heading after the
title when `agentic_first` is on, at the end otherwise. The “for humans” line
links to the heading that follows the section.

```pycon
>>> readme = "# pkg\n\nTagline.\n\n# Install\n\npip install pkg\n"
>>> start, end, heading, level = place_section(readme, agentic_first=True)
>>> readme[start:end], heading.title, level
('', 'Install', 1)
>>> humans_link_for(heading)
'[Install](#install)'
```

### Module Attributes

| [`MARKER_START`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.MARKER_START)      | The comments that delimit the generated section in a README (each on its own line).   |
|--------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`README_NAMES`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.README_NAMES)      | README filenames, in order of preference.                                             |
| [`KINDS`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.KINDS)             | The kinds a check reports on, in display order.                                       |
| [`SECTION_SNIPPET`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SECTION_SNIPPET)   | The snippet names the section is rendered from.                                       |
| [`NEUTRAL_INTRO`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.NEUTRAL_INTRO)     | The opener used when `humor` is off.                                                  |
| [`MAX_BLURB`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.MAX_BLURB)         | Longest blurb (first sentence of a description) shown per skill or agent.             |
| [`HEADLINE_SUFFIXES`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.HEADLINE_SUFFIXES) | Skill name suffixes that make a skill the one named in the install line.              |
| [`SECTION_FIELDS`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SECTION_FIELDS)    | The fields a section snippet may use.                                                 |

### Functions

| [`ai_readme_check`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.ai_readme_check)(project_dir, \*[, format, ...])    | Report whether the README documents the project's agentic aspects; draft or write the section.   |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| [`blurb`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.blurb)(description, \*[, max_length])               | The first sentence of a skill or agent description, short enough for a table cell.               |
| [`check_readme`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.check_readme)(project_dir, \*[, config, ...])       | Which agentic aspects the project has, and whether its README mentions each.                     |
| [`draft_section`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.draft_section)(project_dir, \*[, user_config, ...]) | Render the section for a project as it would be placed: `(section, readme_text, start, end)`.    |
| [`find_readme`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.find_readme)(project_dir)                           | The project's README, by the usual names (`None` when there is none).                            |
| [`github_anchor`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.github_anchor)(title)                               | GitHub's anchor for a heading title.                                                             |
| [`headings_of`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.headings_of)(text)                                  | Every ATX heading outside fenced code blocks.                                                    |
| [`headline_skill`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.headline_skill)(skills)                             | The skill named in the install line: a `*-setup`-like one if any, else the first installable.    |
| [`humans_link_for`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.humans_link_for)(heading)                           | `[Title](#anchor)` for the heading after the section, or a plain fallback.                       |
| [`instruction_text`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.instruction_text)(\*[, snippets])                   | The instruction the skill hands an agent when the policy is `add`.                               |
| [`load`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.load)(project_dir, \*[, config, artifacts, ...])    | Resolve a project once; each argument given is used instead of being loaded.                     |
| [`load_project`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.load_project)(project_dir)                          | `(config, artifacts)` for a project; `config` is `None` for a non-Python tree.                   |
| [`marker_span`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.marker_span)(text, \*[, strict])                    | The character span of the marked section (`None` when there is none).                            |
| [`place_section`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.place_section)(text, \*, agentic_first)             | Where the section goes in `text`: `(start, end, next_heading, level)`.                           |
| [`read_readme`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.read_readme)(path)                                  | `(text, newline)`: the README with `\n` line ends, and the style to write back.                  |
| [`render_section`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.render_section)(artifacts, config, \*, policy)      | The README section for `artifacts`, from the effective snippets and `policy`.                    |
| [`section_snippet_for`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.section_snippet_for)(artifacts)                     | The name of the section snippet `artifacts` calls for.                                           |
| [`ships_tooling`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.ships_tooling)(artifacts)                           | Whether the project ships anything an agent installs or reads as instructions.                   |
| [`splice_section`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.splice_section)(text, section, \*, start, end)      | `text` with `section` in place of `text[start:end]`, blank lines kept sane.                      |
| [`write_section`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.write_section)(project_dir, \*[, user_config])      | Add or update the marked section in the project's README; returns `(path, outcome)`.             |

### Classes

| [`Heading`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.Heading)(line, level, title)                    | A Markdown ATX heading: its line index, level and title text.                                   |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| [`KindCheck`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.KindCheck)(kind, present, documented[, ...])    | One artifact kind: whether the project has it and whether the README covers it.                 |
| [`Project`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.Project)(root, config, artifacts, readme, ...)  | What every entry point needs once: config, artifacts, README, effective policy.                 |
| [`ReadmeReport`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.ReadmeReport)(project_dir, readme, checks, ...) | The outcome of [`check_readme()`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.check_readme) for one project. |

### Exceptions

| [`SectionError`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError)   | The README or a snippet is in a state the tool will not write over.   |
|-----------------------------------------------------------------|-----------------------------------------------------------------------|

### epythet.agentic_readme.HEADLINE_SUFFIXES *= ('-setup', '-quickstart', '-start')*

Skill name suffixes that make a skill the one named in the install line.

### *class* epythet.agentic_readme.Heading(line, level, title)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A Markdown ATX heading: its line index, level and title text.

### epythet.agentic_readme.KINDS *= ('skills', 'subagents', 'instruction_files', 'agent_docs', 'section')*

The kinds a check reports on, in display order.

### *class* epythet.agentic_readme.KindCheck(kind, present, documented, items=(), evidence='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One artifact kind: whether the project has it and whether the README covers it.

#### *property* status *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`n/a` when absent from the project, else `ok` or `warn`.

### epythet.agentic_readme.MARKER_START *= '<!-- epythet:agentic-readme:start -->'*

The comments that delimit the generated section in a README (each on its own line).

### epythet.agentic_readme.MAX_BLURB *= 140*

Longest blurb (first sentence of a description) shown per skill or agent.

### epythet.agentic_readme.NEUTRAL_INTRO *= 'If you are a human'*

The opener used when `humor` is off.

### *class* epythet.agentic_readme.Project(root, config, artifacts, readme, user_config, policy, project_overrides)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What every entry point needs once: config, artifacts, README, effective policy.

### epythet.agentic_readme.README_NAMES *= ('README.md', 'readme.md', 'README.markdown', 'README.rst', 'README.txt', 'README')*

README filenames, in order of preference.

### *class* epythet.agentic_readme.ReadmeReport(project_dir, readme, checks, policy, user_config, project_overrides)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The outcome of [`check_readme()`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.check_readme) for one project.

`policy` is the effective `ReadmePolicy`; `user_config` and
`project_overrides` are where it came from.

#### *property* status *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`warn` when anything present is undocumented, else `ok`.

#### table()

The plain-text listing (the default CLI output).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### to_dict()

A JSON-ready view (`--format json`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### *property* warnings *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[KindCheck](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.KindCheck), ...]*

The kinds present in the project but missing from the README.

### epythet.agentic_readme.SECTION_FIELDS *= frozenset({'docs_block', 'for_humans_intro', 'heading', 'humans_link', 'instructions_block', 'marker_end', 'marker_start', 'name', 'repo_stub', 'site_url', 'skills_block', 'subagents_block'})*

The fields a section snippet may use.

### epythet.agentic_readme.SECTION_SNIPPET *= 'agentic-readme-section'*

The snippet names the section is rendered from. A project whose only agentic
aspect is its agent-readable documentation gets the shorter docs-only variant:
it ships no tooling, so the section must not say it does.

### *exception* epythet.agentic_readme.SectionError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

The README or a snippet is in a state the tool will not write over.

Raised for unpaired or repeated markers, a README that is not UTF-8 or not
Markdown, and a section snippet that fails to format or drops the markers.

### epythet.agentic_readme.ai_readme_check(project_dir, , format='table', fail_on='', draft=False, write=False)

Report whether the README documents the project’s agentic aspects; draft or write the section.

Reuses `epythet ai-artifacts` discovery (skills, subagents, instruction
files) plus the agent-readable outputs the site publishes, and looks for
each in the README: a `gh skill install` line or skill name, a subagent
name, `CLAUDE.md` / `AGENTS.md`, `llms.txt` / `<package>.md`, and a
heading about agents (or epythet’s own section markers). The effective
policy (`~/.config/epythet/config.toml` `[readme]`, overridden by the
project’s `[tool.epythet.readme]`) is part of the output so a skill can
read it. `--write` is explicit: it writes whatever the policy says.

* **Parameters:**
  * **project_dir** – the project root
  * **format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – table (human) or json
  * **fail_on** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `warn` to exit 1 when anything present is undocumented (default: exit 0)
  * **draft** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – print the README section rendered from the effective snippets and policy, without writing
  * **write** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – add or update the section in `README.md` between epythet’s markers

### epythet.agentic_readme.blurb(description, , max_length=140)

The first sentence of a skill or agent description, short enough for a table cell.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> blurb("Find and fix things. Use when asked to fix.")
'find and fix things'
>>> blurb("Do the thing and then some: a, b, c, " + "and more " * 30)
'do the thing and then some'
```

### epythet.agentic_readme.check_readme(project_dir, , config=None, artifacts=None, user_config=None)

Which agentic aspects the project has, and whether its README mentions each.

* **Parameters:**
  * **config** – the [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig) (loaded when omitted)
  * **artifacts** ([`AIArtifacts`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AIArtifacts) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – discovery result (computed when omitted)
  * **user_config** ([`UserConfig`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.UserConfig) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – the user’s policy (read from the config dir when omitted)
* **Return type:**
  [`ReadmeReport`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.ReadmeReport)

### epythet.agentic_readme.draft_section(project_dir, , user_config=None, config=None, artifacts=None)

Render the section for a project as it would be placed: `(section, readme_text, start, end)`.

`readme_text` is the current README (`""` when none exists); `start`
and `end` delimit the span the section replaces.

* **Raises:**
  [**SectionError**](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError) – on unpaired markers, a non-UTF-8 README, or a broken snippet
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### epythet.agentic_readme.find_readme(project_dir)

The project’s README, by the usual names (`None` when there is none).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.agentic_readme.github_anchor(title)

GitHub’s anchor for a heading title.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> github_anchor("For AI agents"), github_anchor("What it *fixes*: `x`")
('for-ai-agents', 'what-it-fixes-x')
>>> github_anchor("my_function and [links](https://x)")
'my_function-and-links'
```

### epythet.agentic_readme.headings_of(text)

Every ATX heading outside fenced code blocks.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Heading`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.Heading)]

```pycon
>>> [h.title for h in headings_of("# A\n```\n# not one\n```\n## B\n")]
['A', 'B']
```

### epythet.agentic_readme.headline_skill(skills)

The skill named in the install line: a `*-setup`-like one if any, else the first installable.

### epythet.agentic_readme.humans_link_for(heading)

`[Title](#anchor)` for the heading after the section, or a plain fallback.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.agentic_readme.instruction_text(\*, snippets=<function snippet_text>)

The instruction the skill hands an agent when the policy is `add`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.agentic_readme.load(project_dir, , config=None, artifacts=None, user_config=None)

Resolve a project once; each argument given is used instead of being loaded.

* **Return type:**
  [`Project`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.Project)

### epythet.agentic_readme.load_project(project_dir)

`(config, artifacts)` for a project; `config` is `None` for a non-Python tree.

A tree without `pyproject.toml` or `setup.cfg` is inspected without a
config. A tree that has one but cannot be loaded raises
[`ConfigError`](_autosummary/epythet.config.html.md#epythet.config.ConfigError): a broken `[tool.epythet]` must not
silently change what gets written.

### epythet.agentic_readme.marker_span(text, , strict=True)

The character span of the marked section (`None` when there is none).

Markers count only on their own line outside fenced code, so a README that
shows them in an example is not mistaken for one that has the section.

* **Parameters:**
  **strict** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – raise [`SectionError`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError) on an unpaired or repeated marker
  (`False`: report such a README as having no section)
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.agentic_readme.place_section(text, , agentic_first)

Where the section goes in `text`: `(start, end, next_heading, level)`.

`text[start:end]` is the span to replace: the existing marked section when
there is one (its position is kept, wherever the author moved it), else an
empty span right before the first heading after the title (`agentic_first`)
or at the end of the file. `next_heading` is the heading that follows the
span (the “for humans” target) and `level` the heading level the section
should use to sit among its siblings. Headings inside the existing section
are ignored, so rewriting never changes the level.

* **Raises:**
  [**SectionError**](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError) – on unpaired markers
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`Heading`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.Heading) | [`None`](https://docs.python.org/3/builtins/constants.html#None), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### epythet.agentic_readme.read_readme(path)

`(text, newline)`: the README with `\n` line ends, and the style to write back.

* **Raises:**
  [**SectionError**](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError) – when the file is not UTF-8
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.agentic_readme.render_section(artifacts, config, \*, policy, level=1, humans_link='the top of the page', snippets=<function snippet_text>, agent='claude-code')

The README section for `artifacts`, from the effective snippets and `policy`.

* **Parameters:**
  * **level** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – heading level (`1` renders `# For AI agents`)
  * **humans_link** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – what the “for humans” sentence points at
  * **snippets** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – `name -> text` resolver (the seam tests use to inject text)
  * **agent** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – the host named in the `gh skill install` line
* **Raises:**
  [**SectionError**](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError) – when the section snippet fails to format or drops a marker
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.agentic_readme.section_snippet_for(artifacts)

The name of the section snippet `artifacts` calls for.

[`SECTION_SNIPPET`](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SECTION_SNIPPET) when the project ships tooling, else the shorter
`DOCS_ONLY_SECTION_SNIPPET`, which makes no “ships tooling” claim.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.agentic_readme.ships_tooling(artifacts)

Whether the project ships anything an agent installs or reads as instructions.

Skills, subagents and instruction files count; published agent-readable
documentation (`llms.txt`, `<package>.md`) does not, because it is a
view of the docs rather than tooling.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### epythet.agentic_readme.splice_section(text, section, , start, end)

`text` with `section` in place of `text[start:end]`, blank lines kept sane.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.agentic_readme.write_section(project_dir, , user_config=None)

Add or update the marked section in the project’s README; returns `(path, outcome)`.

`outcome` is `added`, `updated` or `unchanged`. The README must be
Markdown (`README.md`); a missing README is created with the section alone.
Line endings are kept as found (CRLF stays CRLF).

* **Raises:**
  [**SectionError**](_autosummary/epythet.agentic_readme.html.md#epythet.agentic_readme.SectionError) – when there is nothing agentic to document, the README is
  not Markdown or not UTF-8, it has unpaired markers, or the snippet is broken
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/epythet.ai_artifacts.html.md

# epythet.ai_artifacts

Discover a repository’s AI agent artifacts and render the “For AI agents” page.

A repository that ships tooling for coding agents does so by convention, not
registration: skills are folders holding a `SKILL.md` (the Agent Skills spec),
subagents are Markdown files with a frontmatter, and instruction files carry
fixed names. This module reads those conventions and, when anything is found,
renders one page for the documentation site that says what exists, where it
lives, how to install it, and which machine-readable outputs the site itself
publishes (`llms.txt`, the `.md` twins, the flat `<package>.md`,
`objects.inv`).

Where epythet looks (relative to the project root; `{pkg}` is the package
directory):

| artifact          | locations, in order of preference                                                                                                                                |
|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| skills            | `{pkg}/data/skills/*/SKILL.md` (shipped in the wheel,<br/>`gh skill`-installable), `skills/*/SKILL.md`<br/>(`gh skill`-installable), `.claude/skills/*/SKILL.md` |
| subagents         | `{pkg}/data/agents/*.md`, `.claude/agents/*.md`                                                                                                                  |
| instruction files | `CLAUDE.md`, `.claude/CLAUDE.md`, `AGENTS.md`,<br/>`.github/copilot-instructions.md`, `.cursor/rules`,<br/>`.codex/`                                             |

Symlinks are followed and duplicates removed, so the `.claude/skills/` bridge
that points into `{pkg}/data/skills/` yields one skill, attributed to its real
location. The page is a `PageSpec` (`ai-agents.md`),
produced by [`ai_artifacts_page()`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.ai_artifacts_page) and added to the scaffold by default when
`[tool.epythet] ai_artifacts` is on (the default) and at least one artifact
exists. The default template is [`DEFAULT_TEMPLATE`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.DEFAULT_TEMPLATE); a project can point
`ai_artifacts_template` at its own file, and a hand-written `docsrc/ai-agents.md`
without the epythet marker is never overwritten.

```pycon
>>> import tempfile, pathlib
>>> root = pathlib.Path(tempfile.mkdtemp())
>>> skill = root / "pkg" / "data" / "skills" / "pkg-quickstart"
>>> skill.mkdir(parents=True)
>>> _ = (skill / "SKILL.md").write_text(
...     "---\nname: pkg-quickstart\ndescription: Use pkg.\n---\n\n# Body\n"
... )
>>> found = discover_artifacts(root, package_dir=root / "pkg")
>>> [s.name for s in found.skills], found.skills[0].shipped
(['pkg-quickstart'], True)
>>> found.skills[0].install_command("org/pkg")
'gh skill install org/pkg pkg-quickstart --agent claude-code'
```

### Module Attributes

| [`SKILL_LOCATIONS`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.SKILL_LOCATIONS)           | Skill folders relative to the project root; `{pkg}` is the package directory.                                                                 |
|----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| [`AGENT_LOCATIONS`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AGENT_LOCATIONS)           | Subagent definition folders (one Markdown file per agent).                                                                                    |
| [`INSTRUCTION_LOCATIONS`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.INSTRUCTION_LOCATIONS)     | Instruction files and directories agents read, with the audience each serves.                                                                 |
| [`PAGE_FILENAME`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.PAGE_FILENAME)             | The generated page's filename under `docsrc`.                                                                                                 |
| [`DEFAULT_AGENT_HOST`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.DEFAULT_AGENT_HOST)        | The agent host named in generated `gh skill install` lines.                                                                                   |
| [`DISABLE_ENV`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.DISABLE_ENV)               | Environment variable that switches the page off for a whole fleet build (`0` / `false` / `no` / `off`) without touching any `pyproject.toml`. |
| [`AGENT_OUTPUT_KINDS`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AGENT_OUTPUT_KINDS)        | The machine-readable outputs every epythet site publishes, in display order.                                                                  |
| [`TEMPLATE_FIELDS`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.TEMPLATE_FIELDS)           | The fields a page template may use.                                                                                                           |
| [`DEFAULT_TEMPLATE`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.DEFAULT_TEMPLATE)          | The default page template; `str.format` fields are the section renders.                                                                       |
| [`AGENT_OUTPUT_DESCRIPTIONS`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AGENT_OUTPUT_DESCRIPTIONS) | Descriptions of the machine-readable outputs, keyed by kind.                                                                                  |

### Functions

| [`agent_outputs_for`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.agent_outputs_for)(config)                          | The machine-readable outputs a configuration produces, with URLs when known.   |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`ai_artifacts_page`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.ai_artifacts_page)(config, \*[, artifacts])         | The "For AI agents" `PageSpec` for a project, or `None`.                       |
| [`artifacts_json`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.artifacts_json)(artifacts)                          | `artifacts` as indented JSON (the `--format json` CLI output).                 |
| [`artifacts_table`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.artifacts_table)(artifacts, \*[, repo_stub])        | A plain-text listing of `artifacts` (the default CLI output).                  |
| [`default_pages`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.default_pages)(config)                              | The generated pages a scaffold gets when the caller passes none.               |
| [`discover_artifacts`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.discover_artifacts)(project_dir, \*[, package_dir]) | Find the skills, subagents and instruction files of a project by convention.   |
| [`enabled_by_environment`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.enabled_by_environment)()                           | False when `EPYTHET_AI_ARTIFACTS` is set to `0`, `false`, `no` or `off`.       |
| [`parse_frontmatter`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.parse_frontmatter)(text)                            | The YAML frontmatter of a Markdown file as a dict (`{}` when absent).          |
| [`render_ai_artifacts_page`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.render_ai_artifacts_page)(artifacts, config, \*)    | Render the "For AI agents" page for `artifacts` and a `DocsConfig`.            |
| [`repo_stub_for`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.repo_stub_for)(repo_url)                            | `owner/repo` from a GitHub URL ('' when it is not one).                        |
| [`site_url_for`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.site_url_for)(repo_url)                             | The GitHub Pages URL a GitHub repository publishes to ('' when unknown).       |

### Classes

| [`AIArtifacts`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AIArtifacts)(project_dir[, skills, ...])           | Everything [`discover_artifacts()`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.discover_artifacts) found for one project.   |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| [`AgentOutput`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AgentOutput)(kind, filename, description[, url])   | A machine-readable output of the built site, with its URL when known.                                     |
| [`InstructionFile`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.InstructionFile)(source, audience[, is_dir])       | An instruction file or directory (`CLAUDE.md`, `AGENTS.md`, ...).                                         |
| [`Skill`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.Skill)(name, source[, description, audience, ...]) | One skill folder: its `name`, description, and where the real files live.                                 |
| [`Subagent`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.Subagent)(name, source[, description, tools, ...]) | One subagent definition file (`name`, description, tools, source path).                                   |

### epythet.ai_artifacts.AGENT_LOCATIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('{pkg}/data/agents', '.claude/agents')*

Subagent definition folders (one Markdown file per agent).

### epythet.ai_artifacts.AGENT_OUTPUT_DESCRIPTIONS *= {'aggregate_md': 'the whole documentation as one Markdown file', 'aggregate_pdf': 'the whole documentation as one PDF, for reading', 'llms': 'an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)', 'md_twins': 'a rendered Markdown twin of every page, advertised from each page\\'s \`<head>\` with \`<link rel="alternate" type="text/markdown">\`', 'objects_inv': 'the Sphinx inventory: a symbol-to-URL index (\`sphobjinv convert plain objects.inv -\`)'}*

Descriptions of the machine-readable outputs, keyed by kind.

### epythet.ai_artifacts.AGENT_OUTPUT_KINDS *= ('llms', 'aggregate_md', 'aggregate_pdf', 'md_twins', 'objects_inv')*

The machine-readable outputs every epythet site publishes, in display order.

### *class* epythet.ai_artifacts.AIArtifacts(project_dir, skills=(), subagents=(), instruction_files=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything [`discover_artifacts()`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.discover_artifacts) found for one project.

#### to_dict()

A JSON-ready view (paths relative to the project root).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### *class* epythet.ai_artifacts.AgentOutput(kind, filename, description, url='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A machine-readable output of the built site, with its URL when known.

### epythet.ai_artifacts.DEFAULT_AGENT_HOST *= 'claude-code'*

The agent host named in generated `gh skill install` lines.

### epythet.ai_artifacts.DEFAULT_TEMPLATE *= '{marker}\\n\\n# For AI agents\\n\\n\`{name}\` ships artifacts for coding agents alongside its code. This page lists\\nthem, says where each lives in the repository, and points at the\\nmachine-readable copies of this documentation.\\n{skills_section}{subagents_section}{instructions_section}{outputs_section}'*

The default page template; `str.format` fields are the section renders.

### epythet.ai_artifacts.DISABLE_ENV *= 'EPYTHET_AI_ARTIFACTS'*

Environment variable that switches the page off for a whole fleet build
(`0` / `false` / `no` / `off`) without touching any `pyproject.toml`.

### epythet.ai_artifacts.INSTRUCTION_LOCATIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)], ...]* *= (('CLAUDE.md', 'Claude Code'), ('.claude/CLAUDE.md', 'Claude Code'), ('AGENTS.md', 'Codex, Copilot, Cursor and other agents'), ('.github/copilot-instructions.md', 'GitHub Copilot'), ('.cursor/rules', 'Cursor'), ('.codex', 'Codex'))*

Instruction files and directories agents read, with the audience each serves.

### *class* epythet.ai_artifacts.InstructionFile(source, audience, is_dir=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An instruction file or directory (`CLAUDE.md`, `AGENTS.md`, …).

### epythet.ai_artifacts.PAGE_FILENAME *= 'ai-agents.md'*

The generated page’s filename under `docsrc`.

### epythet.ai_artifacts.SKILL_LOCATIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('{pkg}/data/skills', 'skills', '.claude/skills')*

Skill folders relative to the project root; `{pkg}` is the package directory.

### *class* epythet.ai_artifacts.Skill(name, source, description='', audience='', shipped=False, installable=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One skill folder: its `name`, description, and where the real files live.

`source` is the project-relative POSIX path of the folder that holds the
files (a symlink in `.claude/skills/` is attributed to its target).
`shipped` is true when that folder is under the package directory, so the
skill is inside the wheel; `installable` when `gh skill` can see it (a
non-hidden path).

#### install_command(repo_stub, , agent='claude-code')

The `gh skill install` line, or `None` when `gh skill` cannot see it.

```pycon
>>> Skill("x", "pkg/data/skills/x").install_command("org/repo")
'gh skill install org/repo x --agent claude-code'
>>> Skill("x", ".claude/skills/x", installable=False).install_command("o/r")
```

### *class* epythet.ai_artifacts.Subagent(name, source, description='', tools='', shipped=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One subagent definition file (`name`, description, tools, source path).

### epythet.ai_artifacts.TEMPLATE_FIELDS *= frozenset({'display_name', 'instructions_section', 'marker', 'name', 'outputs_section', 'repo_stub', 'site_url', 'skills_section', 'subagents_section'})*

The fields a page template may use.

### epythet.ai_artifacts.agent_outputs_for(config)

The machine-readable outputs a configuration produces, with URLs when known.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`AgentOutput`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AgentOutput)]

```pycon
>>> from epythet.config import DocsConfig
>>> cfg = DocsConfig(project_dir="/tmp/x", name="x", repo_url="https://github.com/o/x")
>>> [o.filename for o in agent_outputs_for(cfg)]
['llms.txt', 'x.md', '<page>.html.md', 'objects.inv']
>>> agent_outputs_for(cfg)[0].url
'https://o.github.io/x/llms.txt'
```

### epythet.ai_artifacts.ai_artifacts_page(config, , artifacts=None)

The “For AI agents” `PageSpec` for a project, or `None`.

`None` when `config.ai_artifacts` is off, when the `EPYTHET_AI_ARTIFACTS`
environment variable is `0`/`false` (the fleet-wide switch), or when no
artifact was found. The template is `config.ai_artifacts_template` (a
file, relative to the project root) when set, else [`DEFAULT_TEMPLATE`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.DEFAULT_TEMPLATE).

* **Raises:**
  [**ConfigError**](_autosummary/epythet.config.html.md#epythet.config.ConfigError) – when the template file is missing or has a field the
  renderer does not provide (literal braces must be doubled: `{{`).

### epythet.ai_artifacts.artifacts_json(artifacts)

`artifacts` as indented JSON (the `--format json` CLI output).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.ai_artifacts.artifacts_table(artifacts, , repo_stub='')

A plain-text listing of `artifacts` (the default CLI output).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.ai_artifacts.default_pages(config)

The generated pages a scaffold gets when the caller passes none.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### epythet.ai_artifacts.discover_artifacts(project_dir, , package_dir=None)

Find the skills, subagents and instruction files of a project by convention.

* **Parameters:**
  * **project_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – the repository root
  * **package_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – the importable package directory, for `{pkg}/data/...`
    (skipped when `None`)
* **Return type:**
  [`AIArtifacts`](_autosummary/epythet.ai_artifacts.html.md#epythet.ai_artifacts.AIArtifacts)

### epythet.ai_artifacts.enabled_by_environment()

False when `EPYTHET_AI_ARTIFACTS` is set to `0`, `false`, `no` or `off`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> os.environ[DISABLE_ENV] = "0"; enabled_by_environment()
False
>>> del os.environ[DISABLE_ENV]; enabled_by_environment()
True
```

### epythet.ai_artifacts.parse_frontmatter(text)

The YAML frontmatter of a Markdown file as a dict (`{}` when absent).

Uses PyYAML when installed; otherwise, or when PyYAML rejects the block (an
unquoted `description: Use when x: y` is a common slip), a small reader
that understands the subset skills and agents use: `key: value` scalars,
`>`/`|` block scalars, one level of nested mapping, `[a, b]` flow
lists and trailing comments. A malformed frontmatter never raises.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> parse_frontmatter("---\ndescription: Use when a: b\nname: x\n---\n")
{'description': 'Use when a: b', 'name': 'x'}
```

```pycon
>>> parse_frontmatter("---\nname: x\nmetadata:\n  audience: users\n---\nbody")
{'name': 'x', 'metadata': {'audience': 'users'}}
>>> parse_frontmatter("no frontmatter")
{}
```

### epythet.ai_artifacts.render_ai_artifacts_page(artifacts, config, , template='{marker}\\\\n\\\\n# For AI agents\\\\n\\\\n\`{name}\` ships artifacts for coding agents alongside its code. This page lists\\\\nthem, says where each lives in the repository, and points at the\\\\nmachine-readable copies of this documentation.\\\\n{skills_section}{subagents_section}{instructions_section}{outputs_section}', agent='claude-code')

Render the “For AI agents” page for `artifacts` and a `DocsConfig`.

* **Parameters:**
  * **template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – a `str.format` template with the fields `marker`,
    `name`, `display_name`, `repo_stub`, `site_url`, `skills_section`,
    `subagents_section`, `instructions_section`, `outputs_section`
  * **agent** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – the host named in the `gh skill install` lines
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.ai_artifacts.repo_stub_for(repo_url)

`owner/repo` from a GitHub URL (’’ when it is not one).

Deeper paths, fragments and queries are dropped, so an `Issues` URL in
`[project.urls]` still names the repository.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> repo_stub_for("https://github.com/i2mint/epythet.git")
'i2mint/epythet'
>>> repo_stub_for("https://github.com/i2mint/epythet/issues#readme")
'i2mint/epythet'
>>> repo_stub_for("git@github.com:i2mint/epythet.git")
'i2mint/epythet'
>>> repo_stub_for("https://gitlab.com/o/r")
''
```

### epythet.ai_artifacts.site_url_for(repo_url)

The GitHub Pages URL a GitHub repository publishes to (’’ when unknown).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> site_url_for("https://github.com/i2mint/epythet")
'https://i2mint.github.io/epythet/'
>>> site_url_for("")
''
```


# _autosummary/epythet.build.html.md

# epythet.build

### epythet.build(config, target='html', , overrides=None)

Run one build target for a loaded configuration; returns the output directory.

* **Parameters:**
  * **config** ([`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig)) – the project’s configuration
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – a Sphinx builder name or one of `github`, `gitlab`, `clean`
  * **overrides** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – configuration overrides forwarded to the generated `conf.py`
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/epythet.call_make.html.md

# epythet.call_make

Compatibility module: `make` now lives in [`epythet.build`](_autosummary/epythet.build.html.md#epythet.build).


# _autosummary/epythet.cli.html.md

# epythet.cli

Command line access to epythet.

`epythet quickstart PROJECT_DIR [--ignore ...]` is the command the
`publish-github-pages` action runs: it scaffolds `docsrc`, builds the HTML
and writes it to `PROJECT_DIR/docsrc/_build/html`.

### Module Attributes

| [`COMMANDS`](_autosummary/epythet.cli.html.md#epythet.cli.COMMANDS)        | The commands `epythet` exposes, in the order they appear in `--help`.                                                                             |
|------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| [`TOOL_COMMANDS`](_autosummary/epythet.cli.html.md#epythet.cli.TOOL_COMMANDS)   | The v2 source-editing and fleet commands, by their command-line name.                                                                             |
| [`LEDGER_COMMANDS`](_autosummary/epythet.cli.html.md#epythet.cli.LEDGER_COMMANDS) | maintenance of the artifact ledger.                                                                                                               |
| [`CONVENTION`](_autosummary/epythet.cli.html.md#epythet.cli.CONVENTION)      | the command modules use `from __future__ import annotations`, and `list[str]` must still become `nargs="*"` (`--ignore a b`), not a single value. |

### Functions

| [`ai_artifacts`](_autosummary/epythet.cli.html.md#epythet.cli.ai_artifacts)(project_dir, \*[, format])   | List the AI agent artifacts a project ships (skills, subagents, instruction files).   |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`build_info`](_autosummary/epythet.cli.html.md#epythet.cli.build_info)(project_dir, \*[, no_pypi])    | Print the build provenance record for a project as JSON.                              |
| [`check_pages`](_autosummary/epythet.cli.html.md#epythet.cli.check_pages)(repo, \*[, no_url_check])     | Diagnose GitHub Pages setup for a repo.                                               |
| [`configure_pages`](_autosummary/epythet.cli.html.md#epythet.cli.configure_pages)(repo, \*[, branch, path]) | Enable or fix GitHub Pages for a repo.                                                |
| [`epythet_cli`](_autosummary/epythet.cli.html.md#epythet.cli.epythet_cli)([argv])                       | Entry point for the `epythet` console script.                                         |
| [`mk_epythet_parser`](_autosummary/epythet.cli.html.md#epythet.cli.mk_epythet_parser)(\*\*parser_kwargs)      | The full `epythet` parser: the flat commands, the tool commands, the `ledger` group.  |
| [`quickstart`](_autosummary/epythet.cli.html.md#epythet.cli.quickstart)(project_dir, \*[, ignore])     | Scaffold docsrc and build the HTML documentation in one go.                           |

### epythet.cli.COMMANDS *= [<function make_docsrc>, <function make_autodocs>, <function make>, <function quickstart>, <function check_pages>, <function configure_pages>, <function validate>, <function ai_artifacts>, <function ai_readme_check>, <function build_info>]*

The commands `epythet` exposes, in the order they appear in `--help`.

### epythet.cli.CONVENTION *= Convention(naming='by_name_if_has_default', short_flags=True, hyphenate_commands=True, hyphenate_groups=False, default_in_help=True, hints_when_declared=False, resolve_hints=True, decode=<function argh_decode>, egress=<function argh_egress>)*

the command
modules use `from __future__ import annotations`, and `list[str]` must
still become `nargs="*"` (`--ignore a b`), not a single value.

* **Type:**
  cw’s argh-compatible convention, resolving string annotations

### epythet.cli.LEDGER_COMMANDS *= {'propose': <function propose_command>}*

maintenance of the artifact ledger.

* **Type:**
  `epythet ledger <command>`

### epythet.cli.TOOL_COMMANDS *= {'migrate-style': <function migrate_style_command>, 'repair': <function repair_command>, 'sweep': <function sweep_command>}*

The v2 source-editing and fleet commands, by their command-line name.

### epythet.cli.ai_artifacts(project_dir, , format='table')

List the AI agent artifacts a project ships (skills, subagents, instruction files).

Looks where agents and `gh skill` look: `<pkg>/data/skills`, `skills/`,
`.claude/skills`, `<pkg>/data/agents`, `.claude/agents`, `CLAUDE.md`,
`AGENTS.md`, `.cursor/rules`, `.codex`. The same discovery feeds the
generated “For AI agents” documentation page.

* **Parameters:**
  * **project_dir** – the project root
  * **format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – table (human) or json

### epythet.cli.build_info(project_dir, , no_pypi=False)

Print the build provenance record for a project as JSON.

The same record that an `html` build writes to `build_info.json` at the
site root and renders on the about-this-build page: package name and
version, git commit/branch/tags/dirty flag, CI context, tool versions, the
resolved configuration, the latest PyPI release and whether the docs and
the package look aligned. The documented-module counts need a build and
are `null` here.

* **Parameters:**
  * **project_dir** – the project root
  * **no_pypi** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – skip the PyPI lookup (also `EPYTHET_PYPI_CHECK=0`)

### epythet.cli.check_pages(repo, , no_url_check=False)

Diagnose GitHub Pages setup for a repo.

* **Parameters:**
  * **repo** – GitHub repo as ‘owner/repo’, or path to a local git checkout.
  * **no_url_check** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Skip checking if the docs URL actually responds.

### epythet.cli.configure_pages(repo, , branch='gh-pages', path='/')

Enable or fix GitHub Pages for a repo. Requires gh CLI or GITHUB_TOKEN.

* **Parameters:**
  * **repo** – GitHub repo as ‘owner/repo’, or path to a local git checkout.
  * **branch** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Branch to serve Pages from (default: gh-pages).
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Folder within the branch (default: /).

### epythet.cli.epythet_cli(argv=None)

Entry point for the `epythet` console script.

### epythet.cli.mk_epythet_parser(\*\*parser_kwargs)

The full `epythet` parser: the flat commands, the tool commands, the `ledger` group.

### epythet.cli.quickstart(project_dir, , ignore=None)

Scaffold docsrc and build the HTML documentation in one go.

Equivalent to `make-docsrc` then `make html`, with `ignore` applied
to the API generator. An empty `ignore` (the action passes `--ignore`
with no values when its input is unset, or `""`) means “use the configured
default”; each value may itself be comma-separated.

* **Parameters:**
  * **project_dir** – Path to root project directory (pyproject.toml or setup.cfg)
  * **ignore** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – skip file if path contains any ignore strings


# _autosummary/epythet.confgen.html.md

# epythet.confgen

Generate the Sphinx configuration from a [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig).

[`sphinx_settings()`](_autosummary/epythet.confgen.html.md#epythet.confgen.sphinx_settings) returns the plain dict a `conf.py` would define. The
shim `docsrc/conf.py` that `epythet.scaffold.make_docsrc()` writes gets
it through `from epythet.sphinx_conf import *`; tests and tools call it
directly. Every value here is either verified in the v2 research (README
include, nested API tree, typed cross-references, agent twins) or a direct
consequence of the decision record.

```pycon
>>> from epythet.config import DocsConfig
>>> cfg = DocsConfig(project_dir="/tmp/x", name="x", package_dir="x", theme="furo", api_generator="autosummary")
>>> s = sphinx_settings(cfg)
>>> s["html_theme"], s["default_role"], "sphinx.ext.autosummary" in s["extensions"]
('furo', 'code', True)
```

### Module Attributes

| [`BASE_EXTENSIONS`](_autosummary/epythet.confgen.html.md#epythet.confgen.BASE_EXTENSIONS)   | Extensions every epythet site uses, whatever the API generator.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------|
| [`API_ROOT`](_autosummary/epythet.confgen.html.md#epythet.confgen.API_ROOT)          | Where the API pages live under `docsrc` (and in the site URL).    |

### Functions

| [`api_toctree_entry`](_autosummary/epythet.confgen.html.md#epythet.confgen.api_toctree_entry)(config)                 | The document `index.md`'s toctree points at for the API pages.                 |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`merge_settings`](_autosummary/epythet.confgen.html.md#epythet.confgen.merge_settings)(base, extra)               | Merge two settings dicts, concatenating list values instead of replacing them. |
| [`sphinx_settings`](_autosummary/epythet.confgen.html.md#epythet.confgen.sphinx_settings)(config, \*[, build_info]) | The complete Sphinx `conf.py` namespace for `config`.                          |

### epythet.confgen.API_ROOT *= 'api'*

Where the API pages live under `docsrc` (and in the site URL).

### epythet.confgen.BASE_EXTENSIONS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('sphinx.ext.napoleon', 'sphinx_autodoc_typehints', 'sphinx.ext.intersphinx', 'sphinx.ext.doctest', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', 'myst_parser', 'sphinxcontrib.mermaid', 'sphinx_copybutton', 'epythet.sphinx_ext')*

Extensions every epythet site uses, whatever the API generator.

### epythet.confgen.api_toctree_entry(config)

The document `index.md`’s toctree points at for the API pages.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from epythet.config import DocsConfig
>>> api_toctree_entry(DocsConfig(project_dir="/tmp/x", name="x", api_generator="autosummary"))
'api'
>>> api_toctree_entry(DocsConfig(project_dir="/tmp/x", name="x", api_generator="autoapi"))
'api/index'
```

### epythet.confgen.merge_settings(base, extra)

Merge two settings dicts, concatenating list values instead of replacing them.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> merge_settings({"extensions": ["a"], "x": 1}, {"extensions": ["b"], "x": 2})
{'extensions': ['a', 'b'], 'x': 2}
```

### epythet.confgen.sphinx_settings(config, , build_info=None)

The complete Sphinx `conf.py` namespace for `config`.

* **Parameters:**
  **build_info** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – the provenance record of this build
  ([`epythet.provenance.collect_build_info()`](_autosummary/epythet.provenance.html.md#epythet.provenance.collect_build_info)), rendered by the
  extension as the landing-page footer and `build_info.json`; `None`
  renders nothing.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/epythet.config.html.md

# epythet.config

Single source of truth for a project’s documentation configuration.

Everything epythet needs to build a site comes from two places, read in this
order of precedence:

1. `pyproject.toml`: the `[project]` table for name, version and authors,
   and the `[tool.epythet]` table for documentation choices.
2. `setup.cfg`: the `[metadata]` section (legacy projects).

When both files exist, `pyproject.toml` wins as soon as it has a `[project]`
table. (epythet 0.1.x silently preferred `setup.cfg`, which was wrong for the
projects that had migrated to `pyproject.toml` but kept a stale `setup.cfg`.)

The result is a [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig), an immutable dataclass. The legacy 5-tuple
accessor [`epythet.config_parser.parse_config()`](_autosummary/epythet.config_parser.html.md#epythet.config_parser.parse_config) is derived from it and keeps
its signature, so old `docsrc/conf.py` copies keep working.

The `[tool.epythet]` keys, all optional:

```default
[tool.epythet]
display_name = "Dol"          # site title; default: the project name
copyright = "2024, Jane Doe"  # footer line; default: none rendered
theme = "auto"                # "auto" | "furo" | "shibuya" | ... | any installed theme
accent = "#3661ac"            # default: derived from the package name (OKLCH)
mode = "auto"                 # "auto" | "light" | "dark"
ignore = ["tests/", "scrap/", "examples/"]  # path substrings to skip
api_generator = "auto"        # "auto" | "autosummary" (imports the package) | "autoapi" (static)
agent_outputs = true          # llms.txt + .md twins of every page
aggregates = ["md"]           # flat single-document twins at the site root
ai_artifacts = true           # "For AI agents" page when skills/agents/CLAUDE.md exist
ai_artifacts_template = ""    # project-relative file overriding that page's template
provenance = true             # build footer, about-this-build page, build_info.json; "minimal": no page
provenance_template = ""      # project-relative file overriding the about-this-build page template
package_dir = "src/dol"       # default: found by convention
docs_dir = "docsrc"           # where the Sphinx sources live

[tool.epythet.theme_options]  # verbatim passthrough into html_theme_options
[tool.epythet.readme]         # project override of the user-level README policy (see epythet.userconfig)
announcement = "v2 is in beta"
```

```pycon
>>> import tempfile, pathlib
>>> d = pathlib.Path(tempfile.mkdtemp())
>>> _ = (d / "pyproject.toml").write_text('''
... [project]
... name = "my-pkg"
... version = "1.2.3"
... authors = [{name = "Jane Doe"}]
... [tool.epythet]
... theme = "furo"
... ''')
>>> _ = (d / "my_pkg").mkdir()
>>> _ = (d / "my_pkg" / "__init__.py").write_text("")
>>> cfg = load_config(d)
>>> cfg.name, cfg.version, cfg.author, cfg.theme, cfg.package_dir.name
('my-pkg', '1.2.3', 'Jane Doe', 'furo', 'my_pkg')
```

### Module Attributes

| [`DEFAULT_IGNORE`](_autosummary/epythet.config.html.md#epythet.config.DEFAULT_IGNORE)         | Path substrings skipped by default when discovering modules to document.       |
|-------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`ALWAYS_IGNORE`](_autosummary/epythet.config.html.md#epythet.config.ALWAYS_IGNORE)          | a `__main__` is a command line, not an API.                                    |
| [`DEFAULT_DOCS_DIR`](_autosummary/epythet.config.html.md#epythet.config.DEFAULT_DOCS_DIR)       | Directory under the project root holding the Sphinx sources.                   |
| [`PACKAGE_DIR_CANDIDATES`](_autosummary/epythet.config.html.md#epythet.config.PACKAGE_DIR_CANDIDATES) | Directory candidates (relative to the project root) that may hold the package. |
| [`NON_PACKAGE_DIRS`](_autosummary/epythet.config.html.md#epythet.config.NON_PACKAGE_DIRS)       | Top-level directories never taken for the package when guessing by convention. |
| [`IMPORT_PROBE_TIMEOUT`](_autosummary/epythet.config.html.md#epythet.config.IMPORT_PROBE_TIMEOUT)   | Seconds allowed for the import probe behind `api_generator = "auto"`.          |

### Functions

| [`find_package_dir`](_autosummary/epythet.config.html.md#epythet.config.find_package_dir)(project_dir, name)     | Locate the package directory for `name` under `project_dir` by convention.    |
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`load_config`](_autosummary/epythet.config.html.md#epythet.config.load_config)(project_dir, \*\*overrides) | Read a project's documentation configuration.                                 |
| [`resolve_api_generator`](_autosummary/epythet.config.html.md#epythet.config.resolve_api_generator)(config)           | The generator to run: `autosummary` when the package imports, else `autoapi`. |
| [`split_ignore`](_autosummary/epythet.config.html.md#epythet.config.split_ignore)(ignore)                    | Normalise ignore patterns: each item may itself be a comma-separated list.    |

### Classes

| [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig)(project_dir, name[, version, ...])   | Everything needed to generate a project's documentation.   |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------|

### Exceptions

| [`ConfigError`](_autosummary/epythet.config.html.md#epythet.config.ConfigError)   | A project's documentation configuration is missing or invalid.   |
|----------------------------------------------------------------|------------------------------------------------------------------|

### epythet.config.ALWAYS_IGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('_\_main_\_',)*

a `__main__` is a
command line, not an API. (autosummary still imports it once while
discovering the package, as it does every submodule; a `__main__` that
runs argparse at import survives that because Sphinx turns its `SystemExit`
into a skipped import.)

* **Type:**
  Modules that never get a page, whatever `ignore` says

### *exception* epythet.config.ConfigError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A project’s documentation configuration is missing or invalid.

### epythet.config.DEFAULT_DOCS_DIR *= 'docsrc'*

Directory under the project root holding the Sphinx sources.

### epythet.config.DEFAULT_IGNORE *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('tests/', 'scrap/', 'examples/')*

Path substrings skipped by default when discovering modules to document.

### *class* epythet.config.DocsConfig(project_dir, name, version='', author='', description='', display_name='', copyright='', repo_url='', theme='auto', accent='', mode='auto', theme_options=<factory>, readme=<factory>, ignore=('tests/', 'scrap/', 'examples/'), api_generator='auto', agent_outputs=True, aggregates=('md', ), ai_artifacts=True, ai_artifacts_template='', provenance=True, provenance_template='', package_dir=None, docs_dir='docsrc')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything needed to generate a project’s documentation.

Attributes mirror the `[tool.epythet]` keys; see the module docstring.
`project_dir` and `package_dir` are absolute paths.

#### *property* api_ignore *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

what the API generators skip.

```pycon
>>> DocsConfig(project_dir="/tmp/x", name="x", ignore=["tests/"]).api_ignore
('tests/', '__main__')
```

* **Type:**
  `ignore` plus [`ALWAYS_IGNORE`](_autosummary/epythet.config.html.md#epythet.config.ALWAYS_IGNORE)

#### *property* docsrc_dir *: [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)*

Absolute path of the Sphinx source directory.

#### *property* legacy_tuple *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The 5-tuple that [`epythet.config_parser.parse_config()`](_autosummary/epythet.config_parser.html.md#epythet.config_parser.parse_config) returns.

#### *property* package_name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The importable package name (`my-pkg` becomes `my_pkg`).

#### *property* resolved_api_generator *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

see [`resolve_api_generator()`](_autosummary/epythet.config.html.md#epythet.config.resolve_api_generator).

* **Type:**
  `api_generator` with `"auto"` resolved

#### with_overrides(\*\*changes)

A copy with some fields replaced (`None` values are ignored).

* **Return type:**
  [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig)

### epythet.config.IMPORT_PROBE_TIMEOUT *= 120*

Seconds allowed for the import probe behind `api_generator = "auto"`.

### epythet.config.NON_PACKAGE_DIRS *= frozenset({'docs', 'docsrc', 'examples', 'misc', 'scrap', 'test', 'tests'})*

Top-level directories never taken for the package when guessing by convention.

### epythet.config.PACKAGE_DIR_CANDIDATES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('{name}', 'src/{name}')*

Directory candidates (relative to the project root) that may hold the package.

### epythet.config.find_package_dir(project_dir, name)

Locate the package directory for `name` under `project_dir` by convention.

Tries `<name>/` then `src/<name>/` (with `-` mapped to `_`), returning
the first that contains an `__init__.py`; `None` when nothing matches.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> find_package_dir("/nonexistent", "nothing") is None
True
```

### epythet.config.load_config(project_dir, \*\*overrides)

Read a project’s documentation configuration.

* **Parameters:**
  * **project_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – the project root (holding `pyproject.toml` or `setup.cfg`),
    or a path to one of those files.
  * **overrides** – field values that win over the files (`None` is ignored),
    e.g. `ignore=["tests/"]` from a command line flag.
* **Raises:**
  [**ConfigError**](_autosummary/epythet.config.html.md#epythet.config.ConfigError) – when neither configuration file is found.
* **Return type:**
  [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig)

### epythet.config.resolve_api_generator(config)

The generator to run: `autosummary` when the package imports, else `autoapi`.

`autosummary` imports the package and documents what it finds (aliases,
partials, re-exports); when the import fails, in CI typically because an
optional dependency is missing, it produces an *empty* API section and a
successful build. `auto` probes the import once, in a subprocess with the
project root on `sys.path` (as the build has it), and falls back to the
static `autoapi` generator, printing why. An explicit value is returned as
is, and so is `"auto"` when the package directory is unknown.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.config.split_ignore(ignore)

Normalise ignore patterns: each item may itself be a comma-separated list.

The publish action passes its `ignore` input verbatim as one argument,
`--ignore tests/,scrap/,examples/`, and `setup.cfg` values are strings;
both must mean three patterns, not one that never matches.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> split_ignore(["tests/,scrap/", " examples/ ", "", "tests/"])
('tests/', 'scrap/', 'examples/')
```


# _autosummary/epythet.config_parser.html.md

# epythet.config_parser

Legacy configuration accessor kept for the frozen `docsrc/conf.py` copies.

Over a hundred projects committed a `docsrc/conf.py` that does:

```default
project, copyright, author, release, display_name = parse_config(
    Path(__file__).absolute().parent.parent / "setup.cfg"
)
```

That 5-tuple is a published contract, so it stays. New code should use
[`epythet.config.load_config()`](_autosummary/epythet.config.html.md#epythet.config.load_config), which returns the full
[`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig).

### Functions

| [`parse_config`](_autosummary/epythet.config_parser.html.md#epythet.config_parser.parse_config)(config_file)   | Project metadata for Sphinx's `conf.py`, as a 5-tuple.   |
|------------------------------------------------------------------------------|----------------------------------------------------------|

### epythet.config_parser.parse_config(config_file)

Project metadata for Sphinx’s `conf.py`, as a 5-tuple.

`config_file` may be a `setup.cfg` path, a `pyproject.toml` path or the
project directory itself. Whatever is passed, the project directory is what
gets resolved: `pyproject.toml` (with a `[project]` table) wins over
`setup.cfg` when both exist. See [`epythet.config`](_autosummary/epythet.config.html.md#module-epythet.config) for the keys.

`copyright` is an empty string when unset, and the generated docs render no
copyright line in that case.

* **Parameters:**
  **config_file** – `PROJECT_DIR/setup.cfg`, `PROJECT_DIR/pyproject.toml`
  or `PROJECT_DIR`
* **Returns:**
  `(name, copyright, author, version, display_name)`
* **Raises:**
  [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – when neither configuration file exists


# _autosummary/epythet.docs_gen.html.md

# epythet.docs_gen

Documentation generation entry points (re-exported for compatibility).


# _autosummary/epythet.html.md

# epythet

Beautiful, correct documentation from a Python package, with no boilerplate.

Point epythet at a project and its conventions (README, docstrings, package
layout, `pyproject.toml` metadata) produce the site:

```default
epythet quickstart PROJECT_DIR --ignore tests/ scrap/ examples/
```

which writes `PROJECT_DIR/docsrc/_build/html/`: a landing page that *is* the
README, a nested API tree, a light/dark theme with an accent derived from the
package name, and agent-facing twins (`llms.txt`, a `.md` per page, a flat
`<package>.md`, `objects.inv`).

The same, from Python:

```default
from epythet import quickstart
quickstart(PROJECT_DIR, ignore=["tests/"])
```

Or step by step: `make_docsrc()` writes `docsrc/` (a two-line `conf.py`
shim and `index.md`), `make()` runs Sphinx (`html` by default). All
configuration lives in `[tool.epythet]` of `pyproject.toml`; see
[`epythet.config`](_autosummary/epythet.config.html.md#module-epythet.config) for the keys and [`epythet.themes`](_autosummary/epythet.themes.html.md#module-epythet.themes) for the themes.

Rendering fixes for common docstring slips (a doctest glued to the prose above
it, a Markdown fence, `Returns: text` on one line, a stray `*args`) are
applied at build time by [`epythet.normalizer`](_autosummary/epythet.normalizer.html.md#module-epythet.normalizer), so existing docstrings
render correctly without edits.

A repository’s agent artifacts (skills, subagents, `CLAUDE.md` and friends)
are discovered by convention and rendered as a “For AI agents” page, see
[`epythet.ai_artifacts`](_autosummary/epythet.ai_artifacts.html.md#module-epythet.ai_artifacts); epythet’s own skills ship in `epythet/data/skills`.
Whether the README documents them is `check_readme()`
(`epythet ai-readme-check`), with the user’s policy and text snippets from
[`epythet.userconfig`](_autosummary/epythet.userconfig.html.md#module-epythet.userconfig) (`~/.config/epythet`).

Every site states its provenance: a one-line footer on the landing page (build
time, commit, package version), an `about-this-build` page with the full
diagnosis and a `build_info.json` for machines, see [`epythet.provenance`](_autosummary/epythet.provenance.html.md#module-epythet.provenance)
and `collect_build_info()`.

GitHub Pages helpers (`check_pages_setup()`, `enable_pages()`) and
docstring diagnosis tools (`diagnose_doctest_code_blocks()`,
`repair_package()`) live in [`epythet.tools`](_autosummary/epythet.tools.html.md#module-epythet.tools).

### Functions

| [`quickstart`](_autosummary/epythet.html.md#epythet.quickstart)(project_dir, \*[, ignore])   | Scaffold `docsrc` and build the HTML site; returns the output directory.   |
|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|

### epythet.quickstart(project_dir, , ignore=None)

Scaffold `docsrc` and build the HTML site; returns the output directory.

* **Parameters:**
  * **project_dir** – the project root
  * **ignore** – path substrings to skip (default: `[tool.epythet] ignore`)

### Modules

| [`agent_outputs`](_autosummary/epythet.agent_outputs.html.md#module-epythet.agent_outputs)              | Agent-facing outputs: `llms.txt`, Markdown twins, link relations, aggregates.                                                        |
|----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| [`agentic_readme`](_autosummary/epythet.agentic_readme.html.md#module-epythet.agentic_readme)            | Check that a README documents a project's agentic aspects; render and place the section.                                             |
| [`ai_artifacts`](_autosummary/epythet.ai_artifacts.html.md#module-epythet.ai_artifacts)                | Discover a repository's AI agent artifacts and render the "For AI agents" page.                                                      |
| [`build`](_autosummary/epythet.build.html.md#epythet.build)(config[, target, overrides])        | Run one build target for a loaded configuration; returns the output directory.                                                       |
| [`call_make`](_autosummary/epythet.call_make.html.md#module-epythet.call_make)                      | Compatibility module: `make` now lives in [`epythet.build`](_autosummary/epythet.build.html.md#epythet.build).              |
| [`cli`](_autosummary/epythet.cli.html.md#module-epythet.cli)                                  | Command line access to epythet.                                                                                                      |
| [`confgen`](_autosummary/epythet.confgen.html.md#module-epythet.confgen)                          | Generate the Sphinx configuration from a [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig).      |
| [`config`](_autosummary/epythet.config.html.md#module-epythet.config)                            | Single source of truth for a project's documentation configuration.                                                                  |
| [`config_parser`](_autosummary/epythet.config_parser.html.md#module-epythet.config_parser)              | Legacy configuration accessor kept for the frozen `docsrc/conf.py` copies.                                                           |
| [`docs_gen`](_autosummary/epythet.docs_gen.html.md#module-epythet.docs_gen)                        | Documentation generation entry points (re-exported for compatibility).                                                               |
| [`migrate`](_autosummary/epythet.migrate.html.md#module-epythet.migrate)                          | `epythet migrate-style`: rewrite RST field lists as Google (or NumPy) sections, opt-in.                                              |
| [`normalizer`](_autosummary/epythet.normalizer.html.md#module-epythet.normalizer)                    | Build-time docstring normalizer: fix the markup artifacts people actually write.                                                     |
| [`provenance`](_autosummary/epythet.provenance.html.md#module-epythet.provenance)                    | Build provenance: which code, which version, which tools produced a site.                                                            |
| [`repair`](_autosummary/epythet.repair.html.md#module-epythet.repair)                            | `epythet repair`: apply the normalizer's source-safe rewrites to docstrings in place.                                                |
| [`scaffold`](_autosummary/epythet.scaffold.html.md#epythet.scaffold)(config, \*[, verbose, pages]) | Write the docsrc files for an already-loaded configuration.                                                                          |
| [`setup_docsrc`](_autosummary/epythet.setup_docsrc.html.md#module-epythet.setup_docsrc)                | Compatibility module: `make_docsrc` now lives in [`epythet.scaffold`](_autosummary/epythet.scaffold.html.md#epythet.scaffold). |
| [`sphinx_conf`](_autosummary/epythet.sphinx_conf.html.md#module-epythet.sphinx_conf)                  | The star-import target for a project's `docsrc/conf.py`.                                                                             |
| [`sphinx_ext`](_autosummary/epythet.sphinx_ext.html.md#module-epythet.sphinx_ext)                    | epythet's Sphinx extension: the normalizer, agent link relations and theme CSS.                                                      |
| [`sweep`](_autosummary/epythet.sweep.html.md#module-epythet.sweep)                              | `epythet sweep`: validate many packages read-only and rank the queue.                                                                |
| [`templates`](_autosummary/epythet.templates.html.md#module-epythet.templates)                      | Text templates for the generated `docsrc` files.                                                                                     |
| [`themes`](_autosummary/epythet.themes.html.md#module-epythet.themes)                            | Theme registry, deterministic theme choice, and the OKLCH accent palette.                                                            |
| [`tools`](_autosummary/epythet.tools.html.md#module-epythet.tools)                              | Tools to diagnose (and sometimes, repair) documentation                                                                              |
| [`userconfig`](_autosummary/epythet.userconfig.html.md#module-epythet.userconfig)                    | User-level defaults and parametrizable text snippets.                                                                                |
| [`validation`](_autosummary/epythet.validation.html.md#module-epythet.validation)                    | `epythet validate`: tiered documentation validation with a growing artifact ledger.                                                  |


# _autosummary/epythet.migrate.html.md

# epythet.migrate

`epythet migrate-style`: rewrite RST field lists as Google (or NumPy) sections, opt-in.

The fleet keeps both conventions and napoleon renders both, so nothing here
runs by default anywhere (maintainer decision 6: normalizer only, no mass
style conversion). This is the tool for the one package, module or file
whose maintainer wants `:param x:` lines to become an `Args:` section.

It is built on [`epythet.repair`](_autosummary/epythet.repair.html.md#module-epythet.repair)’s machinery and inherits every one of
its guarantees: exact-span rewriting, an unchanged AST outside docstrings,
byte-identical doctest sources, and a level-0.5 re-validation of each
rewritten docstring. On top of that it only converts what round-trips:

- the *field region* is the first contiguous block of `:param`,
  `:type`, `:returns`, `:rtype` and `:raises` lines (with their
  indented continuations); prose before it and everything after it, doctests
  included, is copied verbatim;
- a region holding any other field (`:keyword`, `:var`, `:meta`, …)
  is left alone, because `docstring_parser.compose` drops or mangles those;
- the composed section is parsed back and must describe the same
  parameters, return and exceptions as the original, or the docstring is
  left alone.

The conversion itself is `docstring_parser.parse(..., style=REST)` then
`compose(..., style=GOOGLE)` (research §9); the substrate that writes it
back is the `applier=` seam of [`epythet.repair.repair()`](_autosummary/epythet.repair.html.md#epythet.repair.repair).

```pycon
>>> from epythet.migrate import convert_fields
>>> print(convert_fields(":param x: the x value\n:type x: int\n:returns: x doubled\n:rtype: int", to="google"))
:param x: the x value
:type x: int
```

<BLANKLINE>
:returns: x doubled
:rtype: int

### Module Attributes

| [`CONVERTIBLE_FIELDS`](_autosummary/epythet.migrate.html.md#epythet.migrate.CONVERTIBLE_FIELDS)   | Field names whose conversion round-trips through `docstring_parser`.   |
|-----------------------------------------------------------------------|------------------------------------------------------------------------|

### Functions

| [`convert_fields`](_autosummary/epythet.migrate.html.md#epythet.migrate.convert_fields)(region, \*[, to])                  | Convert one RST field block to a Google or NumPy section block, or `None` if unsafe.            |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| [`field_region`](_autosummary/epythet.migrate.html.md#epythet.migrate.field_region)(lines)                               | `(start, end)` of the first RST field block, or `None`; `end` is exclusive.                     |
| [`field_regions`](_autosummary/epythet.migrate.html.md#epythet.migrate.field_regions)(lines)                              | Every field block of a docstring, in order.                                                     |
| [`migrate_style`](_autosummary/epythet.migrate.html.md#epythet.migrate.migrate_style)(path, \*[, to, write, ignore, ...]) | Convert the RST field lists under `path` (a file, package or project) to `to` sections.         |
| [`migrate_style_command`](_autosummary/epythet.migrate.html.md#epythet.migrate.migrate_style_command)(path, \*[, to, write, ...]) | Rewrite RST field lists (:param x:) as Google or NumPy sections, one file or package at a time. |
| [`rst_fields_to_sections`](_autosummary/epythet.migrate.html.md#epythet.migrate.rst_fields_to_sections)([to])                      | A normalizer-shaped rule (`lines -> lines`) converting the docstring's field block.             |

### epythet.migrate.CONVERTIBLE_FIELDS *= frozenset({'arg', 'argument', 'except', 'exception', 'param', 'parameter', 'raise', 'raises', 'return', 'returns', 'rtype', 'type'})*

Field names whose conversion round-trips through `docstring_parser`.

### epythet.migrate.convert_fields(region, , to='google')

Convert one RST field block to a Google or NumPy section block, or `None` if unsafe.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.migrate.field_region(lines)

`(start, end)` of the first RST field block, or `None`; `end` is exclusive.

A block starts at a field line and takes every following field line at
the same indentation and every continuation (a deeper-indented line, or a
blank line followed by one of those).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> field_region(["Summary.", "", ":param x: the x", "    more", ":returns: y", "", "Then prose."])
(2, 5)
>>> field_region(["No fields."]) is None
True
```

### epythet.migrate.field_regions(lines)

Every field block of a docstring, in order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]]

```pycon
>>> field_regions([":param x: x", "", "prose", "", ":returns: y"])
[(0, 1), (4, 5)]
```

### epythet.migrate.migrate_style(path, \*, to='google', write=False, ignore=(), ledger=None, napoleon=True, run_doctests=True, applier=<function apply_span_edits>)

Convert the RST field lists under `path` (a file, package or project) to `to` sections.

Dry run by default; see [`epythet.repair.repair()`](_autosummary/epythet.repair.html.md#epythet.repair.repair) for the arguments,
which are the same.

* **Return type:**
  [`RepairReport`](_autosummary/epythet.repair.html.md#epythet.repair.RepairReport)

### epythet.migrate.migrate_style_command(path, , to='google', write=False, ignore=None, ledger=None, no_napoleon=False, no_doctests=False, applier='span', quiet=False)

Rewrite RST field lists (:param x:) as Google or NumPy sections, one file or package at a time.

Opt-in and never part of a fleet sweep. Dry run by default: prints the
diff –write would apply. Docstrings whose fields would not round-trip
(:keyword, :var, :meta, …) are left alone and listed.

* **Parameters:**
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – A .py file, a package directory, or a project root.
  * **to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Target convention: google or numpy.
  * **write** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Apply the changes (after re-validating each docstring and re-running doctests).
  * **ignore** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Skip files whose path contains this string (repeat -i for several).
  * **ledger** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Directory of extra rule YAML files overlaid on the bundled ledger.
  * **no_napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Re-validate without napoleon’s Google/NumPy pre-processing.
  * **no_doctests** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Do not run each touched file’s doctests before and after writing.
  * **applier** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The rewriting substrate: span (default) or libcst.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print the summary only, not the diff.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.migrate.rst_fields_to_sections(to='google')

A normalizer-shaped rule (`lines -> lines`) converting the docstring’s field block.

A docstring with more than one field block is left alone: converting one
would leave a mixed-style docstring behind.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]


# _autosummary/epythet.normalizer.html.md

# epythet.normalizer

Build-time docstring normalizer: fix the markup artifacts people actually write.

Docstrings in the wild mix reStructuredText, Google sections and Markdown
habits. Sphinx renders the RST and napoleon handles the Google sections, but a
handful of recurring slips render wrongly, mostly *silently*: a doctest glued
to the prose above it becomes a paragraph starting with `>>>`; a Markdown
fence is printed literally; `Returns: text` on one line is just a sentence;
`*args` opens an emphasis that never closes.

This module rewrites those cases on the fly, in the `autodoc-process-docstring`
event, so the rendered site is right without editing any source. Each rule is a
pure function `list[str] -> list[str]` and [`DEFAULT_RULES`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.DEFAULT_RULES) is the
ordered tuple that runs by default. The rules only touch prose: lines inside
doctest blocks, literal blocks, directive bodies and ASCII-art drawings are
left byte-for-byte alone, because doctests are executed and code is code.

**The principle: rewrite only what is unambiguous; otherwise report.** A rule
fires when the line can mean one thing (a `>>>` glued to prose is a doctest;
a 

```
``
```

\`\` `` ``` fence is a fence) and stays out when the author’s intent has two
readings. So a `#` line becomes a rubric only when it is shaped like a
Markdown heading and stands alone between blank lines, never when it could be
a code comment; `text:` followed by an indented block becomes a literal
block only when the block reads as code, never when it reads as a paragraph or
a definition; a section one-liner folds in the prose that wraps it, never a
field list that follows it; and nothing at all is rewritten inside the body of
a Google section (an argument called `x:` is not a literal block marker, an
argument called `error:` is not a section) or inside a drawing. What the
rules leave alone, `epythet validate` reports (DR014 for the accidental
definition list, DR002 for the bare section header, DR031 for the commented
doctest), so nothing is silently dropped: the same fixture that pins a rule’s
silence pins the finding that replaces it.

The rules, in order:

1. `fences_to_code_blocks`: 

   ```
   ``
   ```

   \`\` ``lang ``` fences become `.. code-block:: lang`.
2. `fix_short_underlines`: a section underline shorter than its title is extended.
3. `google_one_liners`: `Returns: text` becomes a real `Returns:` section.
4. `bare_headers_to_rubrics`: `Examples:` with unindented content becomes a rubric.
5. `markdown_headings_to_rubrics`: `## Title` becomes `.. rubric:: Title`.
6. `literal_block_after_colon`: prose ending in `:` followed by an indented
   block gets the `::` that makes it a literal block.
7. `reflow_list_continuations`: a wrapped list or field line at the marker’s
   own indentation is indented under it.
8. `blank_lines_between_blocks`: a blank line is inserted before a doctest,
   list or field list that follows prose, and after an indented block ends;
   not when the block sits directly under its own section header.
9. `markdown_links_to_rst`: `[text](url)` becomes ``text <url>`_`.
10. `escape_unmatched_stars`: `*args` / `**kwargs` in prose are escaped.

```pycon
>>> print(normalize_text('''Do the thing.
... Options are:
... - fast
... - slow
... Returns: the answer, which may
... span lines.
... '''))
Do the thing.
Options are:

- fast
- slow

:returns: the answer, which may
          span lines.
```

<BLANKLINE>
>>> normalize_docstring([“Text”, “    >>> f(1)”, “    3”])
[‘Text’, ‘’, ‘    >>> f(1)’, ‘    3’]

### Module Attributes

| [`GOOGLE_SECTIONS`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.GOOGLE_SECTIONS)   | Section names napoleon recognises (Google style), lowercase.               |
|--------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`PROSE_WORDS`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.PROSE_WORDS)       | How many plain words make a line read as prose rather than code.           |
| [`ART_MIN_LINES`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.ART_MIN_LINES)     | How many lines of a run must be drawing lines for the run to be a drawing. |
| [`DEFAULT_RULES`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.DEFAULT_RULES)     | The rules that run by default, in order.                                   |

### Functions

| [`bare_headers_to_rubrics`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.bare_headers_to_rubrics)(lines)                 | Turn a section header with no indented body into a rubric.                           |
|-------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`blank_lines_between_blocks`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.blank_lines_between_blocks)(lines)              | Separate prose from the doctest, list, field list or indented block after it.        |
| [`escape_unmatched_stars`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.escape_unmatched_stars)(lines)                  | Escape `*args` and `**kwargs` in prose so they are not read as emphasis.             |
| [`fences_to_code_blocks`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.fences_to_code_blocks)(lines)                   | Turn Markdown code fences into <br/><br/>```<br/>``<br/>```<br/><br/>.               |
| [`fix_short_underlines`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.fix_short_underlines)(lines)                    | Extend a title underline that is shorter than its title.                             |
| [`google_one_liners`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.google_one_liners)(lines)                       | Expand `Returns: text` (and other one-line sections) into real sections.             |
| [`google_section_bodies`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.google_section_bodies)(lines)                   | For every line, the indentation of the Google section body it is in, else `None`.    |
| [`indent_of`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.indent_of)(line)                                | Number of leading spaces (tabs count as one).                                        |
| [`is_art_line`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.is_art_line)(line)                              | Whether `line` is a piece of a drawing: box characters, arrows, or mostly strokes.   |
| [`line_contexts`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.line_contexts)(lines)                           | Classify every line as blank, prose, doctest, literal, list, field, fence or art.    |
| [`literal_block_after_colon`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.literal_block_after_colon)(lines)               | Make `text:` followed by an indented block a proper `::` literal block.              |
| [`markdown_headings_to_rubrics`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.markdown_headings_to_rubrics)(lines)            | Render `## Heading` as a rubric instead of a literal `##`.                           |
| [`markdown_links_to_rst`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.markdown_links_to_rst)(lines)                   | Rewrite `[text](url)` links as RST hyperlinks, outside code and literals.            |
| [`normalize_docstring`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.normalize_docstring)(lines, \*[, rules])        | Apply `rules` in order to a docstring given as lines (no trailing newlines).         |
| [`normalize_text`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.normalize_text)(text, \*[, rules])              | Apply `rules` to a docstring given as one string.                                    |
| [`reflow_list_continuations`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.reflow_list_continuations)(lines)               | Indent a wrapped list or field line that sits at the marker's own indentation.       |
| [`resolve_rules`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.resolve_rules)(rules)                           | Accept rule functions or dotted import paths (`"pkg.mod:func"` or `"pkg.mod.func"`). |
| [`setup`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.setup)(app)                                     | Sphinx extension entry point: `extensions = ["epythet.normalizer"]`.                 |
| [`sphinx_process_docstring`](_autosummary/epythet.normalizer.html.md#epythet.normalizer.sphinx_process_docstring)(app, what, name, ...) | The `autodoc-process-docstring` handler: normalizes `lines` in place.                |

### epythet.normalizer.ART_MIN_LINES *= 2*

How many lines of a run must be drawing lines for the run to be a drawing.

### epythet.normalizer.DEFAULT_RULES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]], [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]], ...]* *= (<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function bare_headers_to_rubrics>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>, <function escape_unmatched_stars>)*

The rules that run by default, in order.

### epythet.normalizer.GOOGLE_SECTIONS *= frozenset({'args', 'arguments', 'attention', 'attributes', 'caution', 'danger', 'error', 'example', 'examples', 'hint', 'important', 'keyword args', 'keyword arguments', 'methods', 'note', 'notes', 'other parameters', 'parameters', 'raise', 'raises', 'receive', 'receives', 'references', 'return', 'returns', 'see also', 'tip', 'todo', 'warn', 'warning', 'warnings', 'warns', 'yield', 'yields'})*

Section names napoleon recognises (Google style), lowercase.

### epythet.normalizer.PROSE_WORDS *= 4*

How many plain words make a line read as prose rather than code.

### epythet.normalizer.bare_headers_to_rubrics(lines)

Turn a section header with no indented body into a rubric.

napoleon only recognises `Examples:` when its content is indented; when
the doctest below sits at the same indentation, the header rendered as a
stray paragraph. A rubric is what napoleon itself emits for the section.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Examples:\n\n>>> f()\n1", rules=[bare_headers_to_rubrics])
'.. rubric:: Examples\n\n>>> f()\n1'
```

### epythet.normalizer.blank_lines_between_blocks(lines)

Separate prose from the doctest, list, field list or indented block after it.

The missing blank line before `>>>` is the single most common artifact in
the fleet: without it Sphinx renders the doctest as a paragraph and
`sphinx.ext.doctest` never runs it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Prose\n>>> f()\n1", rules=[blank_lines_between_blocks])
'Prose\n\n>>> f()\n1'
>>> normalize_text("Prose\n:param x: y\n    more\n:param z: w", rules=[blank_lines_between_blocks])
'Prose\n\n:param x: y\n    more\n:param z: w'
>>> normalize_text("Text:\n    indented\nback", rules=[blank_lines_between_blocks])
'Text:\n    indented\n\nback'
```

Inside a Google section body the entries are a definition list, where
consecutive terms need no blank line between them, so a dedent from a
wrapped `Args:` entry to the next entry is left as written:

```pycon
>>> normalize_text("Args:\n    a: one that\n        wraps.\n    b: two.", rules=[blank_lines_between_blocks])
'Args:\n    a: one that\n        wraps.\n    b: two.'
```

A block immediately under its own section header is left alone: napoleon
renders `Examples:` followed directly by a doctest or list identically
with or without the blank line, so inserting one only trips `D412`
(pydocstyle’s “no blank lines between a section header and its content”).

```pycon
>>> normalize_text("Examples:\n    >>> f()\n    1", rules=[blank_lines_between_blocks])
'Examples:\n    >>> f()\n    1'
```

### epythet.normalizer.escape_unmatched_stars(lines)

Escape `*args` and `**kwargs` in prose so they are not read as emphasis.

Only a star run that is never closed on the same line is escaped, so real
`*emphasis*` and `**strong**` are untouched.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Takes *args and **kwargs, *really*.", rules=[escape_unmatched_stars])
'Takes \\*args and \\*\\*kwargs, *really*.'
```

### epythet.normalizer.fences_to_code_blocks(lines)

Turn Markdown code fences into `.. code-block::` directives.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Run:\n```bash\npip install x\n```\nDone.", rules=[fences_to_code_blocks])
'Run:\n\n.. code-block:: bash\n\n    pip install x\n\nDone.'
```

### epythet.normalizer.fix_short_underlines(lines)

Extend a title underline that is shorter than its title.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Examples\n----\ntext", rules=[fix_short_underlines])
'Examples\n--------\ntext'
```

### epythet.normalizer.google_one_liners(lines)

Expand `Returns: text` (and other one-line sections) into real sections.

Prose lines that wrap the sentence at the same indentation are folded into
the section body; a field list, a bullet list, a Markdown heading or another
section ends it. Inside the body of another section the line is an entry
(an argument called `error`), not a header, and is left alone.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Returns: a thing that\nspans two lines.\n\nNext.", rules=[google_one_liners])
'Returns:\n    a thing that\n    spans two lines.\n\nNext.'
>>> normalize_text("Returns: a thing.\n## Notes\nText.", rules=[google_one_liners])
'Returns:\n    a thing.\n\n## Notes\nText.'
>>> normalize_text("Note: be careful.\n:return: the thing", rules=[google_one_liners])
'Note:\n    be careful.\n\n:return: the thing'
```

### epythet.normalizer.google_section_bodies(lines)

For every line, the indentation of the Google section body it is in, else `None`.

A section is a known header (`Args:`, `Returns:`, …) on its own line
with an indented body below; the body ends at the first non-blank line
that is not deeper than the header. Rules use this to stay out of section
bodies, where `x:` is an argument and not a literal-block lead-in.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

```pycon
>>> google_section_bodies(["Args:", "    x: the x", "        more", "", "Text."])
[None, 4, 4, 4, None]
```

### epythet.normalizer.indent_of(line)

Number of leading spaces (tabs count as one).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> indent_of("    x"), indent_of("x"), indent_of("")
(4, 0, 0)
```

### epythet.normalizer.is_art_line(line)

Whether `line` is a piece of a drawing: box characters, arrows, or mostly strokes.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_art_line("│ 0 │ ──▶ │ 2 │"), is_art_line("  +----+"), is_art_line("- a bullet")
(True, True, False)
>>> is_art_line("func1 --> merge"), is_art_line("x = 1  # comment")
(True, False)
```

### epythet.normalizer.line_contexts(lines)

Classify every line as blank, prose, doctest, literal, list, field, fence or art.

The classification is what keeps every rule away from code: a line inside a
doctest block, a `::` literal block, a directive body, a Markdown fence or
an ASCII-art drawing is never rewritten.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> line_contexts(["Text:", "", "    >>> 1", "    1", "", "- a", "  b", "", ":param x: y"])
['prose', 'blank', 'doctest', 'doctest', 'blank', 'list', 'list', 'blank', 'field']
>>> line_contexts(["    >>> 1", "    1", "back to prose"])
['doctest', 'doctest', 'prose']
>>> line_contexts(["a --> b", "  |", "  v", "- c"])
['art', 'art', 'art', 'list']
```

### epythet.normalizer.literal_block_after_colon(lines)

Make `text:` followed by an indented block a proper `::` literal block.

Only when the block is unmistakably code (`_looks_like_code()`: no
line reads as prose and some line carries a code signal such as `=`,
`(` or `#`), and never inside a Google section body, where `x:` is an
argument. A lead-in over an indented paragraph is a definition list the
author may have meant; it is left alone and DR014 reports it. A lone
`Usage:` or `Output:` over a command or a value is code all the same.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("For example:\n    x = f(1)\nThen more.", rules=[literal_block_after_colon])
'For example::\n\n    x = f(1)\n\nThen more.'
>>> normalize_text("Usage:\n    python run.py  # top 12", rules=[literal_block_after_colon])
'Usage::\n\n    python run.py  # top 12'
>>> normalize_text("specifying:\n    the name of the thing to do.", rules=[literal_block_after_colon])
'specifying:\n    the name of the thing to do.'
>>> normalize_text("Args:\n    x:\n        The x.", rules=[literal_block_after_colon])
'Args:\n    x:\n        The x.'
```

### epythet.normalizer.markdown_headings_to_rubrics(lines)

Render `## Heading` as a rubric instead of a literal `##`.

A `#` line is also how a code comment, a commented-out doctest and its
output (`# True`) or a commented-out paragraph look, so the rule wants a
heading shape: `#` marks, a space, then a title that starts with a
capital letter, a digit or a backtick, holds no code (`=`, `(`,
`>>>`) and no trailing `:` or `.`, and is not a `TODO:` tag. A
single `#` must also follow a blank line (or open the docstring);
`##` and deeper may sit against prose. No heading of any level sits
against another `#` line (that is a commented-out paragraph or doctest)
or right after code.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("Intro.\n## Usage\nText.", rules=[markdown_headings_to_rubrics])
'Intro.\n\n.. rubric:: Usage\n\nText.'
>>> normalize_text("# >>> f()\n# True", rules=[markdown_headings_to_rubrics])
'# >>> f()\n# True'
>>> normalize_text("# Making a signature\nText.", rules=[markdown_headings_to_rubrics])
'.. rubric:: Making a signature\n\nText.'
>>> normalize_text("Intro.\n# Not a heading\n\nText.", rules=[markdown_headings_to_rubrics])
'Intro.\n# Not a heading\n\nText.'
```

### epythet.normalizer.markdown_links_to_rst(lines)

Rewrite `[text](url)` links as RST hyperlinks, outside code and literals.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("See [the docs](https://x.org/a) now.", rules=[markdown_links_to_rst])
'See `the docs <https://x.org/a>`_ now.'
```

### epythet.normalizer.normalize_docstring(lines, \*, rules=(<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function bare_headers_to_rubrics>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>, <function escape_unmatched_stars>))

Apply `rules` in order to a docstring given as lines (no trailing newlines).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_docstring(["Text", ">>> 1", "1"])
['Text', '', '>>> 1', '1']
```

### epythet.normalizer.normalize_text(text, \*, rules=(<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function bare_headers_to_rubrics>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>, <function escape_unmatched_stars>))

Apply `rules` to a docstring given as one string.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> normalize_text("Text\n>>> 1\n1")
'Text\n\n>>> 1\n1'
```

### epythet.normalizer.reflow_list_continuations(lines)

Indent a wrapped list or field line that sits at the marker’s own indentation.

`- a long item that wraps\nonto the next line` is not a continuation in
RST (the list “ends without a blank line”). Indenting the wrapped line under
the marker makes it one. A line that reads like a new sentence after a
finished item is separated with a blank line instead.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> normalize_text("- item that\nwraps\n- two", rules=[reflow_list_continuations])
'- item that\n  wraps\n- two'
>>> normalize_text("- item.\nNext paragraph", rules=[reflow_list_continuations])
'- item.\n\nNext paragraph'
```

### epythet.normalizer.resolve_rules(rules)

Accept rule functions or dotted import paths (`"pkg.mod:func"` or `"pkg.mod.func"`).

Dotted paths are what a `conf.py` can hold: Sphinx cannot pickle functions
in its configuration, and a ledger of autofixable rules ships names.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> [r.__name__ for r in resolve_rules(["epythet.normalizer.fences_to_code_blocks"])]
['fences_to_code_blocks']
```

### epythet.normalizer.setup(app)

Sphinx extension entry point: `extensions = ["epythet.normalizer"]`.

`epythet.sphinx_ext` registers the same hook; listing both is harmless.

### epythet.normalizer.sphinx_process_docstring(app, what, name, obj, options, lines)

The `autodoc-process-docstring` handler: normalizes `lines` in place.

`options` is deliberately never touched (its mapping interface is
deprecated in Sphinx 9). Register with `priority=400` so this runs before
napoleon (priority 500) sees the docstring.


# _autosummary/epythet.provenance.html.md

# epythet.provenance

Build provenance: which code, which version, which tools produced a site.

A documentation site is a snapshot. The reader wants to know whether it matches
the repository they are looking at and the package they installed; the
maintainer wants to know whether the latest push has been published yet
(issue #7). This module collects that diagnosis once per build and the rest of
epythet renders it in three places:

- a one-line footer on the landing page (`built <UTC time> from <commit>
  (<branch>) · <package> <version> · about this build`), appended to the
  rendered page by [`epythet.sphinx_ext`](_autosummary/epythet.sphinx_ext.html.md#module-epythet.sphinx_ext);
- `about-this-build.html`, an orphan page (reachable from the footer, absent
  from the navigation) with the full diagnosis, rendered from
  [`ABOUT_PAGE_TEMPLATE`](_autosummary/epythet.provenance.html.md#epythet.provenance.ABOUT_PAGE_TEMPLATE) or the project’s `provenance_template`;
- `build_info.json` at the site root, the same data for machines, with
  stable keys and a `schema_version`; also listed in `llms.txt` and
  referenced at the top of the `<package>.md` aggregate.

The `[tool.epythet] provenance` key is the seam: `true` (default) renders
all three, `"minimal"` renders the footer line and the JSON but no page,
`false` renders nothing.

Collection never fails a build. No git, no `git` binary, no network, a
detached HEAD: every source degrades to `null` fields plus an entry in the
`warnings` list, and the build prints one warning. The record is published,
so nothing local goes into it: remote URLs lose any credentials, path-shaped
remotes are dropped, git’s error text is scrubbed of paths, and the reproduce
lines name the clone by its remote, not by the local folder.

`SOURCE_DATE_EPOCH` (the reproducible-builds convention Sphinx honours too)
fixes the build time when set.

```pycon
>>> from epythet.config import DocsConfig
>>> cfg = DocsConfig(project_dir="/nonexistent", name="pkg", version="1.0")
>>> info = collect_build_info(cfg, check_pypi=False)
>>> info["schema_version"], info["package"]["name"], info["git"]["available"]
(1, 'pkg', False)
>>> "about this build" in render_footer_line(info)
True
```

### Module Attributes

| [`SCHEMA_VERSION`](_autosummary/epythet.provenance.html.md#epythet.provenance.SCHEMA_VERSION)         | Bumped when a key is renamed or removed; additions keep the version.                                                         |
|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| [`BUILD_INFO_FILENAME`](_autosummary/epythet.provenance.html.md#epythet.provenance.BUILD_INFO_FILENAME)    | File written at the site root.                                                                                               |
| [`ABOUT_PAGE_FILENAME`](_autosummary/epythet.provenance.html.md#epythet.provenance.ABOUT_PAGE_FILENAME)    | Source file (in docsrc) and document name of the full-diagnosis page.                                                        |
| [`BUILD_INFO_ENV`](_autosummary/epythet.provenance.html.md#epythet.provenance.BUILD_INFO_ENV)         | Environment variable carrying the collected JSON into the Sphinx process.                                                    |
| [`PYPI_CHECK_ENV`](_autosummary/epythet.provenance.html.md#epythet.provenance.PYPI_CHECK_ENV)         | Set to `0` to skip the PyPI lookup (offline CI, tests).                                                                      |
| [`SOURCE_DATE_EPOCH_ENV`](_autosummary/epythet.provenance.html.md#epythet.provenance.SOURCE_DATE_EPOCH_ENV)  | seconds since the epoch, fixes `built_at`.                                                                                   |
| [`DEFAULT_PYPI_TIMEOUT`](_autosummary/epythet.provenance.html.md#epythet.provenance.DEFAULT_PYPI_TIMEOUT)   | Seconds allowed for the PyPI lookup, in total; the build never waits longer.                                                 |
| [`GIT_TIMEOUT`](_autosummary/epythet.provenance.html.md#epythet.provenance.GIT_TIMEOUT)            | Seconds allowed for each git command.                                                                                        |
| [`SHORT_COMMIT_LENGTH`](_autosummary/epythet.provenance.html.md#epythet.provenance.SHORT_COMMIT_LENGTH)    | Characters of a commit hash shown in the footer and the summary.                                                             |
| [`AGGREGATE_STAMP_PREFIX`](_autosummary/epythet.provenance.html.md#epythet.provenance.AGGREGATE_STAMP_PREFIX) | First line of the stamp prepended to the `<package>.md` aggregate.                                                           |
| [`ABOUT_PAGE_TEMPLATE`](_autosummary/epythet.provenance.html.md#epythet.provenance.ABOUT_PAGE_TEMPLATE)    | it is how epythet recognises its own file.                                                                                   |
| [`TEMPLATE_FIELDS`](_autosummary/epythet.provenance.html.md#epythet.provenance.TEMPLATE_FIELDS)        | The fields [`render_about_page()`](_autosummary/epythet.provenance.html.md#epythet.provenance.render_about_page) fills; a custom template may use any subset. |

### Functions

| [`about_page`](_autosummary/epythet.provenance.html.md#epythet.provenance.about_page)(info, \*[, template])                  | The about page as a `PageSpec`.                                                                                                               |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| [`about_template`](_autosummary/epythet.provenance.html.md#epythet.provenance.about_template)(config)                            | The about page's template: `[tool.epythet] provenance_template` or the default.                                                               |
| [`alignment`](_autosummary/epythet.provenance.html.md#epythet.provenance.alignment)(info)                                   | Whether the docs can be trusted to match the repository and the package.                                                                      |
| [`build_time`](_autosummary/epythet.provenance.html.md#epythet.provenance.build_time)([environ])                             | Now in UTC, or the instant `SOURCE_DATE_EPOCH` names when it is set.                                                                          |
| [`ci_info`](_autosummary/epythet.provenance.html.md#epythet.provenance.ci_info)([environ])                                | The GitHub Actions context, when the build runs there (else `None` fields).                                                                   |
| [`clone_dirname`](_autosummary/epythet.provenance.html.md#epythet.provenance.clone_dirname)(remote)                             | The directory `git clone <remote>` creates.                                                                                                   |
| [`collect_build_info`](_autosummary/epythet.provenance.html.md#epythet.provenance.collect_build_info)(config, \*[, check_pypi, ...]) | The provenance record for one build of `config`'s project.                                                                                    |
| [`compare_versions`](_autosummary/epythet.provenance.html.md#epythet.provenance.compare_versions)(ours, latest)                    | `same` / `behind` / `ahead` of `latest`, or `unknown` when unparsable.                                                                        |
| [`config_source`](_autosummary/epythet.provenance.html.md#epythet.provenance.config_source)(project_dir)                        | Which file the package metadata came from: the rule of [`epythet.config`](_autosummary/epythet.config.html.md#module-epythet.config). |
| [`footer_text`](_autosummary/epythet.provenance.html.md#epythet.provenance.footer_text)(info)                                 | The provenance line as plain text (no link).                                                                                                  |
| [`git_info`](_autosummary/epythet.provenance.html.md#epythet.provenance.git_info)(project_dir, \*[, exclude])              | What git knows about `project_dir`: commit, branch, tags, dirty flag, remote.                                                                 |
| [`github_web_url`](_autosummary/epythet.provenance.html.md#epythet.provenance.github_web_url)(remote)                            | The `https://github.com/owner/repo` form of a remote URL, or `None`.                                                                          |
| [`human_time`](_autosummary/epythet.provenance.html.md#epythet.provenance.human_time)(iso)                                   | `2026-09-15T14:02:00Z` -> `2026-09-15 14:02 UTC`.                                                                                             |
| [`load_build_info`](_autosummary/epythet.provenance.html.md#epythet.provenance.load_build_info)(raw)                              | Parse the JSON the build process hands over in `EPYTHET_BUILD_INFO`.                                                                          |
| [`prune_site`](_autosummary/epythet.provenance.html.md#epythet.provenance.prune_site)(html_dir, \*, keep_page, keep_json)    | Remove provenance outputs a previous build left in `html_dir`.                                                                                |
| [`publishable_remote`](_autosummary/epythet.provenance.html.md#epythet.provenance.publishable_remote)(url)                           | The form of a remote URL that may appear on a public site, or `None`.                                                                         |
| [`pypi_info`](_autosummary/epythet.provenance.html.md#epythet.provenance.pypi_info)(name, version, \*[, timeout])           | The latest release of `name` on PyPI and how `version` relates to it.                                                                         |
| [`pypi_latest_version`](_autosummary/epythet.provenance.html.md#epythet.provenance.pypi_latest_version)(name, \*[, timeout])          | The `info.version` of `https://pypi.org/pypi/<name>/json` (`None` on 404).                                                                    |
| [`reference_from_agent_outputs`](_autosummary/epythet.provenance.html.md#epythet.provenance.reference_from_agent_outputs)(html_dir, info, ...) | List `build_info.json` in `llms.txt` and stamp the top of `<package>.md`.                                                                     |
| [`render_about_page`](_autosummary/epythet.provenance.html.md#epythet.provenance.render_about_page)(info, \*[, template])           | The Markdown source of `about-this-build.md` for a collected `info`.                                                                          |
| [`render_footer_line`](_autosummary/epythet.provenance.html.md#epythet.provenance.render_footer_line)(info, \*[, about_href])        | The landing-page footer as one small HTML paragraph.                                                                                          |
| [`reproduce_command`](_autosummary/epythet.provenance.html.md#epythet.provenance.reproduce_command)(config, git)                    | The shell lines that rebuild this site from the same commit.                                                                                  |
| [`resolved_config`](_autosummary/epythet.provenance.html.md#epythet.provenance.resolved_config)(config)                           | The documentation choices as the build resolved them (theme, accent, generator...).                                                           |
| [`scrub_paths`](_autosummary/epythet.provenance.html.md#epythet.provenance.scrub_paths)(message)                              | Replace absolute paths in a diagnostic with `<path>`: the record is published.                                                                |
| [`short_commit`](_autosummary/epythet.provenance.html.md#epythet.provenance.short_commit)(sha)                                 | The first [`SHORT_COMMIT_LENGTH`](_autosummary/epythet.provenance.html.md#epythet.provenance.SHORT_COMMIT_LENGTH) characters of a commit hash.                                   |
| [`site_counts`](_autosummary/epythet.provenance.html.md#epythet.provenance.site_counts)(env)                                  | Documented-module and documented-object counts from a Sphinx environment.                                                                     |
| [`strip_credentials`](_autosummary/epythet.provenance.html.md#epythet.provenance.strip_credentials)(url)                            | A scheme URL without any `user:token@` part (scp-style remotes pass through).                                                                 |
| [`tool_versions`](_autosummary/epythet.provenance.html.md#epythet.provenance.tool_versions)()                                   | Versions of epythet, Sphinx, docutils and Python in the build environment.                                                                    |
| [`with_front_matter_and_marker`](_autosummary/epythet.provenance.html.md#epythet.provenance.with_front_matter_and_marker)(template)            | Make a page template an orphan (out of the toctree) that carries the marker.                                                                  |
| [`write_build_info`](_autosummary/epythet.provenance.html.md#epythet.provenance.write_build_info)(html_dir, info)                  | Write `build_info.json` at the site root; returns its path.                                                                                   |

### epythet.provenance.ABOUT_PAGE_FILENAME *= 'about-this-build.md'*

Source file (in docsrc) and document name of the full-diagnosis page.

### epythet.provenance.ABOUT_PAGE_TEMPLATE *= '---\\norphan: true\\n---\\n{marker}\\n\\n# About this build\\n\\n{summary}\\n\\n{alignment_block}\\n\\n## Source\\n\\n| | |\\n|---|---|\\n| Commit | {commit_cell} |\\n| Branch | {branch} |\\n| Tags at this commit | {tags} |\\n| Working tree | {tree_state} |\\n| Remote | {remote} |\\n\\n## Continuous integration\\n\\n{ci_block}\\n\\n## Tools\\n\\n| | |\\n|---|---|\\n| epythet | {epythet_version} |\\n| Sphinx | {sphinx_version} |\\n| docutils | {docutils_version} |\\n| Python | {python_version} |\\n\\n## Configuration as resolved\\n\\n| | |\\n|---|---|\\n| theme | {theme} (Sphinx theme {html_theme}) |\\n| accent | {accent} |\\n| api_generator | {api_generator} |\\n| ignore | {ignore} |\\n| agent_outputs | {agent_outputs} |\\n| aggregates | {aggregates} |\\n| ai_artifacts | {ai_artifacts} |\\n\\n## Package on PyPI\\n\\n{pypi_block}\\n\\n## Reproduce\\n\\n\`\`\`bash\\n{reproduce}\\n\`\`\`\\n\\nThe same data, for machines: <a href="{build_info_filename}"><code>{build_info_filename}</code></a> (schema version {schema_version}).\\n'*

it is how
epythet recognises its own file. Literal braces are doubled. Every value
is already HTML-escaped ([`render_about_page()`](_autosummary/epythet.provenance.html.md#epythet.provenance.render_about_page)), so a custom template
may place the fields anywhere.

* **Type:**
  The Markdown source of the about page. `{marker}` must stay

### epythet.provenance.AGGREGATE_STAMP_PREFIX *= '> built '*

First line of the stamp prepended to the `<package>.md` aggregate.

### epythet.provenance.BUILD_INFO_ENV *= 'EPYTHET_BUILD_INFO'*

Environment variable carrying the collected JSON into the Sphinx process.

### epythet.provenance.BUILD_INFO_FILENAME *= 'build_info.json'*

File written at the site root.

### epythet.provenance.DEFAULT_PYPI_TIMEOUT *= 3.0*

Seconds allowed for the PyPI lookup, in total; the build never waits longer.

### epythet.provenance.GIT_TIMEOUT *= 10*

Seconds allowed for each git command.

### epythet.provenance.PYPI_CHECK_ENV *= 'EPYTHET_PYPI_CHECK'*

Set to `0` to skip the PyPI lookup (offline CI, tests).

### epythet.provenance.SCHEMA_VERSION *= 1*

Bumped when a key is renamed or removed; additions keep the version.

### epythet.provenance.SHORT_COMMIT_LENGTH *= 7*

Characters of a commit hash shown in the footer and the summary.

### epythet.provenance.SOURCE_DATE_EPOCH_ENV *= 'SOURCE_DATE_EPOCH'*

seconds since the epoch, fixes `built_at`.

* **Type:**
  Reproducible-builds convention

### epythet.provenance.TEMPLATE_FIELDS *= frozenset({'accent', 'agent_outputs', 'aggregates', 'ai_artifacts', 'alignment_block', 'api_generator', 'branch', 'build_info_filename', 'ci_block', 'commit_cell', 'docutils_version', 'epythet_version', 'html_theme', 'ignore', 'marker', 'pypi_block', 'python_version', 'remote', 'reproduce', 'schema_version', 'sphinx_version', 'summary', 'tags', 'theme', 'tree_state'})*

The fields [`render_about_page()`](_autosummary/epythet.provenance.html.md#epythet.provenance.render_about_page) fills; a custom template may use any subset.

### epythet.provenance.about_page(info, , template='---\\\\norphan: true\\\\n---\\\\n{marker}\\\\n\\\\n# About this build\\\\n\\\\n{summary}\\\\n\\\\n{alignment_block}\\\\n\\\\n## Source\\\\n\\\\n| | |\\\\n|---|---|\\\\n| Commit | {commit_cell} |\\\\n| Branch | {branch} |\\\\n| Tags at this commit | {tags} |\\\\n| Working tree | {tree_state} |\\\\n| Remote | {remote} |\\\\n\\\\n## Continuous integration\\\\n\\\\n{ci_block}\\\\n\\\\n## Tools\\\\n\\\\n| | |\\\\n|---|---|\\\\n| epythet | {epythet_version} |\\\\n| Sphinx | {sphinx_version} |\\\\n| docutils | {docutils_version} |\\\\n| Python | {python_version} |\\\\n\\\\n## Configuration as resolved\\\\n\\\\n| | |\\\\n|---|---|\\\\n| theme | {theme} (Sphinx theme {html_theme}) |\\\\n| accent | {accent} |\\\\n| api_generator | {api_generator} |\\\\n| ignore | {ignore} |\\\\n| agent_outputs | {agent_outputs} |\\\\n| aggregates | {aggregates} |\\\\n| ai_artifacts | {ai_artifacts} |\\\\n\\\\n## Package on PyPI\\\\n\\\\n{pypi_block}\\\\n\\\\n## Reproduce\\\\n\\\\n\`\`\`bash\\\\n{reproduce}\\\\n\`\`\`\\\\n\\\\nThe same data, for machines: <a href="{build_info_filename}"><code>{build_info_filename}</code></a> (schema version {schema_version}).\\\\n')

The about page as a `PageSpec`.

* **Raises:**
  [**ConfigError**](_autosummary/epythet.config.html.md#epythet.config.ConfigError) – when `template` names a field the renderer does not
  provide (literal braces must be doubled: `{{`).

### epythet.provenance.about_template(config)

The about page’s template: `[tool.epythet] provenance_template` or the default.

The key names a file relative to the project root, with the same contract
as `ai_artifacts_template`; the epythet marker is prepended when absent.

* **Raises:**
  [**ConfigError**](_autosummary/epythet.config.html.md#epythet.config.ConfigError) – when the file does not exist
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.provenance.alignment(info)

Whether the docs can be trusted to match the repository and the package.

`aligned` is `True` when nothing suggests otherwise, `False` when a
note says why they may differ, `None` when there is no git information to
judge by. `notes` are the plain-language reasons, in the order shown.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.provenance.build_time(environ=None)

Now in UTC, or the instant `SOURCE_DATE_EPOCH` names when it is set.

* **Return type:**
  [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime)

```pycon
>>> build_time({"SOURCE_DATE_EPOCH": "0"}).strftime("%Y-%m-%d")
'1970-01-01'
```

### epythet.provenance.ci_info(environ=None)

The GitHub Actions context, when the build runs there (else `None` fields).

`sha_in_history` says whether the event’s commit is in the built HEAD’s
history; the publish action fast-forwards to the branch tip before
building, so HEAD is normally a descendant of `GITHUB_SHA`, not equal to it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> ci_info({"GITHUB_ACTIONS": "true", "GITHUB_REPOSITORY": "o/r",
...          "GITHUB_RUN_ID": "42", "GITHUB_SHA": "abc", "GITHUB_REF": "refs/heads/main",
...          "GITHUB_REF_NAME": "main"})["run_url"]
'https://github.com/o/r/actions/runs/42'
>>> ci_info({})["provider"] is None
True
```

### epythet.provenance.clone_dirname(remote)

The directory `git clone <remote>` creates.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> clone_dirname("https://github.com/org/demo.git"), clone_dirname("git@github.com:o/r")
('demo', 'r')
```

### epythet.provenance.collect_build_info(config, , check_pypi=None, pypi_timeout=3.0, dirty_exclude=None, environ=None, now=None)

The provenance record for one build of `config`’s project.

* **Parameters:**
  * **config** – a [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig)
  * **check_pypi** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – query PyPI for the latest release; `None` means “unless
    the `EPYTHET_PYPI_CHECK` environment variable turns it off”
  * **pypi_timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – seconds allowed for that query
  * **dirty_exclude** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – project-relative paths left out of the dirty check, on
    top of the docs dir; `None` means the directories the `github` /
    `gitlab` targets copy the site into (`epythet.build.COPY_TARGETS`)
  * **environ** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – the environment to read CI variables from (default: `os.environ`)
  * **now** ([`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – the build time (default: `SOURCE_DATE_EPOCH` if set, else now, UTC)
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  a JSON-serialisable dict; see the module docstring for the keys.
  `site` counts are `None` here and filled in by the Sphinx
  extension, which knows what was documented.

### epythet.provenance.compare_versions(ours, latest)

`same` / `behind` / `ahead` of `latest`, or `unknown` when unparsable.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> compare_versions("0.2.4", "0.2.5"), compare_versions("1.0", "1.0.0")
('behind', 'same')
>>> compare_versions("0.3.0.dev1", "0.2.5"), compare_versions("x", "1")
('ahead', 'unknown')
```

### epythet.provenance.config_source(project_dir)

Which file the package metadata came from: the rule of [`epythet.config`](_autosummary/epythet.config.html.md#module-epythet.config).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.provenance.footer_text(info)

The provenance line as plain text (no link).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> info = {"built_at": "2026-09-15T14:02:00Z", "package": {"name": "dol", "version": "0.3.1"},
...         "git": {"short_commit": "a1b2c3d", "branch": "master", "dirty": True}}
>>> footer_text(info)
'built 2026-09-15 14:02 UTC from a1b2c3d+dirty (master) · dol 0.3.1'
```

### epythet.provenance.git_info(project_dir, , exclude=())

What git knows about `project_dir`: commit, branch, tags, dirty flag, remote.

`exclude` names paths (relative to the project) left out of the dirty
check; the build rewrites a committed `docsrc/`, which must not count.
Everything is `None` with `available` false when the directory is not a
repository or `git` is not installed. A detached HEAD has `branch`
`None`. `remote_url` is the publishable form of `origin` (no
credentials, no local paths), see [`publishable_remote()`](_autosummary/epythet.provenance.html.md#epythet.provenance.publishable_remote).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.provenance.github_web_url(remote)

The `https://github.com/owner/repo` form of a remote URL, or `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> github_web_url("git@github.com:i2mint/epythet.git")
'https://github.com/i2mint/epythet'
>>> github_web_url("https://github.com/i2mint/epythet/")
'https://github.com/i2mint/epythet'
>>> github_web_url("https://gitlab.com/x/y.git") is None
True
```

### epythet.provenance.human_time(iso)

`2026-09-15T14:02:00Z` -> `2026-09-15 14:02 UTC`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> human_time("2026-09-15T14:02:00Z")
'2026-09-15 14:02 UTC'
```

### epythet.provenance.load_build_info(raw)

Parse the JSON the build process hands over in `EPYTHET_BUILD_INFO`.

Anything that is not a record of this module’s schema is ignored, so a
stale or foreign value in the environment never breaks a build.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> load_build_info('{"schema_version": 1, "git": {}}')["schema_version"]
1
>>> load_build_info('"str"') is None and load_build_info("{") is None
True
```

### epythet.provenance.prune_site(html_dir, , keep_page, keep_json)

Remove provenance outputs a previous build left in `html_dir`.

Sphinx never cleans its output directory, so a project that turned
`provenance` off (or down to `"minimal"`) would otherwise keep
publishing a stale page or JSON. Returns the paths removed.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### epythet.provenance.publishable_remote(url)

The form of a remote URL that may appear on a public site, or `None`.

Credentials are dropped from scheme URLs, the user part from scp-style
remotes, and path-shaped remotes (a local or `file://` clone) are not
published at all.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> publishable_remote("https://me:ghp_secret@github.com/o/r.git")
'https://github.com/o/r.git'
>>> publishable_remote("thor@myserver.local:repos/demo.git")
'myserver.local:repos/demo.git'
>>> publishable_remote("git@github.com:o/r.git")
'git@github.com:o/r.git'
>>> publishable_remote("/Users/me/bare/demo.git") is None
True
>>> publishable_remote("D:/repos/x.git") is None
True
>>> publishable_remote("file:///srv/git/demo.git") is None
True
```

### epythet.provenance.pypi_info(name, version, , timeout=3.0)

The latest release of `name` on PyPI and how `version` relates to it.

`relation` is `same`, `behind`, `ahead` or `unknown` (not on
PyPI, unreachable, or unparsable versions). Any failure, including the
`timeout` elapsing, is recorded in `error` and never raised.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.provenance.pypi_latest_version(name, , timeout=3.0)

The `info.version` of `https://pypi.org/pypi/<name>/json` (`None` on 404).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.provenance.reference_from_agent_outputs(html_dir, info, , package_name)

List `build_info.json` in `llms.txt` and stamp the top of `<package>.md`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.provenance.render_about_page(info, , template='---\\\\norphan: true\\\\n---\\\\n{marker}\\\\n\\\\n# About this build\\\\n\\\\n{summary}\\\\n\\\\n{alignment_block}\\\\n\\\\n## Source\\\\n\\\\n| | |\\\\n|---|---|\\\\n| Commit | {commit_cell} |\\\\n| Branch | {branch} |\\\\n| Tags at this commit | {tags} |\\\\n| Working tree | {tree_state} |\\\\n| Remote | {remote} |\\\\n\\\\n## Continuous integration\\\\n\\\\n{ci_block}\\\\n\\\\n## Tools\\\\n\\\\n| | |\\\\n|---|---|\\\\n| epythet | {epythet_version} |\\\\n| Sphinx | {sphinx_version} |\\\\n| docutils | {docutils_version} |\\\\n| Python | {python_version} |\\\\n\\\\n## Configuration as resolved\\\\n\\\\n| | |\\\\n|---|---|\\\\n| theme | {theme} (Sphinx theme {html_theme}) |\\\\n| accent | {accent} |\\\\n| api_generator | {api_generator} |\\\\n| ignore | {ignore} |\\\\n| agent_outputs | {agent_outputs} |\\\\n| aggregates | {aggregates} |\\\\n| ai_artifacts | {ai_artifacts} |\\\\n\\\\n## Package on PyPI\\\\n\\\\n{pypi_block}\\\\n\\\\n## Reproduce\\\\n\\\\n\`\`\`bash\\\\n{reproduce}\\\\n\`\`\`\\\\n\\\\nThe same data, for machines: <a href="{build_info_filename}"><code>{build_info_filename}</code></a> (schema version {schema_version}).\\\\n')

The Markdown source of `about-this-build.md` for a collected `info`.

Values from the repository (branch, tags, remote, versions) are rendered
as escaped inline HTML, never as Markdown: a ref name is user input.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.provenance.render_footer_line(info, , about_href='about-this-build.html')

The landing-page footer as one small HTML paragraph.

The commit links to GitHub when the remote is known; `about_href` is the
“about this build” link target (`None` to omit the link, as `minimal` does
without a page: the JSON is linked instead). The style is inline on purpose:
it must hold in every theme without a stylesheet of its own.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.provenance.reproduce_command(config, git)

The shell lines that rebuild this site from the same commit.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.provenance.resolved_config(config)

The documentation choices as the build resolved them (theme, accent, generator…).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.provenance.scrub_paths(message)

Replace absolute paths in a diagnostic with `<path>`: the record is published.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> scrub_paths("fatal: detected dubious ownership in repository at '/home/me/x'")
"fatal: detected dubious ownership in repository at '<path>'"
>>> scrub_paths("fatal: not a git repository (or any of the parent directories): .git")
'fatal: not a git repository (or any of the parent directories): .git'
```

### epythet.provenance.short_commit(sha)

The first [`SHORT_COMMIT_LENGTH`](_autosummary/epythet.provenance.html.md#epythet.provenance.SHORT_COMMIT_LENGTH) characters of a commit hash.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.provenance.site_counts(env)

Documented-module and documented-object counts from a Sphinx environment.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.provenance.strip_credentials(url)

A scheme URL without any `user:token@` part (scp-style remotes pass through).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> strip_credentials("https://me:ghp_secret@github.com/o/r.git")
'https://github.com/o/r.git'
```

### epythet.provenance.tool_versions()

Versions of epythet, Sphinx, docutils and Python in the build environment.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.provenance.with_front_matter_and_marker(template)

Make a page template an orphan (out of the toctree) that carries the marker.

YAML front matter must be the very first thing in the file, so the marker
goes after it; `orphan: true` is added when the front matter lacks it,
and front matter is created when there is none.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(with_front_matter_and_marker("# Build\n"))
---
orphan: true
---
{marker}

# Build
```

```pycon
>>> print(with_front_matter_and_marker("---\ntitle: x\n---\n{marker}\n# B\n"))
---
title: x
orphan: true
---
{marker}
# B
```

### epythet.provenance.write_build_info(html_dir, info)

Write `build_info.json` at the site root; returns its path.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/epythet.repair.html.md

# epythet.repair

`epythet repair`: apply the normalizer’s source-safe rewrites to docstrings in place.

The build-time normalizer ([`epythet.normalizer`](_autosummary/epythet.normalizer.html.md#module-epythet.normalizer)) fixes a docstring’s
markup on the fly, so the rendered site is right without editing anything.
This module writes the same fixes *back into the source*, for the packages
that want their docstrings right at rest (decision D9). It is deliberately
narrow:

- Only the docstring literal changes. The rest of the file is copied byte
  for byte (exact-span rewriting, no unparsing), and the module’s AST with
  docstrings blanked must be identical before and after or the file is not
  written.
- Only the normalizer’s *source-safe* rules run ([`SOURCE_SAFE_RULES`](_autosummary/epythet.repair.html.md#epythet.repair.SOURCE_SAFE_RULES)):
  a blank line before a doctest, list or field list; a Markdown fence to a
  `code-block` (or a `::` literal block, `fence_style="literal"`);
  `Returns: text` one-liners to real sections; `## Heading` to a rubric;
  `[text](url)` to an RST link; short underlines padded. Escaping a
  prose `*args` is *not* source-safe (it changes what the author wrote,
  and a later reader may not know why the backslash is there), so it stays a
  diagnostic (DR010), like unmatched backticks and every other artifact the
  normalizer cannot fix. Nor is turning a bare `Examples:` header into a
  rubric: napoleon renders it as that rubric already, so the rewrite would
  churn the source for no change on the page ([`UNSAFE_RULES`](_autosummary/epythet.repair.html.md#epythet.repair.UNSAFE_RULES) lists both
  with the reason).
- The normalizer’s own rule applies twice over here: \*\*rewrite only what is
  unambiguous, otherwise report.\*\* A `#` line that could be a comment, a
  `term:` over an indented paragraph, an entry inside an `Args:` body, a
  drawing made of arrows: none is touched, and the author’s blank lines
  before the closing quotes are kept as written.
- Every doctest keeps its source lines byte for byte (checked with
  [`doctest`](https://docs.python.org/3/library/doctest.html#module-doctest)’s own parser); a rewrite that would change one is skipped.
- Every rewritten docstring is re-validated at level 0.5: a rewrite that
  introduces a finding the original did not have is dropped. With
  `write=True` the doctests of every touched file are run before and after
  (the module is *imported* for that, so its top level runs; pass
  `run_doctests=False` / `--no-doctests` for code that must not run), and
  a file whose failures went up is restored. A module that cannot be imported
  is written but reported as unverified.
- Line endings (CRLF), a UTF-8 BOM and tab indentation are preserved; a file
  is replaced atomically.

Dry run is the default and prints a unified diff; `write=True` applies.
The seam `applier=` swaps the rewriting substrate: [`apply_span_edits()`](_autosummary/epythet.repair.html.md#epythet.repair.apply_span_edits)
(default, no dependency) or [`apply_with_libcst()`](_autosummary/epythet.repair.html.md#epythet.repair.apply_with_libcst) (when LibCST is
installed; it re-parses the module as a concrete syntax tree and replaces
the string nodes).

```pycon
>>> from epythet.repair import rewrite_docstring_literal
>>> literal = '"""Do it.\n    - one\n    - two\n    """'
>>> new, reason = rewrite_docstring_literal(literal)
>>> new.split("\n"), reason
(['"""Do it.', '', '    - one', '    - two', '    """'], None)
>>> rewrite_docstring_literal('"""Has a \\n escape."""')[1]
'non-raw literal with backslashes: escapes would change'
```

### Module Attributes

| [`UNSAFE_RULES`](_autosummary/epythet.repair.html.md#epythet.repair.UNSAFE_RULES)      | Normalizer rules that stay build-time only, and why.                                 |
|--------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`SOURCE_SAFE_RULES`](_autosummary/epythet.repair.html.md#epythet.repair.SOURCE_SAFE_RULES) | The normalizer rules whose rewrite is safe to commit to source, in normalizer order. |

### Functions

| [`apply_span_edits`](_autosummary/epythet.repair.html.md#epythet.repair.apply_span_edits)(source, edits)                    | Splice each edit's `after` over its `[start, end)` span, last edit first.                |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`apply_with_libcst`](_autosummary/epythet.repair.html.md#epythet.repair.apply_with_libcst)(source, edits)                   | The LibCST applier: replace the matching `SimpleString` nodes of a concrete syntax tree. |
| [`fences_to_literal_blocks`](_autosummary/epythet.repair.html.md#epythet.repair.fences_to_literal_blocks)(lines)                    | The `fence_style="literal"` variant: a fence becomes a `::` literal block.               |
| [`iter_docstring_nodes`](_autosummary/epythet.repair.html.md#epythet.repair.iter_docstring_nodes)(tree)                         | `(qualname, node, constant)` for every docstring in a parsed module.                     |
| [`render_repair`](_autosummary/epythet.repair.html.md#epythet.repair.render_repair)(report, \*[, diff])                  | The human report: the diff (dry run) or what was written, then the refusals.             |
| [`repair`](_autosummary/epythet.repair.html.md#epythet.repair.repair)(path, \*[, write, fence_style, rules, ...]) | Repair the docstrings under `path` (a file, package directory or project root).          |
| [`repair_command`](_autosummary/epythet.repair.html.md#epythet.repair.repair_command)(path, \*[, write, ...])             | Rewrite docstrings so they render right: blank lines, fences, one-liner sections, links. |
| [`repair_source`](_autosummary/epythet.repair.html.md#epythet.repair.repair_source)(source, \*[, rules, ...])            | Repair every docstring of one module's source text; nothing is written.                  |
| [`rewrite_docstring_literal`](_autosummary/epythet.repair.html.md#epythet.repair.rewrite_docstring_literal)(segment, \*[, rules])    | Rewrite one docstring literal's source; returns `(new_segment, reason_if_unsafe)`.       |
| [`rules_for`](_autosummary/epythet.repair.html.md#epythet.repair.rules_for)([fence_style, rules])                    | The rule tuple for a fence style (`literal` swaps the fence rule).                       |
| [`split_literal`](_autosummary/epythet.repair.html.md#epythet.repair.split_literal)(segment)                             | `(prefix, quote, body, closing quote)` of a string literal's source, or `None`.          |

### Classes

| [`DocstringEdit`](_autosummary/epythet.repair.html.md#epythet.repair.DocstringEdit)(qualname, line, start, end, ...)   | One docstring the repair rewrote (or refused to).   |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------|
| [`FileRepair`](_autosummary/epythet.repair.html.md#epythet.repair.FileRepair)(path, original, repaired[, ...])      | What the repair did to one file.                    |
| [`RepairReport`](_autosummary/epythet.repair.html.md#epythet.repair.RepairReport)(root[, files, write, notes])        | Everything one `repair` run did.                    |

### *class* epythet.repair.DocstringEdit(qualname, line, start, end, before, after, reason=None, fixed=<factory>, remaining=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One docstring the repair rewrote (or refused to).

### *class* epythet.repair.FileRepair(path, original, repaired, edits=<factory>, skipped=None, written=False, verification=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What the repair did to one file.

#### diff()

The unified diff of the file, empty when nothing changed.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### *property* refused *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[DocstringEdit](_autosummary/epythet.repair.html.md#epythet.repair.DocstringEdit)]*

a refused rewrite, or findings no rule fixes.

A docstring that was rewritten but still has findings counts too, so
the number is the same on the dry run, the write, and the run after.

* **Type:**
  Docstrings left for a hand

### *class* epythet.repair.RepairReport(root, files=<factory>, write=False, notes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything one `repair` run did.

### epythet.repair.SOURCE_SAFE_RULES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]], [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]], ...]* *= (<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>)*

The normalizer rules whose rewrite is safe to commit to source, in normalizer order.

### epythet.repair.UNSAFE_RULES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {'bare_headers_to_rubrics': 'napoleon already renders a bare Examples: header as that rubric, so the rewrite changes the source without changing the page; a bare Note: is ambiguous and reported as DR002 instead', 'escape_unmatched_stars': 'escaping \*args in prose changes what the author wrote; reported as DR010 instead'}*

Normalizer rules that stay build-time only, and why.

### epythet.repair.apply_span_edits(source, edits)

Splice each edit’s `after` over its `[start, end)` span, last edit first.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.repair.apply_with_libcst(source, edits)

The LibCST applier: replace the matching `SimpleString` nodes of a concrete syntax tree.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.repair.fences_to_literal_blocks(lines)

The `fence_style="literal"` variant: a fence becomes a `::` literal block.

A literal block needs no directive support and, unlike `code-block`,
is safe inside a docstring that a doctest runner reads (a `>>>` line
inside it is still literal, but is never mistaken for a directive body).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> N.normalize_text("Run:\n```bash\npip install x\n```\nDone.", rules=[fences_to_literal_blocks])
'Run::\n\n    pip install x\n\nDone.'
```

### epythet.repair.iter_docstring_nodes(tree)

`(qualname, node, constant)` for every docstring in a parsed module.

### epythet.repair.render_repair(report, , diff=True)

The human report: the diff (dry run) or what was written, then the refusals.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.repair.repair(path, \*, write=False, fence_style='code-block', rules=(<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>), ignore=(), ledger=None, napoleon=True, revalidate=True, run_doctests=True, applier=<function apply_span_edits>)

Repair the docstrings under `path` (a file, package directory or project root).

* **Parameters:**
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – What to repair.
  * **write** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Apply the rewrites; the default only computes them (dry run).
  * **fence_style** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – What a Markdown fence becomes: `code-block` or `literal`.
  * **rules** ([`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]]) – The normalizer rules to apply; [`SOURCE_SAFE_RULES`](_autosummary/epythet.repair.html.md#epythet.repair.SOURCE_SAFE_RULES) by default.
  * **ignore** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Path substrings to skip, as `epythet validate --ignore`.
  * **ledger** – The rule catalog used to re-validate (`None` = bundled).
  * **napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Parse docstrings with napoleon’s Google/NumPy pre-processing.
  * **revalidate** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Drop a rewrite that introduces a level-0.5 finding.
  * **run_doctests** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – With `write`, run each touched file’s doctests before
    and after, and restore a file whose failures went up.
  * **applier** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`DocstringEdit`](_autosummary/epythet.repair.html.md#epythet.repair.DocstringEdit)]], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – The rewriting substrate; [`apply_span_edits()`](_autosummary/epythet.repair.html.md#epythet.repair.apply_span_edits) or
    [`apply_with_libcst()`](_autosummary/epythet.repair.html.md#epythet.repair.apply_with_libcst).
* **Return type:**
  [`RepairReport`](_autosummary/epythet.repair.html.md#epythet.repair.RepairReport)

### epythet.repair.repair_command(path, , write=False, fence_style='code-block', ignore=None, ledger=None, no_napoleon=False, no_doctests=False, applier='span', quiet=False)

Rewrite docstrings so they render right: blank lines, fences, one-liner sections, links.

Dry run by default: prints a unified diff of what –write would change.
Unsafe cases (prose \*args, unmatched backticks, backslashes in a non-raw
docstring) are reported, never rewritten. Exit 0 when nothing is left to
do or every write was verified; 3 when a written file had to be restored.

* **Parameters:**
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – A .py file, a package directory, or a project root.
  * **write** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Apply the changes (after re-validating each docstring and re-running doctests).
  * **fence_style** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – What a Markdown fence becomes: code-block or literal.
  * **ignore** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Skip files whose path contains any of these strings (several after one -i, or -i repeated).
  * **ledger** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Directory of extra rule YAML files overlaid on the bundled ledger.
  * **no_napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Re-validate without napoleon’s Google/NumPy pre-processing.
  * **no_doctests** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Do not run each touched file’s doctests before and after writing.
  * **applier** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The rewriting substrate: span (default) or libcst.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print the summary only, not the diff.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.repair.repair_source(source, \*, rules=(<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>), ledger_rules=(), napoleon=True, applier=<function apply_span_edits>, path=None)

Repair every docstring of one module’s source text; nothing is written.

* **Return type:**
  [`FileRepair`](_autosummary/epythet.repair.html.md#epythet.repair.FileRepair)

### epythet.repair.rewrite_docstring_literal(segment, \*, rules=(<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>))

Rewrite one docstring literal’s source; returns `(new_segment, reason_if_unsafe)`.

The body is dedented the way [`inspect.cleandoc()`](https://docs.python.org/3/library/inspect.html#inspect.cleandoc) does (the first
line stays as written), the rules run, and the result is re-indented to
the original margin. Indentation-only lines (the closing-quote line) are
preserved.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### epythet.repair.rules_for(fence_style='code-block', rules=(<function fences_to_code_blocks>, <function fix_short_underlines>, <function google_one_liners>, <function markdown_headings_to_rubrics>, <function literal_block_after_colon>, <function reflow_list_continuations>, <function blank_lines_between_blocks>, <function markdown_links_to_rst>))

The rule tuple for a fence style (`literal` swaps the fence rule).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### epythet.repair.split_literal(segment)

`(prefix, quote, body, closing quote)` of a string literal’s source, or `None`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> split_literal('r"""x"""')
('r', '"""', 'x', '"""')
>>> split_literal("'y'")
('', "'", 'y', "'")
```


# _autosummary/epythet.scaffold.html.md

# epythet.scaffold

### epythet.scaffold(config, , verbose=True, pages=None)

Write the docsrc files for an already-loaded configuration.

* **Parameters:**
  **pages** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[`PageSpec`]]) – extra generated pages, written next to `index.md` and added
  to its toctree after the API entry. `None` (the default) means the
  conventional pages, i.e. the “For AI agents” page that
  `epythet.ai_artifacts.default_pages` produces when the project has
  agent artifacts and `ai_artifacts` is on; pass `()` for none.
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/epythet.setup_docsrc.html.md

# epythet.setup_docsrc

Compatibility module: `make_docsrc` now lives in [`epythet.scaffold`](_autosummary/epythet.scaffold.html.md#epythet.scaffold).


# _autosummary/epythet.sphinx_conf.html.md

# epythet.sphinx_conf

The star-import target for a project’s `docsrc/conf.py`.

A generated `conf.py` is two lines:

```default
from epythet.sphinx_conf import *  # noqa: F401,F403
# optional overrides below, e.g. html_theme = "alabaster"
```

Importing this module locates the project (the `EPYTHET_PROJECT_DIR`
environment variable, else the nearest ancestor of the current directory with a
`pyproject.toml` or `setup.cfg`; Sphinx runs `conf.py` from the source
directory), loads its [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig), applies any overrides
passed in the `EPYTHET_OVERRIDES` environment variable (JSON, set by
[`epythet.build`](_autosummary/epythet.build.html.md#epythet.build) for command-line flags such as `--ignore`), and exports
the settings from [`epythet.confgen.sphinx_settings()`](_autosummary/epythet.confgen.html.md#epythet.confgen.sphinx_settings) as module globals.

### Module Attributes

| [`PROJECT_DIR_ENV`](_autosummary/epythet.sphinx_conf.html.md#epythet.sphinx_conf.PROJECT_DIR_ENV)   | Environment variable naming the project root (set by `epythet make`).                                                            |
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| [`OVERRIDES_ENV`](_autosummary/epythet.sphinx_conf.html.md#epythet.sphinx_conf.OVERRIDES_ENV)     | Environment variable carrying JSON config overrides (set by `epythet make`).                                                     |
| [`epythet_config`](_autosummary/epythet.sphinx_conf.html.md#epythet.sphinx_conf.epythet_config)    | The [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig) this configuration was generated from. |

### epythet.sphinx_conf.OVERRIDES_ENV *= 'EPYTHET_OVERRIDES'*

Environment variable carrying JSON config overrides (set by `epythet make`).

### epythet.sphinx_conf.PROJECT_DIR_ENV *= 'EPYTHET_PROJECT_DIR'*

Environment variable naming the project root (set by `epythet make`).

### epythet.sphinx_conf.epythet_config *= DocsConfig(project_dir=PosixPath('/home/runner/work/epythet/epythet'), name='epythet', version='0.2.12', author='', description='Beautiful, correct documentation from a Python package, with no boilerplate: Sphinx, README landing page, nested API tree, themes, docstring normalizer, agent-facing outputs, GitHub Pages', display_name='epythet', copyright='', repo_url='https://github.com/i2mint/epythet', theme='auto', accent='', mode='auto', theme_options={}, readme={'humor': True, 'agentic_first': True}, ignore=('tests/', 'scrap/', 'examples/', 'ledger/'), api_generator='autosummary', agent_outputs=True, aggregates=('md',), ai_artifacts=True, ai_artifacts_template='', provenance=True, provenance_template='', package_dir=PosixPath('/home/runner/work/epythet/epythet/epythet'), docs_dir='docsrc')*

The [`DocsConfig`](_autosummary/epythet.config.html.md#epythet.config.DocsConfig) this configuration was generated from.


# _autosummary/epythet.sphinx_ext.html.md

# epythet.sphinx_ext

epythet’s Sphinx extension: the normalizer, agent link relations and theme CSS.

Listed automatically in the generated configuration
(`extensions = [..., "epythet.sphinx_ext"]`); usable on its own in any
`conf.py` too. It registers:

- the docstring normalizer on `autodoc-process-docstring` (priority 400, so
  it runs before napoleon), see [`epythet.normalizer`](_autosummary/epythet.normalizer.html.md#module-epythet.normalizer);
- the `<link rel="alternate" type="text/markdown">` / `rel="describedby"`
  relations on every HTML page when `epythet_agent_outputs` is on, see
  [`epythet.agent_outputs`](_autosummary/epythet.agent_outputs.html.md#module-epythet.agent_outputs);
- the theme accent stylesheet `_static/epythet.css` when the chosen theme
  takes its colours from CSS variables (`epythet_theme_css`);
- the build provenance (`epythet_build_info`): the one-line footer appended
  to the landing page, the documented-module counts on the about page, and
  `build_info.json` at the site root, see [`epythet.provenance`](_autosummary/epythet.provenance.html.md#module-epythet.provenance).

### Functions

| [`setup`](_autosummary/epythet.sphinx_ext.html.md#epythet.sphinx_ext.setup)(app)           | Register epythet's hooks and configuration values (idempotent).   |
|-----------------------------------------------------------------------|-------------------------------------------------------------------|
| [`write_theme_css`](_autosummary/epythet.sphinx_ext.html.md#epythet.sphinx_ext.write_theme_css)(app) | `builder-inited` hook: materialise the theme CSS into `_static`.  |

### epythet.sphinx_ext.setup(app)

Register epythet’s hooks and configuration values (idempotent).

### epythet.sphinx_ext.write_theme_css(app)

`builder-inited` hook: materialise the theme CSS into `_static`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/epythet.sweep.html.md

# epythet.sweep

`epythet sweep`: validate many packages read-only and rank the queue.

The fleet has some two hundred packages and one question about each rule:
how often does it fire, and where? Severities in the ledger are decided by
that distribution, not by intuition (decision D9). The sweep runs
`epythet.validation.validate()` at level 0 (the coverage detectors, no
linters unless asked) and level 0.5 (the doctree of every docstring) over
each package, never writes into any of them, appends what it saw to the
observations file outside the repositories, and reports two things:

- the **rule distribution**: per rule, how many findings, in how many
  packages, and the rate per hundred public objects;
- the **queue**: packages ranked by how much documentation work they hold,
  weighted the way the doc-quality research orders the work (entry points
  first: an entry point without an example outranks a helper without a
  summary; a rendering error outranks both).

Packages come from directories on the command line, or from a manifest: a
`.pth`-style file with one project directory per line (the local package
manifest is exactly that). The manifest is read, never written.

```pycon
>>> from epythet.sweep import queue_score
>>> queue_score({"DQ002": 3, "DR003": 1, "DQ004": 2}, severities={"DR003": "error"})
18.0
```

### Module Attributes

| [`RULE_WEIGHTS`](_autosummary/epythet.sweep.html.md#epythet.sweep.RULE_WEIGHTS)     | How much one finding of a rule weighs in the queue (research_doc_quality §4.3: examples first, then correctness, then the summary, then parameter semantics).   |
|-------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`SEVERITY_WEIGHTS`](_autosummary/epythet.sweep.html.md#epythet.sweep.SEVERITY_WEIGHTS) | Weight per severity for rendering findings (level 0.5) not listed above.                                                                                        |

### Functions

| [`packages_from_manifest`](_autosummary/epythet.sweep.html.md#epythet.sweep.packages_from_manifest)(path)                       | Project directories listed in a `.pth`-style manifest (one per line, `#` comments).   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`queue_score`](_autosummary/epythet.sweep.html.md#epythet.sweep.queue_score)(counts, \*, severities)                | The queue weight of a package from its per-rule finding counts.                       |
| [`render_sweep`](_autosummary/epythet.sweep.html.md#epythet.sweep.render_sweep)(result, \*[, top])                    | The human report: distribution table, then the queue.                                 |
| [`sweep`](_autosummary/epythet.sweep.html.md#epythet.sweep.sweep)([dirs, manifest, levels, ignore, ...])       | Validate every package under `dirs` and `manifest` read-only; return the result.      |
| [`sweep_command`](_autosummary/epythet.sweep.html.md#epythet.sweep.sweep_command)(\*dirs[, manifest, parse_only, ...]) | Validate many packages read-only; print the rule distribution and the work queue.     |
| [`sweeps_path`](_autosummary/epythet.sweep.html.md#epythet.sweep.sweeps_path)()                                      | Where sweep summaries are appended: `<user data dir>/ledger/sweeps.jsonl`.            |

### Classes

| [`PackageSweep`](_autosummary/epythet.sweep.html.md#epythet.sweep.PackageSweep)(path, name[, version, objects, ...])   | What the sweep saw in one package.                               |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`SweepResult`](_autosummary/epythet.sweep.html.md#epythet.sweep.SweepResult)([packages, levels, started, ...])       | Every package swept, the rule distribution and the ranked queue. |

### *class* epythet.sweep.PackageSweep(path, name, version=None, objects=0, undocumented=0, counts=<factory>, severities=<factory>, duration_s=0.0, error=None, notes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What the sweep saw in one package.

### epythet.sweep.RULE_WEIGHTS *= {'DQ001': 3.0, 'DQ002': 4.0, 'DQ003': 1.0, 'DQ004': 0.5, 'DQ005': 1.0}*

How much one finding of a rule weighs in the queue (research_doc_quality §4.3:
examples first, then correctness, then the summary, then parameter semantics).

### epythet.sweep.SEVERITY_WEIGHTS *= {'error': 5.0, 'info': 0.5, 'warning': 2.0}*

Weight per severity for rendering findings (level 0.5) not listed above.

### *class* epythet.sweep.SweepResult(packages=<factory>, levels=(0, 0.5), started='', duration_s=0.0, ledger_severity=<factory>, ledger_title=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Every package swept, the rule distribution and the ranked queue.

#### distribution()

Per rule: findings, packages affected, rate per 100 public objects; most frequent first.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]

#### queue()

Packages by descending queue score.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`PackageSweep`](_autosummary/epythet.sweep.html.md#epythet.sweep.PackageSweep)]

### epythet.sweep.packages_from_manifest(path)

Project directories listed in a `.pth`-style manifest (one per line, `#` comments).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### epythet.sweep.queue_score(counts, , severities)

The queue weight of a package from its per-rule finding counts.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### epythet.sweep.render_sweep(result, , top=20)

The human report: distribution table, then the queue.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.sweep.sweep(dirs=(), , manifest=None, levels=(0, 0.5), ignore=(), ledger=None, napoleon=True, linters=False, observe=True, observations_path=None, record=True, limit=None, on_package=None)

Validate every package under `dirs` and `manifest` read-only; return the result.

* **Parameters:**
  * **dirs** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)]) – Project or package directories.
  * **manifest** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `.pth`-style file of project directories, read only.
  * **levels** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]) – The validate levels to run (`0` coverage, `0.5` parse by default).
  * **ignore** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Path substrings to skip inside each package.
  * **ledger** (`Ledger` | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The rule catalog (`None` = bundled).
  * **napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Parse docstrings with napoleon’s pre-processing.
  * **linters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also shell out to ruff and pydoclint at level 0.
  * **observe** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Append each package’s findings to the observations file.
  * **observations_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override the observations file (tests use this).
  * **record** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Append the sweep summary to `sweeps.jsonl` under the user data dir.
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Sweep at most this many packages.
  * **on_package** – Called with each [`PackageSweep`](_autosummary/epythet.sweep.html.md#epythet.sweep.PackageSweep) as it completes.
* **Return type:**
  [`SweepResult`](_autosummary/epythet.sweep.html.md#epythet.sweep.SweepResult)

### epythet.sweep.sweep_command(\*dirs, manifest=None, parse_only=False, linters=False, ignore=None, ledger=None, no_napoleon=False, no_observe=False, limit=None, format='table', top=20, output=None, quiet=False)

Validate many packages read-only; print the rule distribution and the work queue.

Runs level 0 (coverage detectors) and level 0.5 (every docstring’s
doctree) over each package; never writes into a package or the manifest.

* **Parameters:**
  * **dirs** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Project or package directories to sweep.
  * **manifest** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A .pth-style file listing project directories, one per line (read only).
  * **parse_only** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Run level 0.5 only (skip the coverage detectors).
  * **linters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also run ruff and pydoclint at level 0 (slower).
  * **ignore** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Skip files whose path contains this string (repeat -i for several).
  * **ledger** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Directory of extra rule YAML files overlaid on the bundled ledger.
  * **no_napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Parse docstrings without napoleon’s Google/NumPy pre-processing.
  * **no_observe** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Do not append findings (or the sweep summary) to the user data dir.
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Sweep at most this many packages.
  * **format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – table (human) or json (the full result).
  * **top** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – How many packages the queue shows.
  * **output** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Write the report to this file instead of stdout.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Do not print progress on stderr.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.sweep.sweeps_path()

Where sweep summaries are appended: `<user data dir>/ledger/sweeps.jsonl`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/epythet.templates.html.md

# epythet.templates

Text templates for the generated `docsrc` files.

Only two files are generated: the `conf.py` shim and the `index.md` landing
page. Everything else (the API tree, the agent twins) is produced by Sphinx
extensions at build time. The about-this-build page’s template lives with its
data in [`epythet.provenance`](_autosummary/epythet.provenance.html.md#module-epythet.provenance).

### Module Attributes

| [`CONF_SHIM_MARKER`](_autosummary/epythet.templates.html.md#epythet.templates.CONF_SHIM_MARKER)           | Marker line present in every conf.py epythet generated (v2), used to decide whether an existing file may be overwritten.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`LEGACY_CONF_MARKER`](_autosummary/epythet.templates.html.md#epythet.templates.LEGACY_CONF_MARKER)         | The old (0.1.x) template's signature line, also safe to overwrite.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [`INDEX_MARKER`](_autosummary/epythet.templates.html.md#epythet.templates.INDEX_MARKER)               | Marker in the generated index.md, used to decide whether to overwrite it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| [`aggregates_block`](_autosummary/epythet.templates.html.md#epythet.templates.aggregates_block)           | Footer of the landing page pointing at the single-document twins.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [`LEGACY_DOCSRC_GITIGNORES`](_autosummary/epythet.templates.html.md#epythet.templates.LEGACY_DOCSRC_GITIGNORES)   | What 0.1.x wrote to docsrc/.gitignore; safe to replace.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [`autosummary_module_rst`](_autosummary/epythet.templates.html.md#epythet.templates.autosummary_module_rst)     | autosummary's stock `module.rst` (Sphinx 9) with two changes to the `modules` block: (1) the recursion runs over `all_modules` (every submodule, minus `_`-prefixed ones unless `__all__` names them) rather than `modules`, because with `autosummary_ignore_module_all = False` a package whose `__init__` declares an `__all__` of *objects* would otherwise get no submodule pages at all (a third of the fleet declares one); (2) submodules matching the ignore fragments are left out of the recursion, so no stub is generated (and no second import attempted) for tests/, scrap/, examples/. |
| [`DOCSRC_GITIGNORE_HEADER`](_autosummary/epythet.templates.html.md#epythet.templates.DOCSRC_GITIGNORE_HEADER)    | First line of the generated docsrc/.gitignore; a file starting with it is epythet's own: an exact earlier version is replaced, an edited one is appended to (`epythet.scaffold.refresh_docsrc_gitignore()`).                                                                                                                                                                                                                                                                                                                                                                                           |
| [`PREVIOUS_DOCSRC_GITIGNORES`](_autosummary/epythet.templates.html.md#epythet.templates.PREVIOUS_DOCSRC_GITIGNORES) | Earlier generated versions, replaced wholesale when found verbatim.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

### epythet.templates.CONF_SHIM_MARKER *= 'from epythet.sphinx_conf import \*'*

Marker line present in every conf.py epythet generated (v2), used to decide
whether an existing file may be overwritten.

### epythet.templates.DOCSRC_GITIGNORE_HEADER *= '# Generated by epythet at build time'*

First line of the generated docsrc/.gitignore; a file starting with it is
epythet’s own: an exact earlier version is replaced, an edited one is
appended to (`epythet.scaffold.refresh_docsrc_gitignore()`).

### epythet.templates.INDEX_MARKER *= '<!-- generated by epythet -->'*

Marker in the generated index.md, used to decide whether to overwrite it.

### epythet.templates.LEGACY_CONF_MARKER *= 'from epythet.config_parser import parse_config'*

The old (0.1.x) template’s signature line, also safe to overwrite.

### epythet.templates.LEGACY_DOCSRC_GITIGNORES *= ('_build/', '_build')*

What 0.1.x wrote to docsrc/.gitignore; safe to replace.

### epythet.templates.PREVIOUS_DOCSRC_GITIGNORES *= ('# Generated by epythet at build time\\n_build/\\napi/\\n_autosummary/\\n_templates/\\n_static/epythet.css\\n',)*

Earlier generated versions, replaced wholesale when found verbatim.

### epythet.templates.aggregates_block *= '<p class="epythet-aggregates">This documentation as a single file: {links}.</p>\\n'*

Footer of the landing page pointing at the single-document twins. Raw HTML,
because MyST would read a relative `.md` link as a (missing) page reference.

### epythet.templates.autosummary_module_rst *= '{{{{ fullname | escape | underline}}}}\\n\\n.. automodule:: {{{{ fullname }}}}\\n\\n   {{% block attributes %}}\\n   {{%- if attributes %}}\\n   .. rubric:: {{{{ \_(\\'Module Attributes\\') }}}}\\n\\n   .. autosummary::\\n   {{% for item in attributes %}}\\n      {{{{ item }}}}\\n   {{%- endfor %}}\\n   {{% endif %}}\\n   {{%- endblock %}}\\n\\n   {{%- block functions %}}\\n   {{%- if functions %}}\\n   .. rubric:: {{{{ \_(\\'Functions\\') }}}}\\n\\n   .. autosummary::\\n   {{% for item in functions %}}\\n      {{{{ item }}}}\\n   {{%- endfor %}}\\n   {{% endif %}}\\n   {{%- endblock %}}\\n\\n   {{%- block classes %}}\\n   {{%- if classes %}}\\n   .. rubric:: {{{{ \_(\\'Classes\\') }}}}\\n\\n   .. autosummary::\\n   {{% for item in classes %}}\\n      {{{{ item }}}}\\n   {{%- endfor %}}\\n   {{% endif %}}\\n   {{%- endblock %}}\\n\\n   {{%- block exceptions %}}\\n   {{%- if exceptions %}}\\n   .. rubric:: {{{{ \_(\\'Exceptions\\') }}}}\\n\\n   .. autosummary::\\n   {{% for item in exceptions %}}\\n      {{{{ item }}}}\\n   {{%- endfor %}}\\n   {{% endif %}}\\n   {{%- endblock %}}\\n\\n{{%- block modules %}}\\n{{%- set ignored = {ignored_fragments} %}}\\n{{%- set ns = namespace(kept=[]) %}}\\n{{%- for item in all_modules %}}\\n{{%- if (item in modules or not item.startswith(\\'_\\')) and not (ignored | select("in", \\'.\\' ~ item ~ \\'.\\') | list) %}}\\n{{%- set ns.kept = ns.kept + [item] %}}\\n{{%- endif %}}\\n{{%- endfor %}}\\n{{%- if ns.kept %}}\\n.. rubric:: Modules\\n\\n.. autosummary::\\n   :toctree:\\n   :recursive:\\n{{% for item in ns.kept %}}\\n   {{{{ item }}}}\\n{{%- endfor %}}\\n{{% endif %}}\\n{{%- endblock %}}\\n'*

autosummary’s stock `module.rst` (Sphinx 9) with two changes to the
`modules` block: (1) the recursion runs over `all_modules` (every
submodule, minus `_`-prefixed ones unless `__all__` names them) rather
than `modules`,
because with `autosummary_ignore_module_all = False` a package whose
`__init__` declares an `__all__` of *objects* would otherwise get no
submodule pages at all (a third of the fleet declares one); (2) submodules
matching the ignore fragments are left out of the recursion, so no stub is
generated (and no second import attempted) for tests/, scrap/, examples/.


# _autosummary/epythet.themes.html.md

# epythet.themes

Theme registry, deterministic theme choice, and the OKLCH accent palette.

epythet exposes three semantic knobs (`theme`, `accent`, `mode`) and a
verbatim `theme_options` passthrough. This module translates the three knobs
into each theme’s own vocabulary, so a project never needs to know that furo
wants `light_css_variables` while shibuya wants a Radix colour *name* and
pydata wants a CSS variable in a stylesheet.

Themes are chosen from the curated registry `THEMES`. `theme = "auto"`
(the default) hashes the package name into [`AUTO_POOL`](_autosummary/epythet.themes.html.md#epythet.themes.AUTO_POOL), so the fleet gets
variety while every package keeps the same look across rebuilds. A name that is
not in the registry is passed straight to Sphinx as `html_theme`.

The accent colour, when not configured, is derived from the package name in
OKLCH with fixed lightness and chroma, so any hue clears WCAG AA against white
(light mode) and AAA against a dark background. Same name, same colour, forever;
change [`ACCENT_SALT`](_autosummary/epythet.themes.html.md#epythet.themes.ACCENT_SALT) to reshuffle the whole fleet.

```pycon
>>> accent_for("dol") == accent_for("dol")
True
>>> light, dark = accent_for("dol")
>>> light.startswith("#") and len(light) == 7
True
>>> choose_theme("dol", "furo")
'furo'
>>> choose_theme("dol", "auto") in AUTO_POOL
True
```

### Module Attributes

| [`ACCENT_SALT`](_autosummary/epythet.themes.html.md#epythet.themes.ACCENT_SALT)    | Changing the salt reshuffles every derived hue in the fleet at once.   |
|-----------------------------------------------------------------|------------------------------------------------------------------------|
| [`LIGHT_L`](_autosummary/epythet.themes.html.md#epythet.themes.LIGHT_L)        | 1 on white at C=0.13).                                                 |
| [`DARK_L`](_autosummary/epythet.themes.html.md#epythet.themes.DARK_L)         | 1 on #131415 at C=0.13).                                               |
| [`ACCENT_C`](_autosummary/epythet.themes.html.md#epythet.themes.ACCENT_C)       | OKLCH chroma for both modes.                                           |
| [`THEME_ALIASES`](_autosummary/epythet.themes.html.md#epythet.themes.THEME_ALIASES)  | Aliases accepted in `[tool.epythet] theme` for registry entries.       |
| [`AUTO_POOL`](_autosummary/epythet.themes.html.md#epythet.themes.AUTO_POOL)      | the modern, brand-neutral themes.                                      |
| [`BUNDLED_THEMES`](_autosummary/epythet.themes.html.md#epythet.themes.BUNDLED_THEMES) | Themes epythet depends on and therefore can always use.                |

### Functions

| [`accent_for`](_autosummary/epythet.themes.html.md#epythet.themes.accent_for)(name)                              | `(light_hex, dark_hex)` accents for a package name: same hue, per-mode lightness.                                       |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`choose_theme`](_autosummary/epythet.themes.html.md#epythet.themes.choose_theme)(package_name[, theme])           | Resolve the configured `theme` to a registry key or a raw theme name.                                                   |
| [`contrast_ratio`](_autosummary/epythet.themes.html.md#epythet.themes.contrast_ratio)(hex_a, hex_b)                  | WCAG 2 contrast ratio between two colours.                                                                              |
| [`dark_variant`](_autosummary/epythet.themes.html.md#epythet.themes.dark_variant)(hex_color)                       | The dark-mode twin of an accent: same hue, lightness lifted to [`DARK_L`](_autosummary/epythet.themes.html.md#epythet.themes.DARK_L). |
| [`hex_to_oklch`](_autosummary/epythet.themes.html.md#epythet.themes.hex_to_oklch)(hex_color)                       | `(L, C, h)` in OKLCH for an sRGB hex colour.                                                                            |
| [`hex_to_rgb`](_autosummary/epythet.themes.html.md#epythet.themes.hex_to_rgb)(hex_color)                         | `'#3661ac'` to `(54, 97, 172)`.                                                                                         |
| [`hue_for`](_autosummary/epythet.themes.html.md#epythet.themes.hue_for)(name, \*[, salt])                     | A stable hue in `[0, 360)` for a package name.                                                                          |
| [`nearest_radix_name`](_autosummary/epythet.themes.html.md#epythet.themes.nearest_radix_name)(hue)                       | The Radix colour whose hue is closest (circularly) to `hue`.                                                            |
| [`oklch_to_hex`](_autosummary/epythet.themes.html.md#epythet.themes.oklch_to_hex)(L, C, h)                         | Convert an OKLCH colour to an sRGB hex string (gamut-clipped).                                                          |
| [`resolve_theme`](_autosummary/epythet.themes.html.md#epythet.themes.resolve_theme)(package_name, \*[, theme, ...]) | Translate the semantic knobs into a concrete Sphinx theme configuration.                                                |
| [`theme_spec`](_autosummary/epythet.themes.html.md#epythet.themes.theme_spec)(name)                              | The registry entry for `name`, or a bare passthrough spec for unknown themes.                                           |

### Classes

| [`ResolvedTheme`](_autosummary/epythet.themes.html.md#epythet.themes.ResolvedTheme)(html_theme, ...)                  | Everything the Sphinx configuration needs for the chosen theme.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`ThemeSpec`](_autosummary/epythet.themes.html.md#epythet.themes.ThemeSpec)(html_theme, pip_name[, options, ...]) | How one Sphinx theme expresses epythet's three semantic knobs.    |

### epythet.themes.ACCENT_C *= 0.13*

OKLCH chroma for both modes.

### epythet.themes.ACCENT_SALT *= 'epythet-accent-v1'*

Changing the salt reshuffles every derived hue in the fleet at once.

### epythet.themes.AUTO_POOL *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('furo', 'shibuya', 'pydata_sphinx_theme', 'sphinxawesome_theme')*

the modern, brand-neutral themes.
Order matters (it is what the hash indexes); append, never reorder.

* **Type:**
  What `theme = "auto"` chooses from

### epythet.themes.BUNDLED_THEMES *= ('furo', 'shibuya', 'pydata_sphinx_theme', 'sphinxawesome_theme')*

Themes epythet depends on and therefore can always use.

### epythet.themes.DARK_L *= 0.78*

1 on #131415 at C=0.13).

* **Type:**
  OKLCH lightness for the dark-mode accent (>= 8.6

### epythet.themes.LIGHT_L *= 0.46*

1 on white at C=0.13).

* **Type:**
  OKLCH lightness for the light-mode accent (>= 6.2

### *class* epythet.themes.ResolvedTheme(html_theme, html_theme_options, html_context, css, accent_light, accent_dark)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything the Sphinx configuration needs for the chosen theme.

### epythet.themes.THEME_ALIASES *= {'awesome': 'sphinxawesome_theme', 'book': 'sphinx_book_theme', 'pydata': 'pydata_sphinx_theme', 'pydata-sphinx-theme': 'pydata_sphinx_theme', 'rtd': 'sphinx_rtd_theme', 'sphinx-book-theme': 'sphinx_book_theme', 'sphinx-rtd-theme': 'sphinx_rtd_theme', 'sphinxawesome': 'sphinxawesome_theme', 'sphinxawesome-theme': 'sphinxawesome_theme'}*

Aliases accepted in `[tool.epythet] theme` for registry entries.

### *class* epythet.themes.ThemeSpec(html_theme, pip_name, options=<factory>, accent=<function ThemeSpec.<lambda>>, mode=<function ThemeSpec.<lambda>>, css=<function ThemeSpec.<lambda>>, context=<function ThemeSpec.<lambda>>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How one Sphinx theme expresses epythet’s three semantic knobs.

`options` are the “beautiful default” `html_theme_options`; strings may
contain `{repo_url}`, `{description}` and `{docs_dir}` placeholders.
`accent` maps `(light_hex, dark_hex)` to extra theme options and `mode`
maps `"auto" | "light" | "dark"` likewise. `css` returns stylesheet text
for themes whose colours are CSS variables rather than options. `context`
maps the mode to `html_context` entries.

### epythet.themes.accent_for(name)

`(light_hex, dark_hex)` accents for a package name: same hue, per-mode lightness.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.themes.choose_theme(package_name, theme='auto')

Resolve the configured `theme` to a registry key or a raw theme name.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> choose_theme("x", "pydata")
'pydata_sphinx_theme'
>>> choose_theme("x", "my_custom_theme")
'my_custom_theme'
```

### epythet.themes.contrast_ratio(hex_a, hex_b)

WCAG 2 contrast ratio between two colours.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

```pycon
>>> round(contrast_ratio("#000000", "#ffffff"), 1)
21.0
```

### epythet.themes.dark_variant(hex_color)

The dark-mode twin of an accent: same hue, lightness lifted to [`DARK_L`](_autosummary/epythet.themes.html.md#epythet.themes.DARK_L).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> contrast_ratio(dark_variant("#3661ac"), "#131415") > 7
True
```

### epythet.themes.hex_to_oklch(hex_color)

`(L, C, h)` in OKLCH for an sRGB hex colour.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> L, C, h = hex_to_oklch(oklch_to_hex(0.5, 0.13, 200))
>>> round(L, 2), round(C, 2), round(h)
(0.5, 0.13, 200)
```

### epythet.themes.hex_to_rgb(hex_color)

`'#3661ac'` to `(54, 97, 172)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

```pycon
>>> hex_to_rgb("#ffffff")
(255, 255, 255)
```

### epythet.themes.hue_for(name, , salt='epythet-accent-v1')

A stable hue in `[0, 360)` for a package name.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> 0 <= hue_for("dol") < 360
True
>>> hue_for("dol") == hue_for("dol")
True
```

### epythet.themes.nearest_radix_name(hue)

The Radix colour whose hue is closest (circularly) to `hue`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> nearest_radix_name(206)
'blue'
>>> nearest_radix_name(359)
'red'
```

### epythet.themes.oklch_to_hex(L, C, h)

Convert an OKLCH colour to an sRGB hex string (gamut-clipped).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> oklch_to_hex(0, 0, 0)
'#000000'
>>> oklch_to_hex(1, 0, 0)
'#ffffff'
```

### epythet.themes.resolve_theme(package_name, , theme='auto', accent='', mode='auto', theme_options=None, repo_url='', description='', docs_dir='docsrc')

Translate the semantic knobs into a concrete Sphinx theme configuration.

`theme_options` is merged last and wins over every default. Options whose
placeholder could not be filled (no `repo_url`) are dropped rather than
rendered as an empty string.

* **Return type:**
  [`ResolvedTheme`](_autosummary/epythet.themes.html.md#epythet.themes.ResolvedTheme)

```pycon
>>> rt = resolve_theme("dol", theme="furo", accent="#3661ac")
>>> rt.html_theme, rt.html_theme_options["light_css_variables"]["color-brand-primary"]
('furo', '#3661ac')
>>> resolve_theme("dol", theme="furo", theme_options={"sidebar_hide_name": True}
...     ).html_theme_options["sidebar_hide_name"]
True
```

### epythet.themes.theme_spec(name)

The registry entry for `name`, or a bare passthrough spec for unknown themes.

* **Return type:**
  [`ThemeSpec`](_autosummary/epythet.themes.html.md#epythet.themes.ThemeSpec)


# _autosummary/epythet.tools.docstring_diagnosis.html.md

# epythet.tools.docstring_diagnosis

Tools to manipulate documentation elements

The main purpose of the module:

```pycon
>>> repair_package(PY_FILES_DIRECTORY)
```

If you have tec (pip install tec) installed, you can even input a module object:

```pycon
>>> import epythet
>>> number_of_problems = repair_package(epythet)
---> This is just a diagnosis: No files are being written to
setup_docsrc.py                           : #problems: 0
config_parser.py                          : #problems: 0
...
docs_gen.py                               : #problems: 0
```

As the print out header indicated, this is just a diagnosis of problems found for
each module, and returns the total number of problems.
It’s advised to do this, then have a look at the problems using

```pycon
>>> print_diagnosis(MODULE_PATH_OR_PKG_FOLDER)
```

Once you’re familiar with the problems, you can choose to do:

```pycon
>>> repair_package(PY_FILES_DIRECTORY, write_to_files=True)
```

This will attempt to repair the problems for you.

```pycon
>>> docs = '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
>>>
>>>
>>> new_docs = add_newlines_before_doctests_when_missing(docs)
>>> assert new_docs == '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
```

### Functions

| [`add_newlines_before_doctests_when_missing`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.add_newlines_before_doctests_when_missing)(src)             | Returns the code_string with newlines inserted before code blocks when missing./                                        |
|-------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`binary_transition`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.binary_transition)(transitions, state, symbol)              | Binary state machine transition computation.                                                                            |
| [`diagnose_doctest_code_blocks`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.diagnose_doctest_code_blocks)(src)                          | Yields lines the start code block and need attention,                                                                   |
| [`diagnosis_snippets`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.diagnosis_snippets)(src)                                    | Generate snippets that exhibit the problems in the src                                                                  |
| [`lines_with_two_new_lines_before_doctests`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.lines_with_two_new_lines_before_doctests)(lines)            | Yields the input lines, but interleaving an empty ('') line if a code block starts with out an empty line preceeding it |
| [`print_diagnosis`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.print_diagnosis)(src)                                       | Print diagnosis of one or several files                                                                                 |
| [`repair_package`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.repair_package)(pkg[, write_to_files])                      | Diagnose and/or repair a whole pkg (given by folder or pkg module obj, or a store (see dol).                            |
| [`tag_doctest_blocks_not_preceeded_by_new_lines`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.tag_doctest_blocks_not_preceeded_by_new_lines)(lines)       | Yields (line, True/False) pairs.                                                                                        |
| [`tag_doctest_blocks_not_preceeded_by_new_lines_alt_2`](_autosummary/epythet.tools.docstring_diagnosis.html.md#epythet.tools.docstring_diagnosis.tag_doctest_blocks_not_preceeded_by_new_lines_alt_2)(lines) | Yields (line, True/False) pairs.                                                                                        |

### epythet.tools.docstring_diagnosis.add_newlines_before_doctests_when_missing(src)

Returns the code_string with newlines inserted before code blocks when missing./

A code block is defined by a line that starts with `>>>` (except for optional spaces
before it.

```pycon
>>> docs = '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
>>>
>>>
>>> new_docs = add_newlines_before_doctests_when_missing(docs)
>>> assert new_docs == '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
```

### epythet.tools.docstring_diagnosis.beginning_of_doctest(string, pos=0, endpos=9223372036854775807)

Matches zero or more characters at the beginning of the string.

### epythet.tools.docstring_diagnosis.binary_transition(transitions, state, symbol)

Binary state machine transition computation. Computes next state.

* **Parameters:**
  * **transitions** – Transition “matrix”, in the form of:
    {True: state_inverting_symbols, False: state_inverting_symbols}
  * **state** – Current state. True or False
  * **symbol** – Incoming symbol
* **Returns:**
  New state

```pycon
>>> transitions = {False: {1, 2}, True: {3}}
>>> binary_transition(transitions, False, 3)
False
>>> binary_transition(transitions, False, 1)
True
>>> binary_transition(transitions, True, 1)
True
>>> binary_transition(transitions, True, 2)
True
>>> binary_transition(transitions, True, 3)
False
```

### epythet.tools.docstring_diagnosis.blank_line(string, pos=0, endpos=9223372036854775807)

Matches zero or more characters at the beginning of the string.

### epythet.tools.docstring_diagnosis.diagnose_doctest_code_blocks(src)

Yields lines the start code block and need attention,

```pycon
>>> docs = '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
>>> it = diagnose_doctest_code_blocks(docs)
```

You would usually run `next(it)` to check on the first problem, repair, and rerun.
(Oh, and it’s important to rerun, because the line numbers won’t be valid anymore
once you edit the source code_string!).

But, to doctest this we’ll do this:

```pycon
>>> list(it)
[(3, '    >>> like_this'), (10, '    >>> this_doctest_is_too_close_to_text')]
```

### epythet.tools.docstring_diagnosis.diagnosis_snippets(src)

Generate snippets that exhibit the problems in the src

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.tools.docstring_diagnosis.lines_with_two_new_lines_before_doctests(lines)

Yields the input lines, but interleaving an empty (‘’) line if a code block
starts with out an empty line preceeding it

A code block is defined by a line that starts with `>>>` (except for optional spaces
before it.

### epythet.tools.docstring_diagnosis.print_diagnosis(src)

Print diagnosis of one or several files

### epythet.tools.docstring_diagnosis.repair_package(pkg, write_to_files=False)

Diagnose and/or repair a whole pkg (given by folder or pkg module obj,
or a store (see dol).

A folder or package object is handed to [`epythet.repair.repair()`](_autosummary/epythet.repair.html.md#epythet.repair.repair),
which applies every source-safe normalizer rule (blank lines before
doctests and lists, Markdown fences, one-line `Returns:` sections,
Markdown headings and links) and verifies each rewrite; a store (a
`dol` mapping) keeps the original doctest-only pass, which is the only
one that can write through an arbitrary mapping.

Prints one line per file with the number of docstrings that changed (or
would change) and returns the total, exactly as it always has; wads’
`wads-docstring-render` skill depends on that shape.

### epythet.tools.docstring_diagnosis.tag_doctest_blocks_not_preceeded_by_new_lines(lines)

Yields (line, True/False) pairs. True when line is a >>> without newlines before,

A code block is defined by a line that starts with `>>>` (except for optional spaces
before it. To render well through Sphinx, code blocks must be separated from earlier
text by a blank (only spaces) line.

This function diagnoses that.

It uses three finite (binary) state machines.

### epythet.tools.docstring_diagnosis.tag_doctest_blocks_not_preceeded_by_new_lines_alt_2(lines)

Yields (line, True/False) pairs. True when line is a >>> without newlines before,

A code block is defined by a line that starts with `>>>` (except for optional spaces
before it. To render well through Sphinx, code blocks must be separated from earlier
text by a blank (only spaces) line.

This function diagnoses that.


# _autosummary/epythet.tools.html.md

# epythet.tools

Tools to diagnose (and sometimes, repair) documentation

### Examples

```pycon
>>> repair_package(PY_FILES_DIRECTORY)
```

If you have tec (pip install tec) installed, you can even input a module object:

```pycon
>>> import epythet
>>> number_of_problems = repair_package(epythet)
---> This is just a diagnosis: No files are being written to
setup_docsrc.py                           : #problems: 0
config_parser.py                          : #problems: 0
...
docs_gen.py                               : #problems: 0
```

As the print out header indicated, this is just a diagnosis of problems found for
each module, and returns the total number of problems.
It’s advised to do this, then have a look at the problems using

```pycon
>>> print_diagnosis(MODULE_PATH_OR_PKG_FOLDER)
```

Once you’re familiar with the problems, you can choose to do:

```pycon
>>> repair_package(PY_FILES_DIRECTORY, write_to_files=True)
```

This will attempt to repair the problems for you.

```pycon
>>> docs = '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
>>>
>>>
>>> new_docs = add_newlines_before_doctests_when_missing(docs)
>>> assert new_docs == '''
... The doctest won't render correctly if it's not preceeded by a blank line:
...
...     >>> like_this
...
... But with a blank line before a block of doctests, it's fine
...
... >>> okay
...
...     Is also detected in indentations
...
...     >>> this_doctest_is_too_close_to_text
...     SOME_OUTPUT
...     >>> but_this_is_fine
...     since it's within a doctest block
...
... '''
```

The `published_doc_diagnosis_df` gets you a pandas dataframe (requires pandas to be
installed!) that will tell you if given github `org/repo` url(s) have published
documentation and if a `docs` folder even exists (in master branch):

```pycon
>>> from epythet.tools import published_doc_diagnosis_df
>>> published_doc_diagnosis_df('https://github.com/i2mint/epythet')
                                 url                      doc_page_url  doc_page_exists  repo_has_docs_folder
0  https://github.com/i2mint/epythet  https://i2mint.github.io/epythet             True                  True
>>> published_doc_diagnosis_df([
...     'https://github.com/i2mint/epythet', 'https://github.com/myorg/myrepo',
... ])
                                 url                      doc_page_url  doc_page_exists  repo_has_docs_folder
0  https://github.com/i2mint/epythet  https://i2mint.github.io/epythet             True                  True
1    https://github.com/myorg/myrepo    https://myorg.github.io/myrepo            False                 False
```

### Functions

| `print_diagnosis`(src)   |    |
|--------------------------|----|

### epythet.tools.beginning_of_doctest(string, pos=0, endpos=9223372036854775807)

Matches zero or more characters at the beginning of the string.

### epythet.tools.blank_line(string, pos=0, endpos=9223372036854775807)

Matches zero or more characters at the beginning of the string.

### Modules

| [`docstring_diagnosis`](_autosummary/epythet.tools.docstring_diagnosis.html.md#module-epythet.tools.docstring_diagnosis)   | Tools to manipulate documentation elements                                          |
|-----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| [`published_docs`](_autosummary/epythet.tools.published_docs.html.md#module-epythet.tools.published_docs)             | Elements for a tool to setup docs and check if docs are published, and if not, why. |


# _autosummary/epythet.tools.published_docs.html.md

# epythet.tools.published_docs

Elements for a tool to setup docs and check if docs are published, and if not, why.

```pycon
>>> from epythet.tools.published_docs import published_doc_diagnosis_df
>>> published_doc_diagnosis_df('https://github.com/i2mint/epythet')
                                 url                      doc_page_url  doc_page_exists  repo_has_docs_folder
0  https://github.com/i2mint/epythet  https://i2mint.github.io/epythet             True                  True
>>> published_doc_diagnosis_df([
...     'https://github.com/i2mint/epythet', 'https://github.com/myorg/myrepo',
... ])
                                 url                      doc_page_url  doc_page_exists  repo_has_docs_folder
0  https://github.com/i2mint/epythet  https://i2mint.github.io/epythet             True                  True
1    https://github.com/myorg/myrepo    https://myorg.github.io/myrepo            False                 False
```

### Functions

| [`branch_exists`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.branch_exists)(repo_stub, branch, \*[, ...])       | Check if a branch exists in a repo.                                                                                                                                                                                                      |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`check_pages_setup`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.check_pages_setup)(repo_stub, \*[, ...])           | Diagnose the GitHub Pages setup for a single repo.                                                                                                                                                                                       |
| [`check_token_scopes`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.check_token_scopes)([token, verbose])              | Check the scopes of a GitHub token.                                                                                                                                                                                                      |
| `clog`(condition, \*args, \*\*kwargs)                                                              |                                                                                                                                                                                                                                          |
| [`commit_data`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.commit_data)(repo_stub, branch, \*[, headers])     | Retrieves data about the latest commit on a branch of a GitHub repository.                                                                                                                                                               |
| [`configure_github_pages`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.configure_github_pages)(repo_stub, \*[, ...])      | Configure or update GitHub Pages for a repo.                                                                                                                                                                                             |
| [`configure_github_pages_for_repo_stubs`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.configure_github_pages_for_repo_stubs)(repo_stubs) | Configure Pages for an iterable of repo stubs, or all repos in an organization.                                                                                                                                                          |
| [`default_branch_and_commit_sha`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.default_branch_and_commit_sha)(repo_stub, \*)      | Retrieves the default branch and current commit SHA for a given GitHub repository.                                                                                                                                                       |
| `dflt_headers`([token])                                                                            |                                                                                                                                                                                                                                          |
| [`enable_pages`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.enable_pages)(repo_stub, \*[, branch, path])       | Enable or update GitHub Pages for a repo.                                                                                                                                                                                                |
| [`ensure_branch`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.ensure_branch)(repo_stub, \*, branch[, ...])       | Ensures a branch exists.                                                                                                                                                                                                                 |
| [`github_org_and_repo`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.github_org_and_repo)(github_url)                   |                                                                                                                                                                                                                                          |
| `github_token`([env_var])                                                                          |                                                                                                                                                                                                                                          |
| `is_a_github_repo_root_url`(url)                                                                   |                                                                                                                                                                                                                                          |
| `is_valid_response`(response)                                                                      |                                                                                                                                                                                                                                          |
| [`pages_config`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.pages_config)(repo_stub)                           | Return the GitHub Pages configuration for a repo, or None if not configured.                                                                                                                                                             |
| [`published_doc_diagnosis_df`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.published_doc_diagnosis_df)([urls, url_column])    | The `published_doc_diagnosis_df` gets you a pandas dataframe (requires pandas to be installed!) that will tell you if given github `org/repo` url(s) have published documentation and if a `docs` folder even exists (in master branch). |
| [`repo_data`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.repo_data)(repo_stub, \*[, headers])               | Retrieves data about a GitHub repository.                                                                                                                                                                                                |
| [`repo_stub_from_local_dir`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.repo_stub_from_local_dir)([path])                  | Extract the `owner/repo` slug from a local git checkout's remote URL.                                                                                                                                                                    |
| `repo_stubs_for_org`(org)                                                                          |                                                                                                                                                                                                                                          |
| [`repo_url_to_docs_url`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.repo_url_to_docs_url)(repo_url)                    |                                                                                                                                                                                                                                          |
| [`repo_url_to_repo_docs_url`](_autosummary/epythet.tools.published_docs.html.md#epythet.tools.published_docs.repo_url_to_repo_docs_url)(repo_url)               |                                                                                                                                                                                                                                          |
| `table_url_to_df`(url)                                                                             |                                                                                                                                                                                                                                          |
| `token_user_info`([token, verbose])                                                                |                                                                                                                                                                                                                                          |
| `url_exists`(url)                                                                                  |                                                                                                                                                                                                                                          |
| `url_of_urls_csv_to_urls`(url[, url_column])                                                       |                                                                                                                                                                                                                                          |
| `verify_repo_access`(repo_stub, \*[, verbose])                                                     |                                                                                                                                                                                                                                          |

### epythet.tools.published_docs.branch_exists(repo_stub, branch, \*, headers=<function dflt_headers>, verbose=True)

Check if a branch exists in a repo.

* **Parameters:**
  * **repo_stub** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – A string of the form `org/repo`.
  * **branch** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The branch name to check for.
  * **headers** (`Union`[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict), [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]]) – A function that returns a dictionary of headers

### epythet.tools.published_docs.check_pages_setup(repo_stub, , expected_branch='gh-pages', expected_path='/', check_url=True)

Diagnose the GitHub Pages setup for a single repo.

Returns a dict describing the state of things:

```default
{
    'repo': 'owner/repo',
    'gh_pages_branch_exists': True/False,
    'pages_enabled': True/False,
    'source_branch': 'gh-pages' or None,
    'source_path': '/' or None,
    'correctly_configured': True/False,
    'docs_url': 'https://owner.github.io/repo',
    'docs_url_responding': True/False or None,  # None if not checked
    'diagnosis': 'A human-readable summary of what is wrong (or right).',
}
```

Works with either a `GITHUB_TOKEN` env var or an authenticated `gh` CLI.

```pycon
>>> check_pages_setup('i2mint/epythet')
```

### epythet.tools.published_docs.check_token_scopes(token=None, , verbose=True)

Check the scopes of a GitHub token.

### epythet.tools.published_docs.commit_data(repo_stub, branch, \*, headers=<function dflt_headers>)

Retrieves data about the latest commit on a branch of a GitHub repository.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.tools.published_docs.configure_github_pages(repo_stub, \*, target_branch='gh-pages', folder='/', ensure_branch_exists=True, headers=<function dflt_headers>, verbose=True)

Configure or update GitHub Pages for a repo.

### Example

```pycon
>>> configure_github_pages('i2mint/epythet')
```

### epythet.tools.published_docs.configure_github_pages_for_repo_stubs(repo_stubs)

Configure Pages for an iterable of repo stubs, or all repos in an organization.

```pycon
>>> repo_pages_status = dict(
...     configure_github_pages_for_stubs('i2mint')
... )
```

### epythet.tools.published_docs.default_branch_and_commit_sha(repo_stub, \*, headers=<function dflt_headers>)

Retrieves the default branch and current commit SHA for a given GitHub repository.

* **Parameters:**
  * **repo_stub** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The GitHub repository in “owner/repo” format.
  * **headers** (`Union`[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict), [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]]) – Headers for authentication, e.g., {‘Authorization’: ‘Bearer <token>’}.
* **Returns:**
  A dictionary containing ‘default_branch’ and ‘commit_sha’.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.tools.published_docs.enable_pages(repo_stub, , branch='gh-pages', path='/')

Enable or update GitHub Pages for a repo. Uses `gh` CLI or GITHUB_TOKEN.

This is the recommended way to programmatically set up Pages.  Unlike
`configure_github_pages` (which requires `requests` and a token), this
function works out of the box if you have `gh` installed and authenticated.

Returns the API response dict on success, or None on failure.

```pycon
>>> enable_pages('thorwhalen/denote')
```

### epythet.tools.published_docs.ensure_branch(repo_stub, \*, branch, commit_sha=None, headers=<function dflt_headers>, verbose=True)

Ensures a branch exists. Does nothing if it already does, and creates it if not.

* **Parameters:**
  * **repo_stub** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Owner and name of the GitHub repository, e.g., ‘owner/repo’.
  * **branch** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the branch to be created if it doesn’t exist
  * **commit_sha** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Commit SHA to base the new branch on. By default,
    it’s the SHA of the most recent commit of the default branch.
  * **headers** (`Union`[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict), [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]]) – Headers for authentication, e.g., {‘Authorization’: ‘Bearer <token>’}.
* **Returns:**
  Response from GitHub API as a dictionary.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.tools.published_docs.github_org_and_repo(github_url)

```pycon
>>> github_org_and_repo('https://github.com/i2mint/i2')
{'org': 'i2mint', 'repo': 'i2'}
```

### epythet.tools.published_docs.pages_config(repo_stub)

Return the GitHub Pages configuration for a repo, or None if not configured.

Returns a dict with keys like `source` (containing `branch` and `path`),
`html_url`, `build_type`, etc.  Returns `None` when Pages is not enabled.

Works with either a `GITHUB_TOKEN` env var or an authenticated `gh` CLI.

```pycon
>>> pages_config('i2mint/epythet')
{'source': {'branch': 'gh-pages', 'path': '/'}, 'html_url': '...', ...}
```

### epythet.tools.published_docs.published_doc_diagnosis_df(urls=None, url_column='url')

The `published_doc_diagnosis_df` gets you a pandas dataframe (requires pandas to be
installed!) that will tell you if given github `org/repo` url(s) have published
documentation and if a `docs` folder even exists (in master branch).

* **Parameters:**
  * **urls** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)]]) – A list of urls, a table containing urls, or a single url pointing to
    a csv where this table can be downloaded from.
  * **url_column** – When `urls` is a table, what column name contains the urls.
* **Returns:**
  A dataframe with the diagnosis

### epythet.tools.published_docs.repo_data(repo_stub, \*, headers=<function dflt_headers>)

Retrieves data about a GitHub repository.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### epythet.tools.published_docs.repo_stub_from_local_dir(path='.')

Extract the `owner/repo` slug from a local git checkout’s remote URL.

```pycon
>>> repo_stub_from_local_dir('/path/to/some/git/repo')
'owner/repo'
```

### epythet.tools.published_docs.repo_url_to_docs_url(repo_url)

```pycon
>>> repo_url_to_docs_url('https://github.com/i2mint/i2')
'https://i2mint.github.io/i2'
```

### epythet.tools.published_docs.repo_url_to_repo_docs_url(repo_url)

```pycon
>>> repo_url_to_repo_docs_url('https://github.com/i2mint/i2')
'https://github.com/i2mint/i2/tree/master/docs'
```


# _autosummary/epythet.userconfig.html.md

# epythet.userconfig

User-level defaults and parametrizable text snippets.

epythet ships opinions (what a README section for agents should say, which
humour lines introduce the “for humans” pointer, whether a missing section is a
warning or something to add). A user who wants different opinions sets them
once, outside any repository, and every project on that machine picks them up.
Two things live under the user’s config directory:

- `config.toml`: policy. The `[readme]` table decides what the
  `epythet-agentic-readme` skill does (`agentic_aspects = "warn" | "add"`,
  `humor`, `agentic_first`); the `[snippets]` table can point `dir` at
  a different snippet folder.
- `snippets/<name>.md`: text overrides. A snippet is looked up in the user’s
  folder first, then in the packaged defaults (`epythet/data/snippets`).
  `epythet snippets init` copies the packaged defaults out **once**, with a
  header recording the epythet version they came from, and never overwrites a
  file that exists; `epythet snippets diff` shows how a user’s copy differs
  from the current packaged default, so upstream changes are visible without
  ever being applied silently.

The config directory is `$EPYTHET_CONFIG_DIR`, else `$XDG_CONFIG_HOME/epythet`,
else `~/.config/epythet`: the config-side twin of
`epythet.validation.ledger.user_data_dir()`, which holds mutable data
(ledger observations) under `~/.local/share/epythet`. Skills stay prose: they
call `epythet snippets show <name>` and `epythet ai-readme-check --format json`
and let this module do the resolving.

```pycon
>>> import os, tempfile
>>> _saved = os.environ.get("EPYTHET_CONFIG_DIR")
>>> os.environ["EPYTHET_CONFIG_DIR"] = tempfile.mkdtemp()
>>> load_user_config().readme
ReadmePolicy(agentic_aspects='warn', humor=False, agentic_first=False)
>>> snippet("agentic-readme-humor").source
'packaged'
>>> written = init_snippets()
>>> snippet("agentic-readme-humor").source
'user'
>>> init_snippets()          # a second init writes nothing
[]
>>> _ = os.environ.pop("EPYTHET_CONFIG_DIR") if _saved is None else os.environ.__setitem__("EPYTHET_CONFIG_DIR", _saved)
```

### Module Attributes

| [`CONFIG_DIR_ENV`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.CONFIG_DIR_ENV)           | Environment variable overriding the whole config directory.                              |
|---------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`CONFIG_FILENAME`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.CONFIG_FILENAME)          | The policy file inside the config directory.                                             |
| [`SNIPPETS_DIRNAME`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.SNIPPETS_DIRNAME)         | The snippet folder inside the config directory (unless `[snippets] dir` says otherwise). |
| [`PACKAGED_SNIPPETS_DIR`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.PACKAGED_SNIPPETS_DIR)    | Where the packaged default snippets live.                                                |
| [`AGENTIC_ASPECTS_POLICIES`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.AGENTIC_ASPECTS_POLICIES) | What `agentic_aspects` may be.                                                           |
| [`SNIPPET_COMMANDS`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.SNIPPET_COMMANDS)         | The `epythet snippets` group, by command-line name.                                      |

### Functions

| [`config_dir`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.config_dir)()                                | `$EPYTHET_CONFIG_DIR`, else `$XDG_CONFIG_HOME/epythet`, else `~/.config/epythet`.                                            |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| [`config_path`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.config_path)()                               | The policy file: `<config dir>/config.toml`.                                                                                 |
| [`diff_snippet`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.diff_snippet)(name, \*[, user_dir])          | A unified diff from the current packaged default to the user's copy (`""` when equal).                                       |
| [`epythet_version`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.epythet_version)()                           | epythet's version: the checkout's `pyproject.toml` when running from source, else the installed metadata.                    |
| [`header_version`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.header_version)(text)                        | The epythet version recorded in a user copy's header (`""` when absent).                                                     |
| [`init_snippets`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.init_snippets)(\*[, user_dir, force, names]) | Copy the packaged defaults into the user snippet folder; returns the paths written.                                          |
| [`iter_snippets`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.iter_snippets)(\*[, user_dir])               | Every available snippet, resolved, in name order.                                                                            |
| [`load_user_config`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.load_user_config)([path])                    | Read `config.toml` (default: [`config_path()`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.config_path)); a missing file means defaults. |
| [`packaged_snippet_names`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.packaged_snippet_names)()                    | The names of the snippets epythet ships, sorted.                                                                             |
| [`pool_lines`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.pool_lines)(text)                            | The non-empty, non-comment lines of a pool snippet (one candidate per line).                                                 |
| [`snippet`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippet)(name, \*[, user_dir])               | Resolve `name`: the user's `<name>.md` wins over the packaged default.                                                       |
| [`snippet_header`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippet_header)(name, version)               | The provenance line `init` writes at the top of a user copy.                                                                 |
| [`snippet_names`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippet_names)(\*[, user_dir])               | Every snippet name available: packaged plus user-only files, sorted.                                                         |
| [`snippet_text`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippet_text)(name, \*[, user_dir])          | The effective body of `name` (header stripped).                                                                              |
| [`snippets_diff`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippets_diff)([name])                       | Show how the user's copy of a snippet differs from the current packaged default.                                             |
| [`snippets_dir`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippets_dir)([config])                      | Where user snippets are read: `[snippets] dir` if set, else `<config dir>/snippets`.                                         |
| [`snippets_init`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippets_init)(\*[, force])                  | Copy the packaged default snippets into the user snippet folder, once.                                                       |
| [`snippets_list`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippets_list)()                             | List every snippet with its source (user or packaged), provenance and status.                                                |
| [`snippets_show`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippets_show)(name)                         | Print the effective text of a snippet: the user's copy if it exists, else the packaged default.                              |
| [`snippets_table`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.snippets_table)(\*[, user_dir])              | The `epythet snippets list` output: name, source, provenance, whether modified.                                              |
| [`strip_header`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.strip_header)(text)                          | `text` without the provenance header, if it has one.                                                                         |

### Classes

| [`ReadmePolicy`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.ReadmePolicy)([agentic_aspects, humor, ...])      | The `[readme]` table: what to do about agentic aspects missing from a README.   |
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`Snippet`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.Snippet)(name, path, source, text[, copied_from]) | One resolved snippet: its text and where it came from.                          |
| [`SnippetsConfig`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.SnippetsConfig)([dir])                            | The `[snippets]` table: `dir` overrides where user snippets are read.           |
| [`UserConfig`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.UserConfig)([readme, snippets, path])             | Everything `config.toml` can say, with defaults for what it does not.           |

### Exceptions

| [`UserConfigError`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.UserConfigError)   | `config.toml` has a key epythet does not know or a value it cannot use.   |
|--------------------------------------------------------------------|---------------------------------------------------------------------------|

### epythet.userconfig.AGENTIC_ASPECTS_POLICIES *= ('warn', 'add')*

What `agentic_aspects` may be.

### epythet.userconfig.CONFIG_DIR_ENV *= 'EPYTHET_CONFIG_DIR'*

Environment variable overriding the whole config directory.

### epythet.userconfig.CONFIG_FILENAME *= 'config.toml'*

The policy file inside the config directory.

### epythet.userconfig.PACKAGED_SNIPPETS_DIR *= PosixPath('/home/runner/work/epythet/epythet/epythet/data/snippets')*

Where the packaged default snippets live.

### *class* epythet.userconfig.ReadmePolicy(agentic_aspects='warn', humor=False, agentic_first=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The `[readme]` table: what to do about agentic aspects missing from a README.

`agentic_aspects` is `"warn"` (report only; the packaged default) or
`"add"` (write or update the section). `humor` draws the “for humans”
line from the humour pool; `agentic_first` places the section right after
the README’s intro rather than at the end.

### epythet.userconfig.SNIPPETS_DIRNAME *= 'snippets'*

The snippet folder inside the config directory (unless `[snippets] dir` says otherwise).

### epythet.userconfig.SNIPPET_COMMANDS *= {'diff': <function snippets_diff>, 'init': <function snippets_init>, 'list': <function snippets_list>, 'show': <function snippets_show>}*

The `epythet snippets` group, by command-line name.

### *class* epythet.userconfig.Snippet(name, path, source, text, copied_from='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One resolved snippet: its text and where it came from.

`source` is `"user"` or `"packaged"`; `copied_from` is the epythet
version recorded in a user copy’s header (`""` for a packaged snippet or
a user file written by hand).

#### *property* body *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The text without the provenance header (what templates and pools use).

### *class* epythet.userconfig.SnippetsConfig(dir='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The `[snippets]` table: `dir` overrides where user snippets are read.

A relative `dir` is taken relative to the config directory, so the same
`config.toml` means the same folder from any shell.

### *class* epythet.userconfig.UserConfig(readme=<factory>, snippets=<factory>, path=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything `config.toml` can say, with defaults for what it does not.

#### readme_for(project_overrides=None)

The effective policy for one project: `[tool.epythet.readme]` keys override the user’s.

Committed READMEs should not depend on who ran the tool, so a project
can pin what matters for its text (`humor`, `agentic_first`) in its
`pyproject.toml`; `agentic_aspects` may be pinned too.

* **Return type:**
  [`ReadmePolicy`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.ReadmePolicy)

```pycon
>>> UserConfig().readme_for({"humor": True}).humor
True
>>> UserConfig().readme_for({"humour": True})
Traceback (most recent call last):
...
UserConfigError: unknown key(s) ['humour'] in [tool.epythet.readme]
```

#### to_dict(project_overrides=None)

A JSON-ready view (the `policy` block of `ai-readme-check --format json`).

`readme` is the effective policy after `project_overrides`; `user`
the user’s own table, `project` the overrides, `path` the config file.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### *exception* epythet.userconfig.UserConfigError

Bases: [`ConfigError`](_autosummary/epythet.config.html.md#epythet.config.ConfigError)

`config.toml` has a key epythet does not know or a value it cannot use.

### epythet.userconfig.config_dir()

`$EPYTHET_CONFIG_DIR`, else `$XDG_CONFIG_HOME/epythet`, else `~/.config/epythet`.

XDG-style on every platform, like `epythet.validation.ledger.user_data_dir()`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

```pycon
>>> _saved = os.environ.get(CONFIG_DIR_ENV)
>>> os.environ[CONFIG_DIR_ENV] = "/tmp/x"; config_dir().as_posix()
'/tmp/x'
>>> _ = os.environ.pop(CONFIG_DIR_ENV) if _saved is None else os.environ.__setitem__(CONFIG_DIR_ENV, _saved)
```

### epythet.userconfig.config_path()

The policy file: `<config dir>/config.toml`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### epythet.userconfig.diff_snippet(name, , user_dir=None)

A unified diff from the current packaged default to the user’s copy (`""` when equal).

Headers are ignored, so a freshly `init`-ed copy has no diff. A user-only
snippet (no packaged default) diffs against nothing, so every line is an
addition.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.userconfig.epythet_version()

epythet’s version: the checkout’s `pyproject.toml` when running from source, else the installed metadata.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.userconfig.header_version(text)

The epythet version recorded in a user copy’s header (`""` when absent).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> header_version(snippet_header("x", "0.2.5") + "body")
'0.2.5'
>>> header_version("no header")
''
```

### epythet.userconfig.init_snippets(, user_dir=None, force=False, names=None)

Copy the packaged defaults into the user snippet folder; returns the paths written.

A file that already exists is left alone unless `force` is true (then it
is replaced; `epythet snippets diff` first is the way to see what you lose).

* **Parameters:**
  **names** – which snippets to copy (default: all packaged)
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### epythet.userconfig.iter_snippets(, user_dir=None)

Every available snippet, resolved, in name order.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Snippet`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.Snippet)]

### epythet.userconfig.load_user_config(path=None)

Read `config.toml` (default: [`config_path()`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.config_path)); a missing file means defaults.

* **Raises:**
  [**UserConfigError**](_autosummary/epythet.userconfig.html.md#epythet.userconfig.UserConfigError) – on an unknown table or key, or an invalid value
* **Return type:**
  [`UserConfig`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.UserConfig)

### epythet.userconfig.packaged_snippet_names()

The names of the snippets epythet ships, sorted.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.userconfig.pool_lines(text)

The non-empty, non-comment lines of a pool snippet (one candidate per line).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> pool_lines("# a comment\n\nIf you are a control freak\n  If you like it \n")
['If you are a control freak', 'If you like it']
```

### epythet.userconfig.snippet(name, , user_dir=None)

Resolve `name`: the user’s `<name>.md` wins over the packaged default.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – when neither exists
* **Return type:**
  [`Snippet`](_autosummary/epythet.userconfig.html.md#epythet.userconfig.Snippet)

### epythet.userconfig.snippet_header(name, version)

The provenance line `init` writes at the top of a user copy.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.userconfig.snippet_names(, user_dir=None)

Every snippet name available: packaged plus user-only files, sorted.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.userconfig.snippet_text(name, , user_dir=None)

The effective body of `name` (header stripped).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.userconfig.snippets_diff(name='')

Show how the user’s copy of a snippet differs from the current packaged default.

Without `--name`, every user copy that differs is shown. Exit code 1 when any
difference exists, like `diff`, so scripts can tell.

* **Parameters:**
  **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – one snippet, or omitted for all

### epythet.userconfig.snippets_dir(config=None)

Where user snippets are read: `[snippets] dir` if set, else `<config dir>/snippets`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### epythet.userconfig.snippets_init(, force=False)

Copy the packaged default snippets into the user snippet folder, once.

Each copy starts with a header recording the epythet version it came from.
Existing files are never overwritten unless `--force` is given.

* **Parameters:**
  **force** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – replace existing user copies (run `diff` first to see what you lose)

### epythet.userconfig.snippets_list()

List every snippet with its source (user or packaged), provenance and status.

### epythet.userconfig.snippets_show(name)

Print the effective text of a snippet: the user’s copy if it exists, else the packaged default.

* **Parameters:**
  **name** – the snippet name (`epythet snippets list` shows them)

### epythet.userconfig.snippets_table(, user_dir=None)

The `epythet snippets list` output: name, source, provenance, whether modified.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.userconfig.strip_header(text)

`text` without the provenance header, if it has one.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/epythet.validation.build.html.md

# epythet.validation.build

Level 1: run the documentation build and turn its warning stream into findings.

This is the `backend=` seam of `epythet validate`. Levels 0 and 0.5 read
Python source and docutils doctrees and are backend-independent by
construction; only this level (and level 2, owned by WP3) touches Sphinx. A
future MkDocs backend implements the same two methods, [`SphinxBackend.versions()`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend.versions)
and [`SphinxBackend.build_warnings()`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend.build_warnings), and inherits the whole ledger.

Two Sphinx facts shape the invocation. Since Sphinx 8.1 `-W` runs the whole
build and exits 1 if any warning occurred; `--keep-going` is still passed
because epythet’s Sphinx floor predates 8.1, where `-W` alone stops at the
first warning (it is a no-op on newer versions). Since Sphinx 8.0
`show_warning_types` defaults on, which suffixes every warning with
`[docutils]`-style types; that suffix is what the ledger’s `build-warning`
rules match on, because Sphinx still has no structured warning output.

### Module Attributes

| [`NO_DOCSRC`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.NO_DOCSRC)          | `BuildResult.returncode` when there is no Sphinx source directory to build.                                                          |
|---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| [`WARNINGS_ONLY_EXIT`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.WARNINGS_ONLY_EXIT) | Sphinx's exit status when the only problem was warnings under `-W`.                                                                  |
| [`WARNING_LINE_RE`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.WARNING_LINE_RE)    | `path:docstring of obj:3: WARNING: message [type]` and the simpler `path:12: WARNING: message [type]` and `WARNING: message` shapes. |
| [`RENDER_BUILDERS`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.RENDER_BUILDERS)    | HTML for links and images, text for snapshots, XML for structure (research §5.4: text and xml are complementary).                    |

### Functions

| [`classify_warning`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.classify_warning)(warning, ledger)                 | The first build-warning rule that matches, most specific first.                           |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`default_sphinx_build`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.default_sphinx_build)()                            | `python -m sphinx` when Sphinx is importable here, else `sphinx-build` on PATH.           |
| [`parse_warning_line`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.parse_warning_line)(line, \*[, project_dir])       | Parse one warning line; `None` when the line is not a warning.                            |
| [`parse_warning_stream`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.parse_warning_stream)(text, \*[, project_dir])     | Every warning in a `-w` warnings file or a build log.                                     |
| [`run_build_level`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.run_build_level)(project_dir, ledger, \*, backend) | Level 1: build, classify warnings, and report a crashed build as a finding.               |
| [`warnings_to_findings`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.warnings_to_findings)(warnings, ledger)            | Map each warning to a ledger finding (unclassified warnings keep their type as the rule). |

### Classes

| [`BuildBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildBackend)(\*args, \*\*kwargs)              | What the `backend=` seam requires: a name, versions, and the warning stream.     |
|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`BuildResult`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildResult)(returncode[, warnings, log, ...]) | What one build produced: exit status, parsed warnings, and the raw log.          |
| [`BuildWarning`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildWarning)(severity, message[, type, ...])  | One parsed line of the Sphinx warning stream.                                    |
| [`RenderBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.RenderBackend)(\*args, \*\*kwargs)             | A backend that can also render several builders into a kept directory (level 2). |
| [`RenderResult`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.RenderResult)([outdirs, returncodes, ...])     | What a multi-builder render produced: one output directory per builder.          |
| [`SphinxBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend)([sphinx_build, docsrc, ...])    | The default (and only shipped) backend: `sphinx-build -b html -W`.               |

### *class* epythet.validation.build.BuildBackend(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

What the `backend=` seam requires: a name, versions, and the warning stream.

[`SphinxBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend) is the shipped implementation; a MkDocs backend
implements the same two methods and inherits the whole ledger. Level 2
additionally needs [`RenderBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.RenderBackend).

### *class* epythet.validation.build.BuildResult(returncode, warnings=<factory>, log='', outdir=None, command=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one build produced: exit status, parsed warnings, and the raw log.

### *class* epythet.validation.build.BuildWarning(severity, message, type=None, file=None, line=None, object=None, raw='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One parsed line of the Sphinx warning stream.

### epythet.validation.build.NO_DOCSRC *= -1*

`BuildResult.returncode` when there is no Sphinx source directory to build.

### epythet.validation.build.RENDER_BUILDERS *= ('html', 'text', 'xml')*

HTML for links and images, text for snapshots,
XML for structure (research §5.4: text and xml are complementary).

* **Type:**
  The builders level 2 reads

### *class* epythet.validation.build.RenderBackend(\*args, \*\*kwargs)

Bases: [`BuildBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildBackend), [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

A backend that can also render several builders into a kept directory (level 2).

### *class* epythet.validation.build.RenderResult(outdirs=<factory>, returncodes=<factory>, warnings=<factory>, log='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a multi-builder render produced: one output directory per builder.

`outdirs` maps a builder name (`html`, `text`, `xml`) to the
directory holding its pages; a builder that failed is absent from it and
its exit status is in `returncodes`. `warnings` is the parsed warning
stream of the first builder (the others repeat it).

#### *property* ok *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether every builder exited 0 or with warnings only.

### *class* epythet.validation.build.SphinxBackend(sphinx_build=None, docsrc=None, outdir=None, builder='html', nitpicky=False, name='sphinx')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The default (and only shipped) backend: `sphinx-build -b html -W`.

`docsrc` defaults to `<project>/docsrc`, the directory epythet
generates. `outdir` defaults to a temporary directory so validation
never litters the repository.

#### build_warnings(project_dir)

Run the build and parse its warnings; never raises on a failed build.

Without `outdir` the build goes to a temporary directory that is
removed before returning; only the parsed warnings and the log survive.

* **Return type:**
  [`BuildResult`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildResult)

#### render(project_dir, , builders=('html', 'text', 'xml'), outdir)

Build every builder in `builders` into `outdir/<builder>` (level 2).

Unlike [`build_warnings()`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend.build_warnings), the output is kept: level 2 reads it, and
level 3 packs it for review. The caller owns `outdir`.

* **Return type:**
  [`RenderResult`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.RenderResult)

#### resolve_docsrc(project_dir)

The Sphinx source directory, or `None` when there is none to build.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### versions()

`{"sphinx": ..., "docutils": ...}` as importable here (`None` if not).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### epythet.validation.build.WARNINGS_ONLY_EXIT *= 1*

Sphinx’s exit status when the only problem was warnings under `-W`.

### epythet.validation.build.WARNING_LINE_RE *= re.compile('^(?:(?P<loc>.\*?):\\\\s\*)?(?P<sev>WARNING|ERROR|SEVERE|CRITICAL): (?P<msg>.\*?)(?: \\\\[(?P<type>[\\\\w.\\\\-]+)\\\\])?\\\\s\*$')*

`path:docstring of obj:3: WARNING: message [type]` and the simpler
`path:12: WARNING: message [type]` and `WARNING: message` shapes.

### epythet.validation.build.classify_warning(warning, ledger)

The first build-warning rule that matches, most specific first.

* **Return type:**
  `Rule` | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.validation.build.default_sphinx_build()

`python -m sphinx` when Sphinx is importable here, else `sphinx-build` on PATH.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.validation.build.parse_warning_line(line, , project_dir=None)

Parse one warning line; `None` when the line is not a warning.

* **Return type:**
  [`BuildWarning`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildWarning) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> w = parse_warning_line("/p/dol/base.py:docstring of dol.base.Store:7: WARNING: Inline emphasis start-string without end-string. [docutils]")
>>> (w.file, w.object, w.line, w.type, w.severity)
('/p/dol/base.py', 'dol.base.Store', 7, 'docutils', 'warning')
>>> parse_warning_line("reading sources... [ 10%] index") is None
True
```

### epythet.validation.build.parse_warning_stream(text, , project_dir=None)

Every warning in a `-w` warnings file or a build log.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`BuildWarning`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.BuildWarning)]

### epythet.validation.build.run_build_level(project_dir, ledger, , backend)

Level 1: build, classify warnings, and report a crashed build as a finding.

A missing `docsrc/` is a `NO_DOCSRC` warning, not an error: the fleet
plan deletes committed `docsrc/` directories, and “nothing to build”
must not gate a package whose docstrings are clean.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### epythet.validation.build.warnings_to_findings(warnings, ledger)

Map each warning to a ledger finding (unclassified warnings keep their type as the rule).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]


# _autosummary/epythet.validation.cli.html.md

# epythet.validation.cli

The `epythet validate` command: the CLI adapter over `epythet.validation.validate()`.

This is the only place that prints, and the only place that turns a report
into a process exit code. `epythet.cli` appends [`validate()`](_autosummary/epythet.validation.cli.html.md#epythet.validation.cli.validate) to its
command list; `python -m epythet.validation` dispatches it on its own.

### Functions

| [`validate`](_autosummary/epythet.validation.cli.html.md#epythet.validation.cli.validate)(package, \*[, level, format, ...])   | Check a package's docstrings for rendering artifacts and build problems.   |
|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|

### epythet.validation.cli.validate(package, , level=1, format='table', fail_on='error', ledger=None, style='google', no_napoleon=False, ignore=None, docsrc=None, no_observe=False, no_linters=False, max_per_rule=10, output=None, snapshot=False, update_snapshots=False, snapshot_dir=None, render_dir=None, review_pages='changed', review_sample=8, screenshots=False, packet_dir=None, review_reply=None, fail_on_review=False)

Check a package’s docstrings for rendering artifacts and build problems.

Exit codes: 0 clean; 10/11/12/13 findings at or above –fail-on at level
0 (lint) / 0.5 (parse) / 1 (build) / 2 (render); 14 review findings, only
with –fail-on-review; 20 ledger integrity failure; 1 internal error.

* **Parameters:**
  * **package** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Project root, package directory, or importable package name.
  * **level** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – 0 = lint (ruff D, pydoclint); 1 = lint + parse every docstring’s
    doctree (default, no build needed); 2 = also run the Sphinx build;
    3 = also read the rendered XML/HTML/text; 4 = also write a review packet.
  * **format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – table (human), json (full report), or jsonl (one finding per line).
  * **fail_on** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Severity that makes the exit code non-zero: error, warning, or info.
  * **ledger** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Directory of extra rule YAML files overlaid on the bundled ledger.
  * **style** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Docstring convention for the linters: google, numpy, or sphinx.
  * **no_napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Parse docstrings without napoleon’s Google/NumPy pre-processing.
  * **ignore** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Skip files whose path contains any of these strings (several after one -i, or -i repeated).
  * **docsrc** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Sphinx source directory for level 2 (default: <project>/docsrc).
  * **no_observe** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Do not append findings to the ledger’s observations file.
  * **no_linters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 0 without ruff and pydoclint (coverage detectors only).
  * **max_per_rule** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – How many findings to show per rule in the table.
  * **output** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Write the report to this file instead of stdout.
  * **snapshot** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 3: diff the text render against docsrc/_snapshots/text.
  * **update_snapshots** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 3: rewrite the text snapshots from this render.
  * **snapshot_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Where the text snapshots live (default docsrc/_snapshots/text).
  * **render_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Keep the rendered html/text/xml here instead of a temp dir.
  * **review_pages** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Level 4: which pages go in the packet: changed, sample, or all.
  * **review_sample** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Level 4: how many pages a sample packet holds.
  * **screenshots** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 4: add Playwright screenshots to the packet if installed.
  * **packet_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Level 4: write the packet here (default: the user data dir).
  * **review_reply** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Level 4: a review.json written by a reviewer, to ingest.
  * **fail_on_review** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Exit 14 when the review reply reported findings.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/epythet.validation.core.html.md

# epythet.validation.core

The `validate` orchestrator: resolve the package, run the levels, build the report.

This module is the single source of truth every surface calls. It returns a
[`Report`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Report) (JSON-able via `to_dict`), never
prints and never exits, so the CLI, a future MCP tool or an HTTP endpoint all
wrap the same function.

Seams (one keyword argument each, as in decision D8):

- `backend=` — the build backend for level 1; defaults to
  [`SphinxBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend). Levels 0 and 0.5 never
  > touch it.
- `ledger=` — the rule catalog; `None` is the bundled ledger, a directory
  is a package-local overlay.

Level 2 (render) reuses the backend through its `render` method (a
[`RenderBackend`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.RenderBackend)) and level 3 (review) reads
what level 2 rendered, so a tier-4 run builds exactly once.

### Functions

| [`resolve_package`](_autosummary/epythet.validation.core.html.md#epythet.validation.core.resolve_package)(package)                    | Turn `<package_dir_or_import_name>` into a [`ResolvedPackage`](_autosummary/epythet.validation.core.html.md#epythet.validation.core.ResolvedPackage).   |
|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| [`validate`](_autosummary/epythet.validation.core.html.md#epythet.validation.core.validate)(package, \*[, level, levels, ...]) | Validate a package's documentation and return a `Report`.                                                      |

### Classes

| [`ResolvedPackage`](_autosummary/epythet.validation.core.html.md#epythet.validation.core.ResolvedPackage)(name, package_dir, project_dir)   | Where the package's source lives and which project it belongs to.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|

### *class* epythet.validation.core.ResolvedPackage(name, package_dir, project_dir, version=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where the package’s source lives and which project it belongs to.

### epythet.validation.core.resolve_package(package)

Turn `<package_dir_or_import_name>` into a [`ResolvedPackage`](_autosummary/epythet.validation.core.html.md#epythet.validation.core.ResolvedPackage).

Accepts a project root (contains `pyproject.toml`/`setup.cfg`), a
package directory (contains `__init__.py`) or an importable name.

* **Return type:**
  [`ResolvedPackage`](_autosummary/epythet.validation.core.html.md#epythet.validation.core.ResolvedPackage)

### epythet.validation.core.validate(package, , level=1, levels=None, ledger=None, backend=None, fail_on='error', napoleon=True, style='google', ignore=(), observe=True, observations_path=None, linters=True, snapshot=False, update_snapshots=False, snapshot_dir=None, render_dir=None, review_pages='changed', review_sample=8, screenshots=False, packet_dir=None, review_reply=None)

Validate a package’s documentation and return a `Report`.

* **Parameters:**
  * **package** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – A project root, a package directory, or an importable name.
  * **level** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – The CLI tier: `0` lint only, `1` lint + parse (default),
    `2` adds the Sphinx build, `3` the rendered-output checks,
    `4` the review packet.
  * **levels** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]]) – An explicit set of levels (`[0.5]` for a parse-only sweep);
    overrides `level` when given.
  * **ledger** (`Ledger` | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – `None` for the bundled rules, or a directory overlay.
  * **backend** – The build backend for level 1 (default: `SphinxBackend()`).
  * **fail_on** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Severity threshold recorded on the report for exit codes.
  * **napoleon** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Pre-process Google/NumPy sections the way the fleet’s
    `conf.py` does. Set `False` for a package built without napoleon.
  * **style** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Docstring convention passed to ruff and pydoclint.
  * **ignore** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Path substrings to skip, as `epythet quickstart --ignore`.
  * **observe** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Append findings to the observations JSONL (outside the repo).
  * **observations_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override the observations file (tests use this).
  * **linters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 0: shell out to ruff and pydoclint (`False` keeps the
    coverage detectors only; the fleet sweep uses it).
  * **snapshot** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 2: diff the `-b text` render against the stored snapshots.
  * **update_snapshots** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 2: re-baseline the snapshots instead of diffing.
  * **snapshot_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Where snapshots live (default `<docsrc>/_snapshots/text`).
  * **render_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Keep level 2’s rendered output here instead of a temp dir.
  * **review_pages** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Level 3: `changed` (against the snapshot, else a
    sample), `sample` or `all` pages into the packet.
  * **review_sample** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – How many pages `sample` takes.
  * **screenshots** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Level 3: add Playwright screenshots when it is installed.
  * **packet_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Level 3: write the packet here instead of the user data dir.
  * **review_reply** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Level 3: a `review.json` to ingest as level-3 findings.
* **Return type:**
  [`Report`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Report)


# _autosummary/epythet.validation.coverage.html.md

# epythet.validation.coverage

Level 0 coverage and quality smells: the queue signals, computed from the `ast` alone.

The doc-quality research (`research_doc_quality.md` §2, §6) defines what
the fleet sweep should *queue*, as opposed to gate: a public callable with
no docstring, an entry point with no runnable example, a summary that only
restates the name, a parameter description that only restates the type, a
summary written as meta-language (“This function…”). None of these needs
ruff or pydoclint, so the sweep can run them on a checkout that has neither;
none of them changes the exit code unless `--fail-on info` asks for it.

The public surface follows the R1 decision: `__all__` is honoured where
present, otherwise every non-underscore name; *entry points* are the names
the package’s `__init__` binds (its `__all__`, else what it defines and
imports), and only those owe an example.

Each detector is a function `PublicObject -> list[str]` registered under
the name a `coverage`-kind ledger rule refers to. The two text heuristics
are the research’s, verbatim:

- trivial summary: split the identifier on `snake_case`/`camelCase`,
  split the summary on whitespace, strip stop words, crude lemmatisation;
  flag when the summary’s content words are a subset of the name’s;
- type restatement: flag a parameter description whose content words are a
  subset of the annotation’s tokens (`n: int` described as “an integer”).

```pycon
>>> trivial_summary_words("load_config", "Load the config.")
True
>>> trivial_summary_words("load_config", "Read pyproject.toml and setup.cfg into a DocsConfig.")
False
```

### Functions

| [`annotation_words`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.annotation_words)(annotation)                       | Words a reader could use to restate an annotation: `list[int]` -> int, list, integer...             |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| [`content_words`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.content_words)(text)                                | Content words of prose: lower-cased, stop words out, crudely lemmatised.                            |
| [`coverage_detector`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.coverage_detector)(name)                            | Register a coverage detector under the name a rule's `detector.function` uses.                      |
| [`entry_point_names`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.entry_point_names)(package_dir)                     | Names the package's `__init__` exposes: `__all__`, else what it binds without a leading underscore. |
| [`entry_point_without_example`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.entry_point_without_example)(obj)                   | An entry point (bound by the package `__init__`) whose docstring has no `>>>` (DQ002).              |
| [`evaluate_coverage_rule`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.evaluate_coverage_rule)(rule, obj)                  | Run one `coverage`-kind rule over one public object.                                                |
| [`iter_coverage_cases`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.iter_coverage_cases)(fixture_path)                  | The tagged specimens of a coverage fixture (a specimen may have no docstring at all).               |
| [`iter_public_objects`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.iter_public_objects)(package_dir, \*[, ...])        | Every public module, class and function under `package_dir`.                                        |
| [`meta_language_summary`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.meta_language_summary)(obj)                         | A summary that talks about the object instead of saying what it does (DQ005).                       |
| [`missing_docstring`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.missing_docstring)(obj)                             | A public module, class or function with no docstring at all (DQ001).                                |
| [`name_words`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.name_words)(identifier)                             | Content words of an identifier: `load_config` -> `{"load", "config"}`.                              |
| [`param_descriptions`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.param_descriptions)(docstring)                      | `{name: description}` from an RST, Google or NumPy docstring, first line plus continuations.        |
| [`restates_type`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.restates_type)(param)                               | Whether a parameter's description only restates its annotation.                                     |
| [`run_coverage_level`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.run_coverage_level)(package_dir, ledger, \*[, ...]) | Level 0 coverage: `(findings, objects_checked, objects_undocumented)`.                              |
| [`trivial_summary`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.trivial_summary)(obj)                               | A summary whose content words all come from the object's name (DQ003).                              |
| [`trivial_summary_words`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.trivial_summary_words)(identifier, summary)         | Whether the summary's content words are all in the identifier's (the *lazy* smell).                 |
| [`type_restatement`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.type_restatement)(obj)                              | A parameter description that only restates the annotation (DQ004).                                  |

### Classes

| [`CoverageCase`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.CoverageCase)(name, line, expect_hit, ...)     | One tagged specimen of a coverage fixture: the object and what the tag promises.   |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`Param`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.Param)(name[, annotation, description])        | A signature parameter and, if the docstring describes it, that description.        |
| [`PublicObject`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.PublicObject)(qualname, kind, file, line, ...) | One public module, class or function, with what a detector needs to judge it.      |

### *class* epythet.validation.coverage.CoverageCase(name, line, expect_hit, rule_ids, object)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One tagged specimen of a coverage fixture: the object and what the tag promises.

### *class* epythet.validation.coverage.Param(name, annotation=None, description=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A signature parameter and, if the docstring describes it, that description.

### *class* epythet.validation.coverage.PublicObject(qualname, kind, file, line, docstring, params=<factory>, is_entry_point=False, name='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One public module, class or function, with what a detector needs to judge it.

#### *property* summary *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The first non-blank line of the docstring, or `""`.

### epythet.validation.coverage.annotation_words(annotation)

Words a reader could use to restate an annotation: `list[int]` -> int, list, integer…

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.content_words(text)

Content words of prose: lower-cased, stop words out, crudely lemmatised.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.coverage_detector(name)

Register a coverage detector under the name a rule’s `detector.function` uses.

### epythet.validation.coverage.entry_point_names(package_dir)

Names the package’s `__init__` exposes: `__all__`, else what it binds without a leading underscore.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.entry_point_without_example(obj)

An entry point (bound by the package `__init__`) whose docstring has no `>>>` (DQ002).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.evaluate_coverage_rule(rule, obj)

Run one `coverage`-kind rule over one public object.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.iter_coverage_cases(fixture_path)

The tagged specimens of a coverage fixture (a specimen may have no docstring at all).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`CoverageCase`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.CoverageCase)]

### epythet.validation.coverage.iter_public_objects(package_dir, , ignore=(), files=None, all_entry_points=False)

Every public module, class and function under `package_dir`.

Public means no leading underscore anywhere in the dotted name below the
package; a module’s `__all__`, when present, narrows its public names.
`all_entry_points` treats every top-level name as an entry point (rule
fixtures use it: they have no package `__init__`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`PublicObject`](_autosummary/epythet.validation.coverage.html.md#epythet.validation.coverage.PublicObject)]

### epythet.validation.coverage.meta_language_summary(obj)

A summary that talks about the object instead of saying what it does (DQ005).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.missing_docstring(obj)

A public module, class or function with no docstring at all (DQ001).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.name_words(identifier)

Content words of an identifier: `load_config` -> `{"load", "config"}`.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> sorted(name_words("DocsConfig")), sorted(name_words("mk_parser"))
(['config', 'doc'], ['mk', 'parser'])
```

### epythet.validation.coverage.param_descriptions(docstring)

`{name: description}` from an RST, Google or NumPy docstring, first line plus continuations.

Deliberately not delegated to `docstring_parser` (an optional extra):
a detector’s verdict must not depend on what is installed.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> param_descriptions(":param n: how many\n    retries\n:param delay: seconds")
{'n': 'how many retries', 'delay': 'seconds'}
>>> param_descriptions("Args:\n    n (int): how many\n    delay: seconds\n\nReturns:\n    x")
{'n': 'how many', 'delay': 'seconds'}
>>> param_descriptions("Parameters\n----------\nn : int\n    how many\ndelay\n    seconds\n\nReturns\n-------")
{'n': 'how many', 'delay': 'seconds'}
```

### epythet.validation.coverage.restates_type(param)

Whether a parameter’s description only restates its annotation.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> restates_type(Param("n", "int", "an integer"))
True
>>> restates_type(Param("n", "int", "how many retries before giving up"))
False
>>> restates_type(Param("n", None, "an integer"))
False
```

### epythet.validation.coverage.run_coverage_level(package_dir, ledger, , ignore=())

Level 0 coverage: `(findings, objects_checked, objects_undocumented)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### epythet.validation.coverage.trivial_summary(obj)

A summary whose content words all come from the object’s name (DQ003).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.coverage.trivial_summary_words(identifier, summary)

Whether the summary’s content words are all in the identifier’s (the *lazy* smell).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### epythet.validation.coverage.type_restatement(obj)

A parameter description that only restates the annotation (DQ004).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/epythet.validation.detectors.html.md

# epythet.validation.detectors

Named doctree detectors, referenced from ledger rules by `detector.function`.

Each detector takes a [`ParsedDocstring`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.ParsedDocstring) and
returns a list of evidence strings, one per hit (an empty list means the rule
does not fire). They read the docutils doctree rather than rendered HTML so
that the rules keep working under any Sphinx theme, and under a MkDocs
backend, exactly as decided in D8.

The detectors are the measured prototype from the validation research
(`detect2.py` and `refine.py`), ported one to one; the refinements that
took false positives from 2/14 to 1/14 on the control set are marked inline.

### Module Attributes

| [`DETECTORS`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.DETECTORS)         | Registry filled by [`detector()`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.detector); the ledger loader validates against it.   |
|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`NAPOLEON_SECTIONS`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.NAPOLEON_SECTIONS) | Section names napoleon recognises (Google style), lower-cased.                                                            |
| [`NEAR_SECTIONS`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.NEAR_SECTIONS)     | Common misspellings / near-misses of section names that napoleon ignores.                                                 |

### Functions

| [`blockquote_with_unexpected_indent`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.blockquote_with_unexpected_indent)(parsed)   | A block quote *and* an `Unexpected indentation` message: stray indent (DR016).       |
|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`collapsed_table`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.collapsed_table)(parsed)                     | A simple-table border in the source with no `table` node in the tree (DR019).        |
| [`detector`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.detector)(name)                              | Register a detector under `name` (the name used in rule YAML).                       |
| [`directive_content_lost`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.directive_content_lost)(parsed)              | A directive is written in the source but produced no content node (DR018).           |
| [`markdown_fence_literal`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.markdown_fence_literal)(parsed)              | A Markdown fence collapsed into an inline literal (DR006).                           |
| [`mixed_bullet_markers`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.mixed_bullet_markers)(parsed)                | Two adjacent sibling bullet lists: the marker character changed mid-list (DR022).    |
| [`near_miss_section_term`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.near_miss_section_term)(parsed)              | A definition-list term that is a misspelt section name (DR013).                      |
| [`param_definition_list`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.param_definition_list)(parsed)               | A top-level definition list of single-word terms: probably a parameter list (DR017). |
| [`problematic_nodes`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.problematic_nodes)(parsed)                   | Unbalanced inline markup: every `problematic` node in the tree (DR010).              |
| [`prose_definition_term`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.prose_definition_term)(parsed)               | A definition-list term of three or more words: prose eaten by indentation (DR014).   |
| [`section_as_definition_list`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.section_as_definition_list)(parsed)          | A Google section (`Args:`) became a definition-list term: napoleon is off (DR012).   |
| [`system_messages`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.system_messages)(parsed)                     | Every message docutils reported while parsing the docstring (DR032).                 |
| [`title_references`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.title_references)(parsed)                    | Single backticks parsed as a title reference, i.e. italics not code (DR011).         |

### epythet.validation.detectors.DETECTORS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), Detector]* *= {'blockquote_with_unexpected_indent': <function blockquote_with_unexpected_indent>, 'collapsed_table': <function collapsed_table>, 'directive_content_lost': <function directive_content_lost>, 'markdown_fence_literal': <function markdown_fence_literal>, 'mixed_bullet_markers': <function mixed_bullet_markers>, 'near_miss_section_term': <function near_miss_section_term>, 'param_definition_list': <function param_definition_list>, 'problematic_nodes': <function problematic_nodes>, 'prose_definition_term': <function prose_definition_term>, 'section_as_definition_list': <function section_as_definition_list>, 'system_messages': <function system_messages>, 'title_references': <function title_references>}*

Registry filled by [`detector()`](_autosummary/epythet.validation.detectors.html.md#epythet.validation.detectors.detector); the ledger loader validates against it.

### epythet.validation.detectors.NAPOLEON_SECTIONS *= frozenset({'args', 'arguments', 'attention', 'attributes', 'caution', 'danger', 'error', 'example', 'examples', 'hint', 'important', 'keyword args', 'keyword arguments', 'methods', 'note', 'notes', 'other parameters', 'parameters', 'raise', 'raises', 'receive', 'receives', 'references', 'return', 'returns', 'see also', 'tip', 'todo', 'warning', 'warnings', 'warns', 'yield', 'yields'})*

Section names napoleon recognises (Google style), lower-cased.

### epythet.validation.detectors.NEAR_SECTIONS *= frozenset({'arg', 'argument', 'attribute', 'exception', 'exceptions', 'exemple', 'exemples', 'kwarg', 'kwargs', 'param', 'parameter', 'params', 'rasies', 'retruns', 'returnss', 'yeilds'})*

Common misspellings / near-misses of section names that napoleon ignores.

### epythet.validation.detectors.blockquote_with_unexpected_indent(parsed)

A block quote *and* an `Unexpected indentation` message: stray indent (DR016).

The message gate is what makes this reliable; a block quote alone is how a
legitimate quotation is written (research §3.3).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.collapsed_table(parsed)

A simple-table border in the source with no `table` node in the tree (DR019).

Refinement from the research: the border line must contain two or more
whitespace-separated runs of `=`, otherwise a section underline matches.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.detector(name)

Register a detector under `name` (the name used in rule YAML).

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`ParsedDocstring`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.ParsedDocstring)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]], [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`ParsedDocstring`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.ParsedDocstring)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]]

### epythet.validation.detectors.directive_content_lost(parsed)

A directive is written in the source but produced no content node (DR018).

With stub directives registered, an unknown directive name (`.. codeblock::`)
or a body that docutils rejected leaves only a system message behind; the
content the author wrote is gone from the page.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.markdown_fence_literal(parsed)

A Markdown fence collapsed into an inline literal (DR006).

Refinement from the research: a triple-backtick fence parses as an inline
`literal` whose text starts or ends with a backtick, because two of the
fence characters are consumed as the literal’s delimiters. Testing for
three backticks in the paragraph text does not work.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.mixed_bullet_markers(parsed)

Two adjacent sibling bullet lists: the marker character changed mid-list (DR022).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.near_miss_section_term(parsed)

A definition-list term that is a misspelt section name (DR013).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.param_definition_list(parsed)

A top-level definition list of single-word terms: probably a parameter list (DR017).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.problematic_nodes(parsed)

Unbalanced inline markup: every `problematic` node in the tree (DR010).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.prose_definition_term(parsed)

A definition-list term of three or more words: prose eaten by indentation (DR014).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.section_as_definition_list(parsed)

A Google section (`Args:`) became a definition-list term: napoleon is off (DR012).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.system_messages(parsed)

Every message docutils reported while parsing the docstring (DR032).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.detectors.title_references(parsed)

Single backticks parsed as a title reference, i.e. italics not code (DR011).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/epythet.validation.docstrings.html.md

# epythet.validation.docstrings

Docstring extraction from Python source, without importing anything.

Every level below the build reads docstrings straight from the `ast`, so the
package under validation never has to be importable (and never runs). What
autodoc would see is approximated with [`inspect.cleandoc()`](https://docs.python.org/3/library/inspect.html#inspect.cleandoc), which is
what Sphinx’s `prepare_docstring` does modulo tab expansion.

Each [`Docstring`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.Docstring) carries both the *processed* text (what Python hands
to Sphinx) and the literal’s *source segment* (what the author typed), because
one seed rule (DR020, backslashes eaten by a non-raw string) is only visible
in the latter.

### Functions

| [`count_public_objects`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.count_public_objects)(package_dir, \*[, ignore])   | Count public modules, classes and functions, and those without a docstring.                 |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| [`is_ignored`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.is_ignored)(path, ignore)                          | Whether `path` matches the `--ignore` list: any token is a substring of its POSIX form.     |
| [`iter_docstrings`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.iter_docstrings)(package_dir, \*[, ignore, ...])   | Yield every docstring in a package directory tree.                                          |
| [`iter_file_docstrings`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.iter_file_docstrings)(path, \*[, root, on_skip])   | Yield the module, class and function docstrings of one file, in source order.               |
| [`iter_python_files`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.iter_python_files)(package_dir, \*[, ignore])      | Every `.py` in the package tree, skipping caches, non-package dirs and `ignore` substrings. |

### Classes

| [`Coverage`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.Coverage)([checked, undocumented])              | How many public objects were seen and how many lack a docstring.   |
|-------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`Docstring`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.Docstring)(file, line, def_line, qualname, ...) | One docstring and where it came from.                              |

### *class* epythet.validation.docstrings.Coverage(checked=0, undocumented=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How many public objects were seen and how many lack a docstring.

### *class* epythet.validation.docstrings.Docstring(file, line, def_line, qualname, kind, text, source, is_raw)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One docstring and where it came from.

#### *property* lines *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

The processed text, split into lines.

### epythet.validation.docstrings.count_public_objects(package_dir, , ignore=())

Count public modules, classes and functions, and those without a docstring.

“Public” means no leading underscore anywhere in the dotted name below the
package. Nested functions are counted like any other def.

* **Return type:**
  [`Coverage`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.Coverage)

### epythet.validation.docstrings.is_ignored(path, ignore)

Whether `path` matches the `--ignore` list: any token is a substring of its POSIX form.

The one predicate every level uses, so a file the parse level skips is
also absent from the lint, coverage and repair results.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_ignored("/p/pkg/tests/test_x.py", ["tests/"]), is_ignored("/p/pkg/x.py", ["tests/"])
(True, False)
```

### epythet.validation.docstrings.iter_docstrings(package_dir, , ignore=(), on_skip=None)

Yield every docstring in a package directory tree.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Docstring`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.Docstring)]

### epythet.validation.docstrings.iter_file_docstrings(path, , root=None, on_skip=None)

Yield the module, class and function docstrings of one file, in source order.

A file that does not parse or decode is skipped; `on_skip(path, reason)`
is called so the caller can report it (a syntax error is the linter’s
business, but silence would hide a docstring from the ledger).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Docstring`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.Docstring)]

### epythet.validation.docstrings.iter_python_files(package_dir, , ignore=())

Every `.py` in the package tree, skipping caches, non-package dirs and `ignore` substrings.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]


# _autosummary/epythet.validation.html.md

# epythet.validation

`epythet validate`: tiered documentation validation with a growing artifact ledger.

Levels 0 (lint and coverage), 0.5 (parse), 1 (build), 2 (render) and 3
(review packet) live here. The ledger of known rendering artifacts is under
`epythet/ledger/rules` (one YAML per rule with a sibling `.py` fixture);
observations from real runs are appended outside the repository, and rules a
reviewer proposes go to an overlay there too (`epythet ledger propose`).

```pycon
>>> from epythet.validation import validate
>>> report = validate("path/to/project", level=1)
>>> report.exit_code()
0
```

### Modules

| [`build`](_autosummary/epythet.validation.build.html.md#module-epythet.validation.build)                                 | Level 1: run the documentation build and turn its warning stream into findings.        |
|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
| [`cli`](_autosummary/epythet.validation.cli.html.md#module-epythet.validation.cli)                                     | The `epythet validate` command: the CLI adapter over `epythet.validation.validate()`.  |
| [`core`](_autosummary/epythet.validation.core.html.md#module-epythet.validation.core)                                   | The `validate` orchestrator: resolve the package, run the levels, build the report.    |
| [`coverage`](_autosummary/epythet.validation.coverage.html.md#module-epythet.validation.coverage)                           | Level 0 coverage and quality smells: the queue signals, computed from the `ast` alone. |
| [`detectors`](_autosummary/epythet.validation.detectors.html.md#module-epythet.validation.detectors)                         | Named doctree detectors, referenced from ledger rules by `detector.function`.          |
| [`docstrings`](_autosummary/epythet.validation.docstrings.html.md#module-epythet.validation.docstrings)                       | Docstring extraction from Python source, without importing anything.                   |
| [`lint`](_autosummary/epythet.validation.lint.html.md#module-epythet.validation.lint)                                   | Level 0: static docstring linters, normalised into the finding model.                  |
| [`model`](_autosummary/epythet.validation.model.html.md#module-epythet.validation.model)                                 | The finding and report model shared by every level of `epythet validate`.              |
| [`parse`](_autosummary/epythet.validation.parse.html.md#module-epythet.validation.parse)                                 | Level 0.5: parse each docstring's docutils doctree and run the ledger's detectors.     |
| [`propose`](_autosummary/epythet.validation.propose.html.md#epythet.validation.propose)(reply_path, \*[, overlay, ledger]) | Write every `proposed_rules` entry of a reply into `overlay` as a proposed rule.       |
| [`render`](_autosummary/epythet.validation.render.html.md#epythet.validation.render)(report[, format])                    | Render with the named format (`table`, `json` or `jsonl`).                             |
| [`rendered`](_autosummary/epythet.validation.rendered.html.md#module-epythet.validation.rendered)                           | Level 2: read the *rendered* output (XML, HTML, text) and report what the build hid.   |
| [`review`](_autosummary/epythet.validation.review.html.md#module-epythet.validation.review)                               | Level 3: a review *packet* for an in-session agent, and the ingestion of its reply.    |


# _autosummary/epythet.validation.lint.html.md

# epythet.validation.lint

Level 0: static docstring linters, normalised into the finding model.

Two tools are shelled out to. `ruff check --select D` covers pydocstyle
(presence, summary lines, section formatting for the configured convention).
`pydoclint` covers signature consistency (`DOC1xx`/`DOC2xx`/`DOC4xx`/
`DOC5xx`: undocumented or misnamed parameters, missing returns), which ruff
only previews a handful of; it is optional and skipped with a note when it is
not installed.

Findings keep the tool’s own code as `rule` (`D102`, `DOC101`) and name
the tool in `tool`, so they never collide with ledger ids.

### Module Attributes

| [`STYLES`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.STYLES)            | Docstring styles ruff's pydocstyle convention and pydoclint's `--style` both accept.                                                                                                                                                  |
|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`RUFF_D_IGNORE`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.RUFF_D_IGNORE)     | `D107` (`__init__` must have its own docstring) contradicts pydoclint's `DOC301` (`__init__` must NOT have one; its Args merge into the class docstring) -- the house convention this repo's docstring-style skill already documents. |
| [`PYDOCLINT_OPTIONS`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.PYDOCLINT_OPTIONS) | types live in annotations, never in the docstring.                                                                                                                                                                                    |

### Functions

| [`pydoclint_severity`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.pydoclint_severity)(code)                           | `DOC1xx` (arguments disagree with the signature) are warnings; the rest info.   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`ruff_severity`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.ruff_severity)(code)                                | `D1xx` (missing docstrings) are warnings; other `D` rules are style, so info.   |
| [`run_lint_level`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.run_lint_level)(package_dir, \*, project_dir)       | Level 0: ruff D plus pydoclint, with notes for anything skipped.                |
| [`run_pydoclint`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.run_pydoclint)(package_dir, \*, project_dir[, ...]) | Run `pydoclint` if installed; otherwise return a note and no findings.          |
| [`run_ruff`](_autosummary/epythet.validation.lint.html.md#epythet.validation.lint.run_ruff)(package_dir, \*, project_dir[, style])    | Run `ruff check --select D` and translate its JSON output.                      |

### epythet.validation.lint.PYDOCLINT_OPTIONS *= ('--quiet', '--skip-checking-short-docstrings', 'true', '--arg-type-hints-in-docstring', 'false', '--arg-type-hints-in-signature', 'true', '--check-return-types', 'false', '--check-yield-types', 'false')*

types live in annotations, never
in the docstring. `--arg-type-hints-in-signature true` tells pydoclint that
*is* how a documented signature looks (DOC108 fires on the opposite reading:
`false` means “expect no type hints in the signature”, which trips on every
annotated function). `--arg-type-hints-in-docstring false` keeps it from
asking for types in the docstring text. `--allow-init-docstring` defaults to
`False`, which enforces DOC301 (`__init__` undocumented, its Args merged
into the class docstring) – the convention this house already writes to, so
it is left at its default rather than passed explicitly.

* **Type:**
  pydoclint options for the house convention

### epythet.validation.lint.RUFF_D_IGNORE *= ('D107',)*

`D107` (`__init__` must have its own docstring) contradicts pydoclint’s
`DOC301` (`__init__` must NOT have one; its Args merge into the class
docstring) – the house convention this repo’s docstring-style skill already
documents. Only one side can pass, so the ruff side is dropped.

### epythet.validation.lint.STYLES *= ('google', 'numpy', 'sphinx')*

Docstring styles ruff’s pydocstyle convention and pydoclint’s `--style` both accept.

### epythet.validation.lint.pydoclint_severity(code)

`DOC1xx` (arguments disagree with the signature) are warnings; the rest info.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> pydoclint_severity("DOC101"), pydoclint_severity("DOC201")
('warning', 'info')
```

### epythet.validation.lint.ruff_severity(code)

`D1xx` (missing docstrings) are warnings; other `D` rules are style, so info.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> ruff_severity("D102"), ruff_severity("D205")
('warning', 'info')
```

### epythet.validation.lint.run_lint_level(package_dir, , project_dir, style='google', ignore=())

Level 0: ruff D plus pydoclint, with notes for anything skipped.

`ignore` is the `--ignore` list every other level applies at file
discovery; the linters walk the package themselves, so their findings are
filtered by the same predicate ([`is_ignored()`](_autosummary/epythet.validation.docstrings.html.md#epythet.validation.docstrings.is_ignored))
on the file’s full path.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### epythet.validation.lint.run_pydoclint(package_dir, , project_dir, style='google')

Run `pydoclint` if installed; otherwise return a note and no findings.

pydoclint writes its report to stderr, so both streams are parsed.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### epythet.validation.lint.run_ruff(package_dir, , project_dir, style='google')

Run `ruff check --select D` and translate its JSON output.

Returns `(findings, notes)`; `notes` explains a skipped run.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]


# _autosummary/epythet.validation.model.html.md

# epythet.validation.model

The finding and report model shared by every level of `epythet validate`.

One in-memory model, several renderers: the human table, the JSON document and
the JSONL stream are all views over the same [`Report`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Report), so they can never
disagree. The vocabulary here (severities, levels, exit codes) is the one fixed
in the epythet v2 decision record (discussion #15, decision D8).

Levels are named by *what artifact they read*, not by when they run:

|   level | name   | reads                                                 |
|---------|--------|-------------------------------------------------------|
|     0   | lint   | the source text of each docstring (ruff, pydoclint)   |
|     0.5 | parse  | the docutils doctree of each docstring, in isolation  |
|     1   | build  | the Sphinx warning stream                             |
|     2   | render | the built output: XML, HTML and text pages            |
|     3   | review | a review packet for an in-session agent (never gates) |

The CLI exposes them as a *tier index* (`--level 0` runs level 0,
`--level 1` runs levels 0 and 0.5, `--level 2` adds the build), which is
what [`TIERS`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.TIERS) and [`levels_for_tier()`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.levels_for_tier) translate.

### Module Attributes

| [`SEVERITY_RANK`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.SEVERITY_RANK)      | Lower rank is worse.                                                                    |
|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| [`LEVELS`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.LEVELS)             | Level number -> level name, in run order.                                               |
| [`TIERS`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.TIERS)              | Run order of the levels; index into this list is the CLI `--level` tier.                |
| [`IMPLEMENTED_TIERS`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.IMPLEMENTED_TIERS)  | 0 lint, 1 parse, 2 build, 3 render, 4 review.                                           |
| [`IMPLEMENTED_LEVELS`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.IMPLEMENTED_LEVELS) | The levels those tiers run.                                                             |
| [`REVIEW_LEVEL`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.REVIEW_LEVEL)       | The review level never gates unless the caller asks for it (decision D8).               |
| [`REVIEW_PACKET_RULE`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.REVIEW_PACKET_RULE) | The one level-3 finding every packet run emits; "a packet was written" is not a defect. |
| [`EXIT_FOR_LEVEL`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.EXIT_FOR_LEVEL)     | Level -> exit code when that level has findings at or above the threshold.              |

### Functions

| [`levels_for_tier`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.levels_for_tier)(tier)                     | The levels a CLI tier runs: every level up to and including the tier's.   |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`severity_at_or_above`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.severity_at_or_above)(severity, threshold) | True when `severity` is at least as serious as `threshold`.               |
| [`sort_findings`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.sort_findings)(findings)                   | Stable order for every renderer: severity, then rule id, then location.   |

### Classes

| [`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)(rule, severity, level, message[, ...])   | One problem found in one place.                                              |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`Report`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Report)(package, package_dir, levels_run[, ...])  | Everything one `validate` run produced, plus enough context to reproduce it. |
| [`Timer`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Timer)(durations, key)                            | Records how long each level took, as `report.durations[level_name]`.         |

### epythet.validation.model.EXIT_FOR_LEVEL *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[float](https://docs.python.org/3/builtins/functions.html#float), [int](https://docs.python.org/3/builtins/functions.html#int)]* *= {0: 10, 0.5: 11, 1: 12, 2: 13, 3: 14}*

Level -> exit code when that level has findings at or above the threshold.

### *class* epythet.validation.model.Finding(rule, severity, level, message, file=None, line=None, object=None, detector='', evidence='', fix='', autofixable=False, strategy='', tool='epythet', ledger_occurrences=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One problem found in one place.

`rule` is a ledger rule id (`DR001`) for levels 0.5 and 1, or the
upstream tool’s code (`D102`, `DOC101`) for level 0, in which case
`tool` names the tool. `line` is 1-based and, for docstring findings,
the line the docstring literal starts on.

#### *property* location *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`file:line` for the table renderer, or `-` when unknown.

#### to_dict()

JSON-ready dict; the JSON and JSONL renderers emit exactly this.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### epythet.validation.model.IMPLEMENTED_LEVELS *= (0, 0.5, 1, 2, 3)*

The levels those tiers run.

### epythet.validation.model.IMPLEMENTED_TIERS *= (0, 1, 2, 3, 4)*

0 lint, 1 parse, 2 build, 3 render, 4 review.

* **Type:**
  Every CLI tier is implemented

### epythet.validation.model.LEVELS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[float](https://docs.python.org/3/builtins/functions.html#float), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= {0: 'lint', 0.5: 'parse', 1: 'build', 2: 'render', 3: 'review'}*

Level number -> level name, in run order.

### epythet.validation.model.REVIEW_LEVEL *= 3*

The review level never gates unless the caller asks for it (decision D8).

### epythet.validation.model.REVIEW_PACKET_RULE *= 'REVIEW'*

The one level-3 finding every packet run emits; “a packet was written” is not a defect.

### *class* epythet.validation.model.Report(package, package_dir, levels_run, findings=<factory>, durations=<factory>, objects_checked=0, objects_undocumented=0, notes=<factory>, epythet_version=None, sphinx_version=None, docutils_version=None, ledger_sources=<factory>, schema_version='1')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything one `validate` run produced, plus enough context to reproduce it.

#### counts_by_severity()

`{"error": n, "warning": n, "info": n}` over all findings.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

#### exit_code(fail_on='error', , fail_on_review=False)

The process exit code: `0` when clean, else the code of the first failing level.

The *first* (lowest) failing level is reported because it is the first
gate a CI pipeline would have stopped at.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> r = Report("p", "/p", [0, 0.5])
>>> r.exit_code()
0
>>> r.findings.append(Finding("DR001", "error", 0.5, "leak"))
>>> r.exit_code(), r.exit_code("info")
(11, 11)
>>> r.findings.append(Finding("D102", "warning", 0, "missing"))
>>> r.exit_code(), r.exit_code("warning")
(11, 10)
>>> r = Report("p", "/p", [3], [Finding("REVIEW", "info", 3, "packet")])
>>> r.exit_code(fail_on_review=True)
0
>>> r.findings.append(Finding("DR001", "warning", 3, "reviewer said so"))
>>> r.exit_code(), r.exit_code(fail_on_review=True)
(0, 14)
```

#### failing_levels(fail_on='error', , fail_on_review=False)

Levels with at least one finding at or above `fail_on`, in run order.

Level 3 (review) never counts unless `fail_on_review` is set, and
then any finding a reviewer’s reply produced counts whatever its
severity (the “packet written” finding never does): a review
proposes, it does not gate (decision D8).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

#### summary()

The `summary` block of the JSON document.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### to_dict()

The JSON document described in decision D8.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### epythet.validation.model.SEVERITY_RANK *= {'error': 0, 'info': 2, 'warning': 1}*

Lower rank is worse. Used for `--fail-on` comparisons.

### epythet.validation.model.TIERS *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[float](https://docs.python.org/3/builtins/functions.html#float)]* *= [0, 0.5, 1, 2, 3]*

Run order of the levels; index into this list is the CLI `--level` tier.

### *class* epythet.validation.model.Timer(durations, key)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Records how long each level took, as `report.durations[level_name]`.

```pycon
>>> durations = {}
>>> with Timer(durations, "parse"):
...     pass
>>> list(durations) == ["parse"] and durations["parse"] >= 0
True
```

### epythet.validation.model.levels_for_tier(tier)

The levels a CLI tier runs: every level up to and including the tier’s.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

```pycon
>>> levels_for_tier(0)
[0]
>>> levels_for_tier(1)
[0, 0.5]
>>> levels_for_tier(2)
[0, 0.5, 1]
```

### epythet.validation.model.severity_at_or_above(severity, threshold)

True when `severity` is at least as serious as `threshold`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> severity_at_or_above("error", "warning")
True
>>> severity_at_or_above("info", "warning")
False
```

### epythet.validation.model.sort_findings(findings)

Stable order for every renderer: severity, then rule id, then location.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]


# _autosummary/epythet.validation.parse.html.md

# epythet.validation.parse

Level 0.5: parse each docstring’s docutils doctree and run the ledger’s detectors.

This is the load-bearing level of `epythet validate`: the research measured
that a strict `sphinx-build -W -n` is silent on 12 of 21 artifact classes,
while the doctree of the docstring, parsed in isolation, exposes 20 of them at
about a thousand docstrings per second and without a build.

Two things make a naive implementation fail and are handled here:

- plain docutils knows nothing of `:func:`, `.. versionadded::` and the
  rest of Sphinx’s vocabulary, so stub roles and directives are registered
  first (otherwise nearly every correct Sphinx docstring is flagged);
- with `sphinx.ext.napoleon` enabled fleet-wide, Google sections are
  rewritten before docutils sees them, so the same transform is applied here
  (`napoleon=True`) and rules can opt out via `applies_to.napoleon`.

`file_insertion_enabled` and `raw_enabled` are off so that a docstring can
never make the validator read a file or inject raw HTML.

### Module Attributes

| [`ONE_ARGUMENT_DIRECTIVES`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.ONE_ARGUMENT_DIRECTIVES)   | Directives whose real implementation takes at most one argument.   |
|----------------------------------------------------------------------------|--------------------------------------------------------------------|

### Functions

| [`evaluate_rule`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.evaluate_rule)(rule, parsed)                    | Run one parse-level rule over one parsed docstring; returns the evidence list.      |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| [`findings_for`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.findings_for)(parsed, rules, \*[, level])       | One finding per (docstring, rule) that fired, carrying the first hit and the count. |
| [`install_stubs`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.install_stubs)()                                | Register stub Sphinx roles and directives with docutils (idempotent).               |
| [`napoleon_transform`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.napoleon_transform)(text)                       | Rewrite Google/NumPy sections into RST fields the way `sphinx.ext.napoleon` does.   |
| [`parse_docstring`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.parse_docstring)(docstring, \*[, napoleon])     | Parse one docstring (after the optional napoleon transform).                        |
| [`parse_rst`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.parse_rst)(text)                                | Parse RST text into a doctree, returning it with the docutils messages.             |
| [`run_parse_level`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.run_parse_level)(docstrings, ledger, \*[, ...]) | Level 0.5 over a stream of docstrings.                                              |
| [`sphinx_available`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.sphinx_available)()                             | Whether `sphinx.ext.napoleon` can be imported.                                      |

### Classes

| [`ParsedDocstring`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.ParsedDocstring)(docstring, text, tree, messages)   | One docstring, its (possibly napoleon-transformed) text, doctree and messages.   |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|

### epythet.validation.parse.ONE_ARGUMENT_DIRECTIVES *= frozenset({'code', 'code-block', 'literalinclude', 'math', 'sourcecode'})*

Directives whose real implementation takes at most one argument.

### *class* epythet.validation.parse.ParsedDocstring(docstring, text, tree, messages, \_paragraphs=None, \_literals=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One docstring, its (possibly napoleon-transformed) text, doctree and messages.

#### *property* literals *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Text of every inline literal.

#### *property* paragraphs *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Prose of every paragraph not inside a system message.

Text inside inline `literal` nodes is left out, so a field marker
quoted as code (double backticks around `:param x:`) never trips a
prose regex.

#### texts_for(node)

The text corpus a `regex` detector scans: `paragraph`, `literal` or `any`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.parse.evaluate_rule(rule, parsed)

Run one parse-level rule over one parsed docstring; returns the evidence list.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.parse.findings_for(parsed, rules, , level=0.5)

One finding per (docstring, rule) that fired, carrying the first hit and the count.

A rule whose detector declares `only_if_no_other_hits: true` (the
catch-all DR032) is evaluated last and reported only when nothing more
specific fired on the same docstring, so a docutils message never appears
twice under two rule ids.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]

### epythet.validation.parse.install_stubs()

Register stub Sphinx roles and directives with docutils (idempotent).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.validation.parse.napoleon_transform(text)

Rewrite Google/NumPy sections into RST fields the way `sphinx.ext.napoleon` does.

Returns `text` unchanged when Sphinx is not importable; the caller records
a note in that case.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.validation.parse.parse_docstring(docstring, , napoleon=True)

Parse one docstring (after the optional napoleon transform).

* **Return type:**
  [`ParsedDocstring`](_autosummary/epythet.validation.parse.html.md#epythet.validation.parse.ParsedDocstring)

### epythet.validation.parse.parse_rst(text)

Parse RST text into a doctree, returning it with the docutils messages.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[`document`, [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### epythet.validation.parse.run_parse_level(docstrings, ledger, , napoleon=True)

Level 0.5 over a stream of docstrings.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]

### epythet.validation.parse.sphinx_available()

Whether `sphinx.ext.napoleon` can be imported.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)


# _autosummary/epythet.validation.propose.html.md

# epythet.validation.propose

### epythet.validation.propose(reply_path, , overlay=None, ledger=None)

Write every `proposed_rules` entry of a reply into `overlay` as a proposed rule.

* **Return type:**
  `ProposalResult`


# _autosummary/epythet.validation.render.html.md

# epythet.validation.render

### epythet.validation.render(report, format='table', \*\*kwargs)

Render with the named format (`table`, `json` or `jsonl`).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/epythet.validation.rendered.html.md

# epythet.validation.rendered

Level 2: read the *rendered* output (XML, HTML, text) and report what the build hid.

A strict build is silent about most of what a reader sees wrong (research
§2.2): an object listed with no description, a cross-reference that rendered
as plain code, a link to an anchor Sphinx pruned, an image that is not in the
built tree. This level renders three builders through the `backend=` seam
([`render()`](_autosummary/epythet.validation.build.html.md#epythet.validation.build.SphinxBackend.render)) and reads each for
what it shows best (research §5.4):

- `xml` for structure: empty object descriptions and unresolved
  cross-references (a `literal` with class `xref` that no `reference`
  wraps, detectable without `-n`);
- `html` for links and assets: dangling `#idN` anchors and `<img src>`
  that resolve to nothing in the built tree;
- `text` for snapshots: a 4 KB page that keeps every text-leak artifact
  verbatim and none of the theme, diffed against `docsrc/_snapshots/text`
  when `snapshot=True`.

Only the standard library parses the output (`xml.etree` and `html.parser`):
the pages are Sphinx’s own, not untrusted input. Snapshots are opt-in and
off by default; `update_snapshots=True` re-baselines.

```pycon
>>> from epythet.validation.rendered import dangling_anchors
>>> dangling_anchors('<a id="x"></a><a href="#x">ok</a><a href="#id7">gone</a>')
['#id7']
```

### Module Attributes

| [`SNAPSHOT_DIRNAME`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.SNAPSHOT_DIRNAME)   | Where `-b text` snapshots live, relative to the Sphinx source directory.     |
|---------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`EMPTY_DESCRIPTION`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.EMPTY_DESCRIPTION)  | Rule ids this level reports under (all bundled; see `epythet/ledger/rules`). |
| [`RenderDetector`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderDetector)     | A detector takes `{builder: outdir}` and the Sphinx source dir.              |
| [`RENDER_DETECTORS`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RENDER_DETECTORS)   | Detector name (as in a rule's `detector.function`) -> function.              |

### Functions

| [`changed_pages`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.changed_pages)(diff)                            | Pages level 3 should review: changed or new relative to the snapshot.                 |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| [`compare_snapshots`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.compare_snapshots)(text_dir, snapshot_dir)      | Diff every rendered text page against its stored snapshot.                            |
| [`dangling_anchors`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.dangling_anchors)(html)                         | Fragment links on a page whose target id does not exist on that page.                 |
| [`dangling_anchors_detector`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.dangling_anchors_detector)(outdirs, \_docsrc)   | Level-2 detector over the `html` output (DR027).                                      |
| [`empty_descriptions`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.empty_descriptions)(outdirs, \_docsrc)          | Level-2 detector over the `xml` output (DR026).                                       |
| [`empty_descriptions_in`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.empty_descriptions_in)(root)                    | Objects the page lists or describes with no text at all.                              |
| [`missing_images`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.missing_images)(html, \*, page_dir)             | `<img src>` values that resolve to no file next to the page.                          |
| [`missing_images_detector`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.missing_images_detector)(outdirs, \_docsrc)     | Level-2 detector over the `html` output (DR024 at level 2).                           |
| [`render_detector`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.render_detector)(name)                          | Register a level-2 detector; the ledger loader validates `html` rules against it.     |
| [`render_findings`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.render_findings)(outdirs, ledger, \*, docsrc)   | Run every `html`-kind rule of the ledger over the rendered output.                    |
| [`run_render_level`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.run_render_level)(project_dir, ledger, \*, ...) | Level 2: render html, text and xml into `outdir` and read them.                       |
| [`snapshot_findings`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.snapshot_findings)(diff, ledger)                | One DR035 finding per changed page, plus info findings for new and removed pages.     |
| [`text_snapshots_detector`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.text_snapshots_detector)(_outdirs, \_docsrc)    | DR035's detector is the snapshot diff, driven by `snapshot=`; nothing to scan here.   |
| [`unresolved_xrefs`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.unresolved_xrefs)(outdirs, \_docsrc)            | Level-2 detector over the `xml` output (DR023 at level 2).                            |
| [`unresolved_xrefs_in`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.unresolved_xrefs_in)(root)                      | Cross-references that rendered as plain code: an `xref` literal no `reference` wraps. |
| [`update_snapshots`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.update_snapshots)(text_dir, snapshot_dir)       | Replace the stored snapshots with the current render; returns pages written.          |

### Classes

| [`RenderArtifacts`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderArtifacts)([outdirs, docsrc, snapshot])      | What level 2 leaves behind for level 3: output dirs and the snapshot diff.   |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)(page, evidence[, object])               | One thing a render detector found on one page.                               |
| [`SnapshotDiff`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.SnapshotDiff)([changed, added, removed, compared]) | How the `-b text` render compares with the stored snapshots.                 |

### epythet.validation.rendered.EMPTY_DESCRIPTION *= 'DR026'*

Rule ids this level reports under (all bundled; see `epythet/ledger/rules`).

### epythet.validation.rendered.RENDER_DETECTORS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)], [Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path)], [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[RenderHit](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]]]* *= {'dangling_anchors': <function dangling_anchors_detector>, 'empty_descriptions': <function empty_descriptions>, 'missing_images': <function missing_images_detector>, 'text_snapshots': <function text_snapshots_detector>, 'unresolved_xrefs': <function unresolved_xrefs>}*

Detector name (as in a rule’s `detector.function`) -> function.

### *class* epythet.validation.rendered.RenderArtifacts(outdirs=<factory>, docsrc=None, snapshot=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What level 2 leaves behind for level 3: output dirs and the snapshot diff.

### epythet.validation.rendered.RenderDetector

A detector takes `{builder: outdir}` and the Sphinx source dir.

alias of `Callable`[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)], [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]]

### *class* epythet.validation.rendered.RenderHit(page, evidence, object=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One thing a render detector found on one page.

### epythet.validation.rendered.SNAPSHOT_DIRNAME *= '_snapshots/text'*

Where `-b text` snapshots live, relative to the Sphinx source directory.

### *class* epythet.validation.rendered.SnapshotDiff(changed=<factory>, added=<factory>, removed=<factory>, compared=0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How the `-b text` render compares with the stored snapshots.

### epythet.validation.rendered.changed_pages(diff)

Pages level 3 should review: changed or new relative to the snapshot.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.rendered.compare_snapshots(text_dir, snapshot_dir)

Diff every rendered text page against its stored snapshot.

* **Return type:**
  [`SnapshotDiff`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.SnapshotDiff)

### epythet.validation.rendered.dangling_anchors(html)

Fragment links on a page whose target id does not exist on that page.

This is the research’s exact detector (§6.3): every `problematic` span
Sphinx emits links to a `#idN` system message it then prunes, and a
hand-written `:ref:` to a missing label ends the same way.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.rendered.dangling_anchors_detector(outdirs, \_docsrc)

Level-2 detector over the `html` output (DR027).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.empty_descriptions(outdirs, \_docsrc)

Level-2 detector over the `xml` output (DR026).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.empty_descriptions_in(root)

Objects the page lists or describes with no text at all.

Two shapes: a `desc` whose `desc_content` holds nothing but generated
paragraphs, index entries and nested objects; and an autosummary row
whose summary cell is an empty paragraph (autodoc leaves undocumented
members out of the page entirely, so the table row is all a reader sees).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.missing_images(html, , page_dir)

`<img src>` values that resolve to no file next to the page.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### epythet.validation.rendered.missing_images_detector(outdirs, \_docsrc)

Level-2 detector over the `html` output (DR024 at level 2).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.render_detector(name)

Register a level-2 detector; the ledger loader validates `html` rules against it.

### epythet.validation.rendered.render_findings(outdirs, ledger, , docsrc)

Run every `html`-kind rule of the ledger over the rendered output.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]

### epythet.validation.rendered.run_render_level(project_dir, ledger, , backend, outdir, snapshot=False, update=False, snapshot_dir=None)

Level 2: render html, text and xml into `outdir` and read them.

Returns `(findings, notes, artifacts)`; the artifacts hand level 3 the
output directories and the snapshot diff so it never builds again.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`RenderArtifacts`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderArtifacts)]

### epythet.validation.rendered.snapshot_findings(diff, ledger)

One DR035 finding per changed page, plus info findings for new and removed pages.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]

### epythet.validation.rendered.text_snapshots_detector(\_outdirs, \_docsrc)

DR035’s detector is the snapshot diff, driven by `snapshot=`; nothing to scan here.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.unresolved_xrefs(outdirs, \_docsrc)

Level-2 detector over the `xml` output (DR023 at level 2).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.unresolved_xrefs_in(root)

Cross-references that rendered as plain code: an `xref` literal no `reference` wraps.

Autosummary rows are left out: an unwrapped name there means an
undocumented object (reported by [`empty_descriptions_in()`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.empty_descriptions_in)), not a
bad target.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RenderHit`](_autosummary/epythet.validation.rendered.html.md#epythet.validation.rendered.RenderHit)]

### epythet.validation.rendered.update_snapshots(text_dir, snapshot_dir)

Replace the stored snapshots with the current render; returns pages written.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)


# _autosummary/epythet.validation.review.html.md

# epythet.validation.review

Level 3: a review *packet* for an in-session agent, and the ingestion of its reply.

Level 3 never calls a model itself and never gates (decision D8; research
§7.3). It packs what a reviewer needs into one directory under the user data
dir and stops:

- `pages/<docname>.txt`: the `-b text` render of the pages to review
  (changed against the snapshot when there is one, else a sample), which is
  about nine times fewer tokens than the HTML (research §7.3);
- `screenshots/<docname>.png`: optional, when Playwright is installed and
  `screenshots=True`;
- `rubric.md`: the six-dimension rubric, the ledger’s rule ids for
  grounding, and the review controls;
- `schema.json`: the strict JSON schema the reply must satisfy;
- `packet.json`: the manifest (package, version, pages, prompt hash).

An agent (through a skill; the maintainer’s decision 7) reads the packet and
writes `review.json`. Passing that file back as `review_reply=` turns its
`findings` into level-3 findings and leaves its `proposed_rules` for
`epythet ledger propose`, which writes them as `status: proposed` rules
into an overlay for a human to promote.

```pycon
>>> from epythet.validation.review import REPLY_SCHEMA
>>> sorted(REPLY_SCHEMA["properties"])
['findings', 'model', 'prompt_hash', 'proposed_rules', 'schema_version']
```

### Module Attributes

| [`PACKET_RULE`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.PACKET_RULE)   | The rule id of the one finding a packet run always produces.    |
|----------------------------------------------------------------|-----------------------------------------------------------------|
| [`UNRULED`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.UNRULED)       | The rule id of a reply finding that names no ledger rule.       |
| [`RUBRIC`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.RUBRIC)        | The six dimensions of research_doc_quality §3, scored 0-3 each. |

### Functions

| [`load_reply`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.load_reply)(path)                                   | Read and validate a `review.json`.                                                                         |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| [`reply_findings`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.reply_findings)(reply, ledger, \*[, source])        | Turn a reply's `findings` into level-3 findings (informational by construction).                           |
| [`reviews_dir`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.reviews_dir)()                                      | `<user data dir>/review`: one subdirectory per package, one per run below it.                              |
| [`rubric_text`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.rubric_text)(ledger)                                | The rubric with the ledger's rule ids appended, so replies can name them.                                  |
| [`run_review_level`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.run_review_level)(\*, package, ...[, changed, ...]) | Level 3: write the packet, then ingest `reply` when one is given.                                          |
| [`select_pages`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.select_pages)(available, \*[, mode, changed, ...])  | Which pages go into the packet.                                                                            |
| [`validate_reply`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.validate_reply)(reply)                              | Raise [`ReplyError`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.ReplyError) unless `reply` satisfies `REPLY_SCHEMA`. |
| [`write_packet`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.write_packet)(\*, package, package_version, ...)    | Write a review packet and return where it is.                                                              |

### Classes

| [`ReviewPacket`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.ReviewPacket)(path[, pages, screenshots, ...])   | Where a packet was written and what went into it.   |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------|

### Exceptions

| [`ReplyError`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.ReplyError)   | A review reply is not valid against `REPLY_SCHEMA`.   |
|---------------------------------------------------------------|-------------------------------------------------------|

### epythet.validation.review.PACKET_RULE *= 'REVIEW'*

The rule id of the one finding a packet run always produces.

### epythet.validation.review.RUBRIC *= '# Review rubric\\n\\nScore each documented object on the page 0-3 on the six dimensions below.\\nOnly report an object whose score is 0 or 1 on a dimension, or whose page\\nshows a rendering artifact. Never report a style preference.\\n\\n| Dimension | 0 | 1 | 2 | 3 |\\n|---|---|---|---|---|\\n| A. Summary | absent | restates the name, or meta-language ("This function...") | one verb-first sentence saying what it does | also implies when to reach for it and disambiguates siblings |\\n| B. Parameter and return semantics | absent | descriptions restate the type | meaning, units, default behaviour | plus interactions, ranges, what the return is keyed or ordered by |\\n| C. Example presence and runnability | none | present but not runnable (pseudo-code, \`...\`, no output) | one runnable doctest for the common case | plus a variation or edge case; deterministic |\\n| D. Failure modes | nothing | exceptions named without cause | each exception paired with its trigger | plus non-exception failure modes |\\n| E. Cross-references and orientation | none | related name in prose | See Also with 1-3 adjacent callables and why | plus when \*not\* to use |\\n| F. Module orientation (per module) | none | one line restating the name | purpose and named entry points | plus a minimal example and the relation to the package |\\n\\n# What to look for first (research_doc_quality §4.3)\\n\\n1. Runnable examples. 2. Correctness of every claim (ranks above completeness).\\n3. A precise, disambiguating one-line summary. 4. Parameter \*semantics\*, not types.\\n5. Failure modes. 6. When to use and when not. 7. Consistent terminology.\\n8. Cross-references, one to three, with reasons.\\n\\n# Rendering artifacts\\n\\nA \`-b text\` page keeps text leaks verbatim: a \`:param x:\` in running prose,\\na \`>>>\` inside a paragraph, a literal \`\`\`\` \`\`\` \`\`\`\` fence, \`\*args\` opening\\nan emphasis, a \`##\` heading, a \`[text](url)\` link. Each of those is a ledger\\nrule (below). Name the rule id when one fits; otherwise mark the finding\\n\`proposed\` and draft a rule under \`proposed_rules\`.\\n\\n# Controls\\n\\n- You are proposing, not gating: nothing here fails a build.\\n- Report only what is stable: re-read the page in a different order and keep\\n  the findings you would make both times.\\n- Name a \`rule\` from the list below, or write \`proposed\` and fill in\\n  \`proposed_rules\` with a detector a machine can run (a regex over the\\n  docstring text, or a doctree/html detector name), an \`example_bad\` and an\\n  \`example_good\` docstring, a \`message\` and a \`fix\` hint.\\n- Never invent behaviour: a claim about what code does must come from the\\n  code or its tests, and an example must have been executed.\\n- Put the model name in \`model\` and the packet\\'s \`prompt_hash\` in the reply.\\n\\n# Ledger rules you may name\\n'*

The six dimensions of research_doc_quality §3, scored 0-3 each.

### *exception* epythet.validation.review.ReplyError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

A review reply is not valid against `REPLY_SCHEMA`.

### *class* epythet.validation.review.ReviewPacket(path, pages=<factory>, screenshots=<factory>, prompt_hash='', notes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where a packet was written and what went into it.

### epythet.validation.review.UNRULED *= 'REVIEW-PROPOSED'*

The rule id of a reply finding that names no ledger rule.

### epythet.validation.review.load_reply(path)

Read and validate a `review.json`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### epythet.validation.review.reply_findings(reply, ledger, , source='')

Turn a reply’s `findings` into level-3 findings (informational by construction).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)]

### epythet.validation.review.reviews_dir()

`<user data dir>/review`: one subdirectory per package, one per run below it.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### epythet.validation.review.rubric_text(ledger)

The rubric with the ledger’s rule ids appended, so replies can name them.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### epythet.validation.review.run_review_level(, package, package_version, outdirs, ledger, changed=None, mode='changed', sample=8, screenshots=False, packet_dir=None, reply=None)

Level 3: write the packet, then ingest `reply` when one is given.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Finding`](_autosummary/epythet.validation.model.html.md#epythet.validation.model.Finding)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### epythet.validation.review.select_pages(available, , mode='changed', changed=None, sample=8)

Which pages go into the packet.

`changed` mode uses the snapshot diff when there is one and falls back
to `sample` (the first `sample` API pages, index first) otherwise;
`all` takes every page.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> select_pages(["index", "api", "_autosummary/p", "_autosummary/p.m"], mode="sample", sample=2)
['index', '_autosummary/p']
>>> select_pages(["index", "a"], mode="changed", changed=["a"])
['a']
```

### epythet.validation.review.validate_reply(reply)

Raise [`ReplyError`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.ReplyError) unless `reply` satisfies `REPLY_SCHEMA`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### epythet.validation.review.write_packet(, package, package_version, outdirs, ledger, changed=None, mode='changed', sample=8, screenshots=False, packet_dir=None)

Write a review packet and return where it is.

* **Return type:**
  [`ReviewPacket`](_autosummary/epythet.validation.review.html.md#epythet.validation.review.ReviewPacket)


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-09-15 12:35 UTC** from commit <a href="https://github.com/i2mint/epythet/commit/7f52d703fd6794b5b324d868b15d5b1e14ce5461"><code>7f52d70</code></a> on branch <code>master</code>, for **epythet 0.2.12** (from <code>pyproject.toml</code>).

#### NOTE
Nothing suggests a mismatch: the tree was clean at the commit above, and the documented version is the one on PyPI.

## Source

|                     |                                                                                                                                                       |
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/i2mint/epythet/commit/7f52d703fd6794b5b324d868b15d5b1e14ce5461"><code>7f52d703fd6794b5b324d868b15d5b1e14ce5461</code></a> |
| Branch              | <code>master</code>                                                                                                                                   |
| Tags at this commit | <code>0.2.12</code>                                                                                                                                   |
| Working tree        | clean                                                                                                                                                 |
| Remote              | <code>https://github.com/i2mint/epythet</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>i2mint/epythet</code>                                                                |
| Run          | <a href="https://github.com/i2mint/epythet/actions/runs/34969390270">34969390270</a>       |
| Ref          | <code>refs/heads/master</code>                                                             |
| Event commit | <code>dd8a8c87eb214bb424972795b0687751924963fc</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.12  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.14 |

## Configuration as resolved

|               |                                                                                        |
|---------------|----------------------------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>furo</code>)                                     |
| accent        | <code>#3f6500</code>                                                                   |
| api_generator | <code>autosummary</code>                                                               |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>, <code>ledger/</code> |
| agent_outputs | <code>true</code>                                                                      |
| aggregates    | <code>md</code>                                                                        |
| ai_artifacts  | <code>true</code>                                                                      |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/epythet/0.2.12/">0.2.12</a>, the same as the documented version.

## Reproduce

```bash
git clone https://github.com/i2mint/epythet && cd epythet
git checkout 7f52d703fd6794b5b324d868b15d5b1e14ce5461
pip install "epythet==0.2.12"
epythet quickstart . --ignore tests/ scrap/ examples/ ledger/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`epythet` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `epythet-agentic-readme`

Make sure a repository’s README documents its agentic aspects: the skills, subagents and instruction files it ships (`<pkg>/data/skills`, `.claude/skills`, `.claude/agents`, `CLAUDE.md`, `AGENTS.md`) and the agent-readable docs its site publishes (`llms.txt`, `<package>.md`). Runs `epythet ai-readme-check`, reads the user’s policy (`~/.config/epythet/config.toml`: warn or add, humour, agents first) and either reports what is missing or adds and updates a marked “For AI agents” section rendered from user-overridable snippets (`epythet snippets`). Use when asked “does the README mention the skills”, “add the agent section to the README”, “document the agentic aspects”, when finishing a docs sweep, or before releasing a package that ships skills.

```bash
gh skill install i2mint/epythet epythet-agentic-readme --agent claude-code
```

Source: [`epythet/data/skills/epythet-agentic-readme`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-agentic-readme) (bundled with the pip package).

### `epythet-ai-artifacts`

Find, install and document a repository’s AI agent artifacts: skills (`SKILL.md` folders under `<pkg>/data/skills`, `skills/`, `.claude/skills`), subagents (`.claude/agents`, `<pkg>/data/agents`), instruction files (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules`, `.codex`), and the machine-readable documentation a site publishes (`llms.txt`, `<package>.md`, `.md` page twins, `objects.inv`). Use when arriving in an unfamiliar repo and asking “does this project ship skills or agents”, “how do I install this package’s skill”, “where is the agent-readable version of these docs”, or when adding artifacts to a package so that epythet documents them.

```bash
gh skill install i2mint/epythet epythet-ai-artifacts --agent claude-code
```

Source: [`epythet/data/skills/epythet-ai-artifacts`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-ai-artifacts) (bundled with the pip package).

### `epythet-docstring-style`

Write and improve Python docstrings that render correctly in epythet/Sphinx and that help both humans and AI agents: the epythet docstring dialect (Google sections, doctests, types in annotations), the quality rubric, the behaviour-claim policy (never document behaviour you have not verified), and the anti-patterns to avoid. Use when writing or editing docstrings, when asked to “document this function/module/package”, “improve the docstrings”, “add examples”, “fix the docstring style”, or when reviewing a docstring sweep. Also use before answering “should the type go in the docstring”.

```bash
gh skill install i2mint/epythet epythet-docstring-style --agent claude-code
```

Source: [`epythet/data/skills/epythet-docstring-style`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-docstring-style) (bundled with the pip package).

### `epythet-pages`

Diagnose and fix GitHub Pages publishing for Python documentation built with epythet (or any Sphinx site pushed to a gh-pages branch). Use when docs give a 404, the site does not update, Pages is not enabled, the gh-pages branch is missing, the docs CI fails, or when enabling Pages for one repo or every repo in an organisation. Covers `epythet check-pages`, `epythet configure-pages`, the Python API, the raw `gh api` equivalent, and batch operations.

```bash
gh skill install i2mint/epythet epythet-pages --agent claude-code
```

Source: [`epythet/data/skills/epythet-pages`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-pages) (bundled with the pip package).

### `epythet-repair-migrate`

The per-repository documentation sweep for packages documented with epythet: baseline, validate, repair rendering artifacts in source, improve coverage, correctness and completeness of docstrings, choose a theme, remove a committed docsrc/, and land the change. Use when asked to “migrate the docs to epythet 0.2”, “fix the docstrings across this repo”, “run the docs sweep”, “repair the rendering artifacts”, “upgrade docsrc”, or “clean up this package’s documentation”. Marks which commands exist today and which are coming in a later epythet release.

```bash
gh skill install i2mint/epythet epythet-repair-migrate --agent claude-code
```

Source: [`epythet/data/skills/epythet-repair-migrate`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-repair-migrate) (bundled with the pip package).

### `epythet-setup`

Set up documentation for a Python package with epythet: run the quickstart, read what it produces, configure `[tool.epythet]` in pyproject.toml, add the GitHub Pages publishing workflow, and upgrade a 0.1.x docsrc. Use when asked to “add docs”, “generate documentation”, “set up Sphinx”, “publish docs to GitHub Pages”, “configure epythet”, “what does [tool.epythet] accept”, or when a project has docstrings but no documentation site. Convention over configuration: nothing needs to be added to the package.

```bash
gh skill install i2mint/epythet epythet-setup --agent claude-code
```

Source: [`epythet/data/skills/epythet-setup`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-setup) (bundled with the pip package).

### `epythet-theme`

Choose and parametrize the Sphinx theme of an epythet documentation site: the curated pool (furo, shibuya, pydata-sphinx-theme, sphinxawesome-theme, sphinx-book-theme, alabaster, sphinx_rtd_theme), rules of thumb for picking one, the `theme` / `accent` / `mode` / `theme_options` keys, the derived OKLCH accent colour, and where to browse themes. Use when asked to change, choose or improve a docs theme, set a brand colour, force light or dark mode, why the docs look dated, or “make the docs look like X”.

```bash
gh skill install i2mint/epythet epythet-theme --agent claude-code
```

Source: [`epythet/data/skills/epythet-theme`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-theme) (bundled with the pip package).

### `epythet-validate`

Check a Python package’s docstrings for rendering artifacts and build problems with `epythet validate`: the validation levels (lint, parse, build, render, review), the exit codes, output formats, the artifact ledger of rules (DR001…), how findings are recorded, and how to propose a new rule. Use when asked to “validate the docs”, “check docstrings before publishing”, “why does this docstring render wrong”, “gate docs in CI”, “what does DR003 mean”, or when reading or extending epythet’s ledger of documentation problems.

```bash
gh skill install i2mint/epythet epythet-validate --agent claude-code
```

Source: [`epythet/data/skills/epythet-validate`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/skills/epythet-validate) (bundled with the pip package).

The bundled skills are also on disk after `pip install epythet`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Subagents

Subagents are Markdown files with a frontmatter (`name`, `description`, `tools`) and a system prompt as the body. Copy one into your project’s `.claude/agents/` (or your agent host’s equivalent) to delegate the task it describes.

### `docs-migrator`

Runs the epythet documentation sweep on one repository end to end, baseline, validate, repair rendering artifacts, improve coverage, correctness and completeness of docstrings under the behaviour-claim policy, choose the theme, remove a committed docsrc/, and open a pull request with a before/after report. Use when asked to “sweep the docs of this repo”, “migrate this package to epythet 0.2”, “fix and improve the docstrings across this package”, or to run one repository of a fleet documentation migration.

Source: [`epythet/data/agents/docs-migrator.md`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/agents/docs-migrator.md); tools: `Bash, Read, Edit, Write, Grep, Glob`.

### `docs-reviewer`

Reviews the rendered documentation of a Python package (Level 3 of epythet validate) and returns a review packet of findings that each name a ledger rule or propose a new one, as strict JSON plus draft rule files. Use after a docs build or a docstring sweep when asked to “review the rendered docs”, “look at the built pages for problems”, “find rendering artifacts validate missed”, or “propose ledger rules”. Advisory only, it never gates.

Source: [`epythet/data/agents/docs-reviewer.md`](https://github.com/i2mint/epythet/tree/HEAD/epythet/data/agents/docs-reviewer.md); tools: `Bash, Read, Grep, Glob, Write`.

## Instruction files

Files agents read before working in this repository.

- [`.claude/CLAUDE.md`](https://github.com/i2mint/epythet/tree/HEAD/.claude/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://i2mint.github.io/epythet/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`epythet.md`](https://i2mint.github.io/epythet/epythet.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://i2mint.github.io/epythet/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`epythet`](_autosummary/epythet.html.md#module-epythet)   | Beautiful, correct documentation from a Python package, with no boilerplate.   |
|---------------------------------------------------------------------------|--------------------------------------------------------------------------------|


