Repository navigation
docs: the docs rewrite — every page one kind, every block runs, the SQL beside every query (#698) - #777
Conversation
…docs conventions notes (#703) (#727) Closes #703 ## What AGENTS.md I-8 used to tell agents to order predicate styles "lambda > `col()` > operator" and call operator style "compatible today". Both were removed in 0.14, and `where()` raises `TypeError` on anything that isn't a callable. I-8 now describes the API that exists: ```python adults = await User.where(lambda user: user.age >= 18).all() recent = await Post.order_by(lambda post: post.published_at, "desc").all() ``` It covers lambda predicates, the parameter named after the model in lowercase singular, `order_by`'s string or lambda selector, and the `python -m tests._docs_checks` lint from #704. A new **I-14** sets the docstring rules: Google style as griffe parses it, a one-line infinitive summary (a noun phrase for classes), every parameter in `Args:`, a one-line docstring on every public field, Markdown autorefs, no ADR, issue, codename or Rust references, lambda-style examples against the `reference` prelude, and longer explanations moved into guides. Two new notes in `docs/solutions/patterns/`: - `docs-page-conventions.md` covers one kind per page, one running example, snippet `end:` markers below the result, the session wrapper, generated SQL (once #700 lands), the glossary file, and the one Annotated idiom for twins. - `docstring-conventions.md` shows each I-14 rule as a before/after pair. The Annotated idiom keeps the same `Field(...)` call as the Assignment tab and assigns the default outside `Annotated`: ```python # before (two idioms in today's twins) id: Annotated[int | None, Field(default=None, primary_key=True)] id: Annotated[int | None, FerroField(primary_key=True)] = None # after id: Annotated[int | None, Field(primary_key=True)] = None ``` I checked the reason the note gives. With the default inside `Annotated`, `ty` reports `C(name="a")` as `missing-argument: id`. With the default assigned outside, it passes. ## Verify Ran the ticket's Verify block on `337bcbf`: passed. Full suite: 3296 passed, 339 skipped, 2 xfailed. TDD: RED, the Verify greps against `origin/feat/docs-rewrite`'s AGENTS.md: `grep -q '^## I-14'` exited 1, and `grep -nE 'slated for deprecation|compatible today'` matched lines 433–434. GREEN: the Verify block passed on `337bcbf` (4/4). Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - `docs/solutions/README.md` has no index of notes, only layout and frontmatter rules, so it is unchanged. - I-8 and I-14 name artifacts that #704 creates: `tests._docs_checks`, the `reference` prelude, the `{test=run prelude=…}` fence marker and `tests/test_docstring_examples.py`. They are attributed to #704 in the text. - The I-13 example in AGENTS.md used `lambda m:`. I renamed it to `lambda message:` so the file follows its own I-8. - [ ] Exit steps: issue status updates (#703 progress comment, closure after merge into the integration branch) are complete before merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #702 ## What `ferro migrate rerecord --help` used to read "The step whose edit to accept: :, as 0007:01." because help was rendered as Markdown and `<migration>:<step>` was swallowed as an HTML tag. Both cyclopts apps now use `help_format="plaintext"`, so it prints `<migration>:<step>`, and `new` and `squash` keep `NN_<name>.up.sql`. ## Verify Ran the ticket's Verify block on `57ea0c6`: passed. TDD: RED `pytest tests/test_cli_help.py` failed for `migrate new`, `squash`, `rerecord` (3 failed, 10 passed); GREEN same command passed (13/13). Full suite: 3309 passed. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - `tests/test_cli_help.py` walks the command tree from `app._meta_parent`, collects each command's summary, docstring and parameter help, and asserts every `<placeholder>` is in that command's `--help` (whitespace folded, since the table wraps). - Exit-steps: issue status updates are complete (#702 closes with this PR). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s, lambda and error checks (#704) (#728) Closes #704 ## What A docs fence can now run against a small seeded database. Mark it like this on a page or in a docstring: ````markdown ```python {test=run prelude=reference} print(await Author.where(lambda author: author.id == 1).first()) ``` ```` The block runs in a subprocess after `docs/examples/_preludes/reference.py`, which declares `Author` and `Post`, connects to in-memory SQLite, adds two authors and two posts, and holds a session open. Top-level `await` works. If the block fails, the traceback names the page and its line (`File "docs/pages/guide/queries.md", line 42`). `tests/test_docstring_examples.py` finds every Python fence in `src/ferro` docstrings through griffe. It compiles each one and runs the ones marked with a prelude. `python -m tests._docs_checks PATH...` lists two mistakes and exits 1 if it finds any: ```text docs/examples/aggregations.py:217: lambda passed to select() takes one-letter parameter 't'; name it after the model in lowercase singular docs/examples/aggregations.py:216: try/except asserts on the error but has no else that raises, so it passes when the error stops happening; add else: raise AssertionError("expected TypeError") ``` It is not wired into the suite yet. #724 makes it a gate. ## Verify Ran the ticket's Verify block on `4c85007`: passed (26 passed; `test_docs_examples.py` 234 passed). TDD: RED `uv run --no-sync pytest tests/test_docs_checks.py tests/test_docstring_examples.py -q --no-cov -p no:cacheprovider` failed as expected (`ModuleNotFoundError: No module named 'tests._docs_harness'`, b593745); GREEN, the same command passed (26/26). Full suite: 3327 passed, 339 skipped, 2 xfailed. Verify block corrected: no ## Interfaces Interfaces changed: none ## Test scenarios - Happy path: a `{test=run prelude=reference}` fence that queries `Author` passes and prints `Ada Lovelace`. - Error path: a fence that raises `RuntimeError` fails, and the message names the page and line 5. - Edge case: `lambda author:` passes, `lambda a:` fails with its line, `sorted(key=lambda r: ...)` and `rows.sort(...)` are ignored. - Edge case: `try/except` that asserts without `else` is reported; with `else: raise AssertionError` it is not. - Integration: a scratch module's docstring example marked `{test=run prelude=reference}` is collected and runs. - Added: an unknown prelude fails and names the available ones; `prelude=` without `test=run` is refused; bare and quoted settings both parse; a fence inside a content tab and a fence in a function docstring are checked at their file lines; a default-captured lambda parameter (`f=field`) is not a query parameter. ## Notes for the reviewer - **Today's counts.** `python -m tests._docs_checks docs/examples` reports 74 one-letter lambdas: 68 in the four scripts (`aggregations` 24, `partial_selects` 17, `populated_relations` 16, `existence_tests` 11) plus 6 in their twins. The audit counted 66. It reports **10** unfailing error checks, not 12: `mutations.py:56` and `:113` catch the error without asserting anything, so the ticket's rule ("an `except` that asserts") does not cover them. Widening the rule to any `except` that swallows a named error would flag legitimate fallbacks such as `except ImportError`, so I kept the ticket's definition. The mutations page ticket should give both blocks an `else: raise`. - **The opt-ins never worked.** pytest-examples' `prefix_settings()` only reads quoted values, so `{test=run}` and `{lint=check}` were silently ignored. `fence_settings()` reads both spellings. No page used them, so results are unchanged. Recorded in `docs/solutions/issues/pytest-examples-prefix-settings-need-quotes.md` (I-5). - **Only supplied parameters are checked.** `lambda related, f=fk_field, v=pk_val: ...` captures `f` and `v` by default, and the query only passes the first, so only positional parameters without a default count. - **The method list** comes from the `Query`, `ProjectedQuery`, `Relation`, `Model` and `ModelConnection` signatures (including overloads) whose parameters mention `Callable`, `Predicate` or `RowSelector`, plus the names the ticket lists. - **Markdown inline code spans** (`` `order_by(lambda t: ...)` `` in prose) are not checked. The ticket scopes the checks to fences, and the audit's 57 page lines include spans. ### Outside Files owned - `tests/_docs_harness.py`: fence settings, fence extraction, docstring collection and the prelude runner call. Both test modules and the checks need them, and putting them in `_docs_checks.py` would mix the runner into the checks module. - `src/ferro/relations/descriptors.py`: renamed one internal lambda parameter `t` to `related`, so `python -m tests._docs_checks src/ferro` exits 0 and the docstring tickets start from a clean run. - `docs/solutions/issues/pytest-examples-prefix-settings-need-quotes.md`: the I-5 note above. ## Exit steps - [ ] #704 status updated (progress comment at `pr-open`; the orchestrator closes it after the merge into `feat/docs-rewrite`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…729) Closes #701 ## What Take a page that embeds `docs/examples/predicates.py:filtering`. Today the reader sees ```text adults = await User.where(lambda user: user.age >= 18).all() ``` with eight leading spaces, because the example section sits inside `async def main():`. After this PR it starts at column 0, so a pasted block runs. The rest of the PR is configuration of the same kind: - One click on "Annotated" switches every Assignment/Annotated pair on the site, and tab anchors read `#defining-a-model-annotated` instead of `#__tabbed_1_2`. - Hovering DDL, RLS, FK, PK, GUC, FFI, IR and similar terms shows a definition, from `docs/includes/abbreviations.md`, appended to every page. - `#647` in the changelog links to the issue. Permalinks carry a title for screen readers. - `/guide/migrations/#auto-migration` now lands on `/guide/schema/auto-migrate/` through a generated redirect page. The hand-written stub page is deleted, so it is gone from the sitemap and search. All 16 old anchors are mapped. - The API reference drops source blocks, puts typed and linked signatures under each heading, and shows summaries, symbol badges and backlinks. `Field` renders once, without its six overloads, and each parameter gets an anchor (`ferro.Field(index)`). `Relation.all()` and `.first()` inherit `Query`'s docstrings. - Theme: instant navigation with a progress bar, anchor tracking, breadcrumbs, prev/next footer, line selection, "Copy as Markdown", a third "follow system" palette state, GitHub and PyPI footer links, and `new`/`deprecated` status badges. `search.suggest` is removed because nothing reads it. - Plugins: `redirects`, `llmstxt` (`/llms.txt`, `/llms-full.txt`, a `.md` copy of every page) and `social` cards on the rust background `#B7410E` (checked: the card's first pixel is `b7410e`). - `create`, `upsert`, `get_or_create` and `update_or_create` annotate `**fields: Any`, which clears the griffe warnings. - The docs build runs with `--strict` in the publish workflow and in a new `docs` job in CI, which `All Checks Passed` now requires. ## Test scenarios - Happy path: `tests/test_docs_site.py` builds the site with `zensical build --strict` once per session in a temporary directory and passes (13 tests). - Error path: with `dedent_subsections = false` the indentation test fails and names `/getting-started/quickstart/` (` await connect("sqlite::memory:", auto_migrate=True)`). With the griffe extension removed, the `Relation` test fails because `Relation.all` has no docstring. I ran both by hand and then restored the config. - Integration: the generated `guide/migrations/` page maps every old anchor to the section that replaced it. The test resolves each target against the old URL and checks that the anchor exists on the target page. ## Verify Ran the ticket's Verify block on `d48d7cc`: passed (`No issues found`; 13 passed). TDD: RED `uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheprovider` failed as expected on `13629e7` (3 failed, 10 errors: no glossary, a non-strict build, the stub page present). GREEN: the same command passed 13/13 on `d48d7cc`. Full suite: 3309 passed, 339 skipped, 2 xfailed. Verify block corrected: no ## Interfaces Interfaces changed: none `built_site` (session fixture returning the site `Path`) and `rendered_pages(built_site) -> Iterator[tuple[str, str]]` are in `tests/test_docs_site.py`. `url_path` reads like `/guide/queries/`, and `/` for the home page. Zensical refuses a `docs_dir` outside the project root, so the fixture copies `zensical.toml`, `docs/pages` and `docs/includes` into the temp project and symlinks `docs/examples`, `src`, `README.md`, `CHANGELOG.md` and `CONTRIBUTING.md`. ## Notes for the reviewer - **`filters = ["!^_"]`, not `"public"`.** The ticket asks for `filters = "public"` and also for a Model page with no dunders or `__ferro_*__` attributes, and one setting cannot deliver both. Griffe counts every dunder as public, so `"public"` still rendered `__ferro_checks__`, `__getattr__`, `__repr__`, `__post_init__` and the rest. `["!^_"]` hides them. Every API directive names a class or function, never a module, so `__all__` (the one thing `"public"` adds) does not come into play. Site-wide this also hides protocol dunders a reader might care about: `Session.__aenter__`, `Rows.__len__`/`__iter__`/`__getitem__`, and the `AggregateExpr` comparisons. Those belong in the class docstrings, which is #722's work. The reasoning is also in a comment in `zensical.toml`. - **Outside Files owned:** - `docs/pages/api/fields.md`: the `show_overloads: false` / `parameter_headings: true` options the ticket asks for go on the `::: ferro.Field` directive, which lives in that file. - `ci.yml`'s two pytest jobs now also sync the `docs` group, so `test_docs_site.py` runs in CI and is not skipped. The file uses `pytest.importorskip("zensical")` for environments without the group. - The strict build needed no docstring fixes. - Duplicate `__codelineno-0-N` ids on API pages are still there. `separate_signature` code blocks are all numbered from 0, which the audit flagged as a separate ticket. Strict mode does not flag them. - `llmstxt` still lists `getting-started/next-steps.md`. The nav ticket (#723) folds that page away and should update the section list. ## Exit steps - [ ] Issue #701 status updated (closed by the orchestrator after this PR merges into `feat/docs-rewrite`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… quickstart (#705) (#730) Closes #705 ## What A newcomer who copies the README, home page or quickstart now gets code that runs as shown. Example from the README: `User.create(...)` and a `User.where(lambda user: user.is_active == True)` query run inside `async with engines.session():` and print `Found 1 active users.`, and a test runs exactly that block. - README and home page: one-sentence opening, real dependencies (Pydantic and pydantic-settings), identity map described as session-scoped, no tooling badges on the home page, and the model shown in Assignment and Annotated tabs from `readme_quickstart.py` / `readme_quickstart_annotated.py`. - Why Ferro: migrations are built in (`ferro migrate`, plus an Alembic bridge), atomic update expressions are no longer listed as missing (only `*`, `/`, `.concat()`), the `lambda user` fix, sentence-case headings, and a "Sync API" row replaces "Learning curve". - Installation: output of `ferro.version()`, uv/pip tabs for both extras, a venv-safe source build. - Quickstart: session wrapper on the first working block, lead-in and result sentence on every block, results asserted inside each snippet range, `author=alice` throughout, UTC timestamps, and the three Next steps paths folded into the closing section. ## Verify Ran the ticket's Verify block on `f5cb510`: passed (strict build clean, `tests/test_docs_examples.py` 239 passed, `_docs_checks` exit 0). Full suite: 3358 passed, 339 skipped, 2 xfailed. TDD: RED `pytest tests/test_docs_examples.py -k readme` failed with the README's `is_active=False` changed to `True` (stdout no longer `Found 1 active users.`); GREEN: 4 passed after restoring it. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - Outside Files owned: `tests/test_docs_examples.py` gained two tests (README block runs and prints its stated output; README model equals the home page's embedded model). The ticket asks for the README to be run by a test and names no test file. - Audit rows answered rather than fixed: no generated SQL is shown beside queries, since there is no public way to read a query's SQL yet (conventions note this waits on #700/#725), and hand-typed SQL would drift. The `# noqa: E712` comments stay: the per-file ruff ignore is a config change outside this ticket. The installation version block is compile-only (marker says why): `run_print_check` needs a hard-coded `#> 0.25.0` that would break each release. - Audit finding that was wrong: none found; "AGENTS.md I-8 stale" is flagged, not touched. - Dedent: strict build passes with `dedent_subsections` on. - Glossary terms needed: none new (GIL, FK already defined). - `next-steps.md` left in place for #723. - [ ] Issue status updates complete (I-9): `Closes #705` above. ## Fix report (review round 1) - `bulk_create` stays inside `quickstart.py:create` again (the `bulk-create` section is gone), so `guide/mutations.md` is right. The quickstart page embeds `create` once and explains both calls. The section names `models`, `create` and `update-delete` are unchanged; the old `query` section is still split into `get`, `filter`, `aggregate`, and no other page embeds it. - The quickstart now shows an `imports` range, says the model block is module level and every later block goes inside one `async def main()` run with `asyncio.run(main())`, and points to the script that has that shape. - Dropped the unsupported "about ten minutes". - Covering tests: `tests/test_docs_examples.py` (runs `quickstart.py`, `quickstart_annotated.py` and the README tests) and `tests/test_docs_site.py`. Command: `uv run --no-sync pytest tests/test_docs_examples.py tests/test_docs_site.py -q --no-cov -p no:cacheprovider -n 8` gave `252 passed`. Strict build: no issues. `_docs_checks` over the owned files: exit 0. Verify SHA `2dd3783`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…731) Closes #706 ## What The FAQ, roadmap and contributing pages now match the code. The FAQ says `include()` loads forward foreign keys and links Raw SQL to its own guide. `CONTRIBUTING.md` states Apache 2.0 and lists only channels that exist. The roadmap gives every gap a workaround that runs. Before and after, from the FAQ's eager-loading answer: ``` Before: "Does Ferro have eager loading? Not yet." After: include(lambda post: post.author) populates forward foreign keys in one statement; reverse and many-to-many collections are not populated yet. ``` ## Audit findings | Finding | Result | | --- | --- | | FAQ H eager loading | Fixed, with a running block. | | FAQ H Discussions link | Removed. | | FAQ M Raw SQL link | Now `guide/raw-sql.md`. | | FAQ M "two doors" | Now "two ways, plus a bridge". | | FAQ M/L wording, em dashes, subject-less sentence | Fixed. | | FAQ M split Troubleshooting out | Left: a new page needs a nav entry and a redirect, which this ticket does not own. Headings are keyed by error text instead. | | Roadmap M/L (lead with the gap, split bullet, sentence case) | Fixed. Issue links read "tracked here" because the ticket bars issue numbers in page text. `ilike()`, health checks, `disconnect()`, pool options and `.concat()` have no issue; the page says so. | | CONTRIBUTING H license, Discussions, docs URL | Fixed. | | CONTRIBUTING M tree, PR rules, intro, checklist emoji | Fixed. New "Project rules" section covers I-9, I-10, I-7, I-8, I-1, I-3. | | Changelog L (H1 in caps) | Left: it comes from the release template, not the page (I-10). The page already embeds `CHANGELOG.md` unchanged and magiclink renders its `#NNN` as links (checked in the built site). | Audit rows that were wrong or stale: - The FAQ's `Relationship resolution failed` text is not in the code. The real error is `Model 'P' defines a relationship to 'A' with related_name='posts', but 'A' does not have that field defined as BackRef()...`. The entry now quotes it. - The FAQ had no entry for `No database route for this operation`, the error a reader hits first now that sessions are required. Added. - `cargo test` in CONTRIBUTING fails to link Python; CI runs `cargo test --no-default-features --features testing`. Corrected (ran: 374 passed). ## Verify Ran the ticket's Verify block on the final commit: strict build clean, `tests/test_docs_examples.py` 243 passed, `_docs_checks` exit 0. Full suite: 3362 passed, 339 skipped, 2 xfailed. TDD: docs-only change. The tests are the harness fences on the three pages and the new `faq.py` and `faq_annotated.py` scripts; every error check has an `else: raise AssertionError`. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - Outside Files owned: new `docs/examples/faq.py` and `docs/examples/faq_annotated.py` (the Assignment and Annotated tabs for the running example, plus the multi-database snippet, which cannot run under the session-opening prelude because `using=` conflicts with an ambient session). - The first block on the FAQ is the model tabs, then the first block that does work shows `async with engines.session():`. - The changelog page has no opening sentence of its own, since the embedded file starts with its own H1. - External links checked, all 200: `github.com/syn54x/ferro-orm` (`LICENSE`, `AGENTS.md`, `issues`, issues 290, 291, 309), `ferro-orm.x54.sh`, `conventionalcommits.org`, `sh.rustup.rs`, `astral.sh/uv/install.sh`. - No new glossary terms. ## Exit steps - [ ] Issue status updated (#706 closes with this PR; I-9) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…732) Closes #709 ## Summary The Queries guide and the typed-predicates page now run on one ledger, name every lambda after its model, and show the result of every block. Before, the guide used `User`, `Invoice`, `Card`, four different `Transaction` models and models no page declared. After, one model set, declared once at the top of the guide: ```text Owner ──< Account ──< Transaction >── Category (parent → Category) │ ├──< SplitLine >── Category │ ├── Transfer.outflow / Transfer.inflow │ └──< Tag (many-to-many) ``` Nine transactions a reader can check by eye (Salary, Rent, Market run, the two halves of a transfer, two Lyon rows, Office coffee, Weekly shop), shown as a table on the page. ```diff docs/examples/ +├── _ledger_seed.py # the ledger's rows, shared by every query example +├── _preludes/queries.py # prelude for {test=run prelude=queries} blocks ├── predicates.py # declares the ledger + filtering, ordering, paging ├── predicates_annotated.py # Annotated twin of the ledger, runs a few queries ├── traversal.py # imports the ledger from predicates.py ├── existence_tests.py # imports the ledger ├── populated_relations.py # imports the ledger; `basic` still embedded by relationships.md ├── partial_selects.py # imports the ledger -├── traversal_annotated.py -├── existence_tests_annotated.py -├── populated_relations_annotated.py -└── partial_selects_annotated.py ``` The four deleted twins only declared models the page no longer embeds; the one Annotated tab on the page is `predicates_annotated.py:models` (AGENTS.md I-7). Page changes, one line each: - Predicate style is lambda only; the old three-styles story is a "Changed in 0.14" note linking the upgrade guide. The parameter is named after the model (`transaction`), never `t`. - Every snippet's `end:` marker sits below its assert, so the page shows which rows came back. Unordered results assert as sets. - New section **Paging with `after()` and `before()`** (anchor `#paging-with-after-and-before`, for #716): the four order-key rules, `position_of()`, `before()` with and without a limit, `None` in a non-PK slot. - Every error check in the examples has `else: raise AssertionError(...)`. - Hand-written SQL blocks are deleted (captured SQL waits on #700 / #725). - Reference links on first use (`where()`, `order_by()`, `after()`, `include()`, `select()`, `Row`, `Rows`, `Model.get`), and the concept page's hand-written Reference list is now links. - Explanation moved out of the guide: why a projection returns `Row` lives in `concepts/query-typing.md#projected-queries`; PEP 827 is explained once, under "Where checking stops". - Headings are sentence-case noun phrases; incoming anchors from other pages keep their slugs (checked in the built site). ## Evidence - **Before:** `python -m tests._docs_checks docs/pages/guide/queries.md docs/pages/concepts/query-typing.md docs/examples/` reported 57 violations in the owned files (one-letter lambdas and error checks that cannot fail) and exited 1. **After:** the same check over the owned files exits 0. - Renaming one lambda in `traversal.py` to `t` makes the check exit 1 with `docs/examples/traversal.py:27: lambda passed to where() takes one-letter parameter 't'`. - Replacing the colliding selector in `partial_selects.py:collision` with the dict form makes the script fail with `AssertionError: two fields named 'id' must raise ValueError`. ## Verify Ran the ticket's Verify block on `602f38d`: - `uv run --no-sync zensical build --clean --strict`: passed, no issues. - `uv run --no-sync pytest tests/test_docs_examples.py -q --no-cov -p no:cacheprovider -n 8`: 224 passed (the five query scripts, the Annotated twin, and five `{test=run prelude=queries}` blocks on the concept page). - `uv run --no-sync python -m tests._docs_checks docs/pages/guide/queries.md docs/pages/concepts/query-typing.md docs/examples/`: exits 1, every violation in `aggregations.py`, `aggregations_annotated.py` and `mutations.py`, which sibling tickets own. Over the owned files it exits 0. Full suite: `uv run --no-sync pytest -n 8 --no-cov`: 3343 passed, 339 skipped, 2 xfailed. TDD: RED `python -m tests._docs_checks <owned files>` failed as expected (57 violations); GREEN the same command passed (exit 0). Verify block corrected: yes: the third command scans all of `docs/examples/`, which includes files owned by sibling tickets that still fail; I ran it over the owned files (`docs/pages/guide/queries.md docs/pages/concepts/query-typing.md docs/examples/{predicates,predicates_annotated,existence_tests,partial_selects,populated_relations,traversal,_ledger_seed}.py docs/examples/_preludes/queries.py`). ## Interfaces Interfaces changed: none. Produces `guide/queries.md#paging-with-after-and-before` as the ticket states. ## Notes for the reviewer **Outside Files owned:** - `docs/examples/_ledger_seed.py` (new): the rows, so the Assignment twin, the Annotated twin, the four other scripts and the prelude add the same data. Without it the seed would be copied six times. - `docs/examples/_preludes/queries.py` (new): the brief's "add a new prelude file when your page needs its own models". - Deleted the four `*_annotated.py` twins of traversal, existence tests, populated relations and partial selects (listed under Modify; nothing embeds them now). **Audit findings left or answered:** - M "split the page into four": kept one page. The ticket owns only these two pages, other pages link nine of its anchors, and the single ledger removes the reason the split was proposed (no shared domain). Splitting needs new nav entries and redirects, which is a separate ticket. - M "show the rendered SQL" (existence tests, traversal): deferred to #725, per the ticket's non-goals; the hand-written blocks are deleted. - L "ignore E711/E712 for `docs/examples/` in ruff config": `pyproject.toml` is outside Files owned. E712 is gone (no `== True` / `== False` left); `# noqa: E711` remains on `== None` lines. - L "`Annotated["Employee | None", ...]` if supported": it is not; `"Category | None"` fails with `RuntimeError: Relationship resolution failed: 'Category | None' not found`. The self-reference keeps `nullable=True`, and the page says why. - L the audit's "`t.lines…` in prose" and the error-message tails: fixed; the quoted messages are now asserted in the scripts. **Found while checking facts, not fixed here (outside scope):** - Library error messages still teach `t.` and cite internal records: `where()`'s bare-relation and `and`/`or` messages (`t.account.<column>`, `u.age`), the traversed-mutation message (`t.account == instance`), the reverse-join and `in_(query)` messages (`ADR-0007`), and the aggregate-in-`where()` message (`#291`). The pages assert only the stable prefix of these. Worth a follow-up issue against `src/ferro/query/`. - On SQLite a `Decimal` reads back without trailing zeros (`Decimal("-1450.00")` returns `Decimal('-1450')`); the first block's printed output shows that. - The session link points at `connections.md#sessions-recommended`; if the Connections ticket renames that heading, this link needs the new slug (the strict build will say so). - Glossary: no new term needed. ## Merge Danger **Door:** two-way. Docs and example scripts only. **Blast Radius:** docs. `guide/relationships.md` still embeds `populated_relations.py:basic`, which now prints `Weekly shop | Checking` style lines from the ledger instead of `10 a1`; #712 rewrites that page. ## Exit steps - [x] Issue status updated: progress comment on #709 set to `pr-open`; `Closes #709` above. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ned (#713) (#734) Closes #713 ## Summary The schema pages quoted generated SQL that nobody checked, and three blocks had gone stale. Now every quoted step file is regenerated from the page's own example models and compared byte for byte. ```diff tests/test_docs_migrations.py <!-- output: id --> CLI output (pinned already) + -- NNNN_<name>/<file> SQL step file regenerated by the page's example + # migrations/NNNN_<name>/x.py generated data step + <!-- revision: id --> Alembic upgrade()/downgrade(), autogenerated on SQLite ``` What the pin caught, for example the migrations guide's `0001` (model has `email: str = Field(unique=True)`): ```diff -CREATE TABLE IF NOT EXISTS "author" ( "id" serial PRIMARY KEY NOT NULL, "name" varchar NOT NULL ); +CREATE TABLE IF NOT EXISTS "author" ( "email" varchar NOT NULL, "id" serial PRIMARY KEY NOT NULL, "name" varchar NOT NULL ); + +CREATE UNIQUE INDEX IF NOT EXISTS "uq_author_email" ON "author" ("email"); ``` Also stale and now fixed: the SQLite rebuild (missing the `sqlite_sequence` copy and the recreated `idx_author_name`), the table-rename block (missing the `DO $$` that renames the serial sequence), the enum-label contract (the type is `status_new`, not `orderstatus_new`, and SQLite's contract is a rebuild, not `not-applicable`), and the rename-hint claim (auto-migrate honours a table hint under `migrate_updates`; without it the pass holds the table back with a warning). Page by page: - **Overview:** "Where the old page went" removed (the #701 redirects replace it); the Alembic column says SQLite refuses a required-column add. - **Auto-migrate:** one running example (`Author` + `nickname`, `dev.db` from before the change). Four blocks run under a new `auto_migrate` prelude, including the `PassReport` asserting what the pass ran. Field and kind lists link to the API reference, and the full backend matrix moves into a collapsed note under the six common rows. - **Data steps:** contracts and generated backfill/guard files pinned. The enum-label section shows the real Postgres contract. - **Deploying:** the 0.22 and 0.24→0.25 material is now two notes, with the `grep` commands that find what a 0.24 checkout refuses; status exit codes link to the CLI reference. - **Testing:** the example runs the page's `conftest.py` fixture, tests and `asyncio_mode = "auto"` under real pytest. This caught a bug: the printed fixture never closed its connection, so the second test failed. The irreversible round trip is now an asserted `RoundTripResult`. - **Alembic:** shows the revision `Author.nickname` produces and a destructive marker, both pinned. "Exactly what the pass does" no longer contradicts the next paragraph. - **Adopting:** one name (`0001_create_author`) throughout, staging runs with `--url` against `staging.db`, drift in code returns a `BaselineReport`. - All pages: sentence-case headings (old anchors kept with `{#id}`), no ADR numbers or dated notes, lead-ins and result sentences, and compile-only fences say why (`{test=compile reason="..."}`). ## Evidence - **Before:** `pytest tests/test_docs_migrations.py -k step_file` → `5 failed, 4 passed`, e.g. `guide/schema/migrations.md:112 quotes 0001_create_author/01_schema.up.postgres.sql, which migrations_quickstart writes otherwise` **After:** `179 passed, 1 skipped` (the skip is the existing Postgres-only `ddl-lock-retry`) - Editing one statement of a quoted file fails naming the page and block: `guide/schema/data-steps.md:58 quotes 0002_author_slug/04_contract.up.postgres.sql, which migrations_backfill writes otherwise`. ## Verify Ran the ticket's Verify block on `84e8ac0`: passed (strict build clean; `test_docs_examples` 229 passed; `test_docs_migrations` 179 passed, 1 skipped; `_docs_checks` exit 0). Full suite: 3366 passed. TDD: RED `pytest tests/test_docs_migrations.py -k step_file` failed as expected (stale blocks); GREEN the same, 18/18 after the fixes. Verify block corrected: no ## Interfaces Interfaces changed: none. Produces: step-file pinning in `tests/test_docs_migrations.py` (`STEP_FILE_SOURCES`, keyed by `(page, migration)` or `(page, migration, file)`; a value is an example name, a scenario, or `None` for a data step written by hand). #714 updates the `guide/schema/migrations.md` keys when it splits the page. ## Notes for the reviewer **Outside Files owned:** - `docs/examples/_preludes/auto_migrate.py` (new): the prelude that lets auto-migrate's blocks run. - `tests/test_step_class.py`: one line, so it still finds the rolling-deploy recipe now that its fence carries settings. - `docs/solutions/patterns/docs-page-conventions.md`: records the step-file pinning and the compile-only reason convention (I-5). - `guide/schema/migrations.md`: one link moved off the removed upgrade anchor. **Audit rows left, with reasons:** - Auto-migrate's 29-row matrix (M) has no Reference home yet. #714 creates `reference/migrations.md`, which could take it; for now it sits in a collapsed note under a six-row summary. - Deploying's expand/contract table (M) also has no Reference home; it stays. - Deploying's Python exception texts (L): `DatabaseAheadError` is already pinned by `tests/test_step_class.py`, so that audit row was partly wrong. `PendingMigrationsError` stays unpinned. - The 0.24→0.25 note should move to the upgrade guide when #707 writes its 0.25 section. - `migrations_adopting.py`'s `[start:models]` marker (L) is not unused: `_migrations_project` reads that region. Kept. - Annotated twins outside my files (`quickstart`, `rename`, `retire`, `retire_label`, `squash`) still use `FerroField`; they are #714's. - GitHub `main` links to example scripts (L) are left as they are; the same links appear across #714's pages. **Glossary terms:** none new. - [ ] Exit step: issue #713 status updated (progress comment at `pr-open`, closes on merge) ## Merge danger **Door:** two-way **Blast radius:** docs. Pages and docs tests only. Old anchors are kept, and the strict build checks every link. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mples (#719) (#735) Closes #719 ## What The core API docstrings (`Model`, `Query`, `ProjectedQuery`, `Relation`, `FerroField`, `ForeignKey`, the query proxies, `Row`/`Rows`, `now`, `transaction`, `evict_instance`) are now short Google-style reference entries. Before, the `FerroField` page showed a literal `Attributes:` paragraph full of `:class:` text, because griffe dropped the section over a blank line. Now every parameter renders in an `Args:` table, links are Markdown autorefs, and each main entry point has an example that runs against the `reference` prelude: ```python by_views = Post.select().order_by(lambda post: post.views, "desc").order_by(lambda post: post.id).limit(1) page = await by_views.all() next_page = await by_views.after(page[-1]).all() assert [post.views for post in next_page] == [952] ``` That is the new `after()` example. 85 docstring fences now run under `tests/test_docstring_examples.py`, where none ran before. ## Verify Ran the ticket's Verify block on `3e21b32`: passed (strict build: no issues; 85 docstring examples pass; `_docs_checks` clean; `ruff --select D417` clean; `test_docs_site.py` 13 passed). Full suite: `pytest -n 8 --no-cov` gave 3421 passed. Its one failure (`test_relation_query_methods_inherit_their_docstrings`) is fixed in `3e21b32`, and the docs tests were rerun after that fix. TDD: RED `pytest tests/test_docstring_examples.py -k related` failed with `AttributeError: 'NoneType' object has no attribute 'all'` (block compiled with the runner's postponed annotations). An earlier RED for the same test failed with `SyntaxError` from pydantic's source lookup. GREEN: the same command passed after the harness fixes. A scratch griffe audit of the owned modules went from 395 findings to 0: missing docstrings, wrapped summaries, Sphinx roles, ADR/issue refs, `Args:` gaps, griffe-skipped sections. Verify block corrected: no ## Interfaces Interfaces changed: none. `FieldProxy`, `QueryNode`, `RelationProxy` and `ValueExpr` have a docstring on every public member, for #722. A trial render of them, plus `ReverseRelationProxy` and `ExistsTest`, on the queries page built under `--strict` with no unresolved autorefs. ## Outside Files owned - `docs/examples/_preludes/__init__.py`: a block that declared a model could not run. It ran as `__main__`, so pydantic's `use_attribute_docstrings` read the runner's source and raised `SyntaxError`. It was also compiled with the runner's `from __future__ import annotations`, so a declared `ForeignKey` was never seen and became a `JSON` column. The block now runs as its own module whose `__file__` is the docstring, compiled with `dont_inherit=True`. - `tests/test_docstring_examples.py`: one test, a block that declares a related model pair and reads the relation. - `docs/solutions/issues/docs-block-declaring-a-model.md` (I-5). ## Notes for the reviewer Found while checking facts (not fixed here, since this PR changes docstrings only): - **Silent FK loss under postponed annotations.** A model module with `from __future__ import annotations` and `author: Annotated[Author, ForeignKey(...)]` gets `"author" JSON NOT NULL` and no error. This breaks I-6 (fail loudly). It probably deserves its own issue. - **`offset()` without `limit()` fails on SQLite**: `near "OFFSET": syntax error`. The `offset` example uses `.limit(10).offset(1)`. - **Audit row correction**: `add`/`remove` do *not* accept any primary key name. They read `instance.id`, and the docstrings now say so. - **Audit row correction**: `count()` ignores `limit`/`offset`, and `update()`/`delete()` also refuse `after()`/`before()`. The docstrings say both. - `Query.model_cls`/`where_clause`/`order_by_clause` stay public; renaming them would change behaviour, so each got a one-line docstring instead. - `ModelConnection` is documented, but no API page renders it. Rendering it belongs to #722. - `ProjectedQuery.count`/`exists` have no return annotation, so the types are given in `Returns:` as `(int)`/`(bool)`. - Docstring examples avoid `Field()` (I-7 twins are not possible in a docstring). Model declarations use `FerroField` inside `Annotated`, which has one form, and the `ForeignKey` entry shows only the relation lines of the reference models. - Private helpers keep their technical docstrings; only their Sphinx roles were turned into backticks. - [ ] Exit step: #719 status updated (closed by the orchestrator after merge to `feat/docs-rewrite`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… type table (#708) (#738) Closes #708 ## Summary The Models and fields guide now follows one product catalog, and its field type table is generated by a test, so it can't drift from the schema again. ```diff docs/ ├── includes/ +│ └── field-types.md # generated: annotation → SQLite / PostgreSQL column, db_type token table ├── examples/ +│ ├── models_and_fields.py # Product (+ Category, Order, Review); every SQL block on the page is checked against create_tables() +│ ├── models_and_fields_annotated.py # same models; asserts the same statements as the assignment twin │ ├── table_checks.py # Pair → Payment (card token or bank account, never both), DDL checked │ └── table_checks_annotated.py └── pages/guide/models-and-fields.md # rewritten tests/ +└── test_docs_field_type_table.py # `python -m tests.test_docs_field_type_table` regenerates the include ``` Here are the old table's rows next to what Ferro actually creates (now generated): | Annotation | Old page (SQLite / PG) | Generated (SQLite / PG) | | --- | --- | --- | | `str` | `TEXT` / `TEXT` | `varchar` / `varchar` | | `int` | `INTEGER` / `INTEGER`/`BIGINT` | `integer` / `integer` | | `bool` | `BOOLEAN (0/1)` / `BOOLEAN` | `integer` / `bool` | | `datetime` | `TEXT (ISO 8601)` / `TIMESTAMP`/`TIMESTAMPTZ` | `DATETIME` / `timestamp with time zone` | | `UUID` | `TEXT (36 chars)` / `UUID` | `CHAR(32)` / `uuid` | | `StrEnum` | `TEXT` / native enum | `varchar(7)` (longest label) / native enum type `status` | | `dict` | `TEXT (JSON)` / `JSONB` | `JSON` / `jsonb` | Each cell is the column definition the create pass writes. The test builds the column IR, takes the type from `_core._resolve_storage_type` (a native enum cell is named by it) and renders it with the create pass. A second table lists every `db_type` token. The page sections, in order: the first example (the whole program, session included) · declaration styles · field types (+ `db_type`, enums, JSON) · primary keys (auto, natural, UUID) · uniques and indexes (single, composite, composite index, table checks) · custom table names · validation · **fields on a model base class** (`#fields-on-a-model-base-class`, for #716) · field options · see also. ## Evidence - **Before:** `pytest tests/test_docs_field_type_table.py` failed with `FileNotFoundError` because the include did not exist yet. After a hand edit of one cell (`str` → `TEXT`), it fails with `docs/includes/field-types.md is out of date; regenerate it with python -m tests.test_docs_field_type_table`. **After:** `2 passed`. - Ran the ticket's Verify block on `0810900`: passed (strict build "No issues found"; `test_docs_examples.py` 217 passed; type-table test 2 passed; `_docs_checks` exit 0). The full suite passed: 3338 passed, 339 skipped, 2 xfailed. - TDD: RED `pytest tests/test_docs_field_type_table.py` failed as expected (include missing); GREEN passed (2/2). - Test scenarios: changing the validation example's price to a valid one makes `models_and_fields.py` fail with `AssertionError: expected ValidationError`. Renaming a `where()` lambda parameter to `t` makes `_docs_checks` exit 1 with `models_and_fields.py:146: lambda passed to where() takes one-letter parameter 't'`. - Verify block corrected: no ## Interfaces Interfaces changed: none. The page produces `#fields-on-a-model-base-class`, and `python -m tests.test_docs_field_type_table` regenerates the include. ## Notes for the reviewer - **The base-class hazard.** The old warning said base-class fields "silently produce broken subclass defaults". That did not reproduce: `Review(Timestamped)` gets `created_at` and its default works. What does happen is that `Timestamped` becomes a model with its own table (`CREATE TABLE "timestamped" ("created_at" DATETIME NOT NULL)`, no primary key). The section shows that DDL and the zero-row count. The timestamps how-to still says the fields "are not contributed to its subclasses' tables", which is wrong; that page belongs to #716. - **A bug found and filed: #737.** A nested Pydantic model field (`dimensions: Dimensions`) hydrates as a `dict`, and inside a session an identity-map refresh turns the caller's instance's field into a `dict` too. For that reason the running example uses `dict[str, str]` and `list[str]`, and the page does not claim that nested models round-trip. - **SQL comes from the run, not typed by hand.** Each SQL block is a snippet section of a `DDL` string in the example script. `main()` compares it, whitespace-normalised, with the `PassReport` statements, and the Annotated twin compares its own report with the same string (read through `ast`). The SQL shown is SQLite's, because the run has no Postgres. The Postgres types come from the generated table. - **The `Literal` row.** `Literal["S", "M", "L"]` is a native enum on PostgreSQL whose type is named after the *column* (`size`), not the annotation. The table reports this because it is what the storage decision returns. It may be worth a look. - **Audit rows.** All the High rows are fixed. Medium: no running example, no DDL beside features, claims without code, no reference links, and the tab labels are all fixed. Low: headings, bold run-ins, the slogan, "ferro derives", and the table-check scaffolding are fixed (the violation now asserts the constraint name inside the shown range, and the `else` sits outside it). The `# (1)!` row no longer applies, because the commented JSONB lines are gone. - **Beyond the audit.** The audit's cells were right. The generated table also corrects cells the audit did not list: `bool` on PostgreSQL renders as `bool`, `Decimal` on PostgreSQL as `decimal` (not `NUMERIC`), and the integer primary key as `serial PRIMARY KEY`. - **Field options table kept, shortened.** `Field`'s docstring documents neither `db_type` nor `db_check`, so the table stays as a pointer table, with each row linking its section, and ends with links to `Field`, `FerroField` and `DbType`. - **Release-bound note.** The pre-JSONB upgrade warning became a self-contained "Changed in 0.15" note at the end of the JSON section, linking to the upgrade guide. - **Outside Files owned:** none. `docs/pages/api/exceptions.md` still mentions `ck_pair_at_most_one_side` as an example name; it embeds nothing from `table_checks.py`. - **Glossary terms:** none new. ## Merge Danger **Door:** two-way **Blast Radius:** docs The anchors other pages link to are kept: `#composite-indexes`, `#table-checks` and `#json-storage-json-and-jsonb`. `#composite-constraints` is gone, and nothing linked to it. - [ ] Exit step: issue status updated (#708 closed by the orchestrator after the merge into `feat/docs-rewrite`; the progress comment is current). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…several-databases pages (#711) (#736) Closes #711 ## Summary A reader who copied a block from these pages got `RuntimeError: No database route`, because no page showed the session the block ran in. Now every page's first block opens the session, and later blocks link to one explanation of it, Connections › Sessions (`#sessions`): ```python await connect("sqlite::memory:", auto_migrate=True) async with engines.session(): alice = await Customer.create(email="alice@example.com", name="Alice") assert (alice.id, alice.plan) == (1, "free") ``` ```diff guide/connections.md Connecting → Sessions (moved up, "recommended" dropped) → options → named connections guide/mutations.md one Customer model (tabs), create → save → batch → delete → upsert → get_or_create → refresh → errors guide/transactions.md one Account model (tabs); rollback warning: the row rolls back, the instance does not howto/multiple-databases "How to use several databases in one app"; the guide links here instead of embedding the same snippets +docs/examples/{mutations,transactions,multiple_databases}_annotated.py I-7 twins +docs/examples/_preludes/connections.py runs the Connections guide's inline blocks ``` The rollback warning, with the result inside the embedded range: ```python except RuntimeError: ... # the block raised, so everything in it rolled back assert checking.balance == -925 # the Python object kept the change await checking.refresh() assert checking.balance == 75 # the row did not ``` ## Evidence - **Before:** `python -m tests._docs_checks <owned files>` exited 1: ```text docs/pages/guide/connections.md:112: lambda passed to where() takes one-letter parameter 't'; ... docs/pages/guide/connections.md:123: lambda passed to where() takes one-letter parameter 't'; ... docs/pages/howto/multiple-databases.md:33: lambda passed to where() takes one-letter parameter 't'; ... docs/examples/mutations.py:136: try/except asserts on the error but has no else that raises, ... ``` **After:** it exits 0. - Ran the ticket's Verify block on `d37abde` before the rebase; see the fix report for `db31fa4`: strict build `No issues found`; `tests/test_docs_examples.py` 234 passed; `tests._docs_checks` exit 0. The full suite passed too (3353 passed, 339 skipped, 2 xfailed). - Test scenarios: removing the duplicate email from `mutations.py:create-strict` makes the script fail with `AssertionError: expected UniqueViolationError`. Renaming one query lambda to `t` makes `_docs_checks` exit 1, naming the file and line. - The Connections guide's inline blocks run under `{test=run prelude=connections}`. The outputs on the page (`1`, `True`, the `RuntimeError` text, `['Ada Lovelace', 'Grace Hopper']`, `1 0`, the `CREATE TABLE` statements) come from those runs. Verify block corrected: no ## Interfaces Interfaces changed: none. The Connections guide has a `## Sessions` heading whose anchor is exactly `#sessions`. ## Notes for the reviewer **Outside Files owned:** - `docs/examples/mutations_annotated.py`, `transactions_annotated.py`, `multiple_databases_annotated.py`: I-7 twins for the models each page now shows (the audit asked for the first). - `docs/examples/_preludes/connections.py`: the quickstart's models with no connection, because each Connections block connects for itself. - `docs/pages/{index,why-ferro,howto/testing,getting-started/quickstart}.md`: one-line link change from `connections.md#sessions-recommended` to `#sessions`. The strict build checks anchors, so the rename would have failed it otherwise. **Audit findings answered rather than fixed:** - *SQL beside each call (mutations M, rule 13):* not added. The docs conventions say the SQL must come from the example's own run once #700 ships a public way to read a query's SQL, and #725 applies it. Typing it by hand is what the conventions forbid. The one exception: `create_tables()` already returns the statements it ran, so the Connections page prints them. - *`self.session.__aenter__()` (connections L):* kept, now run under the harness. There is still no public `open()`, so this goes to the API owners. - *"Don't run schema creation concurrently through names on the same database" (multiple-databases M):* dropped. Auto-migrate and `create_tables()` take the per-database run lock, so two such passes run one after the other; the page says so and links to "Two processes at once". I did not test two names against one physical file. - *Transactions page embedding `multiple_databases.py:transaction` (M):* the snippet now uses an unqualified call plus the `ValueError` check. It lives on the how-to, and the transactions page links there so that it keeps one running example (`Account`). **Found while verifying:** a rollback clears the session's identity map. After a rolled-back block, `Account.get(id)` returns a new instance, not the stale one you hold. The rollback warning says this. No new glossary terms. - [ ] Exit steps: #711 status updated (progress comment at `pr-open`; the orchestrator closes it after the merge into `feat/docs-rewrite`) ## Fix report (review round 1) Rebased onto `origin/feat/docs-rewrite` at `b8dbf12`. The new head is `db31fa4`, which adds commit `db31fa4 fix(docs): repoint anchors renamed by the guide rewrite and the schema pages`. **Critical: 11 broken anchors on the trial merge.** Every link to a heading this PR renamed now points at the new heading: | Link target before | After | Where (one link line each) | | --- | --- | --- | | `connections.md#sessions-recommended` | `#sessions` | `concepts/query-typing.md`, `guide/queries.md`, `faq.md` (×2), `roadmap.md`, `guide/schema/auto-migrate.md` (link line only, since #714 is editing that page) | | `mutations.md#recipe-updates` | `#expression-updates` | `roadmap.md`, and the `Query.update` docstring in `src/ferro/query/builder.py` | | `connections.md#connection-pooling` | `#pool-size` | `roadmap.md` | | `mutations.md#refreshing-from-the-database` | `#refresh` | `Model.refresh` docstring, `src/ferro/models.py` | | `mutations.md#bulk-operations-and-the-identity-map` | `#creating-rows` | `Model.bulk_create` docstring, `src/ferro/models.py` | | `connections.md#sessions-recommended` | `#sessions` | a docstring in `src/ferro/models.py` | The three docstring links render into the API pages, where the strict build does not check anchors. They were broken too, and `grep` found them. This PR's own links into `guide/schema/auto-migrate.md`, which #734 re-headed: - `#what-the-pass-did-passreport` now links to [`PassReport`](../api/connection.md#ferro.PassReport) in the reference, from `guide/connections.md` and `howto/multiple-databases.md`. The auto-migrate page no longer has a `PassReport` section of its own. - `#two-processes-at-once` → `#concurrent-passes-and-the-run-lock` (`howto/multiple-databases.md`). To confirm nothing is left, `grep -rn "sessions-recommended\|recipe-updates\|connection-pooling\|refreshing-from-the-database\|bulk-operations-and\|what-the-pass-did\|two-processes-at-once" docs/pages src README.md` finds nothing. **Minor findings, all fixed:** - The `Changed in 0.13` note is now the last block of Sessions; the reference pointer sits above it. - Mutations no longer mentions `author=alice`; the sentence links to Relationships. - The how-to's `Model.using` method list is gone; the reference link replaces it. **Outside Files owned in this round:** the six pages and two source files in the table above, one link line each. **Verify on `db31fa4`** (the pushed `HEAD`): ```text $ uv run --no-sync zensical build --clean --strict No issues found $ uv run --no-sync pytest tests/test_docs_examples.py -q --no-cov -p no:cacheprovider -n 8 228 passed $ uv run --no-sync python -m tests._docs_checks <the seven owned files> exit 0 $ uv run --no-sync pytest tests/test_docs_site.py tests/test_docstring_examples.py tests/test_docs_checks.py tests/test_documentation_features.py -q --no-cov -p no:cacheprovider -n 8 146 passed, 4 skipped ``` The strict build is the test for this round: before the fix it aborted with `11 issues found`, and now it reports none. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ides rewrite #738 and #736 merged in parallel: models-and-fields linked connections#sessions-recommended and mutations#recipe-updates, which #736 renamed to #sessions and #expression-updates. The strict build failed on the integration branch. Part of #698. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce (#715) (#733) Closes #715 ## What The 736-line RLS guide becomes three pages. A reader who wants "scope my invoices by tenant" now sees, on the first screen: ```python async with engines.session("app", settings={"app.tenant_id": str(ACME)}): invoices = await Invoice.select().order_by(lambda invoice: invoice.total).all() assert [(invoice.customer, invoice.total) for invoice in invoices] == [ ("Hooli", 120), ("Initech", 450), ] ``` and, a few blocks down, that the same query in a session without settings returns `[]`, that the migrating role sees all four rows, and that a write into another tenant fails with SQLSTATE `42501`. - `guide/row-level-security.md` (how-to, retitled "Scope rows by tenant with Postgres row-level security"): the policy, the application's role, scoped sessions, `set_config` after open, framework middleware, admin jobs, deploy order, JSON claims. - `concepts/row-level-security.md` (explanation): the `NULLIF` contract, `FORCE`/`BYPASSRLS`, **`#session-settings`** (the one statement of round trips and `set_config` visibility, for #720), the pooler constraint, connection delivery, drift and ownership. - `reference/row-level-security.md`: `RowSecurity`, `RowPolicy`, policy names, the command table (from `_core._rls_command_matrix`), the shorthand's casts (from `row_policy_shorthand_cast`), composing policies, the session settings API and its validation, SQLite, warnings. The example script now runs on Postgres: `python docs/examples/row_level_security.py <url> <role>` migrates as the connecting role, creates a NOSUPERUSER app role, and asserts every result the pages show. A new `postgres_only` test runs both twins that way (it skips without a Postgres provider, and runs in CI's backend matrix job). Without arguments the script still runs on SQLite. ## Verify Ran the ticket's Verify block on `140abd6`: passed (strict build: no issues; `tests/test_docs_examples.py`: 230 passed, 2 skipped; `tests._docs_checks`: exit 0). Full suite: 3349 passed, 341 skipped, 2 xfailed. TDD: RED `python -m tests._docs_checks <copy of the example with lambda t: t.total>` exited 1 naming `row_level_security.py:126` and `:162`; RED `python <copy of the example whose SQLite session drops settings=>` failed with `AssertionError: settings= on SQLite must be refused`; GREEN the Verify block above. Verify block corrected: no Postgres: no server is available on this machine (libpq only, no `postgres` binary, Docker daemon off), so the Postgres path did not run locally. It ran in CI: `test_row_level_security_example_runs_on_postgres` PASSED for both twins in the "Python backend matrix (SQLite + Postgres)" job (run 39296785119, on `cbf5a68`; `140abd6` only applies ruff format to the two scripts). ## Interfaces Interfaces changed: none. `concepts/row-level-security.md#session-settings` exists as the ticket's Produces says. The new pages are not in the nav (#723). ## Facts checked against the code - **Round trips (audit M, lines 208/595).** Three is right. Outside a `transaction()`, an operation in a settings-bearing session sends `BEGIN` (`backend.rs` `begin_transaction_connection_with`, its own awaited `execute`), then `SELECT set_config(...)` (`apply_session_settings`), then the operation, then `COMMIT`. The `engines.session` and `PoolConfig` docstrings' "about two" is the outlier; #720 owns those docstrings. - **`set_config` visibility (audit M, lines 291–297).** The old page was right and the `Session.set_config` docstring is too narrow. `reapply_settings_to_open_transactions` re-applies to every entry in the session's `transaction_registry`, and `begin_transaction` registers every explicit `transaction()` there whichever task opened it. So an open `transaction()` in *any* task sharing the session sees the new value from its next statement. Self-wrapped operations (`OperationScope`) are never registered, so they finish with the old value. The concepts page states this once under `#session-settings`, for #720 to align the docstring to. - **`membership` (audit H).** Declaring a `Membership` model next to the policy does not fix the example. Ferro orders tables for creation by foreign keys only (`order_models_for_create`), and Postgres resolves a policy's tables at `CREATE POLICY`. With `Membership` FK'd to `Document`, `ferro migrate new` writes `document`'s `CREATE POLICY ... FROM membership` before `CREATE TABLE "membership"`, which fails on Postgres. The composed example now reads only its own table (owner policy plus `published_read`), and the reference has a Note on policies that read another table. **This is a product gap worth its own issue:** a raw policy that reads a table created in the same pass fails mid-pass. - **JSON-claims function.** The old `auth_uid()` returned `->> 'sub'` (text) from a function declared `RETURNS uuid`. Postgres has no assignment cast from text to uuid, so that function fails to create. The page now casts explicitly. - **FastAPI and Litestar recipes.** I checked both with a scratch app (FastAPI + TestClient; Litestar with both middleware styles, with a contextvar standing in for the Ferro session). A contextvar set in an app-level `yield` dependency reaches both async and sync handlers. Both Litestar classes scope `/read` and leave the opted-out `/health` unscoped. ## Audit rows answered rather than fixed - **M 403–570, recipes untested.** They stay compile-only, each fence marked `{compile-only="..."}` with the reason (Litestar/FastAPI are not dev dependencies, and the calls need Postgres). I checked them by hand as described above, and the page now states where each recipe expects authentication to put the user. - **L 692–701, `<!-- output: -->` pin for the migrations listing.** The listing is regenerated for `Invoice` (`0002_invoice_rls`, `rls_invoice_tenant_id`), copied from a real `ferro migrate new` run. It is not pinned: the `<!-- output: -->` mechanism compares CLI stdout, not step-file contents, and its page list lives in `tests/test_docs_migrations.py`, which this ticket does not own. - **L 33, `Doc` → `Document`.** Done. - **L 681, bare "(ADR-0019)".** Removed. ## Notes for the reviewer - **Outside Files owned:** `tests/test_docs_examples.py`, which gets one new test, `test_row_level_security_example_runs_on_postgres`. The ticket's Postgres scenario needs a Postgres-gated test, and the docs harness has no other Postgres hook. - One running example per page is `Invoice`. The reference's "Composing policies" uses a second model, `Document`, because three policies on one table need columns `Invoice` lacks. - "The first block shows the wrapper": all three pages open with the scoped-query block, which has `async with engines.session(...)`. - Signature blocks in the reference are written as valid Python stubs (`class RowSecurity: def __init__(...)`, the `RowPolicy` dataclass fields), so the harness can compile them. - Glossary: no new term needed; RLS, GUC and ASGI are already defined, and `abbreviations.md` is untouched. ## Exit steps - [ ] Issue status updated: #715 closes via this PR (the orchestrator closes it after the merge into `feat/docs-rewrite`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#736 and #733 merged in parallel: the Connections guide linked row-level-security.md#session-settings and #connection-delivery-mode, which #733 moved to concepts/row-level-security.md (#session-settings, #connection-delivery). The strict build failed on the integration branch. Part of #698. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng example each (#712) (#739) Closes #712 ## What A reader who copied the Raw SQL page's placeholder example got `no such table: users`: Ferro names a table after its model in lowercase, so the table is `event`, not `events`. Every raw SQL block now runs against the `event` table the page declares, and the first block shows the session it needs. Outside a session, a raw call raises `RuntimeError: No database route for this operation`. The Relationships page used nine model sets. It now uses one roster (Team, Player, Profile, Tournament) and shows the tables Ferro creates for it, printed from the `create_tables()` report. For example, the many-to-many join table: ```sql CREATE TABLE IF NOT EXISTS "player_tournaments" ( "player_id" integer NOT NULL, "tournament_id" integer NOT NULL, CONSTRAINT "fk_player_tournaments_player_id_player" FOREIGN KEY ("player_id") REFERENCES "player" ("id") ON DELETE CASCADE, CONSTRAINT "fk_player_tournaments_tournament_id_tournament" FOREIGN KEY ("tournament_id") REFERENCES "tournament" ("id") ON DELETE CASCADE ) CREATE INDEX IF NOT EXISTS "idx_player_tournaments_tournament_id_player_id" ... CREATE UNIQUE INDEX IF NOT EXISTS "uq_player_tournaments_player_id_tournament_id" ... ``` The example script asserts these statements byte for byte, so the page cannot drift from what Ferro runs. ## Verify Ran the ticket's Verify block on `1511130` (rebased onto `a10707f`): passed (strict build: no issues; `tests/test_docs_examples.py`: 200 passed, 2 skipped; `tests._docs_checks` over the owned files: exit 0). Full suite: 3355 passed, 339 skipped, 2 xfailed. TDD: RED `pytest tests/test_docs_examples.py -k raw-sql`, with the page's original SQLite placeholder block marked `{test=run prelude=raw_sql}`, failed with `OperationalError: ... no such table: users`; GREEN, the same command after the rewrite: 6/6 passed. Test scenarios: renaming a lambda parameter to `t` in a copy of `relationships.py` makes `tests._docs_checks` exit 1 naming file and line; replacing the `else: raise` in `raw_sql.py`'s error check makes it exit 1 too. Verify block corrected: no ## Interfaces Interfaces changed: none ## Audit findings **Relationships** - H, I-8 lambdas: fixed. `populated_relations.py:basic` already used `lambda transaction` after #709. The page no longer embeds it and instead has its own `include()` block on Team/Player (`relationships.py:populated`), which also resolves the Transaction/Account cast finding. - M, `NO ACTION`: fixed, verified on SQLite. `NO ACTION` and `RESTRICT` both refuse the delete with `ForeignKeyViolationError`. I left out the audit's "deferrable on Postgres": Ferro never emits `DEFERRABLE`, so the check cannot be deferred. - M, join table undocumented: fixed. The page shows the `player` DDL (shadow columns, `fk_*`, `idx_player_team_id`) and the join-table DDL. On Postgres, only the `id` column differs (`serial PRIMARY KEY`), which I checked with the core's create-table renderer because no Postgres server was available. - M, nine casts: fixed. M, lead-in and result sentences: fixed. M, roadmap language: fixed. L, "Overview" heading: folded into the intro. L, Title Case headings: fixed. L, unnamed exception: now `RuntimeError`, verified. - L, `profile: "Profile" = BackRef()` annotation: the page now says awaiting it returns the instance or `None`, verified. The annotation stays. `Relation["Profile"]` raises `TypeError: Field 'profile' must use Relation[list[T]]`, so a typed one-to-one reverse needs a library change. That belongs to the type-safety work, not this page. - M, CASCADE never exercised: fixed. The `on-delete` block deletes a team and asserts CASCADE (players, profile) and SET NULL (tournament winner). - M, `remove()` then `clear()` with no result between: fixed. L, `crab`: fixed. L, `"Employee"` without `None`: kept on purpose, because `"Player | None"` fails at `connect()` (verified) and the page says so. **Raw SQL** - H, plural tables: fixed. H, `RuntimeError` outside a session: fixed and asserted (`raw_sql.py:no-session`). - M, `autocommit=True` and `session=`: added under "Statements that refuse a transaction" and "Routing", with the RLS connection-delivery caveat linked. - M, heading: now "Use cases". M, results: shown. L, "ORM territory now": fixed. L, Warning: now a Note. - `raw_sql.py`: asserts moved inside the ranges. ## Notes for the reviewer - **Outside Files owned:** `docs/examples/raw_sql_annotated.py` is the I-7 twin. The page now shows the `Event` model, and I-7 requires both tabs to be backed by a runnable script. `docs/examples/_preludes/raw_sql.py` is the prelude that the page's `{test=run prelude=raw_sql}` SQLite placeholder block runs under. - Postgres-only blocks (the `$1` placeholder, the `::timestamptz` cast, `autocommit=True`) are marked `{test=compile reason="needs a Postgres database"}`. The cast example's error text, `operator does not exist: timestamp with time zone >= text`, comes from `tests/test_query_joins.py`. - **Library footguns found (not fixed here, worth issues):** 1. `Annotated[Team, ForeignKey(...)] | None` connects silently, with no foreign key, no `*_id` column and no error. The page warns about it, but under I-6 Ferro should refuse it. 2. `spring.winner = rustaceans` raises Pydantic's `AttributeError: 'winner' is a ClassVar`, so the page sets `winner_id`. 3. `add()`/`remove()` read `instance.id` (#719 limit). The page states the limit. - `execute()`'s docstring says "Outside any transaction, runs on a one-off pool connection". That is true but reads as if no session were needed. Docstrings are out of scope here. - No new glossary terms are needed. ## Fix report, round 1 Rebased onto `origin/feat/docs-rewrite` at `a10707f`. The fix commit is `1511130`. - **Spec, dead anchors:** `connections.md#sessions-recommended` is now `connections.md#sessions` (raw-sql.md:31, relationships.md:17). `row-level-security.md#connection-delivery-mode` is now `../concepts/row-level-security.md#connection-delivery` (raw-sql.md:72), where the `autocommit=True` caveat now lives. I also checked every other cross-page anchor on both pages against the rebased tree: `connections.md#named-connections`, `queries.md#querying-across-relationships`, `queries.md#populating-relations-with-include` and `models-and-fields.md#composite-indexes` all still exist. - **Quality, compile-only marker:** the three Postgres-only blocks now use `{test=compile reason="needs a Postgres database"}`. Covering tests: the strict build, which fails on a missing anchor, and `tests/test_docs_examples.py`, which compiles every block. ``` $ uv run --no-sync zensical build --clean --strict No issues found $ uv run --no-sync pytest tests/test_docs_examples.py -q --no-cov -p no:cacheprovider -n 8 200 passed, 2 skipped in 105.57s $ uv run --no-sync python -m tests._docs_checks <owned files> exit=0 ``` ## Exit steps - [ ] Issue status updated: #712 closes through this PR when the orchestrator merges `feat/docs-rewrite` work (`Closes #712` above); the progress comment is current. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #710 ## What The aggregations guide now uses a four-row ledger with distinct totals (Checking 30, Savings 45, orphan 40), so the grouped, top-N and descending examples show a real effect: top-N returns `[{"account_label": "Savings", "total": 45}]` and the lambda sort returns `[(2, 45), (1, 30)]`. - The wrong "one of four has a price" comment is gone; the comment now says three of four, matching the assert. - Each error check has `else: raise AssertionError(...)`, and each limit has its own snippet and shows its message. - All 24 one-letter lambdas are named `transaction`. - The inline partition block runs under a new `aggregations` prelude (`docs/examples/_preludes/aggregations.py`). - Headings are sentence case; ADR and issue references and "pinned" are removed; the page links to the API reference. - `price: Decimal | None = None` in both twins; Annotated twin follows the one idiom. ## Verify Ran the ticket's Verify block on `ed1ed45`: passed (strict build, 233 docs example tests, `_docs_checks` exit 0). Full suite: 3370 passed. TDD: RED `python -m tests._docs_checks docs/examples/aggregations.py` with one `else` removed exited 1 naming the line; GREEN the same command exited 0 with it restored. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - Outside Files owned: new `docs/examples/_preludes/aggregations.py` (the harness needs a prelude for the inline block). - Audit row left: "show the generated `GROUP BY` SQL". The brief says no hand-written SQL until captured SQL lands (#725), so none is added. - Audit finding found wrong in spirit: the builtin-`sum` error text hardcodes `t.` (`did you mean t.amount.sum()?`) whatever the lambda parameter is named. The page says so; fixing the message is a source change outside this ticket. - The `!= None # noqa: E711` comments stay: there is no per-file ruff ignore config to move them to. - `docs/pages/api/queries.md` still links the old page title and cites `#291`; not owned here. - No new glossary terms needed. - [ ] Issue status updates complete before merge (I-9) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Fix round 1 - Both Sessions links now point at `connections.md#sessions` (the heading was renamed on the base). Rebased onto current `origin/feat/docs-rewrite`. - The error messages shown on the page are now asserted by the example itself (full text where the page shows all of it, the shown prefix otherwise), so the page cannot drift from the code. The unnamed-aggregate message carries an issue number in the source; the page elides it. - Covering tests: `tests/test_docs_examples.py` (aggregations.py, the inline block) and `tests/test_docs_site.py`. - Command: `uv run --no-sync zensical build --clean --strict` printed "No issues found"; `pytest tests/test_docs_examples.py tests/test_docs_site.py` gave 213 passed, 2 skipped; `_docs_checks` exit 0. Verify SHA `3813312`. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #707 ## What A reader on 0.15 now finds a section, or a row in a no-action table, for every release up to 0.25. The page reads one model, `Account`, and every recipe runs. ```python async with engines.session(): adults = await Account.where(lambda account: account.age >= 40).order_by("name").all() ``` - New sections for 0.25, 0.24, 0.23, 0.22, 0.21, 0.18, 0.17.1 and 0.16.3, each listing only what the upgrade asks (from `CHANGELOG.md` and the feature PRs, checked against the code). Every other release from 0.13 is a row in "Releases that need no action". - Every old recipe uses `ferro.Model` (there is no `ferro.Ferro`), `lambda account:` parameters, Assignment and Annotated tabs backed by `docs/examples/upgrade_*.py` pairs, and a result sentence. "After" blocks run under `{test=run prelude=upgrade}`; "Before" blocks of removed APIs are `{test=compile reason=...}`. - Implicit routing is attributed to 0.14 (the 0.13 tag still has `allow_legacy_default`; 0.14 raises `No database route`). ## Verify Ran the ticket's Verify block on `1cb2616`: passed (strict build "No issues found"; `tests/test_docs_examples.py` and `tests/test_docs_site.py` green; `_docs_checks` exit 0). Full suite: 3381 passed, 339 skipped, 2 xfailed. TDD: RED `python -m tests._docs_checks <old page>` listed one-letter lambdas (`t`) at lines 164, 165, 184, 194; GREEN `python -m tests._docs_checks` over the new page and examples exit 0, and `pytest tests/test_docs_examples.py -k upgrade` 28 passed. Verify block corrected: no ## Interfaces Interfaces changed: none ## Audit rows - H coverage to 0.25, H `Ferro` class, H I-7 twins, H lambdas: fixed. - M routing release: audit was right (0.14); page, table and runtime message now agree. M "catches every deprecation": added the skip-past-0.14 paragraph. M stale "reviewed Alembic migration": now "a hand-written step in `ferro migrate`, or an Alembic revision". M result sentences: added. - L title case, headings, table as range rule, ADR and repo-path links: fixed. L collapsible prompts: kept. - Audit leads that were wrong or incomplete: - `db_type="varchar"` is not a valid token (only `text` and `varchar(N)`), so the old "keep varchar" recipe never worked. The enum keep recipe is now `db_type="text"`. A `datetime.time` field accepts no text `db_type` at all, so the page says there is no keep spelling and gives the convert recipe. `docs/solutions/patterns/derived-type-and-naming-decision-table.md` still says `db_type="varchar"` in its keep-recipe table (not touched here). - The audit's "retired enum labels refusing writes" is opt-in, so it is a "also new" line in 0.23, not an ask. - Two anchors other pages link to are kept with explicit ids (`#predicates-operator-style-and-col-are-gone`, `#connection-routing-requires-a-session`). ## Outside Files owned - `src/ferro/state.py`: the runtime `No database route` message and a docstring said "removed in v0.13"; now "v0.14" and "see the upgrade guide" (the audit asked for the message to match). No test matches the text. - `docs/examples/_preludes/upgrade.py`: the prelude for the page's blocks (the brief allows a new prelude). ## Notes for the reviewer - `docs/pages/guide/connections.md` still says "removed in `0.13`" (owned by another ticket). - Glossary terms the page could hover but I did not add: none new. - Exit steps: [x] issue #707 status updated through this PR's `Closes`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #717 ## What The SQLAlchemy migration page no longer says Ferro has "no session". Before, a SQLAlchemy user copying `await User.create(...)` hit `RuntimeError: No database route`. Now the first block shows `async with engines.session():`, and the page says what the session is (routing plus identity map, no `add`/`flush`/`commit`). Every Ferro block is embedded from `docs/examples/from_sqlalchemy.py` (and an Annotated twin) and shows its asserted result. Each SQLAlchemy/Ferro pair is a tab pair with a result sentence. The table names `ferro migrate` with the Alembic bridge as an option. Lambdas are named after the model, headings are sentence case, and the page runs on one `User`/`Post` pair. ## Verify Ran the ticket's Verify block on `c24afa5`: passed (strict build clean; `tests/test_docs_examples.py` 193 passed, 2 skipped; `_docs_checks` exit 0). TDD: RED, the old page's Ferro blocks raised `No database route` outside a session (audit, reproduced as stated); GREEN `pytest tests/test_docs_examples.py` passed, with both new scripts running end to end. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - Audit rows all addressed. SQLAlchemy-side blocks are `test=compile` with a reason (running them needs an async SQLAlchemy engine; left compile-only deliberately). - The "Annotated" twin follows the recorded idiom (`Annotated[T, Field(...)] = default`). - Removed the `#291` link and "recipe door" jargon from the text. - No new glossary terms needed. - Outside Files owned: none. - [ ] Issue status updates confirmed (I-9): #717 closed by this PR's merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… reference entries (#721) (#745) Closes #721 ## Summary Every docstring in the migrations API, the pass report and the CLI is now a Google-style reference entry: one-line summary, every parameter in `Args:`, `Returns:` / `Raises:` sections that render as one row per item, and no Sphinx roles, RST `::` blocks, ADR numbers or issue numbers. `up()` before and after, as the API page shows it: ```diff -Apply every pending step of every pending migration, in order, under -the run lock (``ferro migrate up``). - -Works on ``database`` of ``settings`` over ``using`` ... (:meth:`~ferro.migrations.target.Target.resolve`) ... -``allow_ahead`` lets a database ... through (ADR-0038) ... (:data:`AllowAhead`). +Apply every pending migration, in order, under the run lock. + +Args: + settings: The project's configuration. Defaults to `FerroSettings()`. + ... + allow_ahead: Run beside a database that has applied migrations the + directory lacks: `"expand-only"` ... Defaults to `False`, which refuses. + progress: ... + on_event: ... +Returns: ... +Raises: ... +Examples: (fenced, {test=compile reason="needs a migrations project and a database"}) ``` What changed, by area: ```text src/ferro/ ├── pass_report.py # a docstring on every PassReport / Report / Subject field, role values listed inline, │ # a runnable example (drop "post", create_tables() recreates it) ├── migrations/ │ ├── chunked.py → _chunk_loop.py # ferro.migrations.chunked now resolves to the decorator, not this private module │ ├── runner.py / report.py / generate.py # RunReport, StatusReport, CheckReport and every field documented (for #722) │ ├── steps.py # atomic / chunked / irreversible / nothing_to_reverse / todo: Args, Raises, examples │ ├── errors.py, report.py # exception attributes documented where they are set │ ├── testing.py # the slug-backfill walk-through moved into harness()'s Examples │ ├── _drift.py # render_op example runs against the reference prelude │ └── alembic.py, translate.py, _baseline.py, layout.py, scaffold.py, context.py, target.py, historical.py, rerecord.py └── cli/ ├── __init__.py, output.py # Args on every helper; roles and ADR refs gone └── migrate.py # squash help prints no literal backticks; check's summary is one line; # `--replace` help puts the condition first; status --steps/--json lose --no-*; # `ferro --help` reads "Ferro's command line: ... for Ferro models." ``` ## Evidence TDD: `tests/test_cli_help.py` gained two checks over every command: help carries no Markdown (backticks, bold, links) and its summary is one capitalised line ending with a period. - **Before:** `uv run --no-sync pytest tests/test_cli_help.py -q --no-cov -p no:cacheprovider`: 3 failed, 34 passed (`ferro migrate squash` help carries Markdown ``['`', '`']``; `ferro` summary starts lowercase; `ferro migrate check` summary spans two lines). **After:** 37 passed. Docstring audit (a scratch griffe script, not committed: google parser warnings, multi-line summaries, Sphinx roles, RST blocks, ADR/issue refs, parameters missing from `Args:` over every public object in the owned files): 495 problems before, 0 after. `ruff check --select D417` catches a dropped parameter: removing `progress` from `up()`'s `Args:` reports `D417 Missing argument description in the docstring for up: progress`. ## Verify Ran the ticket's Verify block on `456544e` (rebased on the current `feat/docs-rewrite`): passed. Strict build "No issues found"; `test_docstring_examples.py` 112 passed; `_docs_checks` exit 0; `ruff --select D417` all checks passed; `test_docs_site.py` 13 passed; `test_cli_help.py` 37 passed. Full suite `pytest -n 8 --no-cov`: 3408 passed, 339 skipped, 2 xfailed before the rebase. After it: 3462 passed and 3 failed, all in `tests/test_docs_migrations.py` on `guide/row-level-security.md:273` (quoted step files of `0002_invoice_rls` with no `STEP_FILE_SOURCES` entry). This PR touches neither file; the failure comes from #715's RLS page split meeting #713's step-file pin on the integration branch. Verify block corrected: yes: `src/ferro/migrations/chunked.py` is now `src/ferro/migrations/_chunk_loop.py` (the audit's own fix for `ferro.migrations.chunked` resolving to the private module), so the `_docs_checks` and `ruff --select D417` commands ran with that path. ## Interfaces Interfaces changed: none. `ferro.migrations.chunked` (the decorator) is unchanged; only the private module `ferro.migrations.chunked` became `ferro.migrations._chunk_loop`. ## Notes for the reviewer - **Outside Files owned:** `tests/test_chunked_steps.py` and `tests/test_migrate_rerecord.py` import `encode_cursor` / `decode_cursor` from the renamed private module; one import line each. - **Not done here:** `ferro.migrate()` and `ferro.create_tables()` live in `src/ferro/__init__.py`, which #720 owns; their broken `Returns:` rows are #720's to fix. `docs/pages/api/migrations.md` still renders `::: ferro.migrations.steps.chunked`; `::: ferro.migrations.chunked` now works too, for #722 to switch. - **Links:** `RunReport`, `StatusReport`, `CheckReport`, `InheritedEvent`, `StepEvent` are in plain code, not autorefs, because no page renders them yet and the strict build would fail; #722 can turn them into links once it renders them. Page links in docstrings point at pages only, no anchors (`guide/schema/{auto-migrate,deploying,data-steps,testing}.md`, `guide/raw-sql.md`, `howto/adopting-migrations.md`), all present after the rebase. - **CLI commands** keep their parameter help in `Parameter(help=...)`, which `--help` prints; their docstrings carry no `Args:` (D417 does not ask for one, and a second copy would drift). - **Examples:** `PassReport` and `render_op` run against the `reference` prelude. Examples that need a migrations project or a database are `{test=compile reason="..."}`, the convention #713 introduced. - **Audit row corrected:** the H row on `<placeholder>`s in CLI help was already fixed by #702; this PR covers the leftover literal backticks in `squash`. - **Behaviour:** the only CLI behaviour change is that `ferro migrate status` no longer accepts `--no-steps` / `--no-json` (the audit's L row; nothing used them). ## Merge Danger **Door:** two-way. **Blast Radius:** small. Docstrings and help text, plus one private module rename; anyone importing `ferro.migrations.chunked` as a module (not the decorator) would break. ## Exit steps - [ ] #721 is closed by the orchestrator after this merges into `feat/docs-rewrite` (`Closes #721` above). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…that run (#716) (#742) Closes #716 ## What The four short how-tos now teach what Ferro does today, and every block on them runs: - **Pagination** pages with `Query.after()` / `.before()` and `position_of()` instead of a hand-built `where(id > cursor)`. The API recipe asks for one extra row to know whether a next page exists, and the FastAPI endpoint opens `engines.session()`, which the old one never did (it raised `No database route`). - **Timestamps**: the batch-update recipe was `update(lambda note: {"updated_at": now})`. On SQLite that stores `2026-10-19 18:18:47` while `save()` stores `2026-10-19T18:18:47.714809Z`, so the updated row sorts before every row saved the same day and reads back naive (`TypeError` against `utcnow()`). The recipe is now `update(..., updated_at=utcnow())`, which Ferro stores exactly like `save()` (I-13); `timestamps.py` asserts the stored text has one shape, the rows sort by time, and the values compare with `utcnow()`. A warning explains the `now` behaviour on SQLite. - **Timestamps and soft deletes** both say a `Model` base class gets a table of its own and link to `guide/models-and-fields.md#fields-on-a-model-base-class` (the old "fields are not contributed" claim was false). - **Testing**: `tests/test_docs_testing_fixtures.py` builds the project the page describes in a temp directory (`myapp/models.py`, `tests/conftest.py` from `testing_conftest.py`, the page's `pyproject.toml` block, and every page fence marked `{test=pytest path="tests/..."}`) and runs pytest on it. It checks the page's own tests pass, that `pytest -q tests/test_users.py` prints the count the page shows, that `db` isolates tests, that `db_transaction` rolls a created row back, and (Postgres CI) that `pg_db` works. The unique test expects `UniqueViolationError`. - Titles: "How to test a Ferro app with pytest", "How to paginate query results", "How to record created and updated times", "How to soft-delete rows". ## Verify Ran the ticket's Verify block on `5e00650` (rebased onto `6f8e7ca`): passed. TDD: RED `uv run --no-sync pytest tests/test_docs_testing_fixtures.py -q --no-cov -p no:cacheprovider` against the original `testing.md` and `testing_conftest.py` failed as expected (`4 failed, 1 passed, 1 skipped`: the page had no runnable test modules, and `db_transaction` yielded `None` and kept the row); GREEN the same command on this branch passed (`5 passed, 1 skipped`, the skip is the Postgres-only test, which CI's `--db-backends=sqlite,postgres` job runs). Timestamps: a probe on the base branch confirmed the `now` recipe stores `'2026-10-19 18:18:47'` and hydrates naive; `docs/examples/timestamps.py` now pins the fix and passes under `tests/test_docs_examples.py`. Full suite `uv run --no-sync pytest -n 8 --no-cov`: 3414 passed, 342 skipped, 3 failed. The 3 failures are `tests/test_docs_migrations.py` cases for `guide/row-level-security.md:273` (`0002_invoice_rls`), which have no `STEP_FILE_SOURCES` entry since the RLS split (#715) landed on the integration branch; the page and the test are files this branch does not touch. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer **Outside Files owned** (all new example files, no existing file outside the list touched): - `docs/examples/pagination_annotated.py`: the pagination page now declares its model once at the top, which needs the I-7 Annotated tab. - `docs/examples/testing_models.py` / `testing_models_annotated.py`: the audit asked for `User`/`Post` to exist instead of the phantom `myapp.models`; the fixtures test copies the first to `myapp/models.py`. - `docs/examples/testing_conftest_postgres.py`: `pg_db` with its own imports. Keeping it in `testing_conftest.py` needed mid-file imports with `# noqa: E402` on the page. **Decisions the ticket left open** - `db_transaction` now rolls back (raises a private `RollBack` inside `transaction()`) and yields the transaction handle, so raw SQL in the test runs on the test's connection. The old fixture committed and yielded `None`. - The page's test modules run from the page itself: a fence `{test=pytest path="tests/test_users.py"}`. `tests/test_docs_examples.py` compiles them (it only acts on `test=run`), and the fixtures test runs them. - The compile-only fence follows the recorded convention, `{test=compile reason="..."}` (`docs/solutions/patterns/docs-page-conventions.md`). The FastAPI endpoint is the one compile-only block (FastAPI is not installed); `article_page()`, which it calls, runs in `pagination.py`. - Each page's first block shows the session: the model tab includes a second snippet range with `connect()` and `async with engines.session():`. - The soft-delete example makes `number` unique and shows the `UniqueViolationError` a reused number raises; the "enforce uniqueness in application code" option is replaced by a sentence saying it races. The mixin's `active()` names its lambda `row`, and the page says why. - `from ferro.query import Query` stays: `ferro.query.__all__` exports `Query`, so it is a public import (the audit row called it internal). **Audit rows not fixed, with the reason** - Generated SQL beside each query (pagination M rows): no public way to read a query's SQL exists yet (#700); #725 adds it to the guides. - Nav labels in `zensical.toml` still read "Testing", "Pagination", "Timestamps", "Soft Deletes": the file is outside Files owned and shared with the sibling tickets. The page titles changed. - `pg_db` is gated on Postgres and skipped locally (no server here); CI's Postgres job runs it. **Follow-ups worth an issue** (not filed) - `now` in an update recipe on SQLite writes `CURRENT_TIMESTAMP` text, unlike every other datetime write, so it mis-sorts against `save()`'s rows and hydrates naive. The page warns about it; the library could render `now` on SQLite in `save()`'s form. - `ferro_ddl_lowering::timestamp_tz_conversion_warning` still sends users to "a reviewed migration (Alembic)"; the page now points at a hand-written `ferro migrate` step or Alembic. ## Exit steps - [ ] #716 status is updated (progress comment `pr-open`; the orchestrator closes it after the merge into `feat/docs-rewrite`). ## Fix report (review round 1) - Spec, `pagination.md:60`: the page now says the order keys must *include* the primary key, which is what `Query._assert_position_order_keys`, the `after()` docstring and the Queries guide say. "End with" was wrong. - Quality, `pagination.md:80`: the FastAPI fence is now `{test=compile reason="..."}`. It was the only compile-only fence in this PR's files. The note above that called `{compile-only=...}` the merged convention was wrong and is corrected. - Rebased onto `origin/feat/docs-rewrite` at `6f8e7ca`. The strict build reports no issues and no dead anchors. The Verify block passed on `5e00650`: build clean, `test_docs_examples.py` 200 passed and 2 skipped, `test_docs_testing_fixtures.py` 5 passed and 1 skipped (the Postgres test), `tests._docs_checks` exit 0. - The base still fails the 3 RLS tests in `tests/test_docs_migrations.py`. PR #744 fixes them, and this branch does not touch those files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…xample (#718) (#746) Closes #718 ## What The five concept pages (architecture, identity map, type safety, database backends, performance) now share one running `User` model and say only what the code does today. Take the architecture page's old example: `User.where(lambda t: t.active)` raised `TypeError: where() predicate callable must return QueryNode`. The page now opens with the model and a query that runs, with its result shown: ```python async with engines.session(): users = await ( User.where(lambda user: user.active == True) .order_by(lambda user: user.age, "desc") .limit(10) .all() ) assert [user.username for user in users] == ["grace", "ada", "alan"] ``` Each page runs from four rows (ada, grace, alan, edsger), declared once in `docs/examples/concepts.py` with its Annotated twin. Every code block runs under the harness: either a snippet from that script or a `{test=run prelude=concepts}` fence. The two blocks left compile-only say why in their fence (a pytest fixture, and a Postgres-only URL). ## Facts corrected (checked by running them) - **Architecture.** The dated "implementation status" note, the `docs/plans/` link and the "Phase 8.5" claim are gone. `build_model_schema()` no longer exists: the metaclass compiles SchemaIR, and `connect()` installs it into the engine. The schema-changes section links to Auto-migrate rather than restating the flags, and names no tests or crates. - **Identity map.** The tabbed example is now runnable twins. The `RuntimeError` text is shown. Rollback eviction and bulk-update eviction were run and both hold. - **Type safety.** `save()` does **not** validate. Setting `alan.age = "old"` and then calling `save()` wrote `"old"` to SQLite with only a serializer warning. The page now says validation runs when an instance is built (construction and `create()`), and points to `validate_assignment`. The field-completion-in-lambdas claim is gone. The `ValidationError` output is the real one. The `ferro/_core.pyi` path is fixed. - **Backends.** Postgres JSON is `jsonb` (`db_type="json"` for `json`). The type table now links to the generated table in Models and fields instead of repeating it. Raw SQL links point to `raw-sql.md`. The Supabase claim is dropped: CI runs `postgres:17`, not Supabase, and the page links Connections › Supabase instead. The troubleshooting how-to is gone: `Engine not initialized` already lives in the FAQ, the casts live in Raw SQL and the SQLite drift case lives in Schema changes. - **Performance.** The identity map is weak-valued and session-scoped. `include()` loads forward foreign keys today, and BackRef/M2M population links to the roadmap. The audit missed one: `exists()` is `count() > 0` (`query/builder.py`), so the "prefer exists() over count()" tip was false. It is replaced by an indexes tip. The `bulk_create` mechanics are corrected too: Python dumps each instance, and Rust binds the values and chunks them into multi-row `INSERT` statements. ## Audit rows answered rather than applied - performance L "Slow/Fast comments without SQL": no SQL block. The docs conventions say SQL is captured from a run, never typed by hand, and #700 (public SQL inspection) is still open. The prose names the statements instead (N `INSERT`s against one multi-row `INSERT` per chunk). - performance M "benchmark has no shown output": the debug build here gave unrepresentative timings, so the page describes the two printed lines rather than inventing a sample. - backends H "generate the table from `ferro_ddl_lowering`": done by linking to the generated `docs/includes/field-types.md` (#708) instead of keeping a second table. - type-safety: `assert_type` on `User.where(...).first()` fails under ty 0.0.56, which infers `Query[User | UnspecializedTypeVar]` for `User.where(lambda ...)`. The page only asserts what ty confirms (`get`, `get_or_none`, `all`, `count`, `exists`). This looks like a ty inference issue with `Predicate[Self]`. It may deserve its own issue. ## Verify Ran the ticket's Verify block on `646bc32` (round 1: `02ebf8c`, below): passed (strict build no issues; `tests/test_docs_examples.py` 193 passed, 2 skipped; `_docs_checks docs/pages/concepts` exit 0). TDD: RED `python -m tests._docs_checks <copies of the five original pages>` exited 1 with 7 one-letter-lambda violations (identity-map:103, performance:52/58/76, type-safety:51/52/53); GREEN `python -m tests._docs_checks docs/pages/concepts` exited 0, and the 21 concepts examples pass under `pytest -k concepts`. Verify block corrected: no Full suite: 3402 passed, 3 failed. All three are in `tests/test_docs_migrations.py` and concern `guide/row-level-security.md` step files missing from `STEP_FILE_SOURCES` (from #715). This PR touches neither file. ## Interfaces Interfaces changed: none ## Outside Files owned - `docs/examples/concepts.py`, `docs/examples/concepts_annotated.py`, `docs/examples/_preludes/concepts.py` (new): the runnable model twins and prelude that the acceptance criteria require. - `docs/pages/howto/multiple-databases.md`, `src/ferro/models.py` (one link each): they linked to identity-map anchors that the sentence-case headings renamed (`#no-session-no-identity` is now `#sessionless-operations`, `#eviction-and-invalidation` is now `#eviction`). ## Notes for the reviewer - A hydrated instance's `model_dump()` lists fields in alphabetical (column) order, not declaration order as a constructed instance does. The type-safety example compares with a dict, so the page does not depend on that order. It may be an I-2 observational-equivalence gap worth an issue. - The nav labels in `zensical.toml` stay Title Case ("Identity Map", "Type Safety", "Database Backends") because that file is not owned here. - No new glossary terms. - [ ] Exit step: issue #718 status updated (progress comment at pr-open) before merge. ## Fix report (review round 1) Rebased onto `origin/feat/docs-rewrite`. The strict build on the rebased head finds no dead anchors. - **type-safety.md:3 (Important):** the opening said every write is validated, which `save()` disproves. It now reads "building an instance validates its fields at runtime". The Pydantic models section already says what is validated and when: construction and `create()` validate, an assignment is not validated unless `validate_assignment=True`, and `save()` writes the values the instance holds. - **performance.md Benchmarking (Important):** the lead-in now says the block runs inside a [session](../guide/connections.md#sessions). - **backends.md unsupported URL (Minor):** I re-ran it and changed the block to the real exception, `InterfaceError: DB Connection failed for mysql://***: Unsupported database URL scheme 'mysql'. ...`. A result sentence now follows the block: the failed call registers no connection, so you can fix the URL and call `connect()` again (also checked by running it). - **performance.md "today" (Minor):** removed. Verify re-ran on `02ebf8c`, the pushed HEAD: strict build found no issues; `tests/test_docs_examples.py` 195 passed, 2 skipped; `_docs_checks docs/pages/concepts` exit 0. Verify block corrected: no. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ts, squash and a migrations reference (#714) (#744) Closes #714 ## What The 664-line migrations guide was a tutorial wrapped around five how-tos and a stack of reference tables. It is now four pages: ```diff guide/schema/migrations.md the loop: init, new, read, up, check, down, --replace, the PK recipe +guide/schema/renames-and-retirements.md rename hints, a rename under a rolling deploy, retiring a column or a label +guide/schema/squash.md squashing a feature branch's migrations (Range, Inheritance) +reference/migrations.md step file headers, step classes, door coverage, auto-migrate per backend, PassReport ``` Each reference table now has one home. The guides that used to carry a copy (overview's door table, auto-migrate's per-backend matrix and its short summary, deploying's expand/contract table, migrations' header table and its `check`/`status`/`drift` exit-code table) keep a summary in prose and link to it. The exit codes stay in the CLI reference, which already held them. Each new page has one running example. The renames page declares one `Author` at the top (from a new `base` region in `migrations_rename.py`). Its three examples each change that model once, and a test pins that each example's "before" state is that model. The squash page follows one teams branch on top of the loop's `0001_create_author`. Its rename-inside-the-range example squashes to the same `0002_teams`. ## Verify Ran the ticket's Verify block on `cf83b7a` (rebased on `feat/docs-rewrite`): passed (strict build, `test_docs_examples.py`, `test_docs_migrations.py`, `tests._docs_checks` over the owned pages). TDD: RED `uv run --no-sync pytest tests/test_docs_migrations.py -k "registry_entry or step_file_has_a_source or renames_page or squash or rename"` failed against the old pages and examples (12 failed: the six `starts_from_the_page_s_running_example` cases, the output and step-file registries, the `squash` output, three quoted step files). GREEN: the same file passes in full (189 passed, 1 skipped). Edge cases run by hand: renaming the PK recipe's `lambda book:` to `lambda t:` makes `tests._docs_checks` exit 1 naming `migrations.md:399`. The retire-label example's error check `raise`s in its `else:` when the label is accepted. Full suite: `uv run --no-sync pytest -n 8 --no-cov` passed (3422 passed, 341 skipped, 2 xfailed). Verify block corrected: no ## Interfaces Interfaces changed: none. Produces `guide/schema/squash.md`, `guide/schema/renames-and-retirements.md` and `reference/migrations.md`. They are not in the nav, and `zensical.toml` is untouched; the section globs already pick the pages up. ## Moved anchors, for #723's redirects | Old | New | | :--- | :--- | | `guide/schema/migrations.md#renames` | `guide/schema/renames-and-retirements.md#rename-hints` | | `guide/schema/migrations.md#retiring-a-column` | `guide/schema/renames-and-retirements.md#retiring-a-column` | | `guide/schema/migrations.md#retiring-an-enum-label` | `guide/schema/renames-and-retirements.md#retiring-an-enum-label` | | `guide/schema/migrations.md#headers` | `reference/migrations.md#step-file-headers` | | `guide/schema/migrations.md#changes-existing-rows-need-a-value-for` | `guide/schema/migrations.md#backfills` | | `guide/schema/migrations.md#checking` | `guide/schema/migrations.md#check-status-and-drift` | | `guide/schema/migrations.md#going-back` | `guide/schema/migrations.md#reverting-with-down` | | `guide/schema/migrations.md#editing-a-migration` | `guide/schema/migrations.md#edited-migrations` | | `guide/schema/migrations.md#two-branches-one-number` | `guide/schema/migrations.md#duplicate-migration-numbers-and-replace` | | `guide/schema/migrations.md#squashing-an-epics-migrations` | `guide/schema/squash.md` | | `guide/schema/migrations.md#a-rename-inside-the-range` | `guide/schema/squash.md#renames-inside-the-range` | | `guide/schema/migrations.md#every-database-that-applied-the-originals-inherits-the-squash` | `guide/schema/squash.md#inheritance-on-databases-that-ran-the-originals` | | `guide/schema/migrations.md#a-squash-is-pre-release` | `guide/schema/squash.md#squash-timing` | | `guide/schema/migrations.md#squash-or---replace` | `guide/schema/squash.md#squash-vs-replace` | `#install-and-set-up` keeps its id under the new heading "Installation and setup". `overview.md#what-each-door-covers` stays, now as a short summary. Every in-repo link to a moved anchor is updated, and the strict build checks them. ## Audit findings - **H, line 214 (table hint):** fixed already by #713. The text now lives on the renames page. - **H, lines 112–121 (`0001` SQL):** fixed and pinned by #713. Unchanged. - **H, lines 327–339 (SQLite rebuild):** the rebuild is quoted once, on data-steps (pinned). The migrations guide lists the four rebuild steps and links there. The rebuild refusal there now uses the running example (`author.nickname` made required on the loop's database). - **M, line 598 (pins paragraph):** deleted. The squash page states the behaviour instead: "A squash writes exactly the files `new` would write across the range." - **M, lines 458–598 (squash section):** moved to its own page. The page says "feature branch", not "epic". Headings: "Squash timing", "Inheritance on databases that ran the originals", "Renames inside the range", "`squash` vs `--replace`". - **M, page kind:** split as above, with two departures from the audit's suggestion: - "Changing a primary key" stays on the migrations guide, because `ferro migrate new`'s refusal prints "see the Migrations docs § Changing a primary key". - `--replace` stays in the loop as "Duplicate migration numbers and `--replace`", because the ticket gives it no page. - **M, inline-code commands:** `init`, `--sql-step`, `--data-only --data-step`, `--replace`, `down --yes` and `rekey` are now fenced `bash` blocks, each with a lead-in and a result. - **M, lines 7 and 92 (lead-ins):** added. - **M, `add_x`/`add_y` placeholders:** the two branches are now your `0002_author_bio` and the teammate's `0002_author_nickname` (the loop's migration). The regenerated migration reads `0003_author_bio`. - **M, I-7 in the retirement table:** the cells are prose ("required, indexed" → "nullable, indexed, `retired=True`"). - **L, header table version history:** moved into a self-contained note under the table. - **L, "a one-way step for older ferro releases":** replaced by a note at the end of "Squash timing". It links the 0.24→0.25 note on the deploying page through a new `squashes-and-older-releases` anchor, so the link lands on the note, not the section top (carried from #713's review). - **L, headings:** renamed as the audit suggests. - **L, GitHub `main` links to examples:** the pages name the example's repository path in code, with no link. A link to a tag would be release-bound, and embedding a whole `main()` would bury the page. - **L, `migrations_retire.py` default:** now `Field(default=None, index=True, retired=True)`. - **L, empty `FerroField()` in the retire-label twin:** removed. Every migrations twin, `migrations_quickstart_annotated.py` included, now uses `Annotated[T, Field(...)] = default`, with no `FerroField`. - **PassReport kinds:** no page held this table any more (only `Report`'s docstring listed some kinds). `reference/migrations.md` now lists every `role` and every `ReportKind`. A test pins both against `ferro.pass_report.Role` and the Rust `ReportKind` enum, in order. ## Notes for the reviewer - **Outside Files owned:** - `docs/pages/howto/upgrade-guide.md` (merged #707): two link lines that pointed into the moved squash and retirement sections now point at `squash.md` and `renames-and-retirements.md#retiring-a-column`. - `docs/pages/guide/models-and-fields.md`, `src/ferro/base.py` and `src/ferro/metaclass.py`: their links into moved anchors now point at the renames page. The strict build fails on the page link; the docstring links rendered dead. - `tests/test_docs_migrations.py` (owned) gains a scenario for `guide/row-level-security.md`'s quoted `0002_invoice_rls` step files. #715 merged that page with no `STEP_FILE_SOURCES` entry, so `test_docs_migrations.py` was red on `feat/docs-rewrite`. The quoted files match what `new` writes. - **Session wrapper criterion:** the new pages and the reworked guide make no ORM call in a block. Their blocks are models, CLI commands and output, and generated SQL. The one data step (the PK recipe) is marked `{test=compile reason=...}` and says it runs inside the step's own transaction. - **Running-example limits:** the migrations guide's "Hand-written steps" outputs come from a fresh project (`0002_seed`), and the PK recipe uses its own `Writer`/`Book`, as before. - **New terms:** none for `docs/includes/abbreviations.md`. ## Fix report (review round 1) - **Spec, Critical (dead anchors on the trial merge):** rebased on `feat/docs-rewrite` (`b0e5348`). `howto/upgrade-guide.md:74` now links [Squashing a feature branch's migrations](../guide/schema/squash.md), with "epic" gone from the link text, and `:100` links `renames-and-retirements.md#retiring-a-column`. I grepped `docs/pages` and `src/ferro` for links into the split pages: none points at a moved anchor, and the strict build on `cf83b7a` reports no issues. - **Quality, Important (`migrations.md:55`):** removed the repeated ", offline, and says what to run:". - Verify re-run on the rebased head `cf83b7a`: strict build clean, `test_docs_examples.py` 202 passed, `test_docs_migrations.py` 189 passed, `tests._docs_checks` exit 0. ## Exit steps - [ ] Issue status updated: #714 closes via this PR's merge into `feat/docs-rewrite` (the orchestrator closes it), and its progress comment shows `pr-open`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ings docstrings as reference entries (#720) (#747) Closes #720 ## Summary The docstrings for connections, sessions, fields, raw SQL, row security, settings and exceptions are now short Google-style reference entries. Each public object has a one-line summary, every parameter in `Args:`, Markdown autorefs and an example that runs. The long explanations were already on the guide pages. The docstrings now link to them, and the few facts the guides were missing were added. `ferro.migrate`'s `Returns:` used to render as five table rows of sentence fragments. It is now one row: ```diff Returns: - The ``PassReport``: every statement the pass sent to the database, in - order (``ExecutedStatement(subject, sql, role)``), and every warning - it raised (also raised as a ``UserWarning``). A pass that fails - partway raises its usual error with ``.report`` set to what committed - before the failure. + Every statement the pass sent, in order, and every warning it raised, + as a [`PassReport`][ferro.PassReport]. Each warning is also raised + as a `UserWarning`. ``` `version`, `reset_engine` and `set_default_connection` used to render the Rust doc comment ("Selects the default connection used by legacy unqualified operations", plus a rustdoc `# Errors` heading). They are now thin Python functions in `ferro/__init__.py` with their own docstrings: ```text ferro.set_default_connection(name) "Set the connection `engines.session()` opens when called without a name." -> _core.set_default_connection(name) ``` Where the essays went: ```text connect (95 lines) -> guide/schema/auto-migrate.md (already there), linked; one line added to guide/connections.md PoolConfig (~90 lines) -> concepts/row-level-security.md#connection-delivery (+ "A late first setting" bullet) Session.set_config (68) -> concepts/row-level-security.md#visibility-of-a-new-value (+ the failure case) ``` ## Evidence - **Before:** `ruff check --select D417` on the owned files found 3 errors: `Field` (`db_check`, `db_type`, `reverse_index`), `ManyToMany` (`**kwargs`) and `execute` (`*args`, `session`, `sql`, `using`). A griffe sweep of the owned modules found about 150 public objects or fields with no docstring, 51 Sphinx roles and 6 RST `::` blocks. **After:** D417 is clean. The sweep finds nothing missing except Pydantic's `model_config`. No roles or `::` are left in the owned files, and the API pages render none. - Error path: when the `autocommit` line is removed from `execute`'s `Args:` (in a scratch copy), `ruff check --select D417` fails with `Missing argument description ... execute: autocommit`. - `tests/test_docstring_examples.py`: 141 passed. There are 26 new runnable examples in the owned modules, all marked `{test=run ...}`. ## Verify Ran the ticket's Verify block on `ea7c2a9`: passed. The strict build found no issues; 141 docstring examples passed; `_docs_checks` was clean; `ruff --select D417` was clean; `test_docs_site.py` gave 13 passed. The full suite (`pytest -n 8 --no-cov`) gave 3498 passed, 342 skipped and 2 xfailed. The RLS step-file failures the brief mentioned were already fixed on the base by #744. TDD: RED `ruff check --select D417 <owned files>` failed with 3 errors as expected (above); GREEN, the same command printed `All checks passed!`. Verify block corrected: no ## Interfaces Interfaces changed: none. `engines.session()` is documented in full, with Args, Returns, Raises and two examples. `ModelConnection` already had full docstrings from #719, and #722 renders it. ## Notes for the reviewer - **Engine helpers as Python wrappers.** The site builds API pages by inspecting the compiled `_core` module, not by reading `_core.pyi`. A docstring in the stub would never render, and the docstring harness would not run its examples either, because tests import `ferro` first. Each of `version`, `reset_engine` and `set_default_connection` is now a one-line Python wrapper in `__init__.py` with the docstring, so the site, `help()` and the harness all read one text. `_core.pyi` is unchanged. The Rust doc comments in `src/connection.rs` (outside Files owned) still use the old wording and only appear in `help(ferro._core.reset_engine)`. - **`PoolConfig.validate` is now `_check_bounds`** (audit row). The public name shadowed Pydantic's deprecated `BaseModel.validate` classmethod and rendered as an undocumented member. No caller used it. - **Preludes.** The `connect`, `reset_engine`, `set_default_connection` and `clear_registry` examples use `prelude=connections`, not `reference`. The `reference` prelude holds a session open, and `reset_engine()` inside it breaks the prelude's exit. - **Scope of "public".** Every non-underscore object in the owned modules now meets I-14. That includes internal modules no page renders (`columns`, `registry`, `state`, `composite_*`). `_core` is a private module, so its other stubs are untouched. Docstrings of underscore-prefixed helpers keep their maintainer notes, ADR numbers included. User-facing error messages are unchanged (no behaviour change), including `retired_clause`'s "(ADR-0053)". - **Audit rows that turned out wrong or moot.** The `exceptions.py` row about `ck_pair_at_most_one_side`: that name is in `docs/pages/api/exceptions.md` (not owned), and it already has the `ck_<table>_<suffix>` form (table `pair`). The audit's `fetch_one` claim "the rest of the result is fetched and discarded" is not what I wrote. The docstring says "Ferro does not add `LIMIT 1`; write it when more than one row can match", which matches the Raw SQL guide. The `connect` row said renames go to `ferro migrate new`. Auto-migrate now applies `renamed_from` itself (`RENAME COLUMN`), so the docstring sends only primary-key changes to `#changing-a-primary-key`. - **Dead anchors outside my files**, found by checking every link on the built API pages: `base.py` links `models-and-fields.md#overriding-the-column-type-with-db_type`, `models.py` links `#composite-constraints`, and `query/builder.py` links `queries.md#every-traversed-hop-is-an-inner-join`. None of the three headings exists. The strict build does not check anchors on API pages. - **Outside Files owned:** none. - [x] Exit step: issue status is current (progress comment on #720 updated; `Closes #720` above; the orchestrator closes the ticket after the merge into `feat/docs-rewrite`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#748) Closes #722 ## What The API reference had no entry for `QueryNode` or `FieldProxy`, the two types every predicate signature names. `Query` was documented as `ferro.query.builder.Query`. Now every exported name has exactly one entry under the path a reader imports it from: ``` ::: ferro.query.builder.Query -> ::: ferro.query.Query ::: ferro.query.Row -> ::: ferro.Row ::: ferro.migrations.steps.chunked -> ::: ferro.migrations.chunked ::: ferro.migrations.alembic.get_metadata -> ::: ferro.migrations.get_metadata ``` `FieldProxy`, `RelationProxy`, `QueryNode`, `ValueExpr`, `ModelConnection`, `EngineManager.session`, `RunReport`, `StatusReport` and `CheckReport` get entries. `show_if_no_docstring` is off and `show_category_heading` is off. Each API page is titled after its objects (`Query`, `Model`, `ferro.migrations`, `connect, Session and the registry`, ...). ## Verify Ran the ticket's Verify block on `bdff07d`: passed (strict build: no issues; `tests/test_docs_site.py`: 19 passed). Full suite: 3514 passed, 342 skipped, 2 xfailed. TDD: RED `uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheprovider` on `fa03a58` failed as expected: `exported names with no API entry: ['ferro.Row', 'ferro.Rows', 'ferro.migrations.chunked', ..., 'ferro.query.QueryNode', ...]`, plus the non-public-path, title, empty-entry and dead-anchor tests. GREEN: the same command passed (19/19). Error path: deleting `::: ferro.query.QueryNode` fails with `exported names with no API entry: ['ferro.query.QueryNode']`. Verify block corrected: no ## Interfaces Interfaces changed: none `test_every_export_has_an_api_entry` is in `tests/test_docs_site.py`, as the ticket names it. Beside it: `test_api_entries_use_public_paths`, `test_api_pages_are_titled_after_their_objects`, `test_no_api_entry_renders_empty`, `test_every_public_object_and_member_has_a_docstring` (griffe; with `show_if_no_docstring` off, an undocumented member would vanish from the page without a warning) and `test_links_from_api_pages_land_on_existing_anchors`. ## Notes for the reviewer - **Not exported, documented under their module.** `RunReport`, `StatusReport`, `CheckReport`, `ModelConnection`, `EngineManager` and `Subject` are in no `__all__`, so they render as `ferro.migrations.runner.RunReport` and so on. They are listed in `RETURNED_TYPES` in the test. Exporting them would be an API change, outside this ticket. - **`Relation`** now shows `add`, `remove` and `clear` (`inherited_members`), and `Query` filters them out, since they work only on a many-to-many relation (audit row). `where()` and the other query methods stay on `Query`. - **`model_config`** is filtered globally: on `PoolConfig`, `FerroSettings` and `DatabaseSettings` it was the only member with no docstring, and on `Model` and `Row` it rendered Pydantic's own docstring. - **Audit rows applied:** the queries intro is cut to a list of links (the `select()` table and the aggregates section stay, as a result-type table; ADR and issue numbers are gone); the connection page is grouped into Connecting, Sessions, Registry, Auto-migrate reports and Project settings; the exceptions page's 150-word refusal sentence now points at the migrations reference's Errors section; the migrations "Running migrations" paragraph is a parameter table. The `#aggregates-and-grouped-queries`, `#running-migrations`, `#data-step-declarations` and `#the-test-harness` anchors that guides link to are kept. - **Audit rows already resolved upstream:** Sphinx roles, Rust `# Errors` headings, source blocks and dunder members no longer render (#701, #719 to #721). The `Field` docstring rows (Pydantic parameters, broken Pydantic links) are docstring rewrites and stay out of scope. - No audit row turned out wrong. ### Outside Files owned - `src/ferro/base.py`, `src/ferro/models.py`, `src/ferro/query/builder.py`: link-only fixes to three renamed guide headings, as the brief allows. - `docs/pages/guide/queries.md`: one link pointed at `api/queries.md#include-and-populated-relations`, a section this PR removes; it now points at `#ferro.query.Query.include`. - `tests/test_docs_migrations.py`: its page check counted every `::: ferro.migrations.*` target; it now counts only `ferro.migrations.<name>`, so the report entries under their modules do not break it. - `docs/solutions/patterns/docstring-conventions.md` (I-5): the strict build does not check anchors in docstring links, and the tests that now do. ## Exit steps - [ ] Issue status updated: #722 is closed by the orchestrator after this PR merges into `feat/docs-rewrite`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) (#749) Closes #723 ## What The site had nine tabs, two of them a single page, and two pages both called "Migrations". It now has six: | Tab | Holds | | :--- | :--- | | Get started | Ferro ORM (home), Why Ferro, Installation, Quickstart | | Guide | the product areas; Schema management now includes Renaming and retiring, Squashing, and Adopting migrations | | How-to | the five "How to ..." recipes and Migrate from SQLAlchemy | | Concepts | adds Row-level security and session settings | | Reference | Python API (the eight API pages), CLI, Configuration, FerroError and its subclasses, Migrations reference, Row-level security | | Project | FAQ, Roadmap, Upgrading between versions, Changelog, Contributing | Every nav label is now its page's H1, and every page has a one-sentence `description` in front matter, which reaches the page's `<meta name="description">`. The five pages new in this epic carry `status: new`. Old URLs keep working. A reader with a bookmark to `/howto/upgrade-guide/#upgrading-to-014` lands on `/upgrading/#upgrading-to-014`; one following an old link to `/guide/row-level-security/#the-nullif-contract` lands on `/concepts/row-level-security/#policy-expressions`. ## Changes - Moved: `howto/adopting-migrations.md` → `guide/schema/adopting-migrations.md`; `howto/upgrade-guide.md` → `upgrading.md`. Deleted: `getting-started/next-steps.md` (#705 folded its content into the quickstart's "Next steps"). - `redirect_maps`: one entry per moved or deleted page, each Next steps section → the quickstart's Next steps, the 14 anchors #744 lists, and 19 anchors of the old row-level security guide (derived by diffing its headings at `2a45de9` against the three current pages). An anchor on a page that still exists redirects through the theme's `redirect.json`. - Retitled so labels can equal H1s: "Typed query predicates" (sentence case), "Alembic bridge", "Migrations reference" (it shared "Migrations" with the guide), "Upgrading between versions", and `changelog.md` now has its own "# Changelog" and embeds `CHANGELOG.md` from line 2 (CHANGELOG.md itself untouched, I-10). - `llmstxt` sections: Next steps dropped, "Community" → "Project" with the upgrade page added. - Links to the moved pages updated in `docs/pages`, the `ferro.migrations.baseline` docstring, and the example files' docstrings. ## Verify Ran the ticket's Verify block on `d1a7219`: passed (strict build: no issues; `test_docs_site.py` 30 passed; `test_docs_migrations.py` 198 passed, 1 skipped; `test_docs_examples.py` 194 passed, 2 skipped). Full suite: 3525 passed, 342 skipped, 2 xfailed. TDD: RED `uv run --no-sync pytest tests/test_docs_site.py -q --no-cov -p no:cacheprovider` failed as expected (11 failed: six tabs, nav coverage, labels, descriptions, status, redirects); GREEN the same command passed (30/30). Verify block corrected: no ### Test scenarios - Happy path: the strict build passes with every page in the nav (`test_every_page_is_in_the_nav_once`, `test_nav_has_the_six_tabs`, `test_every_nav_label_equals_its_page_h1`, `test_nav_labels_are_unique`). - Integration: the old URLs of the moved pages resolve to redirects in the built site (`test_moved_page_is_gone_and_its_url_redirects`, three cases), and every moved anchor maps to a section that exists and no longer exists at its old address (`test_moved_anchors_redirect_to_existing_sections`). - Added: `test_every_page_has_a_one_sentence_description`, `test_page_descriptions_reach_the_built_pages`, `test_pages_new_in_the_rewrite_are_marked_new`. ## Interfaces Interfaces changed: none ## Notes for the reviewer - **Outside Files owned:** the new tests live in `tests/test_docs_site.py`, because they need its `built_site` fixture (#722 owned that file and has merged). Link fixes touch `src/ferro/migrations/_baseline.py` and the docstrings of `docs/examples/upgrade_*.py` and `migrations_adopting.py`, as the brief's scope allows. - **Ticket vs merged titles:** the ticket names "Testing with Ferro". The how-to tickets already retitled that page "How to test a Ferro app with pytest", which is distinct from "Testing migrations", so I kept it, and kept the other how-to H1s as those tickets wrote them. - **Audit rows that turned out moot:** `guide/migrations.md` is no longer a stub to exclude from search; #701 replaced it with a generated redirect. The Next steps rows are resolved by the deletion. - The "Schema management" section's first page is also titled "Schema management" (its H1); the label rule forces this, and the site has no section index pages. - The anchor redirects on pages that still exist work through the theme's client-side `redirect.json`, which Zensical builds from `redirect_maps`; a reader with JavaScript off lands at the top of the page. - New glossary terms: none. ## Exit steps - [ ] Issue status updated: #723 closes via this PR's merge into `feat/docs-rewrite` (the orchestrator closes it), and its progress comment shows `pr-open`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #724 ## What The docs checks that were opt-in are now gates. Say a docstring gains this: ```python def where(...): """Return a :class:`Query`. Returns: A query over the matching rows; nothing runs until you await one of its methods. """ ``` The built page shows `:class:` as literal text and splits the `Returns:` description into two table rows, the first ending at "await one". Both now fail `tests/test_docs_site.py`. The two `Model` docstrings that really did split this way (`where`, `using`) are fixed. - **Rendered site** (`markup_defects`, `docstring_warnings` in `tests/test_docs_site.py`): fails on a literal Sphinx role, a `[text][target]` left unresolved, a list merged into its paragraph (a `<p>` holding a `- ` or `1. ` line), a docstring section rendered as prose, a section table row that stops mid-sentence, and any griffe warning in the build log (griffe drops an unreadable `Args:` item with only a warning, which `--strict` lets through). A second build, of a page and a module seeded with each defect, shows each check catching it. It also fails on a `# noqa` in any rendered code block, and checks that CI's docs job runs every `tests/test_doc*.py` file and the docs lint. - **Page fences** (`tests/_docs_harness.py`): every Python fence on a page says how it is tested: `{test=run prelude=<name>}`, `{test=compile reason="..."}` or `{test=pytest path="..."}`. An unmarked fence, `{compile-only="..."}`, a bare `{test=run}`, an unknown mode or a mode without its setting fails with the marker to write. The 17 `compile-only=` fences are converted, `upgrading.md`'s bare `{test=run}` runs with `prelude=upgrade`, and the 7 unmarked API/reference fences state why they cannot run. - **Docs lint**: `tests/test_docs_lint.py` runs `python -m tests._docs_checks docs/pages docs/examples src/ferro` (exits 0) and fails on a seeded `lambda t: t.id` page. - **`tests/test_documentation_features.py`**: the four many-to-many tests skipped with a reason that pointed at a deleted page are gone. `add`, `remove`, `clear` and the reverse side all run in `docs/examples/relationships.py`. So does every other hand copy, except an `|` predicate and two concurrent transactions, which are what the file keeps. - **`# noqa: E711/E712`**: 43 comments are removed from `docs/examples` and the pages. The ignore is now a ruff per-file ignore for `docs/examples/*` in `pyproject.toml`. - **CI**: the `docs` job builds the extension, runs the docs lint and every docs test file. - **Review follow-ups from #723**: the schema overview's nav label and H1 are both "Overview of schema management", and the `llmstxt` section is "Get started". ## Verify Ran the ticket's Verify block on `ccc1616`: passed (strict build clean; site 43 passed; examples 202 passed, 2 skipped (Postgres); docstrings 141 passed; migrations 198 passed, 1 skipped; lint + features 4 passed; `_docs_checks` exit 0). Full suite: 3514 passed, 338 skipped, 2 xfailed. TDD: RED `pytest tests/test_docs_site.py -k "markup or docstring_parses or seeded"` failed as expected (1 failed, 10 passed: the seeded checks passed and the real tree showed the two split `Returns` rows and four merged lists); GREEN after the fixes (41/41). RED `pytest tests/test_docs_examples.py -k "fence or prelude or compile or refused"` failed at import (`page_fence_mode` did not exist); GREEN 17/17, then the page sweep listed 25 unmarked or misspelled fences, now 202 passed. Verify block corrected: no ## Interfaces Interfaces changed: none ## Notes for the reviewer - **Outside Files owned**: `zensical.toml` and the H1 of `docs/pages/guide/schema/overview.md` (#723 review follow-ups, asked for in this ticket's brief); `src/ferro/models.py` (indent two `Returns:` continuation lines, which the new gate caught); `CONTRIBUTING.md` and `docs/pages/guide/schema/deploying.md` (a blank line before four lists that rendered as paragraphs, and 4-space nesting in the release steps); `AGENTS.md` (the two ticket-number references, rewritten to describe the gate); `docs/solutions/patterns/docs-page-conventions.md` (the three fence markers, `{test=pytest path=...}` included). - `ruff format` rejoined the lines in five example files once their `# noqa` comments were gone, so a few lambdas now sit on one line. - Docstring fences keep their rule: an unmarked one is compiled. Only page fences must carry a `test=` mode, because the audit row and the acceptance criterion are about inline page blocks; 19 docstring fences are compile-only today. - Audit row 6 counted 184 compile-only page blocks. On this branch, after the page tickets, 8 page fences were unmarked and 17 used `compile-only=`. The rest already ran or carried `test=compile reason=`. - `test_documentation_features.py` is a `backend_matrix` file, and the docs examples run on SQLite only. CRUD and query behaviour runs on both backends in `test_crud.py` and `test_query_builder.py`. Many-to-many `add` and `remove` run on Postgres in `test_auto_migrate.py` and `test_catalog_cache.py`. The four deleted many-to-many tests had been skipped, so they never ran on either backend. I found no Postgres test of `clear()` or of reading the reverse side, and this PR does not add one. If you want that covered on Postgres, say so and I will put it back here as a backend-matrix test. ## Exit steps - [ ] #724 status confirmed (closed by this PR's merge into the integration branch, via the orchestrator) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g and .sql()) Resolves the docstring-vs-terminal conflicts in the query builder by keeping main's terminals and signatures with the rewrite's reference docstrings; the Relation overrides keep their documented examples as terminals at runtime behind main's typing descriptors. The Logging section lands before the Connections guide's See also; RenderedSql, REDACTED and Terminal join the Query API page under their public paths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…tic docstring harness After merging main: export Terminal from ferro.query so the API page and every autoref use a public path; make RenderedSql.params a one-line field docstring and indent Terminal.sql's Returns continuation so neither renders as table fragments; mark the Logging section's fence to run under the connections prelude; and make the docstring harness load griffe statically and skip compiled sources, since an imported ferro let griffe hand back _core's .so for the new RenderedParts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…own test (#725) (#775) The Queries, Aggregations, Relationships and Mutations guides show the SQL Ferro sends beside each example: 132 sql blocks, each opening with a marker (-- <example>.py:<snippet> (<dialect>)) that tests/test_docs_sql.py regenerates from the example's own .sql() (or the statement log for an instance write) and compares byte for byte, with SQLite and PostgreSQL tabs where more than placeholders differ. A .sql() failure must match the call's failure or the capture fails naming the example line. The page conventions carry the recipe. Interfaces changed: none Closes #725 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
Verdict: FIX FIRSTOne Critical (a docs commit removed two Spec: FAIL
Every other user story checked has its page, example or test: 1-16, 18-32, 34-81 (prose stories 28-40 sampled on six pages). CoherenceThe slices agree on every shared interface except two low-severity seams:
Checked and consistent: fence grammar (every marker is run, compile or pytest; every FindingsCritical
Important
Minor
Invariants and ADRsChecked: AGENTS.md in full (I-7, I-8, I-10, I-11, I-13, I-14), GLOSSARY.md (Include, Populated relation, Session settings, Statement log, Rendered statement, Run, Step, Squash, Inheritance and their Avoid lists), CONTRIBUTING.md, .cursorrules, docs-page-conventions.md, docstring-conventions.md, the writing-for-humans rules 1–24, ADR-0038 (tracked-database refusal on overview.md and auto-migrate.md), ADR-0049 (auto-migrate.md report text), ADR-0053 (renames-and-retirements.md), ADR-0056 (squash.md), ADR-0057 (connections.md Logging: attributes, operations, pool hooks, NullHandler); I-14 scan of every public docstring outside ferro/ir; I-8 scan of every query-method lambda in pages, examples, src and README; every page scanned for minimising words, ADR/issue numbers, dated notes, non-sentence-case headings and lead-ins (6 blocks without a colon, prose-introduced, acceptable); CHANGELOG.md untouched; CoveragePersonas: correctness, testing, maintainability, standards-and-invariants, history, depth, adversarial (25k executable lines: the harness runs scripts in subprocesses), api-contract (rendered API surface; Residual risk
|
…panel F2 on #777) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…g.validate, save() columns in field order, docstring fences marked (#779) Panel findings on PR #777 that a docs commit must not have made: ferro migrate status --no-steps/--no-json restored (F1), PoolConfig.validate restored (F3), Model.select/Query.select Raises name AttributeError (F4); the typed Session.query (F5) and the ferro.migrations._chunk_loop rename (F16) are kept and noted in the upgrade guide. Every write (save new and fetched, save(only=), save(exclude=), bulk_create, update) now lists columns in model field order (#776), so the Mutations guide's regenerated SQL no longer depends on a hash-seed pin. The 19 unmarked docstring fences are marked. Interfaces changed: the routeless-operation RuntimeError text reads "… removed in v0.14 (deprecated since v0.12); see the upgrade guide." (the pre-rewrite text named v0.13 and a removed page; routing was removed in v0.14.0), pinned exactly and quoted on the Connections guide. Closes #776 Part of #698 (panel round 1 on PR #777) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…erated, one fence reader and runner, twin equivalence, CLI reference complete (#780) Panel findings on PR #777 in the harness and pages: every printed-output block is marked and regenerated from its example (F7, including the twelve Ferro error/warning texts, three of which had gone stale); the nine entry points must keep a running Examples block (F8); one fence reader, one subprocess runner and one check_fence in the harness (F9, F10, F21); _annotated twins generated from their Assignment scripts and pinned equal in source, SchemaIR and stdout (F11, F15); the Aggregations opening is a whole program (F13); the SQLite caveat is back (F14); the CONTRIBUTING fence is collected (F17); six renamed anchors redirect and MOVED_ANCHORS says what it covers (F18); the CI docs job runs by glob (F19); the SQL capture is a helper module (F22, F23); every migrate verb's flags appear in the CLI reference, pinned by a --help comparison (Spec, F24); the RLS how-to sits in the How-to tab (Spec); eight plain-Postgres page blocks run under {test=run ... backend=postgres} (F25; the five framework blocks stay compile-only); an unmarked docstring fence is refused and every prelude is cited. Interfaces changed: the printed-output marker <!-- prints: <example>.py:<snippet> [(postgres)] --> / <!-- prints: above --> / <!-- shows: <what> -->; harness functions fenced_blocks, run_python, check_fence; helper module tests/_docs_sql.py (captured, check_block); fence setting backend=postgres. Part of #698 (panel round 1 on PR #777) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
Verdict: FIX FIRST · Round 2Verify is red on the new head (two Earlier findings
Spec: PASS
CoherenceThe slices agree on every shared interface except one minor seam:
New findingsCriticalnone Important
Minor
Invariants and ADRsChecked: AGENTS.md (I-3, I-6, I-7, I-8, I-10, I-13, I-14), GLOSSARY.md, the two conventions pages as changed, the writing-for-humans rules, ADR-0057 (the CoverageFix diff: Residual risk
|
…e redirect and one region parser, the shows: guard, the 0.26 notes (#782) Round 2 panel findings on PR #777: the Deploying guide's refusal pin reads the regenerated numbering (F26, the suite is green again); the label-addition fence runs on Postgres against a live enum with only 'free' and shows ['free', 'pro'] afterwards (F27); one connect() redirect and one snippet-region parser in tests/_docs_shared.py (F28, F29); the shows: guard matches every kind of Ferro output and VARIES is keyed by page:line (F30); the 0.26 upgrade notes state the reworded no-route message and the field-order writes, with update() stated separately (F31); one name per migration number on the Deploying guide (F32); the Postgres redirect keeps ferro_search_path and the sslmode block is compile-only with a true reason (F33); one Postgres-block rule in the conventions, the session.py docstring fences run on Postgres (F34); true compile reasons (F35); error regions print the exception's own class (F36); output:/refusal: notes outside the compared set are refused (F37). F38 and F41 partly; F39 and F40 left for Learnings. Interfaces changed: tests/_docs_shared.py (point_connect_at, snippet_regions); check_printed takes database_url. Part of #698 (panel round 2 on PR #777) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
Verdict: HUMAN DECISION · Round 3Verify is green on Earlier findings
Spec: PASS
CoherenceThe slices agree on every shared interface except one minor seam:
Quality: PASS
New findingsCriticalnone Important
Minor
Invariants and ADRsChecked: AGENTS.md (I-1, I-6, I-7, I-8, I-10, I-14), GLOSSARY.md, both conventions pages as changed by this round, the writing-for-humans rules, ADR-0057 (the CoverageFix diff: Residual risk
|
…and documented (panel Round 3 F42, F44) (#784) Two findings from the panel's Round 3 report on #777, fixed before merge. ## F42 · the 0.26 notes misstated the secret-column change `docs/pages/upgrading.md`, "Upgrading to 0.26": - The field-order bullet no longer says a secret column came last. In 0.25 a `SecretStr`/`SecretBytes` column was dumped in field order, as the mask. "The rows written are the same" is now "The order changes no value written". `update()`'s clause names only `bytes`. - A fifth bullet states what changed for secrets: the real value is stored and loads back as `SecretStr`/`SecretBytes`, where 0.25 wrote `**********` (on `save()`, `bulk_create()` and `update()`) and loaded the `str` `**********`. A row 0.25 saved holds the mask and cannot be recovered. A `SecretBytes` column is now `BYTEA`/`BLOB`, where 0.25 created `varchar`, and it needs a migration before 0.26 saves into it. - The count is now "Five changes". I checked one fact by hand, and it differs from #762's body. On this branch a `SecretBytes` field over a 0.25 `varchar` column **loads without error** on SQLite and on Postgres; I saw no `Failed to hydrate`. The real hazard is the write. On Postgres, `save()` of `SecretBytes(b"\xff\x00k")` into the `varchar` column stores the text `\xff006b`, and that text loads back as the bytes `b"\\xff006b"`. The bullet says exactly that. On SQLite the value round-trips. Covering test: `tests/test_save_column_order.py::test_the_upgrade_guide_states_the_secret_column_change`. It checks that the order bullet has no "secret column came last" and that the section opens with "Five changes". It also checks that the secret bullet names `SecretBytes`, `**********`, "cannot be recovered", `varchar`, `BYTEA`, `BLOB` and "migration". ``` $ uv run --no-sync pytest tests/test_save_column_order.py -k upgrade --no-cov -p no:cacheprovider RED (before the page edit): 1 failed, 1 passed GREEN: 2 passed ``` ## F44 · the Overview printed `RunRefused`, a class nothing exported - `ferro.migrations.RunRefused` is exported (`__all__`). It appears in the API migrations page as `::: ferro.migrations.RunRefused` and in the exceptions tree, between `MigrationRefused` and its two subclasses `AlreadyTrackedError` and `AppliedAboveBaselineError`. - Its docstring (in `report.py`, docstring only) now says that `connect(auto_migrate=True)` also raises it and that `except MigrationRefused` catches it. It gains an `Examples:` block that matches on `kind`. `kind` keeps its attribute docstring. I did not add an `Attributes:` section: every class in the package documents fields that way, and a section would render `kind` twice. - `overview.md` now says, after the block, that the error is a `RunRefused`, a subclass of `MigrationRefused`, so `except ferro.migrations.MigrationRefused` catches it. It also says that `kind` names a run's refusal and is `None` for this one. I checked: this refusal's `kind` is `None`, so "whose `kind` names the refusal" would have been wrong here. - `tests/test_docs_output.py`: `_ferro_exception_names` now lists only public classes. A public class is one in `ferro.__all__` or `ferro.migrations.__all__`, or a public class of `ferro.exceptions`. A new check, `test_a_printed_exception_name_is_one_a_reader_can_import`, runs over every text block and fails a line that opens with an internal Ferro exception class. `test_a_block_naming_an_internal_exception_class_fails` pins the check with `StepRefused`, bare and dotted, and `test_the_recogniser_names_only_public_exception_classes` pins the list. ``` $ uv run --no-sync pytest tests/test_docs_output.py -n 8 --no-cov -p no:cacheprovider RED (before the export): 1 failed (guide/schema/overview.md:46 prints ['RunRefused']), 331 passed GREEN: all passed ``` Interfaces changed: ferro.migrations.RunRefused is exported (new public name; the class existed and was raised before) ## Verify (on 640726c) | Command | Result | |---|---| | `uv run --no-sync zensical build --clean --strict` | No issues found | | `pytest tests/test_doc*.py tests/test_docstring_examples.py tests/test_routing_errors.py tests/test_save_column_order.py tests/test_step_class.py -n 8 --db-backends=sqlite,postgres` | 1429 passed | | `python -m tests._docs_checks docs/pages docs/examples src/ferro` | exit 0 | | `ruff check src/ferro tests docs/examples && ruff format --check .` | clean | | `pytest tests --db-backends=sqlite,postgres -n 8` | 5857 passed, 69 skipped, 2 xfailed | Verify block corrected: no ## For the reviewer - On Postgres, a `SecretBytes` value saved into a 0.25 `varchar` column is stored as hex text and loads back wrong, with no error. The upgrade note warns about it. The code may deserve its own issue; I left `src/` alone here. - `src/ferro/migrations/errors.py`'s module docstring still says `RunRefused` lives "beside it in `ferro.migrations.report`". That file is outside this slice, and the sentence still holds. - [x] Issue status: no issue closes here; this is part of #698. Part of #698 (panel Round 3 on PR #777) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Post-Round 3 fix mergedF42 and F44 from the Round 3 report are fixed in #784, squash-merged as 0b72ba5 after one fresh review (Spec FAIL on S1, the Verify on 0b72ba5 ( Interfaces changed since Round 3: Ready to merge. |
Summary
Closes #698.
The docs rewrite: every page on the site is one kind, carries one running example, shows the result of every block and the SQL beside every query, and runs under a harness that fails the build when a snippet, a docstring example, a quoted migration file or a captured SQL block drifts from the code. 25 tickets, 211 files, +24,331 / −11,840.
What a reader gets, in order of what the audit found worst:
update()canonicalization andafter()/before()instead of hand-rolled recipes.async with engines.session():or sits under a sentence that says it runs in one; snippet ranges end below the assert or print that holds the result; inline fences run under a named prelude or say why they only compile..sql()(or the statement log for an instance write), with SQLite / PostgreSQL tabs where more than placeholders differ.__ferro_*__members, every export under its public path,Returns:/Attributes:as tables, runnableExamples:on the sharedAuthor/Postprelude.Merged into this branch on the way:
mainat f3e46f8 (#700's statement log and.sql(), #760's SecretStr fix), with the docstring harness made to load griffe statically so an importedferrocannot hand it the compiled_coreas a source file.Evidence
IndentationErrorand thenRuntimeError: No database route; 280 literal:class:roles on the API pages; five public objects with no entry; 50 snippets ending above their result; four hand-simplified SQL blocks and none on Aggregations.After on
00dd767:zensical build --clean --strictclean; docs tests 506 passed (examples, docstring examples, site checks, lint, SQL regeneration);tests/test_docs_sql.py223 passed on sqlite+postgres;python -m tests._docs_checksoverdocs/pages,docs/examples,src/ferroexit 0; full suite on the branch at 7c19ce7 (before Show captured SQL beside query and mutation examples (needs #700) #725, which changed docs and tests only) 5078 passed, 69 skipped, 2 xfailed on sqlite+postgres.Merge Danger
Door: two-way for the site and the tests; one-way for readers who bookmarked old URLs (redirects cover every moved page).
Blast Radius: docs site, docstrings, CI's docs job
Published pages change wholesale; the docs CI job gains tests that fail on drift (a snippet that no longer runs, a docstring example that fails, a quoted migration file or SQL block that differs), so future code changes that alter rendered SQL or step files must regenerate the docs blocks. One behaviour fix outside docs:
ferro --helpkeeps its<placeholders>(#702). Theferro.querypackage exportsTerminal(new public name, typing only).🤖 Generated with Claude Code
https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP