Skip to content

docs: panel round 2 on #777 — green suite, honest Postgres fences, one redirect and one region parser, the shows: guard, the 0.26 notes - #782

Merged
0x054 merged 5 commits into
feat/docs-rewritefrom
sdd/698-fix-round-2
Oct 21, 2026
Merged

0x054 merged 5 commits into
feat/docs-rewritefrom
sdd/698-fix-round-2

Conversation

@0x054

@0x054 0x054 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Panel round 2 on #777, every finding F26–F37 in one slice. The full suite is green on both dialects again; four Postgres fences now prove what they claim; there is one connect() redirect and one snippet-region reader; the shows: guard recognises every block of Ferro output; and the 0.26 upgrade notes name the two behaviour changes.

Head: c70297e. Four commits: f994165 (F26, F32, F36), e112bfb (F31), 3fae90c (F27, F28, F29, F33, F34, F35), c70297e (F30, F37, part of F38).

Findings

F26: stale numbering pin in test_step_class.py

The test is renamed test_expand_only_refuses_a_contract_step_as_the_deploying_guide_shows. Its runtime half is unchanged: it raises the real DatabaseAheadError, checks contract_steps == ["0002_drop_bio:01"], and checks the message names the step. Its page half no longer compares a hand-typed number. It now checks two things: the guide shows the expand-only recipe, and the page's refusal is the regenerated <!-- prints: migration_errors.py:ahead-by-contract --> block, which test_docs_output.py pins.

  • RED (before the change): pytest tests/test_step_class.py -k deploying gave FAILED …test_the_deploying_guide_quotes_the_refusal_expand_only_raises[sqlite].
  • GREEN: the full suite, below.

F27: the label-addition block never added a label

  • The models now live in docs/examples/enum_labels.py, with its generated _annotated twin, and the page's two tabs embed them. This replaces the two compile-only tabs.
  • A new prelude, label_addition, builds the earlier release's schema on the target database before the block runs: CREATE TYPE "plan" AS ENUM ('free') plus author in the shape the create pass leaves.
  • The block ({test=run prelude=label_addition backend=postgres}) connects with migrate_updates=True, then prints the type's labels under <!-- prints: above -->: ['free', 'pro'].
  • I ran it once by hand with the ferro.sql log at DEBUG. The only DDL it sent was ALTER TYPE "plan" ADD VALUE IF NOT EXISTS 'pro'.
  • Harness change: a prints: above block under a backend=postgres fence is compared in the Postgres job (check_printed(..., database_url=)). Pinned by test_a_block_under_a_postgres_fence_is_compared_only_on_postgres and test_a_block_under_a_postgres_fence_holds_what_it_prints_there[same|edited]. RED: an import error until printing_fence existed.

F28: two connect() redirects

There is now one, point_connect_at(url, *, before=None) in tests/_docs_shared.py, re-exported by tests/_docs_harness.py. The capture (with before=capture.snapshot) and the prelude runner both install it. Pinned by test_the_capture_and_the_prelude_runner_install_the_one_redirect.

F29: four snippet-region parsers

There is now one, snippet_regions(text, *, where=). It returns name → SnippetRegion(start, end, body) and refuses a region that starts twice, ends without starting, or never ends. Four callers read regions through it: _docs_sql (via region_lines), the harness's include reader, the README check, and _migrations_project._region.

  • It lives in a stdlib-only module, so the prelude runner and _migrations_project can import it without the harness's griffe and pytest-examples dependencies.
  • Pinned by tests/test_docs_shared.py: the parse, the Markdown form, three malformed cases, every region file reading cleanly, and test_the_four_region_readers_are_one.
  • RED: 2 failed before the refactor.

F30: the shows: guard missed most Ferro output

  • FERRO_OUTPUT now also matches:
    • the <database> · <target> line and ferro migrate[ verb]:
    • any exception class Ferro defines, found by walking its subclasses, in the Name: form
    • a traceback frame
    • a path:N: prefix before a warning
    • No database route and Cannot add NOT NULL column
    • the config, squash-empty and primary-key refusals
  • test_the_recogniser_matches_every_block_of_ferro_output runs it over every output: and refusal: block, and over every prints: block whose source prints what Ferro raised or warned (f"{type(e).__name__}: {e}" or warning.message).
  • RED: 85 failed. GREEN: 221 passed.
  • Six regions printed a caught error bare. They now print it in the convention's form, and seven blocks were regenerated.
  • VARIES is keyed by page:line. test_each_varies_entry_names_a_shows_block catches an entry that has gone stale.

