Skip to content

Concepts pages: architecture, identity map, type safety, backends and performance #718

Closed

Description

@0x054

Parent

#698

What to build

Stale facts: architecture has a dated implementation-status note, links into docs/plans/, and an example (lambda t: t.active) that raises; backends says Postgres stores dict/list as JSON (it is jsonb) and has no time row; performance says the identity map lives for the connection and eager loading is on the roadmap (include() exists); type safety claims field-name completion inside query lambdas that does not happen. Every claim is checked against the code, headings become sentence-case noun phrases, and about 100 ADR numbers, codenames and test-file names give way to links where the reasoning helps.

Pages: concepts/architecture.md, concepts/identity-map.md, concepts/type-safety.md, concepts/backends.md, concepts/performance.md

Acceptance criteria

  • Every fact on the page matches the code at the time of the PR; each High finding in the audit rows below is fixed, and each Medium/Low finding is fixed or answered in the PR body with the reason it was left.
  • Each page is one kind (tutorial, how-to, reference or explanation) and opens with one sentence saying what it covers, then an example on the first screen (writing-for-humans rules 1–2).
  • Each page uses one running example, declared once near the top and reused; each section shows only what changed (rule 3).
  • Every code block has a lead-in sentence ending in a colon and a result sentence after it, and shows its result: printed output, or the asserted value inside the embedded snippet range (rules 7–8). Snippet end: markers sit below the assert or print that holds the result.
  • Every copyable block either shows the async with engines.session(): wrapper or sits under a sentence that says it runs inside one and links to Connections › Sessions; the page's first block shows the wrapper.
  • Every inline Python block that does real work runs under the harness ({test=run prelude=<name>}) or moves into an example script; a block left compile-only says why in its fence marker.
  • Every lambda passed to a query method names its parameter after the model in lowercase singular (AGENTS.md I-8); python -m tests._docs_checks reports no violation for the owned files.
  • Every error check in an owned example fails when the expected error is not raised (try/except/else: raise AssertionError, or the harness helper).
  • Field-declaring examples show both declaration styles as tabs, each backed by a runnable script (AGENTS.md I-7), and the twins use the one Annotated idiom recorded in the docs conventions.
  • Headings are short sentence-case noun phrases naming the feature (rule 5); how-to titles name the goal (rule 24); no minimising words (rule 17); admonitions follow rule 20.
  • No ADR numbers, issue numbers, codenames, dated or release-bound notes, or contributor-only references in page text; version markers are self-contained blocks at the end of a section.
  • Reference material that has a home in Reference is linked, not repeated; each topic ends with a pointer to its reference entry (rule 4).

Files owned

  • Modify: docs/pages/concepts/architecture.md, docs/pages/concepts/identity-map.md, docs/pages/concepts/type-safety.md, docs/pages/concepts/backends.md, docs/pages/concepts/performance.md

Interfaces

Test scenarios

  • Happy path: the strict site build succeeds and every snippet range on the owned pages renders without leading indentation.
  • Happy path: every owned example script runs to completion under tests/test_docs_examples.py, and every inline block marked {test=run ...} runs.
  • Error path: deleting the raise an owned example's error check expects makes that example's test fail.
  • Edge case: python -m tests._docs_checks over the owned files exits 0; renaming one lambda parameter to t makes it exit 1 naming the file and line.

Verify

uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docs_examples.py -q --no-cov -p no:cacheprovider -n 8
uv run --no-sync python -m tests._docs_checks docs/pages/concepts

Blocked by

Audit findings

Rows from the docs audit this ticket covers (file, line, rule, fix)

docs/pages/concepts/architecture.md

  • Kind (rule 1): explanation (with reference-ish IR detail)
  • Running example (rule 3): User (introduced at l.169, after three diagrams that use User already)
  • Verdict: The layer story and trade-offs are good. It carries a dated, now-false "implementation status" note, a link into docs/plans/, and an example predicate that raises at runtime. C
