Lesson 8: Documentation and Packaging
Docstrings and ADRs
Docstrings explain what a reader cannot see. The signature already says the types.
async def extract_with_repair(
document: Document, *, schema: type[T], max_attempts: int = 2
) -> T:
"""Extract structured data, feeding validation errors back for repair.
The model's first response frequently fails validation on trailing
commas, markdown fences, or a wrong type. Rather than failing, the
validation error is sent back and the model corrects itself, which
succeeds on the second attempt in most cases.
Truncated generations are not repaired, because the same token limit
produces the same cut. Those raise immediately.
Raises:
OutputError: validation failed after max_attempts, or the response
was truncated at the token limit.
"""That docstring records a decision and a reason, which is what nobody can recover from reading the code.
An architecture decision record does the same for choices too large for a docstring.
# ADR 007: Hybrid retrieval with reciprocal rank fusion
## Status
Accepted, 2026-04-12
## Context
Dense retrieval alone had recall@50 of 0.71 on our labelled query set.
Failures clustered on queries containing exact identifiers and rare
ingredient names, which embed poorly.
## Decision
Run BM25 and vector search concurrently and fuse with reciprocal rank
fusion at k=60, then rerank the top 50 with a cross-encoder.
## Consequences
Recall@50 rose to 0.89 and recall@5 to 0.81. Query latency rose by
about 40ms because the searches run concurrently rather than serially.
We now maintain a keyword index alongside the vector store, which is an
additional operational dependency.
## Alternatives considered
A larger embedding model: improved recall@50 to only 0.76 and tripled
embedding cost. Query expansion: helped paraphrase queries and not
identifier queries, which were the actual failures.The alternatives section is the most valuable part. In a year someone will propose the larger embedding model, and this record tells them it was tried and what happened.
Write an ADR when a decision is hard to reverse, when a reasonable person would choose differently, or when you had to try several things. Do not write one for choices with an obvious answer.
A README a new hire can follow
The test is literal: someone who has never seen the project runs it in ten minutes without asking a question.
# recipe-extractor
Extracts structured recipe data from scraped web pages.
## Quick start
git clone <url> && cd recipe-extractor
cp .env.example .env # fill in RECIPE_API_KEY
uv sync
uv run alembic upgrade head
uv run pytest -m "not integration and not live"
uv run uvicorn recipe_extractor.main:app --reload
Open http://localhost:8000/docs
## Architecture
API → Service → AI pipeline → Ports and adapters.
Business logic imports neither HTTP nor SQL. See ADR 001.
## Common tasks
- Add a provider: implement the adapter, register it. See ADR 004.
- Change a prompt: edit src/recipe_extractor/pipeline/prompts/,
bump the version, run `uv run pytest -m eval`.
- Run the eval suite: `uv run pytest -m eval`
## Operations
- Health: /healthz (liveness), /readyz (readiness)
- Dashboards, alerts, on-call: see docs/operations.mdWhat belongs in a README. Running it, the shape of the codebase, the tasks people do most often, and where to look next. What does not: exhaustive API documentation, which the generated schema provides, and anything that will rot faster than someone will update it.
Test it on a real person. Every step they get stuck on is a bug in the README, and the person who wrote it cannot find those steps.
Packaging and versioning
[project]
name = "recipe-extractor"
version = "1.4.0"
requires-python = ">=3.12"
dependencies = [...]
[project.scripts]
recipe-extractor = "recipe_extractor.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"uv build # produces a wheel and a source distribution in dist/Semantic versioning, meaning major for breaking changes, minor for additions, patch for fixes. For a deployed service this matters less than for a library, and it still gives you a way to say what a release contains.
What counts as breaking in this system is broader than it looks. An API response field removed, obviously. But also an output schema change that breaks a client's parsing, a prompt change that alters output format, and an embedding model change that invalidates a stored index. Modules 6, 9, and 10 each covered one of these, and none of them is a code signature change that a type checker would catch.
Keep a changelog written for the person upgrading, and record model and prompt version changes in it, since those change behaviour as surely as code does.