F31: the 0.26 notes said 0.26 "asks nothing"

That claim is gone. Two bullets are added:

  • The no-route message has a new ending. Code matching the old ending needs updating; a match on No database route still holds.
  • Writes now list columns in field order. Tests or logs that compare captured SQL need new expected text.

test_no_route_message_is_the_shipped_text is renamed …_is_the_current_wording, with a comment that says what it pins. test_the_upgrade_guide_names_the_reworded_no_route_message pins the section. RED: 1 failed.

F32: one migration, two shapes; one number, two migrations

migration_errors.py now builds the two-release chain from the guide's table:

  • 0002_author_slug_nullable: one expand step
  • 0003_author_slug_required: backfill, add-constraint, contract

The table, the prose and the three regenerated blocks name exactly these. The DDL lock-retry output uses the same nullable migration. The four-step 0002_author_slug stays the one-migration shape used elsewhere on the page, and no number names two migrations. The prose now says what a deployment does about release 1 meeting the contract step.

F33: the Postgres redirect dropped URL options

I chose to carry options. redirected_url puts a PostgreSQL block's own query options onto the test URL. When a block names a ferro_search_path, that schema is created first and dropped at exit. So connections.md:298 and backends.md:88 run with tenant_a and test_42. The sslmode=require block is back to {test=compile reason="needs a TLS-enabled server"}. Pinned by test_a_redirect_keeps_the_postgres_options_the_block_names (five cases).

F34: two opposite rules for a Postgres-only block

  • The conventions example is now a framework reason. The rule says what to do with an option the server cannot honour, and documents the shared module.
  • The fences that followed the old rule:
    • session.py:295, :422: now backend=postgres with a new invoices prelude; test_docstring_example_on_postgres runs both.
    • auto-migrate.md:104, :125: now embedded models (F27).
    • row-level-security.md:313: runs on Postgres with a new jwt_claims prelude that creates the guide's auth_uid(), and prints the claimed sub under prints: above.

F35: untrue compile reasons

  • rowsecurity.py:5: "a model fragment; its imports are left out".
  • testing.py:56, :63: "a value round_trip returns; RoundTripResult is not imported here".

F36: hand-typed class path

migration_errors.py prints f"{type(error).__name__}: {error}". The Deploying blocks now read PendingMigrationsError: … and DatabaseAheadError: …. The tracked-database block on Overview reads RunRefused: ….

F37: output:/refusal: notes unchecked off the listed pages

_marked(kind) reads those notes from every page, through page_fences. _registry_gaps compares them with OUTPUTS and REFUSALS, so a block under either note on any page must name a registered scenario. Pinned by test_a_marked_block_on_any_page_is_compared[output|refusal], which adds a fabricated block on howto/testing.md. No separate RED run: the test exercises the new helper.

F38–F41

  • F38: partly done. test_docs_migrations._quoted_refusals (regex) now uses the one fence reader. _PYTEST_OUTPUT and test_docs_migrations.py:1396 are left.
  • F39: left.
  • F40: left.
  • F41: partly done. check_printed takes database_url, and the Postgres selection is _on_postgres. A full selector is left.

Interfaces changed

  • New tests/_docs_shared.py: snippet_regions, SnippetRegion, point_connect_at(url, *, before=None), redirected_url, all re-exported by tests/_docs_harness.py.
  • Removed: _docs_sql.snippet_ranges and SNIPPET, _docs_harness._section (now _included), test_docs_examples._MODELS_SECTION, and the prelude runner's own point_connect_at.
  • _docs_sql: added region_lines, printing_fence and on_postgres; check_printed takes database_url=.
  • New preludes: label_addition, invoices, jwt_claims. New example: enum_labels.py and its twin.

Verify

All on c70297e:

  • uv run --no-sync zensical build --clean --strict: no issues found.
  • pytest tests/test_doc*.py tests/test_docstring_examples.py tests/test_step_class.py tests/test_routing_errors.py -n 8: 1153 passed, 126 skipped.
  • The same with --db-backends=sqlite,postgres: 1297 passed.
  • Full suite, pytest tests --db-backends=sqlite,postgres -n 8: 5739 passed, 69 skipped, 2 xfailed.
  • python -m tests._docs_checks docs/pages docs/examples src/ferro: exit 0.
  • ruff check src/ferro tests docs/examples and ruff format --check .: clean.

Verify block corrected: no

Exit steps

  • Issue status: no sub-issue closes here. This is panel follow-up on the epic PR.

Part of #698 (panel round 2 on PR #777)

