Skip to content

Get started pages: home page and README, Why Ferro, installation and quickstart #705

Closed

Description

@0x054

Parent

#698

What to build

A newcomer's path that is accurate and runs as shown. The home page (and the README it embeds) states the real dependencies (pydantic-settings too), the real migration system and the real feature set; Why Ferro drops the claims that migrations are Alembic-based and atomic updates are missing; the quickstart shows the session wrapper on its first block, a lead-in and result sentence on every block, and the output of each step. The Next steps page's three use-case paths fold into the quickstart's closing section (the nav ticket (#723) deletes the old page). The README quick start gets an Annotated tab on the site and is run by a test.

Pages: index.md, why-ferro.md, getting-started/installation.md, getting-started/quickstart.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).
  • The README's model is declared in a runnable example pair, embedded on the home page in both tabs.

Files owned

  • Create: docs/examples/readme_quickstart.py, docs/examples/readme_quickstart_annotated.py
  • Modify: README.md, docs/pages/index.md, docs/pages/why-ferro.md, docs/pages/getting-started/installation.md, docs/pages/getting-started/quickstart.md, docs/examples/quickstart.py, docs/examples/quickstart_annotated.py

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 README.md docs/pages/index.md docs/pages/why-ferro.md docs/pages/getting-started docs/examples/quickstart.py docs/examples/quickstart_annotated.py

Blocked by

Audit findings

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

docs/pages/index.md (and README.md, which supplies its body)

  • Kind (rule 1): mixed: landing page + explanation (Architecture) + tutorial fragment (Quick Start) + link list.
  • Running example (rule 3): a User model with username/is_active (not the Author/Post blog the quickstart uses).
  • Verdict: The front door renders the README almost verbatim: eleven badges, a sales-voiced feature list with stale claims, and a model example that breaks I-7. Needs a purpose-built home page that says what Ferro is in one sentence, shows a model + query + the SQL it runs, and routes readers. C
Sev Line Rule Finding Suggested fix
H README 54-57 I-7 Quick Start declares id: int | None = Field(default=None, primary_key=True) with no Annotated tab. Rendered on the home page, so the invariant applies. Also untested: tests/test_docs_examples.py only scans docs/pages/, never README.md (rule 9). On the home page, replace the README include with tabbed snippets from a docs/examples/home.py / home_annotated.py pair (or reuse quickstart.py:models). Keep the README's own copy short.
H README 17, 23 stale fact "it only has one dependency: Pydantic". pyproject.toml runtime deps are pydantic>=2.0 and pydantic-settings>=2.0. "Its runtime dependencies are Pydantic and pydantic-settings."
H README 25 stale fact Identity map "tracking active model instances in a thread-safe registry". The identity map is session-scoped and weak-referenced (concepts/identity-map.md: "There is no process-global map"). "Within a session, one database row is one Python object." Link to Identity Map.
M README 32 stale fact "GIL-free row parsing and object instantiation". Under I-2, Rust fills the model __dict__ directly; instantiation is not GIL-free. "parses rows outside the GIL and fills each model's fields directly, without calling __init__."
M README 17 rule 2, 16 / humanizer 16 Opening paragraph: "high-performance", "Designed for simplicity and it only has one dependency" (comma splice), "Delivering ergonomics familiar to the modern Pythonista..." (fragment, sales voice). "Ferro is an async ORM for Python: you declare Pydantic models, and a Rust engine builds the SQL, runs it and turns rows into model instances."
M README 19-25 humanizer 19 "Key Features" is a bold-label list ("High-Performance Core: ...", "Zero-Copy Intent: ..."); labels repeat the sentence. Prose, or plain bullets without bold labels.
M README 63-67 rule 8, 13 Quick Start shows no output and no SQL. alice is assigned and never used. Show printed output (Found 1 active users.) and the SELECT it ran, or link to the quickstart instead of a second mini-tutorial.
M README 5-15 rule 2 Eleven badges (Pytest, Ruff, Zensical, UV, Rust...) fill the first screen of the docs home; tooling badges tell a reader nothing about the ORM. Keep the badges in the GitHub README; exclude them from the main snippet (move [start:main] below them).
L README 27-32 rule 1 Architecture section duplicates concepts/architecture.md. Drop it from the home page; link instead.
L index 7-12 rule 22, 24 Bold link labels with em dashes; "Guide" links to models-and-fields.md (link text promises the whole guide). No link to Installation or Schema Management, which most first-time readers need. Plain descriptive links; add Installation and Schema Management.
L README 19, 27, 34, 48 rule 5 Title Case headings ("Key Features", "Quick Start"). Sentence case.

docs/pages/why-ferro.md

  • Kind (rule 1): explanation (correct for the page).
  • Running example (rule 3): none; one inline User.where(...) line.
  • Verdict: The right kind of page with honest trade-offs, but several facts are stale (migrations, update expressions, dependencies) and the one query example breaks I-8. B once facts are fixed (currently C on accuracy).
