Repository navigation
Docs rewrite: every snippet runs, every fact matches the code, and the API reference renders cleanly #698
Description
Problem Statement
A developer reads Ferro's quickstart, copies the "Filtering with where()" block into a script, and runs it:
adults = await User.where(lambda user: user.age >= 18).all()
IndentationError: unexpected indent
They remove the indentation and run it again:
RuntimeError: No database route
Since 0.13 every ORM call has to run inside async with engines.session():. Every embedded snippet starts inside that block, so no page shows it. The SQLAlchemy migration page goes further and tells the reader there is "no session to manage".
A full audit of the site against the writing-for-humans rules and the repo's invariants (I-7, I-8, I-11) found this pattern everywhere. It covered 51 pages, 46 example scripts and 334 public docstrings, and graded 21 of the 51 pages C (substantial rework). It counted about 100 serious findings (wrong facts, broken examples, invariant breaches), 300 rule breaches a reader would notice, and 240 polish items.
Wrong facts on the pages people read first:
- The field type table says
strbecomesTEXT; Ferro createsvarchar. The same page's own DDL contradicts the table. - The upgrade guide stops at 0.15 although the package is at 0.25.0, and one of its recipes subclasses a
Ferroclass that doesn't exist. - The FAQ says Ferro has no eager loading, but
include()ships. - The front page says Pydantic is the only dependency;
pydantic-settingsis required too. - The contributing guide gives the wrong license and links to a GitHub Discussions page that is turned off.
Recipes that teach the wrong thing:
- The Pagination how-to builds keyset paging by hand and never mentions
Query.after()/.before(). - On SQLite, the Timestamps how-to's
update(...)recipe writes a different datetime format fromsave(), which is the exact bug I-13 exists to prevent. - The schema guide says auto-migrate ignores a rename hint, contradicting the rest of the section.
An API reference that shows markup and internals instead of the API:
- About 280 Sphinx roles render as literal text, for example ":class: ~ferro.checks.Check".
- About 180 members with no docstring, the dunder methods and the internal
__ferro_*__registry attributes all appear, alongside 191 source blocks. ferro.migrate'sReturns:section renders as five broken table rows.- The
FerroFieldoption table is silently dropped. - Five public objects (
FieldProxy,QueryNode,RelationProxy,ValueExpr,ModelConnection) have no entry.
Pages that hide the answer:
- About 50 snippet sections end just above the assert that holds the result, so the reader sees a query but never what it returns.
- Most guide pages switch between 3 and 14 unrelated models.
- No query example shows the SQL it produces.
- About 200 headings are in Title Case or phrased as slogans.
- User-facing pages cite about 100 ADR numbers, issue numbers and internal codenames the reader cannot follow.
A CLI whose help text loses words: ferro migrate rerecord --help prints "The step whose edit to accept: :, as 0007:01". The <migration>:<step> placeholders are swallowed as markup.
An invariant that no longer matches the code: AGENTS.md I-8 still orders predicate styles as "lambda > col() > operator" and calls operator style "compatible today". Both col() and operator predicates were removed in 0.14, and where() now rejects anything that isn't a callable. The part of I-8 that does still apply, naming the lambda parameter after the model, is broken about 160 times (lambda t:), and the Queries guide teaches "t by convention".
These defects got through because nothing catches them:
- 184 inline code blocks are compile-checked and never run.
- 12 error checks in the examples assert only inside
except, so they pass when the error stops being raised. - Quoted generated migration files are not compared with what the generator writes.
- The site build is not strict.
- No test looks at the rendered pages.
Solution
A developer can copy any block from any page and run it. Each page opens by saying what it is, keeps one set of models from top to bottom, and shows what every call returns, with the SQL beside the query. When the reader picks the Annotated tab once, every tab pair on the site follows. The API reference shows the public API, with linked types, parameter tables and one-line summaries in one consistent form. A reader who meets "RLS" or "DDL" can hover over it for the definition. Old URLs redirect to their new location.
Three test seams keep it that way:
-
The built site. One test builds the site the way CI does (strict, so a warning fails the build). It then checks the rendered HTML for the defect classes this audit found:
- literal Sphinx roles
Returns:sections split into broken rowsAttributes:/Args:sections griffe dropped- snippet code that keeps its leading indentation
- public exports with no API entry
This catches docstring and config regressions together, because both end up on the rendered page.
-
The docs examples harness, extended.
- Inline page blocks that do real work run, or move into examples that run.
- Each lambda passed to a query method takes a parameter named after its model.
- An error check fails when the error it expects is not raised.
- Docstring examples run against a shared running example.
-
The migrations docs harness, extended. Every quoted generated step file is compared with a fresh generation, and every
ferro … --helptext quoted on a page keeps its placeholders.
Prose rules that no test can check, such as page kind, lead-in and result sentences, headings and minimising words, are checked in review against writing-for-humans.
The site tooling moves to what Zensical 0.0.71, pymdown-extensions 12.3 and mkdocstrings-python 2.0.9 offer. On this branch the docs dependencies are already raised to those versions and relocked, and the site builds with one warning, which this work removes.
User Stories
Copying and running code
- As a new user, I want every embedded snippet to render without leading indentation, so that pasting it into a file doesn't raise
IndentationError. - As a new user, I want the first code block on each page to show the
engines.session()wrapper, so that my copy runs instead of raisingNo database route. - As a new user, I want every page that shows a fragment to say where it runs and link to Connections › Sessions, so that I know which code surrounds it.
- As a reader of any page, I want every code block I can copy to have been executed by the test suite, so that I can trust it runs as shown.
- As a reader of the testing how-to, I want the fixtures it tells me to copy to be run by the test suite, so that my test setup works the first time.
- As a maintainer, I want an example that claims "this raises" to fail its test when the error is no longer raised, so that the documented limits stay true.
- As a reader of the schema guide, I want every generated migration file quoted on a page to match what
ferro migrate newwrites today, so that what I review in my repo matches the docs. - As a CLI user, I want
ferro migrate <command> --helpto print its<name>,<model>,<table>.<column>and<migration>:<step>placeholders, so that I can read the argument formats.
Correct facts
- As a reader choosing column types, I want the field type table to show the type Ferro actually creates on each database, so that I can predict my schema.
- As a maintainer, I want the field type table built from the same storage resolution the emitters use (I-1 item 3), so that it cannot drift from the DDL.
- As a reader evaluating Ferro, I want the front page, Why Ferro and the FAQ to state the real dependencies, the real migration system and the real feature set, including
include()and atomic updates, so that I judge Ferro on facts. - As a user upgrading Ferro, I want the upgrade guide to cover every release up to the current one, so that I know what each upgrade asks of me.
- As a user upgrading Ferro, I want every upgrade recipe to use real classes and to show both declaration styles, so that I can apply it.
- As a reader of the connections, mutations and performance pages, I want the identity map described as per-session, so that I understand when two loads return the same object.
- As a Postgres user, I want the backends page to say JSON fields are stored as
jsonband to list thetimetype, so that I can index and query them correctly. - As a contributor, I want the contributing guide to state the actual license and link only to channels that exist, so that I contribute in the right place under the right terms.
- As a CLI user, I want the CLI reference to list every flag the CLI accepts, including
ferro migrate up --allow-ahead, so that the reference is complete. - As a reader of the schema guide, I want every page to describe rename hints the same way auto-migrate actually handles them, so that I am not surprised by a held-back table.
- As a reader of the raw SQL guide, I want examples to query the table names Ferro creates (
user, notusers), so that my queries hit real tables. - As a reader of the relationships guide, I want the
NO ACTIONrow to say the constraint is enforced, so that I choose delete behaviour correctly. - As a reader of the transactions guide, I want to be told that a rollback leaves my Python objects modified, so that I reload them before reusing them.
- As a reader of the model and field docstrings, I want
nullabledescribed as theNOT NULLit sets in every emitter, so that I don't think it only affects Alembic.
Recipes that teach the right approach
- As a developer paging results, I want the Pagination how-to built on
Query.after()and.before(), so that I use the cursor paging Ferro ships instead of hand-rolling it. - As a developer exposing paged results over HTTP, I want the API endpoint example to open a session and run, so that I can start from it.
- As a developer adding timestamps, I want the Timestamps recipe to write datetimes the same way
save()does, so that ordering, precision and timezone survive on SQLite. - As a developer sharing fields across models, I want one page to explain what happens when a
Modelbase class declares fields, so that I learn the one true side effect (the base gets a table of its own) rather than three conflicting reasons. - As a developer writing tests, I want the testing how-to to catch
UniqueViolationErrorrather thanException, so that my tests check the error I mean.
Reading a page
- As a reader, I want each page to be one kind (tutorial, how-to, reference or explanation), so that I find the facts I came for without wading through other material.
- As a reader, I want each page to open with one sentence saying what it covers, then an example on the first screen, so that I can tell at once whether the page answers my question.
- As a reader, I want each page to use one running example, introduced once, so that each new section shows only what changed.
- As a reader, I want every code block introduced by a sentence ending in a colon and followed by a sentence on what changed, so that I know what to look at.
- As a reader, I want to see what each query returns, as printed output or as the asserted value, so that I can check my understanding.
- As a reader coming from SQL, I want the generated SQL shown beside each query and each schema feature, so that the ORM call becomes concrete.
- As a reader, I want example data that reads like real records, has about four rows and has no tied values, so that I can verify results by eye.
- As a reader scanning a page, I want headings that are short sentence-case noun phrases naming the feature, often with the API name in code, so that I find sections and link text makes sense.
- As a reader, I want how-to titles to name the goal ("Paginate query results"), so that the page title tells me what it gives.
- As a reader, I want sections ordered from the common case to the specialised one and ending with a link to the reference, so that I can stop once my case is covered.
- As a reader, I want no words that minimise difficulty ("simply", "easy", "trivial"), so that I'm not made to feel slow when something is hard.
- As a reader, I want admonitions used sparingly, each kind for one job, so that warnings stand out when they matter.
- As a reader, I want version markers ("Added in 0.18") as self-contained blocks at the bottom of a section, and no dated or release-bound notes in the body, so that pages don't go stale when the next release ships.
- As a reader, I want pages and docstrings free of ADR numbers, issue numbers, codenames and contributor-only references, so that every reference I see is one I can follow.
- As a reader of the guide, I want option tables, exit codes, the auto-migrate matrix and the door comparison to live once in the reference and be linked from guides, so that the copies can't disagree.
- As a reader of the row-level security page, I want explanation, setup steps and reference split apart, and a scoped query shown next to an unscoped one, so that I see what RLS does to my results.
- As a reader of the migrations guide, I want squash on its own page and the tutorial loop on another, so that each page has one job.
- As a reader, I want every lambda in the docs to name its parameter after the model (
lambda user: user.age), so that the predicate reads like the sentence it means (I-8).
Navigating the site
- As a reader, I want one Reference tab covering the Python API, CLI, configuration and exceptions, so that I don't have to guess which of two reference tabs holds
FerroSettings. - As a reader, I want nav labels that match page titles and name what each page gives ("Testing migrations" or "Testing with Ferro", not "Testing" twice), so that search results and breadcrumbs are unambiguous.
- As a reader, I want Why Ferro inside Get started, and the upgrade guide beside the changelog, so that the nav follows the product rather than the page history.
- As a reader following an old link to the former migrations page, I want to be redirected to the section that replaced it, so that bookmarks keep working.
- As a reader, I want a breadcrumb path, previous and next links in the footer, and instant page loads with a progress bar, so that moving through a section is quick.
- As a reader who prefers the Annotated style, I want my tab choice to carry across every Assignment/Annotated pair on the site, so that I pick it once.
- As a reader, I want tab anchors that read as words (
#defining-a-model-annotated), so that I can link to a specific tab. - As a reader, I want hover definitions for terms such as DDL, RLS, FK, PK, GUC and FFI, so that I don't leave the page to look them up.
- As a reader of the changelog, I want
#NNNreferences linked to their GitHub issues, so that I can read the change behind an entry. - As a reader, I want a theme toggle that can follow my system setting, so that the site matches my OS.
- As a reader, I want pages marked
newordeprecatedin the nav, so that I notice changes. - As a reader sharing a link, I want each page to carry its own description and a social card in Ferro's colour, so that previews say what the page is.
- As a reader using an AI assistant, I want an
llms.txtindex and a "Copy as Markdown" action on each page, so that I can hand the docs to a tool. - As a reader using a screen reader, I want permalink anchors announced with a meaningful title, so that the heading links make sense.
The API reference
- As an API reader, I want only public members listed, constructors merged into their class, and no source blocks, so that the page shows the API rather than the implementation.
- As an API reader, I want signatures on their own line, with type annotations that link to their definitions (Ferro, Pydantic and the standard library), so that I can follow a type.
- As an API reader, I want a symbol-type badge and a summary table at the top of each object, so that I can scan a module.
- As an API reader, I want cross-references to render as links, not as
:class:text, so that I can move between related objects. - As an API reader, I want every parameter in an
Args:table with its default and type, includingsessionandusing, so that I don't dig through paragraphs for them. - As an API reader, I want every dataclass and Pydantic field documented with one line, so that report and config objects explain themselves.
- As an API reader, I want every summary line to be one line, in the infinitive ("Fetch one record by primary key."), ending with a period, so that the reference reads as one pattern.
- As an API reader, I want
Returns:andAttributes:sections to render as proper tables, so that return values and field options are readable. - As an API reader, I want
Fieldshown once rather than as six near-identical overloads, so that I can read its parameter list. - As an API reader, I want inherited methods such as
Relation.all()and.first()to show their inherited docstring, so that they don't appear blank. - As an API reader, I want
FieldProxy,QueryNode,RelationProxy,ValueExpr,ModelConnection,engines.session(),RunReport,StatusReportandCheckReportdocumented, so that every public object and every return type has an entry. - As an API reader, I want docstrings to stay short reference entries and link to the guide for the reasoning, so that
connect,PoolConfigandSession.set_configdon't open with 90-line essays. - As an API reader, I want the main entry points (
create,get,where,save,delete,after,before,position_of,migrate) to carry an Examples section that runs, so that each one shows its use.
Maintainers and contributors
- As a maintainer, I want CI to build the docs in strict mode, so that a broken reference or warning fails the PR.
- As a maintainer, I want a test over the rendered site that fails on literal Sphinx roles, split return rows, dropped sections, indented snippets and undocumented exports, so that rendering regressions are caught where readers see them.
- As a maintainer, I want a lint that fails on a one-letter lambda parameter in docs examples, inline blocks and docstring examples, so that I-8 holds without review effort.
- As a maintainer, I want AGENTS.md I-8 to describe the lambda-only query API that exists today, so that agents stop enforcing rules for removed APIs.
- As a maintainer, I want the docstring conventions (Google style, infinitive one-line summary, Markdown cross-references, every parameter in
Args:, no internal references) written down as an AGENTS.md invariant, so that new code keeps the API reference consistent. - As a maintainer, I want the conventions this work settles recorded in
docs/solutions/, so that the next contributor finds them (I-5): running example per page, snippet markers below the asserts, generated SQL beside the query, glossary terms. - As a maintainer, I want
# noqacomments kept out of rendered examples, so that readers don't see lint noise. - As a maintainer, I want the Assignment/Annotated twins to use one Annotated idiom, so that the tabs differ only in declaration style (I-7).
- As a maintainer, I want the skipped many-to-many documentation tests either running or deleted, so that no test is skipped for a reason pointing at a page that no longer exists.
Implementation Decisions
Dependencies. On this branch the docs dependency group and the dev group require Zensical 0.0.71, mkdocstrings-python 2.0.9 and pymdown-extensions 12.3, which brings griffe 2.3.2 (published as griffelib). The griffe-inherited-docstrings extension joins the docs group. Neither changelog breaks the current config.
Zensical configuration
- Theme features added:
content.tabs.link,navigation.instant,navigation.instant.progress,navigation.tracking,navigation.path,navigation.footer,content.code.select,content.tooltips,content.action.copy. search.suggestis removed, because nothing in 0.0.71 reads it.- The palette gets a third "follow system" toggle.
extra.sociallinks GitHub and PyPI.extra.statusdefinesnewanddeprecatedbadges.- Plugins added:
redirects, with anchor redirects that replace the hand-written redirect page for the old migrations URL; that page is deleted.llmstxt.social, with Ferro's rust colour as the card background.
- Every page gets front matter with a
description. - The docs build in CI runs in strict mode.
- The
llmstxtMarkdown copies drop content-tab labels. That is reported upstream; it does not block adoption.
Markdown extensions
pymdownx.snippets.dedent_subsectionsis on.pymdownx.snippets.auto_appendadds one glossary of abbreviations to every page. Its terms come from CONTEXT.md's language where one exists.pymdownx.tabbedusescombine_header_slugwith a lower-case slugify.abbris on.pymdownx.magiclinkpoints atsyn54x/ferro-orm.toc.permalink_titleis set.def_list,footnotes,pymdownx.blocks.captionandpymdownx.saneheadersare turned on by the first page that needs them.arithmatex,caret,tilde,critic,tasklistand theblocks.*admonition syntax are not adopted. No page uses math, and the existing!!!and===syntax covers the rest.
mkdocstrings and griffe
- Inventories: Python, plus Pydantic's.
- Options:
filters = "public"merge_init_into_classwithignore_init_summaryseparate_signature,show_signature_annotations,signature_crossrefsshow_symbol_type_heading,show_symbol_type_tocsummarybacklinks = "tree"scoped_crossrefs,relative_crossrefsshow_source = falseshow_if_no_docstring = false, once every public object has a docstring
- Extensions:
griffe-inherited-docstrings. - Directive-level options:
show_overloads: falseonField, andparameter_headingsonField, so guides can link straight to one option. - Not adopted:
griffe-pydantic: it hides field constraints and lists a private validator.unpack_typeddictandgriffe-typingdoc: both crash the build.griffe-sphinx: Sphinx roles are converted to Markdown cross-references rather than kept.- generics, modernized-annotations and the public-alias extensions: they change nothing for Ferro.
griffe-warnings-deprecatedis adopted in the same change that moves Ferro's deprecation helper onto the standard library'swarnings.deprecated, not before.
CLI help text renders as plain text, not Markdown, in both the root app and the migrate app, so placeholders survive. Command docstrings follow the same writing rules as API docstrings.
Docstring conventions, written into AGENTS.md as a new invariant beside I-7, I-8 and I-13:
- Google style as griffe parses it.
- A one-line summary in the infinitive, ending with a period.
- Every parameter in
Args:. - Every public dataclass or Pydantic field gets a one-line docstring.
- Cross-references as Markdown autorefs, never Sphinx roles.
- No ADR numbers, issue numbers, codenames or Rust internals.
- Examples use the lambda style and the docs' running example.
- Explanations longer than a paragraph move to a guide or concepts page and are linked.
AGENTS.md I-8 is rewritten to the lambda-only API that exists: lambda predicates, the parameter named after the model in lowercase singular, and order_by's string or lambda selector. The col() and operator-style clauses and the "slated for deprecation" text go. The writing-for-agents rules apply to the edit. The Queries guide's predicate-styles section and the Typed Query Predicates concepts page change to match.
Information architecture
- The nav becomes these tabs:
- Get started: Why Ferro, installation, quickstart.
- Guide
- How-to
- Concepts
- Reference: Python API, CLI, configuration, exceptions.
- Project: FAQ, roadmap, upgrading between versions, changelog, contributing.
- Labels are sentence case and match their page's H1.
- Adopting migrations moves under Schema management.
- Same-named pages get distinct names: "Testing with Ferro" and "Testing migrations"; API pages are named after their object.
- The Next steps page is folded into the quickstart's closing section.
Page structure
- The row-level security guide splits into an explanation, a how-to and a reference.
- The migrations guide splits into the tutorial loop, separate how-tos (squash gets its own page) and reference tables.
- Reference tables that today appear in two or three places each get one home in Reference and are linked from the guides: the auto-migrate matrix, the
PassReportkinds, the migration header table, the exit codes, the door comparison and the expand/contract table.
Running examples
- Each guide page declares one model set at the top and reuses it.
- The API reference and docstring examples share one running example, declared once and preloaded by the docs examples harness.
- Snippet section markers move below the asserts or prints that hold each result, so the page shows the answer.
- Example data uses about four real-looking rows with distinct totals.
Generated SQL is shown beside each query and each schema feature. It is captured from what Ferro actually runs, not written by hand, so the test that runs the example also produces the SQL it shows.
The field type table is generated from the storage resolution function the emitters already share (I-1 item 3), over both dialects, and the page embeds the result.
Content fixes cover every high-severity finding in the audit: the facts and recipes listed in user stories 9–27, and the snippet-level breakages (the ForeignKey docstring example, undefined names in docstring examples, the raw SQL table names, the aggregations comment that contradicts its assert, and the row-level security example's undeclared membership table).
Testing Decisions
A good test here checks what a reader gets, not how the site is built:
- the page renders as intended
- the block runs and produces what the page claims
- the quoted file matches what the tool writes
- the help text prints its placeholders
No test inspects the docs config or the Markdown source for its own sake.
Seam 1: the built site (new).
- One test module builds the site in strict mode into a temporary directory, once per session.
- It asserts over the rendered HTML: no literal Sphinx roles, no
Returns:orAttributes:table with sentence-fragment rows, no snippet<code>whose lines all share leading indentation, an API entry for every name inferro.__all__andferro.migrations.__all__, and no unresolved cross-reference. - It runs in the docs CI job.
- Prior art: the strict build the publish-docs workflow already runs, and the existing documentation feature tests.
Seam 2: the docs examples harness (extended).
- Inline blocks that do real work run through the harness's existing
{test=run}opt-in, or move into example scripts. A block left as compile-only states why in its marker. - A lint over example scripts, inline blocks and docstring examples fails when a lambda passed to a query method (
where,select,order_by,include,existsand the others) takes a one-letter parameter. - An error-check pattern fails when the expected error is not raised.
- Docstring examples are collected with pytest-examples, already a dev dependency, and run against the shared running example inside a session.
- Twins are checked for equivalent models, the way the migration pairs already are.
- The testing how-to's fixtures run in a real test.
- Prior art: the existing docs examples harness, and the migration twin-equivalence check.
Seam 3: the migrations docs harness (extended).
- Each quoted generated step file is regenerated from the example models in a scratch project and compared after normalising timestamps.
- Each
--helptext a page quotes is compared with the CLI's actual output, and asserted to contain its placeholders. - Prior art: the harness already pins every quoted CLI output block. The adopting-migrations page is the model for pinned output.
Field type table: a test generates the table from the storage resolution function and compares it with what the page embeds. Prior art: the cross-emitter type parity tests.
Reviewed, not tested: page kind, opening sentence, lead-in and result sentences, headings, minimising words and admonition use are checked in review against writing-for-humans. The review of each ticket that touches a page cites the rule numbers it applied.
Out of Scope
- Versioned docs (a version selector). Today it needs a mike fork that pushes to
gh-pages, which clashes with the current Pages deploy. Zensical has native versioning on its roadmap; this is revisited when it ships. griffe-pydantic. Its trade-offs (hidden constraints, a listed private validator) outweigh the sections it adds. Revisit if those are fixed upstream.- Math rendering (
arithmatex). No page contains math. - Changing Ferro's runtime behaviour. Where the audit found a doc and the code disagreeing, the doc changes. The one exception is the CLI help-format setting, which is user-facing text. Where a finding is unverified (the RLS round-trip count and which open transactions receive a new
set_configvalue), the ticket confirms the code's behaviour first and documents it. - CHANGELOG.md edits. I-10 still applies; release tooling records this work.
- The internal docs trees (ADRs, brainstorms, plans, research, solutions) are not part of the published site, so they are not audited or rewritten. Only links from published pages into them change.
Further Notes
- The audit report, which has line-level findings, rule numbers and suggested fixes for every page, example and module, is at https://claude.ai/artifact/TgPXfCFqL5VAUPmHEhqsCv. Tickets should cite its section numbers (01–13).
- Python-Markdown needs a blank line before a list and 4-space indentation for a nested list. A list that breaks either rule renders as a paragraph. The pages haven't been scanned for this yet; the ticket that adds the rendered-site test should include the check.
- The suggested order of work:
- Config-only changes, which fix the indentation, the build warning, the API noise, tab linking and the CLI placeholders in one small PR.
- Correctness, plus the I-8 rewrite.
- The three test seams.
- The docstring pass.
- Page restructuring.
Spec deltas
- MODIFIED: captured SQL beside each query ships on four pages (Queries, Aggregations, Relationships, Mutations) in this epic; the remaining query-running pages and the hand-typed RLS and upgrading blocks are Docs: captured SQL beside the queries on the remaining pages (pagination, soft-deletes, timestamps, transactions, connections, quickstart, RLS, upgrading) #778. (Narrowing approved 2026-10-21.)
- MODIFIED: mkdocstrings
filtersis["!^_", "!^model_config$"]rather than"public", because griffe treats dunders as public; the public-surface outcome is tested. - ADDED: the
sqlfence marker-- <example>.py:<snippet> (<dialect>)andtests/test_docs_sql.py, regenerating every marked block from the example's own.sql()or the statement log (Show captured SQL beside query and mutation examples (needs #700) #725). - MODIFIED (behaviour, declared on PR fix: panel round 1 on #777 (src) — restore status flags, PoolConfig.validate and the no-route message; save(only=) in field order; docstring fences marked #779):
save(),save(only=),save(exclude=)andbulk_create()list their columns in model field order, sosave(only=)/save(exclude=)send the same statement text in every process (save(only=) / save(exclude=) emit columns in set order, so the UPDATE text changes between runs #776;update()keeps the call's keyword order, unchanged); the routeless-operationRuntimeErrortext reads "… removed in v0.14 (deprecated since v0.12); see the upgrade guide." (the old text named v0.13 and a removed page).Session.queryis typedtype[M] -> Query[M]; the private moduleferro.migrations.chunkedis_chunk_loop. All four are in the upgrade guide's 0.26 section. - REMOVED: none.
Activity
ferro migrate --helpprints<name>,<migration>:<step>and the rest #70211 remaining items
0x054 commentedon Oct 19, 2026
Plan
Goal: Every snippet on the docs site runs as shown, every fact matches the code, and the API reference renders the public API cleanly, kept that way by three test seams.
Constraints: Zensical 0.0.71, pymdown-extensions 12.3, mkdocstrings-python 2.0.9 / griffe 2.3.2 (#699 must merge first). AGENTS.md I-7, I-8 (as rewritten by #703), I-10, I-13 and the new I-14; writing-for-humans rules 1–24. Prose rules are reviewed, not tested. The audit report (private; owner can share) is the source of each ticket's findings rows.
Integration branch:
feat/docs-rewrite(created by build-epic)docs/includes/abbreviations.md,tests/test_docs_site.py; M:zensical.toml,.github/workflows/publish-docs.yml,.github/workflows/ci.yml,pyproject.toml+2 more; D:docs/pages/guide/migrations.mdferro migrate --helpprints<name>,<migration>:<step>and the resttests/test_cli_help.py; M:src/ferro/cli/__init__.py,src/ferro/cli/migrate.pydocs/solutions/patterns/docs-page-conventions.md,docs/solutions/patterns/docstring-conventions.md; M:AGENTS.md,docs/solutions/README.mdtests/_docs_checks.py,tests/test_docs_checks.py,tests/test_docstring_examples.py,docs/examples/_preludes/reference.py+1 more; M:tests/test_docs_examples.pydocs/examples/readme_quickstart.py,docs/examples/readme_quickstart_annotated.py; M:README.md,docs/pages/index.md,docs/pages/why-ferro.md,docs/pages/getting-started/installation.md+3 moredocs/pages/faq.md,docs/pages/roadmap.md,docs/pages/contributing.md,docs/pages/changelog.md+1 moredocs/examples/upgrade_*.py; M:docs/pages/howto/upgrade-guide.mddocs/pages/guide/queries.md,docs/pages/concepts/query-typing.md,docs/examples/predicates.py,docs/examples/existence_tests.py+8 moredocs/pages/guide/aggregations.md,docs/examples/aggregations.py,docs/examples/aggregations_annotated.pydocs/pages/guide/schema/overview.md,docs/pages/guide/schema/auto-migrate.md,docs/pages/guide/schema/data-steps.md,docs/pages/guide/schema/deploying.md+9 moredocs/pages/concepts/row-level-security.md,docs/pages/reference/row-level-security.md; M:docs/pages/guide/row-level-security.md,docs/examples/row_level_security.py,docs/examples/row_level_security_annotated.pydocs/examples/from_sqlalchemy.py,docs/examples/from_sqlalchemy_annotated.py; M:docs/pages/howto/migrate-from-sqlalchemy.mddocs/pages/concepts/architecture.md,docs/pages/concepts/identity-map.md,docs/pages/concepts/type-safety.md,docs/pages/concepts/backends.md+1 moresrc/ferro/models.py,src/ferro/base.py,src/ferro/metaclass.py,src/ferro/query/__init__.py+6 moresrc/ferro/pass_report.py,src/ferro/migrations/__init__.py,src/ferro/migrations/runner.py,src/ferro/migrations/report.py+18 moredocs/includes/field-types.md,tests/test_docs_field_type_table.py,docs/examples/models_and_fields.py,docs/examples/models_and_fields_annotated.py; M:docs/pages/guide/models-and-fields.md,docs/examples/table_checks.py,docs/examples/table_checks_annotated.pydocs/pages/guide/mutations.md,docs/pages/guide/transactions.md,docs/pages/guide/connections.md,docs/pages/howto/multiple-databases.md+3 moredocs/pages/guide/relationships.md,docs/pages/guide/raw-sql.md,docs/examples/raw_sql.py,docs/examples/relationships.py+1 moredocs/pages/guide/schema/squash.md,docs/pages/guide/schema/renames-and-retirements.md,docs/pages/reference/migrations.md; M:docs/pages/guide/schema/migrations.md,docs/pages/guide/schema/overview.md,docs/pages/guide/schema/auto-migrate.md,docs/pages/guide/schema/deploying.md+12 moretests/test_docs_testing_fixtures.py; M:docs/pages/howto/testing.md,docs/pages/howto/pagination.md,docs/pages/howto/timestamps.md,docs/pages/howto/soft-deletes.md+6 moresrc/ferro/__init__.py,src/ferro/_core.pyi,src/ferro/fields.py,src/ferro/columns.py+12 moredocs/pages/api/connection.md,docs/pages/api/exceptions.md,docs/pages/api/fields.md,docs/pages/api/migrations.md+6 morezensical.toml,docs/pages/**; D:docs/pages/getting-started/next-steps.mdtests/test_docs_lint.py; M:tests/test_docs_examples.py,tests/test_docs_site.py,tests/test_documentation_features.py,tests/_docs_checks.py+4 moredocs/pages/guide/queries.md,docs/pages/guide/aggregations.md,docs/pages/guide/relationships.md,docs/pages/guide/mutations.md+15 moreWaiting outside the epic: #725 is
needs-infoand blocked by #700 (SQL inspection and logging, not yet specced). It gets its Interfaces line andready-for-agentonce #700's spec names the API; it does not block closing the rest of the epic's work.Narrowed: #725 delivers captured, regenerated SQL on four pages (Queries, Aggregations, Relationships, Mutations); the epic asked for it beside every query and schema feature. The remaining pages (pagination, soft-deletes, timestamps, transactions, connections, quickstart, RLS, upgrading) are #778. Approved by the maintainer on 2026-10-21 at the PR #777 panel.
Deviated: #701/#722
filters = "public"→filters = ["!^_", "!^model_config$"]in zensical.toml (griffe treats dunders as public; the public-surface outcome is pinned bytest_model_api_page_shows_the_public_surface_only). Panel round 1 on PR #777:Session.querygainsdef query[M: Model](self, model_cls: type[M]) -> Query[M]andferro.migrations.chunkedis renamed_chunk_loop(kept, noted in the upgrade guide);migrate status --no-steps/--no-jsonandPoolConfig.validateare restored (PR #779); the routeless-operationRuntimeErrortext is the accurate wording, "… removed in v0.14 (deprecated since v0.12); see the upgrade guide." (the pre-rewrite text named v0.13 and a removed page; declared on PR #779 and pinned);save,save(only=),save(exclude=)andbulk_createlist columns in model field order (#776, PR #779;update()keeps the call's keyword order, unchanged), so the Mutations guide's SQL no longer depends on a hash-seed pin.chore(docs): upgrade Zensical, mkdocstrings-python and pymdown-extens…
0x054 commentedon Oct 19, 2026
Waves
Integration branch:
feat/docs-rewrite, cut frommainat2a45de9(after #699); now at7c19ce7after mergingmain(f3e46f8: #700's statement log and.sql(), #760's SecretStr fix) on 2026-10-21. Strict docs build clean; full suite 5078 passed, 69 skipped, 2 xfailed on sqlite+postgres.Status: 25 of 25 sub-issues merged and closed (#701–#725). PR #777 to
mainopened 2026-10-21; review-panel Round 1: FIX FIRST (#777 (comment)): 1 Critical (a docs commit removedmigrate status --no-steps/--no-json), 12 Important, 12 Minor; Spec FAIL on--allow-aheadmissing from the CLI reference and captured SQL on four pages only. Maintainer decisions 2026-10-21: restore the status flags,PoolConfig.validateand the no-route message (keep the typedSession.queryand the_chunk_looprename, noted in the upgrade guide); the four-page SQL scope is a declared narrowing (rest in #778); #776 fixed in this round. F2 (ruff format) fixed by the orchestrator at 2a0205b. Maintainer decision (2026-10-19): hold the PR tomainuntil #700 ships and #725 is done.f1bf2a420d9f97e8c49cca6fa5224e49af3606e5796c7126f5001767853aaacacd11273482c64a7cbca6f8aa4b294b04ae3fc13f7b340fee6f8e7ca7bf0e39b8dbf125c2bdd5b0e534809714e8de9eaffc1fcefbEdges added: none
Orchestrator commits on the integration branch (trivial link repoints after parallel merges):
8fe7610,a10707f.Accepted deviations: #701
filters = ["!^_"]instead of"public"(griffe treats dunders as public). #721 removes the no-op--no-steps/--no-jsonflags offerro migrate statusand renamesferro/migrations/chunked.pyto_chunk_loop.py(oldfrom ferro.migrations.chunked import encode_cursornow fails). #720 removesPoolConfig.validate(it shadowed Pydantic's deprecated classmethod).Library bugs found during the epic, filed: #737, #751 (FK → JSON under
__future__annotations), #752 (optional FK → JSON), #753 (RLS cross-table policy ordering), #754 (hydrated field order, I-2), #755 (relation assignment), #756 (offset()withoutlimit()on SQLite), #757 (having()in an error), #758 (t.in error messages), #759 (export report types; feature).For close-epic's retro:
scripts/ready.shin the build-epic skill callsgh issue viewwith both--commentsand--json, which gh 2.102 rejects. Most fix rounds were dead anchors from pages merging in parallel; the anchor rule was added to later briefs. One wave briefly ran six workers (limit five). The sharedghlogin flipped to a second account twice mid-run.Wave: #725
Started: 2026-10-21T13:14Z
Edges added: none (edge #725 ← #700 removed: #700's API is merged; #700 stays open only for #767, which this epic blocks).
Fix round 1 on PR #777
Started: 2026-10-21T18:19Z
Edges added: none
Round 2 of the panel on PR #777 on 8ae9aee: FIX FIRST (#777 (comment)): Verify red on two
tests/test_step_class.pypins of the Deploying guide's old numbering (F26); five new Important (F27 the label-addition fence proves nothing on Postgres; F28 two connect() redirects; F29 four region parsers; F30 the shows: guard misses most Ferro output; F31 the 0.26 upgrade notes omit two behaviour changes); Spec and Coherence PASS; every Round 1 item ADDRESSED or declared.Fix round 2 on PR #777 (the last)
Started: 2026-10-21T20:11Z
Round 3 re-review follows; a FIX FIRST there becomes HUMAN DECISION.
Round 3 of the panel on PR #777 on f20bcca: HUMAN DECISION (#777 (comment)): Verify green (full suite 5743 passed on sqlite+postgres), Spec and Coherence PASS, every Round 2 item ADDRESSED or declared; three Important open after the last fix round (F42 the 0.26 notes misstate the secret-column change; F43 the shows: guard's self-test is per block; F44
RunRefusedprinted but not exported). Maintainer decision 2026-10-21: fix F42 and F44 in one short PR before merging; F43 and the Minors filed as #783.Pre-merge fix (F42, F44)
Started: 2026-10-21T21:32Z
Verify on
feat/docs-rewriteat 0b72ba5: strict build clean,_docs_checks0, ruff clean, full suite 5858 passed / 69 skipped / 2 xfailed on sqlite+postgres. Follow-up filed from the fix: #785 (aSecretBytessave into a 0.25varcharcolumn on Postgres stores escaped text with no error). PR #777 is ready for the maintainer's merge.fix(docs): repoint two models-and-fields links renamed by the core gu…
fix(docs): repoint two connections links to the RLS concepts page
feat: SQL inspection and logging (#700) (#771)
fix: panel round 1 on #777 (src) — restore status flags and PoolConfi…
docs: panel round 1 on #777 (harness and pages) — output blocks regen…
docs: panel round 2 on #777 — green suite, honest Postgres fences, on…
docs: 0.26 notes state the secret-column change; RunRefused exported …