# Python API reference

MkDocs product sites build their API reference with `mkdocstrings`, which has
no Starlight equivalent. `helia-ui-pyref` is the replacement, and it runs in two
steps. An extractor maps a [griffe](https://mkdocstrings.github.io/griffe/) JSON
dump onto the reference model; a renderer turns that model into MDX pages that
compose the package's `RefSymbol`, `RefParams`, `RefSection` and `RefMembers`
parts, into the JSON the model is, and into the two text artifacts an agent
reads instead of the pages.

The split is what keeps four languages looking like one reference. Only
`scripts/lib/pyref-extract.mjs` knows Python. A C, C++ or TypeScript extractor
produces the same model and gets the same pages without touching the renderer.

The generator never imports the package it documents. griffe loads by AST, so a
package with hardware or compiler dependencies dumps on any machine, and the
site build needs no Python at all.

## The contract

The JSON is the source of truth and the pages are a view of it.

That ordering decides everything else on this page. The model is written out
whole and per module, with sorted keys and no timestamp, so it diffs. Every
symbol keeps a stable id, and that id is the page anchor, so a URL plus a
fragment names one symbol and keeps naming it after the reference is
regenerated or restyled. A consumer that wants the data takes the JSON and
never parses HTML.

The model is `@ambiqai/helia-ui/reference-model`: TypeScript types plus
`validateReferenceModel`, a hand-written guard with no dependencies. A site
that loads a `reference.json` validates it before rendering, and an extractor
that adds a field its own language happens to have fails that guard rather
than reaching the parts.

## Running it

```sh
# In the Python repository, with griffe installed:
griffe dump helia_aot --docstyle google -f > griffe.json

# In the docs site:
npx helia-ui-pyref \
  --input ../griffe.json \
  --out src/content/docs/reference/api \
  --public public \
  --base /helia-aot/ \
  --sidebar src/generated/api-sidebar.json
```

Pages land at `<out>/<module path>/index.mdx`, so `helia_aot.air.model` becomes
`reference/api/helia_aot/air/model/index.mdx` and serves at
`/helia-aot/reference/api/helia_aot/air/model/`. That is the same shape the
MkDocs `api/` tree had, so inbound links survive the move.

Every route segment is slugged the way Astro derives a content id from a file
path, lowercased and with punctuation dropped, so the page a generator writes
and the URL it advertises are one string; two module paths that slug to one
route fail the render. The module path itself is unchanged as the module's
identity, title and cross-reference target. A module whose source carries no
summary still gets a description on its page, worded mechanically per language,
since a page without one fails the discoverability check.

The nine `mkdocstrings` options the HELIA product sites set are the defaults,
so a run with no flags reproduces the pages those sites publish today. Each has
a flag that overrides it.

`--docstring-style` exists for completeness only. The dump arrives already
parsed, so a different style means re-dumping with
`griffe dump <package> --docstyle numpy -f`; the generator refuses rather than
pretend it can reinterpret sections it is handed.

## The artifacts

Alongside the pages, a run writes four kinds of file under
`<public>/<route prefix>/`.

`public/` is served at the site root, so `reference/api/helia_aot/air.json`
resolves at `/helia-aot/reference/api/helia_aot/air.json`. Every generated page
carries a **Machine-readable model** link to exactly that file, one line under
the module description.

The JSON is byte-stable across runs: keys are sorted and nothing is stamped
with a time. A reference regenerated from an unchanged dump produces unchanged
files, which is what makes `--check` mean something.

## The griffe pin

griffe's `dump` output is a convenience format, not a versioned schema. The
extractor targets **griffe 1.7.3** and checks the top-level shape before it
reads anything: a JSON object of packages, each a `module` with `name`, `path`
and `members`. Anything else is refused with the command that produces a dump
it understands, rather than emitting plausible-looking pages from a tree it
misread.

Pin griffe in the Python repository. When you lift the pin, run `--check` and
read the diff before committing it.

## Keeping the pages honest in CI

Generated pages and artifacts are committed, so the site builds without Python.
`--check` renders in memory and compares against what is on disk, exiting 1 on
drift and naming every file that is stale, missing or orphaned.

```yaml
- run: uv run griffe dump helia_aot --docstyle google -f > griffe.json
- run: npx helia-ui-pyref --input griffe.json --out docs/src/content/docs/reference/api --public docs/public --base /helia-aot/ --check
```

Put it in the same workflow that builds the docs. A docstring edited without
regenerating the reference then fails the pull request rather than shipping a
page that disagrees with the code.

A run records what it wrote in `<out>/.pyref-manifest.json` and removes only
those files next time. A hand-written index page or a partial kept in the same
directory survives, which was not true when the directory was emptied before
every run.

## The sidebar fragment

`--sidebar` writes a Starlight sidebar entry nested by package path, replacing
the literate-nav summary. Spread it into `astro.config.mjs`:

```js
import apiSidebar from './src/generated/api-sidebar.json';

sidebar: [
  { label: 'Guides', autogenerate: { directory: 'guides' } },
  { label: 'API reference', items: apiSidebar.items },
];
```

Each package becomes a collapsed group whose first item is its own page,
labeled `Overview`.

## What the output looks like

Google-style sections become fields of the model, and the renderer puts each
one where it belongs: parameters, returns and raises become `RefParams` tables
inside a labeled `RefSection`, examples become prose and a fenced block, and a
Google admonition stays an aside in the symbol's description. A module page
opens with its own `RefMembers` index, because a generated page carries more
symbols than a table of contents will show.

```mdx
---
title: 'handler'
description: 'Base class for all AOT code-generation handlers.'
sidebar:
  order: 2
---

import RefMembers from '@ambiqai/helia-ui/astro/RefMembers';
import RefParams from '@ambiqai/helia-ui/astro/RefParams';
import RefSection from '@ambiqai/helia-ui/astro/RefSection';
import RefSymbol from '@ambiqai/helia-ui/astro/RefSymbol';

[Machine-readable model](/helia-aot/reference/api/helia_aot/aot/handlers/handler.json)

<RefMembers
  label="On this page"
  items={[
    {
      id: 'helia_aot.aot.handlers.handler.AotHandler',
      name: 'AotHandler',
      kind: 'class',
    },
  ]}
/>

<RefSymbol
  id={"helia_aot.aot.handlers.handler.AotHandler"}
  name={"AotHandler"}
  kind={"class"}
  language={"python"}
  signature={"AotHandler(context: CodeGenContext)"}
  level={2}
  summary={"Base class for all AOT code-generation handlers."}
  source={{ "path": "src/helia_aot/aot/handlers/handler.py", "line": 41 }}
>

:::note[Lifecycle]
An AOT handler resolves, plans and emits, in that order.

:::

<RefSection title="Parameters">
  <RefParams
    caption={'Parameters of AotHandler'}
    density={'compact'}
    rows={[
      {
        name: 'context',
        type: 'CodeGenContext',
        default: 'Required',
        description: 'Code generation context.',
      },
    ]}
  />
</RefSection>

</RefSymbol>
```

## Anchors

`RefSymbol` renders the symbol's id as the element id, and that id is the fully
qualified dotted path — the same anchor `mkdocstrings` produced. A link written
against the MkDocs site still lands on the right symbol, and so does a link
written against a page this generator produced two designs ago.

Cross-references resolve to those anchors. The symbol heading is rendered by the
part rather than written as Markdown, so it does not appear in Starlight's table
of contents; the `RefMembers` index at the top of each page is what lists the
symbols instead.

## Cross-references

`[Name][module.path]` in a docstring becomes a link to the page and anchor that
carries the symbol, prefixed with `--base`. The extractor leaves the syntax
alone and the renderer resolves it, because only the renderer knows the routes.
A reference that resolves to nothing keeps its text and is reported as a warning
rather than shipped as a dead link. Cross-reference syntax inside backticks or a
fenced block is left alone, so `[w][z][y][x]` describing an array layout stays
what it is.

## Where it differs from mkdocstrings

:::note[Deliberate differences]
Four, all of them reported rather than silent:

- **Imported names are not re-documented.** `mkdocstrings` renders an imported
  symbol wherever it is imported. A module that declares `__all__` instead gets
  a **Re-exports** table linking to the canonical page, which keeps one page
  per symbol.
- **Attributes are symbols.** The `Attributes:` docstring section and the public
  attribute members are merged into one list of attribute symbols, so a
  dataclass and a pydantic model render the same way whichever way the fields
  are documented. An UPPER_CASE module-level name is labeled a constant.
- **Docstring headings are shifted.** A module docstring starting at `#` would
  emit a second top-level heading under the page title; headings are shifted so
  the shallowest sits at `--heading-level`. Slugs are unaffected, so anchors
  into docstring headings still work.
- **Prose is escaped for MDX.** A page is MDX now, so a `{` or a `<` in a
  docstring is escaped before it reaches the page. Code spans and fenced blocks
  are left alone. The Markdown in `llms-full.txt` is not escaped, because it is
  not MDX.

:::

Material-specific content in docstrings — `:material-icon:` shortcodes,
`pymdownx` fences — is passed through untouched. Converting it is part of moving
the authored pages, not of generating the reference.