Sev Line Rule Finding Suggested fix
H 43–50 facts The "Implementation status (2026-06-26)" note says the runtime DDL path "re-derives its own SchemaIR" and "src/schema.rs uses its own canonical type system rather than the shared ferro-ddl-lowering crate". AGENTS.md I-1 says the opposite today: one planner (ferro_migrate::plan_from_ir), the create pass is ferro_migrate::render_create_table, and types come from ferro_ddl_lowering::resolve_column_storage. It also contradicts this page's own l.209 ("all derive from the Python-compiled SchemaIR modelset"). Delete the note. If the history matters, it belongs in an ADR, not a concepts page.
H 114 facts, 24 The SchemaIR bullet repeats the stale "re-derives its own SchemaIR" claim and links "Phase 8.5" to docs/plans/2026-06-19-001-ir-first-roadmap.md. That is a contributor planning file; the repo's agent rules say plans live in Issues. Drop the parenthetical and the link.
H 107 facts, I-8 User.where(lambda t: t.active) raises TypeError: where() predicate callable must return QueryNode, got FieldProxy (verified). User.where(lambda user: user.active == True).order_by(lambda user: user.age, "desc").limit(10).
H 169–193 I-7 The tabbed User model is inline, and no test compile-checks it. tests/test_docs_examples.py globs only docs/examples/*.py, so the I-7 requirement that "inline blocks … are compile-checked" is not met. The same applies to identity-map.md. Move the model into docs/examples/concepts_user.py and _annotated.py, and include it with --8<--.
M 107, 142, 158 I-8 lambda t: in three predicate examples, including the sequence diagram (User.where(lambda t: t.age > 18)). lambda user:.
M 5–7 2 "## Overview" heading, then a sentence about two pipelines. The first example (a mermaid chart of internals) is not something the user wrote. Lead with the user-facing anchor (I-11): the 6-line User model and await User.where(...).all(), then "here is what happens in each layer".
M 206–207 20, facts "migrate_updates=True (0.11.0)" and "(0.11.0)" are inline version markers. "Runtime updates are planned from SchemaIR diffing (ferro-migrate)" restates the schema guide. Drop the inline versions (or move them to a version note), and link to Auto-migrate instead of restating the flags.
M 209 1, 15 One 110-word paragraph names three test files and ferro-ddl-lowering tokens. That is contributor detail on a user page. Cut it to "Every door plans with the same planner, so they agree byte for byte; see Schema management." Leave the test-file names to docs/solutions/.
M 99, 131, 165, 211, 236 5 Title Case headings: "The Layers", "Life of a Query", "Model Registration", "Async Architecture", "See Also". Use sentence case ("Life of a query"). "The Layers" → "Layers".
L 87–97 7 A text block of seven "X owns Y" lines has no lead-in or result sentence. Make it a two-column table (layer, owns) with a lead-in.
L 118, 223 3 "The GIL is released while Rust waits" is stated three times (l.118, 145, 223) and again in performance.md. State it once and link.

docs/pages/concepts/identity-map.md

  • Kind: explanation with how-to sections (evict, opt out, tests)
  • Running example: User(id, username, email), consistent throughout. Good.
  • Verdict: Strong page: a clear guarantee, runnable-looking examples, and assertions as output. It has some minor history and heading issues, and its inline examples are untested. B
Sev Line Rule Finding Suggested fix
H 27–91 I-7, 9 The tabbed example is inline and untested (see the architecture row). Back it with docs/examples/identity_map.py and _annotated.py.
M 5, 23, 95, 116, 141, 156, 193, 205 5 Headings are Title Case. Some are slogans or questions in form: "What It Is", "Why It Matters", "No Session, No Identity", "The Guarantee: Refresh-on-Load". "Weak-reference map", "Refresh on load", "Sessionless operations", "What enters the map", "Eviction", "Turning the map off", "Identity map in tests".
M 191 1, 2 "this is unchanged from before weak-valued maps and refresh-on-load existed" is history the reader does not need. Delete the clause.
M 136 8 await User.get(created.id) is shown as "Raises RuntimeError" in a comment, without the message. Show the exception line: RuntimeError: No database route for this operation. Open a session (…) (the text from state.py:47).
L 93 14 "Merging old and new field values was considered and rejected" uses the passive voice. "Ferro does not merge old and new values: a merge policy is how stale-read bugs survive."
L 177 facts evict_instance("User", str(user.id)) matches the signature (pk: str). OK. The str() cast deserves a note ("pk is the primary key as a string").

docs/pages/concepts/type-safety.md

  • Kind: explanation
  • Running example: User(username, age), then Account for validators
  • Verdict: The overview is good, but one IDE claim is stale (lambda proxies give no field completion) and two predicates break I-8. B−
Sev Line Rule Finding Suggested fix
H 64 facts "Field names complete when you type User. inside a query expression." Predicates are lambda-only, and the proxy's attributes are FieldProxy[Any] through __getattr__, so the IDE offers no field names on user. inside a lambda. The page's own l.102 and query-typing.md say so. Delete the bullet, or replace it with: "Inside a lambda predicate, attribute names are not completed yet; a typo raises AttributeError at build time (see Typed Query Predicates)."
M 51–53 I-8 lambda t: ×3 lambda user:.
M 35 8 # age: Input should be a valid integer is not what print(e) prints. Pydantic prints a multi-line message ("1 validation error for User\nage\n Input should be a valid integer, unable to parse string as an integer …"). Show the real first lines.
M 102 15, 3 The Limits paragraph uses user.age >= "eighteen" but the earlier examples use t. PEP 827 is explained here and twice more in query-typing.md. Keep one sentence and link to query-typing.md#…; drop the PEP detail here.
L 5, 38, 58, 68, 74, 100 5 Title Case headings ("Pydantic at the Core", "The Rust Boundary"). Sentence case: "Pydantic models", "Rust boundary".
L 70 1 src/ferro/_core.pyi is a repo path, so readers who installed the wheel do not have src/. "ferro/_core.pyi".

docs/pages/concepts/backends.md

  • Kind: mixed: reference (type table), explanation, how-to (troubleshooting)
  • Running example: none
  • Verdict: Useful page, but the type table is wrong for JSON on Postgres, omits time, and the raw SQL link goes to the wrong guide. C
Sev Line Rule Finding Suggested fix
H 40 facts "dict / list → PostgreSQL JSON". Derived JSON storage is jsonb on Postgres (ADR-0005, canonical_from_parts l.381–388 in crates/ferro-ddl-lowering/src/lib.rs); db_type="json" is the opt-out. "jsonb (db_type="json" for plain json)".
H 32–43 facts The table omits datetime.time (time on both dialects, canonical_from_parts l.379). int → "INTEGER/BIGINT" is ambiguous: the derived type is integer, and bigint comes only from db_type. float → "double precision" is correct. Add a time row, write integer (use db_type="bigint" for 64-bit), and generate the table from ferro_ddl_lowering or the db_type parity test so it cannot drift again. Link to the db_type token list in api/fields.md.
H 51 24 "raw SQL" points at the queries guide; the raw SQL guide is ../guide/raw-sql.md. The same mismatch appears at l.107 ("Queries — the query builder and raw SQL escape hatch"). Link ../guide/raw-sql.md.
M 85–101 1 The troubleshooting how-to sits inside a concepts page. Move it to the FAQ or a how-to, or keep it with a Tip linking out.
M 25–26 20 The Tip admonition holds feature documentation (ferro_search_path). Rule 20 says a Tip is for a pointer elsewhere. "a private ferro_search_path URL parameter" ("private" is unclear). Make it a short section "PostgreSQL schema isolation" with a URL example: postgresql://…/app?ferro_search_path=test_42.
M 3, 30, 45 17, 18 "the practical difference is mostly on the PostgreSQL side"; "In practice SQLite's type affinity makes declared-type drift mostly cosmetic" (l.81) uses hedges without a condition. State the conditions: "SQLite stores any value in any column, so a declared-type mismatch does not reject writes."
L 21 16 "Ferro's test matrix runs against Supabase-managed databases" is a claim to verify. Confirm it in CI config, or drop it.
L 1, 5, 28, 47, 85 5 Title Case headings; the page title "Backends" against nav "Database Backends". "Database backends", "Type mapping", "Backend differences", "Troubleshooting".

docs/pages/concepts/performance.md

  • Kind: explanation and how-to ("Getting the Most Out of Ferro")
  • Running example: User (but with email sometimes required, sometimes omitted)
  • Verdict: Honest framing, but two claims are stale and contradict other pages: the identity map caching and eager loading on the roadmap. C
Sev Line Rule Finding Suggested fix
H 80 facts "Repeated fetches of the same row return the cached instance rather than re-hydrating… every hydrated instance stays cached for the connection's lifetime, so long-running jobs… should paginate and evict". identity-map.md (l.3, 18, 21, 25) says the opposite: the map holds weak references, every fetch runs a query and refreshes in place, entries die with the instance, and the map is session-scoped, not connection-lifetime. "The identity map holds weak references and is scoped to a session, so it does not grow a long job's memory; it saves object identity, not queries. identity_map=False drops the dedup work for ETL-style reads."
H 82 facts "Eager loading (prefetch_related-style) is on the roadmap; until then, restructure hot paths…". The example post.author is a forward FK, which include(lambda post: post.author) populates in the same statement today (roadmap.md l.9; api/queries.md l.13). Only BackRef/M2M population is future work. "Use Post.where(...).include(lambda post: post.author) to load authors in the same query. Reverse and M2M collections are not populated yet (roadmap); batch them by id with in_()."
M 52, 58, 76 I-8 lambda t: ×3. lambda user:.
M 39–43, 91 9, 3 User.create(username=…) without email, while l.13 builds User(username=…, email=…). One of the two is not valid for the same model. No model is shown. Show the User model once (from a docs/examples file) and keep the calls consistent.
M 9, 19, 27 16 "largely disappears", "far less time", "in the same neighborhood": unmeasured hedges on a page that says it publishes no numbers. Say what moves to Rust, and leave magnitude to the reader's benchmark.
M 86–100 8 The benchmark has no shown output. Add a sample output line, labeled as a sample from one machine, or say "prints two timings".
L 5, 23, 33, 84 5 "Where It Doesn't" and "Getting the Most Out of Ferro" are not noun phrases; Title Case. "Where Rust helps", "Where it does not help", "Tuning checklist", "Benchmarking".
L 38, 42 13 "Slow:"/"Fast:" comments without SQL. Show "N × INSERT" against one "INSERT … VALUES (…), (…)" (rule 13).

Activity

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

    @0x054
    ContributorAuthor

    Status: verify-passed
    Worker: worker-718
    Branch: sdd/718-concepts
    PR: #746
    Verify: passed 02ebf8c

    Scoped re-review by reviewer-718: Spec PASS, Quality PASS.
    Both Important findings ADDRESSED (type-safety.md:3, performance.md:130); both Minor fixed.
    The new InterfaceError text in backends.md matches a fresh run. Base unchanged since the rebase (b0e5348), so no trial merge was needed.

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #746 into feat/docs-rewrite

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

Metadata

Metadata

Assignees

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions