Contributing¶
Run uv run python tools/check.py and uv run pytest -q before every commit.
CI runs both, plus ruff.
Conventions¶
These are enforced by validators, not by review. Each has a reason.
C1 — Directory names are stable, number-free slugs. conflict-resolution/,
never 03-conflict-resolution/. Numeric prefixes turn every reorder into a
URL-breaking rename. Ordering is data, and it lives in curriculum/syllabus.yml.
C2 — Linear order lives in exactly one file. syllabus.yml holds it;
SYLLABUS.md is generated. Never hand-edit generated output.
C3 — The prerequisite graph lives in lesson frontmatter, not in syllabus.yml.
These are two different objects: the syllabus is one valid linearization of the
DAG. validate_graph.py proves they agree.
C4 — id equals the directory name (lessons) or the filename (concepts).
Renaming is a breaking change; add a redirect.
C5 — Cross-links are relative and end in .md. This is the only form that
renders correctly both when browsing the repo on GitHub and in the built site.
Absolute links fail validation.
C6 — Diagrams are fenced ```mermaid blocks, not image files. Diffable,
reviewable, no asset pipeline. tools/validate_diagrams.py enforces it, and
three things it left unsaid: a diagram lives under ## Mechanism, it uses the
three-colour palette (blue mechanism, yellow gate, red failure), and it may not
be the only place a measured figure appears — the expected-output check
strips fences, so a number written only inside a diagram is a number nothing
verifies.
C7 — Labs are .py, never notebooks. Each lesson with a lab ships
lab.py (stub), solution.py (reference), and test_lab.py.
C8 — Generated regions sit between <!-- graph:begin --> and
<!-- graph:end -->. Never edit inside them; run tools/build_graph.py.
C9 — Named tools, vendors, and benchmark numbers appear only in landscape/,
or inside a <!-- landscape:begin -->…<!-- landscape:end --> block in a lesson.
The conceptual spine must survive any product being renamed or abandoned. Add
new names to landscape/registry.yml as soon as they appear in a draft.
Writing a lesson¶
Lessons carry ten ## sections in a fixed order — see LESSON_SECTIONS in
tools/validate_frontmatter.py. Two are load-bearing:
Why this isn't RAGis mandatory. If it feels redundant, reconsider the lesson. (rag_contrast: n/ain frontmatter exempts a lesson, and needs a reason.)Failure modesneeds at least two table rows. A mechanism without its failure modes is marketing.
Use the canonical corpus. capstone/fixtures/corpus.jsonl is seeded with a
job change, entity aliases, a diet chain, a genuine contradiction, relative time
references, PII plus a deletion request, a taught procedure, and low-authority
hearsay. gold.yml maps each seeded phenomenon to the lesson that confronts it.
Cite it rather than inventing an example — the continuity across 84 lessons is
the whole design.
Assert exact output. The fake backend is deterministic, so labs can assert precise rankings and scores. When a lesson quotes a number, a test should pin it.
Writing a lab¶
Labs must run offline with no API key. Embeddings come from
memlab.llm.fake.embed_text. Completions are fixture-backed — author them with
register_fixture(messages, response, schema), which computes the key and writes
JSON without calling a model, so fixtures stay reviewable in a diff.
Lab tests¶
Load the lab's own modules through memlab.labkit, never by bare name:
from memlab import labkit
_solution = labkit.solution(__file__)
_lab = labkit.lab(__file__)
Eighty-four labs means eighty-four modules called solution. A bare
from solution import ... resolves to whichever one Python cached first, so the
second lab in a run silently tests the first lab's code. labkit namespaces by
lesson; validate_structure.py enforces it.
Every lab test starts with test_stub_is_runnable, asserting the stub raises
NotImplementedError — proof the exercise is actually left undone. Because the
suite pins the reference solution, it can never tell a learner their own code
is right, and solving a lab turns that first test red. That is what
uv run python tools/show.py --check <lesson-id> is for: it runs the learner's
lab.py unpatched and diffs it against lab_output, the same function CI uses.
A lab stub should import everything its TODO names, even though the stub
body is one raise and the imports are therefore unused —
[tool.ruff.lint.per-file-ignores] exempts lab.py from F401 for exactly this.
A TODO that says "score every turn with cosine()" and leaves the reader to
guess which module cosine lives in is a puzzle the lesson did not intend to
set.
Landscape pages¶
Every page needs last_verified, category, and volatility. High-volatility
pages fail CI at 180 days. Re-verify or delete; do not extend the date
without re-checking. Mark vendor-reported figures with
claims_are_vendor_sourced: true.
Licensing of contributions¶
Contributions are offered under the same split as the repository: CC BY 4.0 for prose, MIT for code. See LICENSE and LICENSE-CONTENT.