Sev Line Rule Finding Suggested fix
H 17 I-8 User.where(lambda t: t.age >= 18): lambda parameter must be user. User.where(lambda user: user.age >= 18).all()
H 44, 56 stale fact "Alembic-based migrations" and table row "Migrations: Alembic (optional extra)". Ferro ships its own migrations (ferro migrate new/up, cli extra); Alembic is a bridge. Row: "Built in (ferro migrate), plus an Alembic bridge". Line 44: "reviewed migrations (ferro migrate)".
H 44 stale fact Lists "atomic update expressions" as not implemented. Query.update(lambda counter: {"n": counter.n + 1, "updated_at": now}) runs +/-, now and Postgres .merge() in the database (builder.py:1281; roadmap line 12 says the same). Drop it, or narrow to "*, / and .concat() in update recipes".
H 57, 70 stale fact "Runtime dependencies: Pydantic only" / "Pydantic is the only runtime dependency". pydantic-settings is also required. "Pydantic and pydantic-settings".
M 1 rule 5, 24 H1 "Why Ferro?" is a question; nav says "Why Ferro". "Why Ferro" or "Ferro compared with other Python ORMs".
M 1-17 rule 2, 13 No example on the first screen; the claim "Python only builds a small filter AST" is the place to show the query next to the SQL Ferro generates. Add one lambda query and its rendered SQL under "Rust core".
M 21 humanizer 1 "Unlike ORMs that wrap Pydantic or use it as a serialization layer, Ferro models are Pydantic models". "Ferro models are Pydantic models:"
M 55 rule 16 "Learning curve: Low" for Ferro vs "High" for SQLAlchemy is an unsupported claim in a comparison table. Drop the row or replace with something checkable (e.g. "Sync API: no / yes / yes / no").
L 3, 9, 13, 19, 30, 38, 48, 63 rule 5 Title Case and slogan headings ("The Problem", "How Ferro is Different", "What You Give Up"). "Where Python ORMs spend time", "Rust core", "Trade-offs", "Comparison with other ORMs", "When to choose Ferro".
L 34, 61, 67-79 humanizer 8, 19 Em dashes throughout; bold on phrases inside bullets ("async services", "lots of rows"). Commas/colons; drop bold.

docs/pages/getting-started/installation.md

  • Kind (rule 1): how-to (correct).
  • Running example (rule 3): n/a.
  • Verdict: Accurate and short (wheels for all three OSes confirmed in publish.yml, ferro.version() exists and prints 0.25.0, extras match pyproject.toml). Small fixes only. B
Sev Line Rule Finding Suggested fix
M 68-74 rule 8 "If this prints a version number, you're ready to go." Show it. Add text block 0.25.0 and "The number is the installed Ferro version."
M 40 rule 12 The alembic extra gets only an inline pip command while cli gets uv/pip tabs. Same uv/pip tabs, or fold both extras into one tabbed block.
L 5 rule 22 Python 3.14 is supported (README badge says "3.13 | 3.14"); page says only "3.13 or newer". Fine, but "tested on 3.13 and 3.14" is more useful. Optional.
L 44 rule 14 "You don't need any." as the first line under "Database Drivers" (heading echo, humanizer 24). "Ferro's Rust engine bundles SQLite and PostgreSQL support..."
L 58-61 rule 9 maturin develop installs into the active virtualenv; the block never creates one, so it fails outside a venv. CONTRIBUTING.md uses uv run maturin develop. uv sync then uv run maturin develop --release, or add python -m venv .venv && source .venv/bin/activate.
L 3, 42, 46, 66, 76 rule 5 Title Case headings; "Verify Your Installation" is an instruction. "Requirements", "Database drivers", "Build from source", "Installed version".

docs/pages/getting-started/quickstart.md

  • Kind (rule 1): tutorial (correct), backed by docs/examples/quickstart.py (tested by test_docs_example_scripts).
  • Running example (rule 3): a blog: Author and Post, kept throughout. Good.
  • Verdict: Good bones: one tested script, both field styles, lambda predicates. But the snippets silently run inside engines.session(), which the page never shows, so the identity-map claim fails for a reader who copies the blocks; no block shows output; and every block renders indented 8 spaces. C
