Skip to content

API reference completeness: every public object has an entry, pages named after their objects #722

Closed

Description

@0x054

Parent

#698

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 = false and 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

Test scenarios

  • Happy path: the new assertion passes on the built site.
  • Error path: removing ::: ferro.QueryNode makes it fail naming QueryNode.

Verify

uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheprovider

Blocked by

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 PassReport snippet, settings prose)
  • Running example (rule 3): none (the author.slug PassReport snippet only)
  • Verdict: The prose is accurate and short. Problems come from the rendered entries (engines, the Rust Errors headings, 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: User duplicate email, Pair check
  • 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 Field entry 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 Model entry is buried in internals (10 __ferro_*__ attributes before save). 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 Relation entry lacks its inherited and M2M methods; BackRef shows 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 Transaction methods 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."

Activity

  1. added
    documentationImprovements or additions to documentation
    ready-for-agentUnblocked and fully specified; an agent may claim it
    size:MNeeds a plan; one or two local workers
    on Oct 19, 2026
  2. added theissue type on Oct 19, 2026
  3. self-assigned this
    on Oct 19, 2026
  4. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Status: verify-passed
    Worker: worker-722
    Branch: sdd/722-api-completeness
    PR: #748
    Verify: passed bdff07d

    Review (reviewer-722): Spec PASS, Quality PASS. Verify passed on bdff07d (strict build clean, tests/test_docs_site.py 19 passed, full suite 3514 passed). Base 5c2bdd5 unchanged, 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 both zensical.toml options (from the audit rows).
    One Minor follow-up: RunEvent, the up(on_event=...) payload, has no entry.

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #748 into feat/docs-rewrite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itsize:MNeeds a plan; one or two local workers

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions