Repository navigation
API reference completeness: every public object has an entry, pages named after their objects #722
Copy link
Copy link
Closed
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itUnblocked and fully specified; an agent may claim itsize:MNeeds a plan; one or two local workersNeeds a plan; one or two local workers
Description
Parent
What to build
FieldProxy, QueryNode, RelationProxy, ValueExpr, ModelConnection, engines.session(), RunReport, StatusReport and CheckReport have no API entry; two entries use private module paths (query.builder.Query, migrations.steps.chunked); every page lists docstring-less members because show_if_no_docstring is on.
Every name in ferro.__all__ and ferro.migrations.__all__ (and ferro.migrations.testing) has exactly one entry under its public path; API pages are titled after their objects; show_if_no_docstring is turned off. tests/test_docs_site.py gains an assertion that every exported name has an entry in the built site.
Acceptance criteria
- Every exported name has one API entry under its public path.
-
show_if_no_docstring = falseand no public object renders empty. - API page titles name their objects.
Files owned
- Modify:
docs/pages/api/connection.md,docs/pages/api/exceptions.md,docs/pages/api/fields.md,docs/pages/api/migrations.md,docs/pages/api/model.md,docs/pages/api/queries.md,docs/pages/api/raw-sql.md,docs/pages/api/relationships.md,docs/pages/api/transactions.md,zensical.toml - Test:
tests/test_docs_site.py
Interfaces
- Consumes:
built_site,rendered_pages(site config ticket (Docs site config: Zensical features, Markdown extensions, API reference options and a strict build #701)) - Consumes: docstrings of the three docstring tickets (Docstrings: Model, Query and relations #719, Docstrings: connect, fields, checks, row security, raw SQL, sessions, settings and exceptions #720, Docstrings: the migrations API, PassReport and the CLI commands #721)
- Produces: Assertion
test_every_export_has_an_api_entryintests/test_docs_site.py.
Test scenarios
- Happy path: the new assertion passes on the built site.
- Error path: removing
::: ferro.QueryNodemakes it fail namingQueryNode.
Verify
uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheproviderBlocked by
- Docs site config: Zensical features, Markdown extensions, API reference options and a strict build #701, Docstrings: Model, Query and relations #719, Docstrings: connect, fields, checks, row security, raw SQL, sessions, settings and exceptions #720, Docstrings: the migrations API, PassReport and the CLI commands #721
Audit findings
Rows from the docs audit this ticket covers (file, line, rule, fix)
Summary table
| Page | Rendered size | Entries with "Source code" | Problems |
|---|---|---|---|
| connection | 590 KB | 30 | Rust # Errors headings, engines without session(), dataclass __init__/__post_init__ |
| exceptions | 120 KB | 15 | __init__/__reduce__ on FerroError, ModelDoesNotExist |
| fields | 330 KB | 4 | unresolved autoref, 2 broken pydantic links, :func:/:data: roles shown as literal text |
| migrations | 549 KB | 43 | __init__/__repr__ on every report/error class, steps.chunked path |
| model | 598 KB | 26 | 10 __ferro_*__ internals and __getattr__ listed as members |
| queries | 981 KB | 54 | 75 literal Sphinx roles, 54 ADR mentions, 67 #NNN mentions, a stray heading from #294 |
| raw-sql | 66 KB | 3 | clean |
| relationships | 164 KB | 10 | __new__, __class_getitem__, __get_pydantic_core_schema__; Relation hides its inherited methods |
| transactions | 89 KB | 6 | __slots__, __init__(route) |
Findings
| Sev | Where | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| H | ferro.query (api/queries.md, api/relationships.md) |
21, 22 | Missing public objects. ferro.query.__all__ exports FieldProxy, QueryNode, RelationProxy and ValueExpr, and none has an entry. FieldProxy and QueryNode are the two types every predicate signature names (Predicate = Callable[[QueryProxy[TModel]], QueryNode]), concepts/query-typing.md:145 lists ferro.query.FieldProxy as reference, and docstrings cite :class:FieldProxy and :class:QueryNode, which have no target to link to. ModelConnection (the return type of Model.using()) is also undocumented. |
Add ::: ferro.query.FieldProxy, ::: ferro.query.QueryNode, ::: ferro.query.RelationProxy and ::: ferro.query.ValueExpr to api/queries.md, and ModelConnection to api/model.md beside Model.using. |
| H | ferro.engines (api/connection.md) |
21 | engines renders as engines = EngineManager() module-attribute with one sentence. session(), the call the page intro and every concept page rely on, has no signature, parameters or entry. |
Document ferro.session.EngineManager (or EngineManager.session) with members: [session], or give engines an explicit ::: ferro.session.EngineManager.session entry. |
| H | All pages | 22 | Sphinx roles render as literal text. docstring_style = "google" does not resolve :class:, :func:, :meth: or :data:, so readers see ":class: QueryNode" and ":func: varchar". Counts: queries 75, migrations 60, model 39, fields 20, transactions 12, relationships 5, connection 4, exceptions 4. |
Convert the roles to mkdocstrings cross-refs ([QueryNode][ferro.query.QueryNode]) or turn on the backlinks/scoped_crossrefs autorefs feature and use bare-name cross-refs. A docstring-wide sed is mechanical. |
| H | RowSelector (api/queries.md) |
22 | A docstring line that starts with #294 is parsed as a Markdown heading. The page gets an h3 reading "294; aggregates are user-named, so the dict form is their only home)." in the TOC. |
Reflow the docstring so no line starts with #, or write "issue 294". |
| M | Every page | 21 | Dunder and implementation members are listed because show_if_no_docstring = true and no filters are set. Examples: __init__ on every dataclass (it duplicates the class signature), __post_init__, __repr__, __reduce__, __aenter__/__aexit__, __slots__, __get_pydantic_core_schema__, __class_getitem__, all eight AggregateExpr comparison dunders, Model.__getattr__ (which is if not TYPE_CHECKING only), Model.model_config, PoolConfig.model_config, Row.model_config, and the ten Model.__ferro_*__ class attributes. Only __ferro_checks__, __ferro_rls__, __ferro_composite_uniques__ and __ferro_composite_indexes__ are user-facing; __ferro_columns__, __ferro_pk__, __ferro_relation_specs__, __ferro_reverse_specs__, __ferro_retired_columns__ and __ferro_retired_names__ are internal registry state. |
Set filters = ["!^_", "^__ferro_(checks|rls|composite_uniques|composite_indexes)__$"] (or per-directive members: lists), set merge_init_into_class = true, and turn show_if_no_docstring off. Then document the user-facing undocumented members (next row) instead of letting bare signatures through. |
| M | Every page | 21 | About 180 rendered members have no docstring. They appear as a bare name = value instance-attribute line. Examples: every FerroField and ForeignKey option (unique, index, nullable, on_delete...), every RowPolicy field, PoolConfig.max_connections/min_connections/settings_delivery, DatabaseSettings.* (9 fields), FerroError.sqlstate/constraint/driver_message (the exceptions page prose leans on them), DatabaseAheadError.ahead/contract_steps, Relation.all/first, Transaction.execute/fetch_all/fetch_one, and Rows.__len__/__iter__/__getitem__. reset_engine, set_default_connection and version have no Python docstring; their text comes from Rust /// comments. |
Add attribute docstrings to the dataclass/pydantic fields (use_attribute_docstrings already exists on Model) and one-line docstrings on public methods. The options of FerroField and ForeignKey belong in their class docstring's Args:, which merge_init_into_class will show once. |
| M | connection page: set_default_connection, reset_engine |
22 | Rust doc comments are rendered as is: a # Errors section becomes an h3 Errors ("Returns a PyErr if the engine lock cannot be acquired"). That is Rust API language with no Python meaning, and the three builtin entries look different from every Python entry. |
Give these three a Python-facing docstring (wrap them in Python, or add a docstring to the _core.pyi stubs and have mkdocstrings read the stub) that says which Python exception is raised. |
| M | show_source = true (zensical.toml) |
— | Source listings make up most of each page: 54 on queries (981 KB of HTML), 43 on migrations. A collapsed block on every method also breaks the visual rhythm of the reference. The reader of an API reference wants the signature and contract; contributors have the repo. | Set show_source = false globally. Optionally turn it on for a few small helpers (varchar). |
| M | Relation (api/relationships.md) | 21 | Relation subclasses Query, but only its overrides all/first render. where, add, remove and clear (the M2M verbs a reader looks up on a relation) appear under Query on another page, where they do not apply ("RuntimeError if not bound to a many-to-many context"). |
Use inherited_members: true on ::: ferro.Relation, and move add/remove/clear docs to Relation (or say on Query that they work only on a relation). |
| M | Every page | 21 | Entry openers mix verb forms. Imperative: "Build field metadata...", "Store database column metadata...", "Describe...", "Provide the base class...", "Represent lazy collection...", "Establish a connection", "Run a raw SQL...". Third person: "Returns the current version", "Shuts down the global engine", "Selects the default connection". Noun phrases: "One table check...", "A query projected...", "What baseline did.". Several openers have no final period ("Provide the base class for all Ferro models", "Store database column metadata for a model field", "Add links to a many-to-many relationship"). |
Pick one form, infinitive/imperative ("Return…", "Create…"), which most Python entries already use, and apply it. End every summary line with a period. The docstring auditor can do the sweep; this row records the pattern. |
| M | queries, model, migrations, relationships, connection | 22 | Internal tracking IDs in reader-facing text: "ADR-0009", "(FF-F F-2)", "(#246)", "v5 expr record field", "bulk install (#246)". ADRs appear 54 times on queries and 18 on model, none linked. | Remove the ID, or link the ADR on GitHub where the reasoning helps. Never leave a bare internal code such as "FF-F F-2". |
| L | ferro.migrations.steps.chunked (api/migrations.md:65) |
22 | Documented under its private module path, while the others use ferro.migrations.atomic and so on. The public path is ferro.migrations.chunked (__init__.py:37,53). The anchor and heading id become ferro.migrations.steps.chunked. |
::: ferro.migrations.chunked. |
| L | ferro.query.builder.Query / ProjectedQuery |
22 | Documented by private module path. Anchors are #ferro.query.builder.Query, so a cross-ref to the public ferro.query.Query relies on alias resolution. |
::: ferro.query.Query, ::: ferro.query.ProjectedQuery. |
| L | Category headings | 5 | The rendered category headings are inconsistent: "Attributes" against "Methods:" (with a colon). They become h3s in the TOC, so every class adds two TOC lines. | Set show_category_heading = false; members already carry their kind label. |
| L | FerroNullable docstring |
22 | It opens "Alembic column nullability:", but the type is used by every emitter, not only Alembic. | "Column nullability: …". |
docs/pages/api/connection.md
- Kind (rule 1): reference with an embedded how-to (the
PassReportsnippet, settings prose) - Running example (rule 3): none (the
author.slugPassReportsnippet only) - Verdict: The prose is accurate and short. Problems come from the rendered entries (
engines, the RustErrorsheadings, dunders). B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 1 | 5, 6 | The title "Connection & Registry" groups sessions, connect, the registry, the pass report and settings. The Session entry comes first, before connect, which reverses the reader's order. |
Order the directives connect, PoolConfig, Session/engines/current_session, transaction-adjacent, registry, then reports and settings. Use H2 groups like migrations.md does ("Connecting", "Sessions", "Registry", "Auto-migrate reports", "Project settings"). |
| L | 3 | 22 | "registers a (optionally named) connection pool" | "registers a connection pool, optionally under a name." |
| L | 37–46 | 7, 8 | Good lead-in and output shown. The result sentence after the block is missing. | Add: "Each ExecutedStatement names its table (subject) and its role; warnings are typed Reports." |
docs/pages/api/exceptions.md
- Kind: reference with explanation (the long refusal paragraph, l.74–94)
- Running example:
Userduplicate email,Paircheck - Verdict: The tree diagram is good. One 20-line paragraph (l.74–94) restates the migrations page's error entries. B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 74–94 | 1, 15 | One sentence of about 150 words describes four subclasses and their attributes, which repeats the DatabaseAheadError etc. entries on api/migrations.md. |
Cut it to "A refused migration call raises MigrationRefused or a subclass; see Migration errors" and keep the try/except example. |
| M | 39 | 8 | print(exc.sqlstate) # SQLSTATE "23505" on Postgres, result code "2067" on SQLite is good. The CheckViolationError example uses "e.g." in its output comment. |
Show the actual value: # ck_pair_at_most_one_side. |
| L | 24 | 22 | "— it is not rewritten." (em dash aside) | "exc.sqlstate is the raw driver code; Ferro does not rewrite it." |
| L | 50 | 15 | The ModelDoesNotExist paragraph is one long sentence with two dashes. |
Split it into three sentences. |
docs/pages/api/fields.md
- Kind: reference
- Running example: none
- Verdict: The intro is fine. The rendered
Fieldentry inherits Pydantic's docstring with broken links and an unresolved autoref (see the build warnings). B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| H | rendered Field |
24 | Two 404 links (concepts/strict_mode, concepts/unions) and the unresolved FieldInfo autoref. |
See "Build warnings explained". |
| M | rendered Field |
21 | The Field signature lists about 40 Pydantic parameters before Ferro's. The Ferro-specific options (primary_key, unique, index, db_type, db_check, renamed_from, retired, nullable) are lost in the list. |
Lead the docstring with a "Ferro options" paragraph or table, and say the rest are Pydantic's Field with a link to Pydantic's docs. |
| L | 3 | 22 | "FerroField, DbType, and friends" |
Name them all, or say "the types below". |
docs/pages/api/migrations.md
- Kind: reference with how-to intro
- Running example: none (
0008_drop_bio) - Verdict: The best-structured API page: H2 groups, and links out to the guides. A−/B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 15 | 15 | The "Running migrations" paragraph is 6 sentences of parameter semantics (using=/url=/settings=). It is useful but dense. |
Turn it into a 3-row table: parameter, meaning when None, error when both are given. |
| L | 65 | 22 | ::: ferro.migrations.steps.chunked uses the private path. |
ferro.migrations.chunked. |
| L | 83 | 22 | "These three load on first use, so the calls above never import Alembic." | Fine; "these three functions". |
docs/pages/api/model.md
- Kind: reference
- Running example: none
- Verdict: The intro is good. The rendered
Modelentry is buried in internals (10__ferro_*__attributes beforesave). B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 5 | 21 | ::: ferro.Model renders __ferro_columns__, __ferro_pk__, __ferro_relation_specs__ and so on, ahead of the CRUD methods, because members_order = "source". |
Filter them (see the structure table). Consider members_order = "alphabetical" or explicit members: grouped as declare, read, write. |
| L | 3 | 15 | "The lifecycle is: define… connect… then perform CRUD…" is a long chain. | Fine; optionally use a 3-item list. |
docs/pages/api/queries.md
- Kind: mixed: explanation and reference. Lines 3–37 are guide-level design explanation with 7 ADR mentions.
- Running example: fragments only (
t.account,t.amount) - Verdict: Accurate, but the page front-loads about 1,200 words of semantics before the first entry, which duplicates guide/queries.md and guide/aggregations.md. C
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| H | 3–37 | 1 | Six dense paragraphs explain negation, traversal, existence tests, include, select overloads and aggregates. Each ends by pointing at the guide that already covers it. A reader looking up Query.order_by scrolls past all of it. |
Keep the select() overload table (real reference). Reduce the rest to one sentence each with a link, or move it into the entries' docstrings. |
| M | 5, 7, 9, 13 | 22 | "(ADR-0008)", "(ADR-0006)", "(ADR-0007)" and "#309" are unlinked internal references. | Remove them, or link them. |
| M | 13–15 | 15 | "Loud limits, at build time:" opens a 70-word sentence. | Use a 3-item list of the errors. |
| M | 3 | 7 | "Predicates are lambda-only (User.where(lambda user: user.age >= 18))" is correct against the code (operator style raises AttributeError). |
Keep it. Note for the lead: AGENTS.md I-8 is stale here. |
| L | 21–26 | 22 | The table uses lambda t:; the guide convention is the model name. |
Use lambda transaction: in predicate rows; t is fine for selectors if the guide agrees. |
docs/pages/api/raw-sql.md
- Kind: reference
- Running example: none
- Verdict: Short, correct, and it renders cleanly. A
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| L | 3 | 22 | "doesn't cover" | "does not cover" (reference register); optional. |
docs/pages/api/relationships.md
- Kind: reference
- Running example: none
- Verdict: The intro is fine. The rendered
Relationentry lacks its inherited and M2M methods;BackRefshows only__new__/__class_getitem__. B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 11 | 21 | ::: ferro.Relation shows all, first and __get_pydantic_core_schema__ only. |
options: {inherited_members: true} plus filters, and move add/remove/clear. |
| L | 3 | 22 | "ManyToMany(...) for join-table relations" |
Fine. |
docs/pages/api/transactions.md
- Kind: reference
- Running example: none
- Verdict: Clear intro. The
Transactionmethods have no docstrings, and__init__(route)leaks an internal constructor. B
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 7 | 21 | Transaction.execute/fetch_all/fetch_one render with no text; __init__(route) and __slots__ render as public. |
Add one-line docstrings ("Run sql on this transaction's connection; see ferro.execute.") and filter the dunders. |
| L | 3 | 15 | "It can route by using= or by an explicit/ambient session" |
"Route it with using=, an explicit session=, or the ambient session." |
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itUnblocked and fully specified; an agent may claim itsize:MNeeds a plan; one or two local workersNeeds a plan; one or two local workers
Activity
feat(docs): site configuration fixes and a strict docs build (#701) (#…
docs(api): core API docstrings as reference entries with runnable exa…
docs(api): migrations, pass report and CLI docstrings as Google-style…
docs(api): connection, session, field, raw SQL, row security and sett…
0x054 commentedon Oct 19, 2026
Status: verify-passed
Worker: worker-722
Branch:
sdd/722-api-completenessPR: #748
Verify: passed bdff07d
Review (reviewer-722): Spec PASS, Quality PASS. Verify passed on
bdff07d(strict build clean,tests/test_docs_site.py19 passed, full suite 3514 passed). Base5c2bdd5unchanged, so no trial merge was needed.Accepted: report types documented under their module paths (no
__all__exports them; exporting them is a human API decision), the declared out-of-owned edits, and bothzensical.tomloptions (from the audit rows).One Minor follow-up:
RunEvent, theup(on_event=...)payload, has no entry.docs(api): every export has one API entry under its public path (#722) (
0x054 commentedon Oct 19, 2026
Merged in #748 into
feat/docs-rewritedocs: six-tab nav, page descriptions and redirects for moved pages (#723