Sev Line Rule Finding Suggested fix
H 40-88, 59-60 rule 9, factual Every snippet after connect sits inside async with engines.session(): in quickstart.py:45, but the page never shows or mentions engines.session(). The note "Post.get(post.id) returns the same Python object" is only true inside a session (concepts/identity-map.md: "within a single session"). A reader assembling the blocks gets a different result. Include the async with engines.session(): line in the connect snippet (or a "Open a session" section) with one sentence on what it scopes; link Identity Map.
H 42-85 rendering Snippet sections are cut from inside async def main() / async with, and pymdownx.snippets has no dedent_subsections = true; the built site/getting-started/quickstart/index.html renders alice = await ... with 8 leading spaces. Copying a block gives an IndentationError. Set dedent_subsections = true in zensical.toml (config auditor), or restructure the script.
M 32, 42, 51, 64, 75, 84 rule 7 Six code blocks follow their heading with no lead-in sentence; results come as bullet lists describing the code rather than one sentence on what changed. e.g. before Create: "Create an author and three posts:"; after: "post.id is now 1; the database assigned it."
M all blocks rule 8, 13 No output anywhere. The script asserts len(published) == 2, total == 3, updated == 1 but the reader sees none of it, and no SQL is shown beside any query. Add print(...) lines to the script and show their output; show the SELECT ... WHERE "published" = ? ORDER BY ... for the query block.
M 5 rule 17 "or just run the script". "or run the script".
M 7, 82, 98 rule 5 Instruction / question headings ("Define Your Models", "Wrap It in a Transaction", "What's Next"), Title Case. "Models", "Transactions", "Next steps" (sentence case). Tutorials may keep step verbs, but drop Title Case.
L 59 rule 20 !!! note "What happened" used for the identity map result, which is the main lesson of the block, not background. Make it the block's result sentence.
L 57 I-8 / AGENTS drift Page says lambda is "the only predicate style where() accepts", which matches the code (where raises TypeError on a non-callable, builder.py:675). AGENTS.md I-8 still says operator style "is compatible today". Not a page bug; AGENTS.md I-8 is stale. Flag for AGENTS.md update.
L 9, 23, 46, 56, 88 humanizer 8 Em dashes in most prose paragraphs. Commas/colons.

docs/pages/getting-started/next-steps.md

  • Kind (rule 1): navigation page (link list); not one of the four kinds.
  • Running example (rule 3): none.
  • Verdict: A second copy of the nav, organised by persona, with Testing listed three times and Pagination twice. It adds little over the nav and the quickstart's own "What's Next". Fold the three persona paths into the quickstart's last section, or keep this page but cut the duplicate lists. C
Sev Line Rule Finding Suggested fix
M 15, 32, 38; 14, 39; 31, 42 rule 1, 6 Testing appears 3 times, Pagination 2, Multiple Databases 2; "Common Patterns" and "Reference Material" restate the nav. Keep the three use-case paths; replace the rest with one link each to How-To and API Reference.
M 29-32 gap "Production Deployment" omits Installation of the cli extra, ferro migrate, and the CLI/Configuration reference, which are what shipping needs. Add CLI reference and Configuration.
L 11-47 humanizer 19 Every item is a bold link plus em dash. Plain link + short clause.
L 54 rule 24 "star the repo to follow releases" as the description of a GitHub link. "Releases on GitHub".
L 56 humanizer 2 Closing line "Build something real and reach for the guides above as questions come up." restates nothing new. Cut.
L 5 rule 5 "Learn by Use Case" Title Case. "Paths by use case".

docs/examples/quickstart.py + quickstart_annotated.py

  • Embedded by: getting-started/quickstart.md (whole file + 7 sections), guide/models-and-fields.md (models), guide/mutations.md (create, update-delete)
  • Running example: Author/Post blog
  • Verdict: B. A clean, real-looking tutorial companion with correct I-7 and I-8. The models block drags in unrelated imports, and results are hidden below the section markers.
Sev Line Rule Finding Suggested fix
M 9–18 vs annotated 14 I-7 / 12 The models section imports connect, engines, transaction, which the model code never uses. The Annotated tab imports a different set (no transaction), so the two tabs differ in more than the declaration style. Import only BackRef, Field, ForeignKey, Model, Relation inside models. Put the runtime imports in their own section or above the marker.
M 71–90 8 / 11 The query block stacks four queries (get, filtered list, count, exists). Their results (same_post is post, len == 2, total == 3) are asserted after [end:query], so the page never shows them. Split into two or three blocks, or add # -> 2 posts style result comments inside the section.
M 56–67 11 create mixes author=alice (line 53) and author_id=alice.id (lines 63, 65) in one block, which teaches two spellings at once. Use author=alice throughout. Show author_id once on the relationships page.
L 77, 85, 98, 110 9 / mechanics # noqa: E712 appears 4 times in the embedded code; the rendered quickstart shows 8 noqa comments. Linter noise in a tutorial. Add [tool.ruff.lint.per-file-ignores] "docs/examples/*" = ["E711", "E712"] and drop the comments (18 across the examples).
L 33 10 default_factory=datetime.now makes naive datetimes. predicates.py and timestamps.py use datetime.now(UTC). Use lambda: datetime.now(UTC) or a shared utcnow, and be consistent.

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: sdd/705 worker
    Branch: sdd/705-get-started
    PR: #730
    Verify: passed 2dd3783

    Re-review of the fix round: both Important findings ADDRESSED. Spec PASS, Quality PASS. No trial merge needed (base unchanged).

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Status: pr-open. PR #730 fix round 1 applied, Verify SHA 2dd3783 (equals pushed HEAD).

  6. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #730 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