Fix report: review round 1 (commit 74ee91e)

F31: update() column order

The bullet named update() among the writes that list columns in field order. That was wrong: update() keeps the order the call passes its keyword arguments, as it did in 0.25.

  • The bullet now names only save(), save(only=...), save(exclude=...) and bulk_create() for field order.
  • It says separately that update() is unchanged: it lists the columns in the order the call passes them, a bytes or secret column included.
  • test_the_upgrade_guide_states_each_write_door_s_column_order pins that wording. It replaces the old check for the bare words "field order". RED: 1 failed.
  • test_query_update_keeps_a_raw_column_where_the_call_puts_it pins the behaviour: update(e=, blob=b"y", a=) sends SET "e", "blob", "a".

F32: one number per migration

Three outputs on the Deploying guide now come from the same two-release chain as the rest of the page: ci-check, server-up and status-steps. The new scenarios are _chain_written and _chain_late_row_failed in tests/test_docs_migrations.py. The status prose names 0003_author_slug_required.

A grep of every NNNN_ name on the page now gives 0001_create_author, 0002_author_slug_nullable and 0003_author_slug_required, one name per number. test_the_deploying_guide_names_one_migration_per_number pins it. RED: the page at c70297e had 0002_author_slug and 0002_author_slug_nullable.

Verify on 74ee91e

  • Strict build: no issues found.
  • Docs set plus test_step_class, test_routing_errors and test_save_column_order: SQLite 1163 passed (126 skipped); with --db-backends=sqlite,postgres 1315 passed.
  • _docs_checks: exit 0.
  • ruff check and ruff format --check .: clean.

🤖 Generated with Claude Code
https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP

0x054 and others added 4 commits October 21, 2026 16:14
…se chain

migration_errors.py now builds the chain the guide's table describes:
0002_author_slug_nullable (one expand step) and 0003_author_slug_required
(backfill, add-constraint, contract), so one migration has one shape on the
page and no number names two migrations. The DDL lock-retry output uses the
same nullable migration. Each error region prints the class it caught
(type(error).__name__), never a typed class path.

The step-class test keeps its runtime half (expand-only raises
DatabaseAheadError naming the contract step) and checks only that the page's
refusal is the regenerated prints: block, which test_docs_output pins.

Panel round 2 on #777: F26, F32, F36.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…ld-order writes

The section no longer says 0.26 asks nothing: it adds the routeless
RuntimeError's new ending (v0.14, the upgrade guide) for code matching the
old text, and the field-order column lists of every write for tests and logs
that compare captured SQL. The no-route pin is renamed to say it pins the
current wording, and a test holds the section to both notes.

Panel round 2 on #777: F31.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…es that prove what they say

tests/_docs_shared.py holds snippet_regions (name -> start, end, body;
a duplicated, unopened or unclosed region is refused naming its line) and
point_connect_at, the one redirect, which keeps the PostgreSQL query options
a block's URL carries and creates a ferro_search_path schema first. The
capture, the include reader, the README check and the migrations examples'
helper read regions through it; the capture and the prelude runner install
the redirect. tests/test_docs_shared.py pins both.

The label-addition block runs on Postgres against a plan type holding only
'free' (prelude label_addition, models from docs/examples/enum_labels.py and
its generated twin, now embedded in the page's tabs) and prints the type's
labels under a prints: above block. A prints: above block under a
backend=postgres fence is compared in the Postgres job. The JSON-claims block
and the two Session docstring fences run on Postgres (preludes jwt_claims,
invoices); the sslmode=require block is compile-only ("needs a TLS-enabled
server"). The conventions state one rule for a Postgres block; the
rowsecurity and testing docstring fences give true reasons.

Panel round 2 on #777: F27, F28, F29, F33, F34, F35.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…utput: and refusal: are compared on every page

FERRO_OUTPUT now matches every block the pages show as Ferro's own words:
each ferro migrate output and refusal (the `<database> · <target>` line,
`ferro migrate[ verb]: `, the config refusals), a line naming any exception
class Ferro defines (found by walking its subclasses), a traceback frame, and
a `path:N: ` prefix before a warning. A test runs it over every output:,
refusal: and prints: block whose source prints what Ferro raised or warned,
and VARIES is keyed by page and line, with a test that each entry still names
a shows: block.

Regions that caught an error and printed it bare now print
f"{type(error).__name__}: {error}", as the conventions say; seven blocks
regenerated.

output: and refusal: blocks are read from every page through the one fence
reader (no regex reader for refusals), so a typed block under either note on
any page must name a registered scenario; a fabricated-block test pins it.

Panel round 2 on #777: F30, F37 (and F38's refusal reader).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP

@0x054 0x054 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec: FAIL

Spec = the Round 2 panel findings F26–F41 (#777 (comment)).

  • Important · docs/pages/upgrading.md:79 at c70297e · Misunderstood (F31) · The new bullet says every write, update() included, "lists its columns in the model's field order". update() does not: update_bind_payload (src/ferro/_bind_payload.py:323-344) keeps the order of the call's keyword arguments. Probe on SQLite, model fields alpha, beta: bytes, gamma: Thing.where(...).update(gamma=2, alpha="b", beta=b"y") sends UPDATE "thing" SET "gamma" = ?, "alpha" = ?, "beta" = ? WHERE .... A reader who rewrites the expected SQL of an update() to field order, as the bullet says, gets a failing test. F31 asked the 0.26 section to name the behaviour changes truthfully. · Fix: say what update() does now (the columns in the order the call passes them, with a bytes or secret column no longer moved last), or take update() off the list. test_the_upgrade_guide_names_the_reworded_no_route_message only checks for the words "field order", so it cannot catch this.
  • Minor · docs/pages/guide/schema/deploying.md:16,46-49,222-230 vs :71,124,131,142,213-215 at c70297e · Missing (F32, partly) · Each migration now has a single shape, but the number 0002 names two migrations on the page: the four-step 0002_author_slug and the one-step 0002_author_slug_nullable. F32 objected to one number naming two migrations (0003 then). The PR body says "no number names two migrations", which is not true for 0002. · Fix: give the one-migration scenario and the two-release chain numbers that differ, or say in one line that they are alternative histories of the same project.
  • Minor · docs/examples/migration_errors.py:67,80,100,108 at c70297e · F36 pin · The hand-typed class paths are gone. But no test would fail if a region printed a typed name again: test_docs_output compares the page with whatever the region prints.

Cannot verify: none.
F38 (partly), F39, F40 and F41 (partly) were optional; they are recorded in the table below, not counted against the verdict.

Finding Status Where
F26 ADDRESSED tests/test_step_class.py:460 (runtime half kept; the page half points at the regenerated prints: block)
F27 ADDRESSED docs/examples/_preludes/label_addition.py:192-213, docs/pages/guide/schema/auto-migrate.md:116-131. Probe: live labels ['free'] before the block; the only DDL the block sends is ALTER TYPE "plan" ADD VALUE IF NOT EXISTS 'pro'; then ['free', 'pro']. Collected as test_a_postgres_text_block_is_what_its_example_prints_there[postgres-guide/schema/auto-migrate.md:127]
F28 ADDRESSED tests/_docs_shared.py:123
F29 ADDRESSED tests/_docs_shared.py:45
F30 ADDRESSED tests/test_docs_output.py:103,128,188,198. Matches ferro migrate: …, No database route…, Cannot add NOT NULL column…, ModelDoesNotExist: … (with or without the module path), path:N: UserWarning:, the · target line
F31 PARTLY docs/pages/upgrading.md:74-79, tests/test_routing_errors.py:34,44. The update() claim is wrong (above)
F32 PARTLY deploying.md: 0002 names two migrations (above)
F33 ADDRESSED tests/_docs_shared.py:100,156. connections.md:57 is compile-only, reason="needs a TLS-enabled server"; no tenant_a/test_42 schema is left after the run
F34 ADDRESSED docs/solutions/patterns/docs-page-conventions.md:197-218; src/ferro/session.py:295,422 (collected as test_docstring_example_on_postgres[postgres-src/ferro/session.py:295-300] and […:422-425]); row-level-security.md:313
F35 ADDRESSED src/ferro/rowsecurity.py:5, src/ferro/migrations/testing.py:56,63
F36 ADDRESSED docs/examples/migration_errors.py:67,80,100,108 (no regression pin, above)
F37 ADDRESSED tests/test_docs_migrations.py:230,251,277
F38 PARTLY _quoted_refusals (tests/test_docs_migrations.py:268) now uses the shared fence reader; _PYTEST_OUTPUT (tests/test_docs_testing_fixtures.py:34) is left
F39 LEFT tests/_docs_sql.py:56
F40 LEFT tests/test_docs_lint.py:20
F41 PARTLY check_printed(database_url=) (tests/_docs_sql.py:551); _on_postgres (tests/test_docs_output.py:58) still re-matches PRINTS

Quality: PASS

  • Minor · docs/examples/enum_labels.py:6, docs/examples/enum_labels_annotated.py:6 at c70297e · Both docstrings send the reader to docs/examples/_preludes/enum_labels.py, which does not exist. The prelude is label_addition.py.
  • Minor · depth (Q4, Follow-up) · tests/test_docs_shared.py:91-106,139-147 at c70297e · test_the_capture_and_the_prelude_runner_install_the_one_redirect checks the source text for "ferro.connect =" and "point_connect_at(". test_the_four_region_readers_are_one checks that old attribute names are absent. Both test past the interface: renaming a function breaks them while behaviour is unchanged. test_a_redirect_keeps_the_postgres_options_the_block_names and the region tests already pin the behaviour.
  • Minor · docs/pages/guide/schema/overview.md:47 at c70297e · The regenerated block names RunRefused. That class is not exported from ferro.migrations and appears in no docs page (it is defined in src/ferro/migrations/report.py:46), while the region catches MigrationRefused. A reader cannot look the name up.

Checked: (g) both sys.path.insert(0, <repo root>) calls resolve from __file__, so they work from any working directory. The only regular packages at the repo root are tests and benchmarks, and nothing in the import chain shadows an installed package. (f) The seven blocks whose caught errors now print their class (connections, mutations, auto-migrate, renames-and-retirements, multiple-databases ×2, overview) are regenerated, and their lead-ins still read correctly. The conventions text (docs-page-conventions.md:99-101) states the type(exc).__name__ form.

Verify: PR head c70297e passed (strict build: no issues; targeted docs tests 1153 passed, 126 skipped on SQLite and 1297 passed with --db-backends=sqlite,postgres; full suite pytest tests --db-backends=sqlite,postgres -n 8: 5739 passed, 69 skipped, 2 xfailed; _docs_checks exit 0; ruff check and ruff format --check clean)
Trial merge: not run (base unchanged, feat/docs-rewrite at 8ae9aee)

…ng names one migration per number

update() keeps the order the call passes its columns, as it did in 0.25;
the note now says so, and lists only save(), save(only=), save(exclude=)
and bulk_create() for field order. A test pins that wording, and a new
column-order test pins update() keeping a bytes column where the call put it.

The Deploying guide's CI check, deploy-step run and status report now come
from the two-release chain the rest of the page follows
(0002_author_slug_nullable, 0003_author_slug_required), so no number names
two migrations; a test pins it.

Review of #782: F31 (update order), F32 (one number per migration).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP

@0x054 0x054 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped re-review, c70297e..74ee91e.

Spec: PASS

  • Important · docs/pages/upgrading.md:79 (the update() field-order claim): ADDRESSED at 74ee91e. The bullet now names save(), save(only=...), save(exclude=...) and bulk_create() for field order. It says update() is unchanged and lists the columns in the order the call passes them. I checked that against git show v0.25.0:src/ferro/_bind_payload.py: update_bind_payload already kept the call's order there, so "unchanged" is accurate. Pinned by test_the_upgrade_guide_states_each_write_door_s_column_order (tests/test_save_column_order.py:215), and the behaviour by test_query_update_keeps_a_raw_column_where_the_call_puts_it (tests/test_save_column_order.py:193).
  • Minor · docs/pages/guide/schema/deploying.md (F32, 0002 naming two migrations): ADDRESSED at 74ee91e. The page names exactly 0001_create_author, 0002_author_slug_nullable and 0003_author_slug_required. The ci-check, server-up and status-steps outputs are regenerated from the two-release chain (_chain_written and _chain_late_row_failed, tests/test_docs_migrations.py:806-822). Pinned by test_the_deploying_guide_names_one_migration_per_number (tests/test_docs_migrations.py:1499).

Cannot verify: none

Quality: PASS

No new Critical or Important findings in the fix diff.

Verify: PR head 74ee91e passed. Strict build: no issues. Docs set + test_step_class.py test_routing_errors.py test_save_column_order.py: 1163 passed, 126 skipped on SQLite; 1315 passed with --db-backends=sqlite,postgres. Full suite pytest tests --db-backends=sqlite,postgres -n 8: 5743 passed, 69 skipped, 2 xfailed. _docs_checks exit 0. ruff check and ruff format --check clean.
Trial merge: not run (base unchanged, feat/docs-rewrite at 8ae9aee)

@0x054
0x054 merged commit f20bcca into feat/docs-rewrite Oct 21, 2026
7 checks passed
@0x054
0x054 deleted the sdd/698-fix-round-2 branch October 21, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant