Add a validated tutorial¶
Tutorials under docs/tutorials/ are executed and checked by just tutorials-check
(which runs byexample over each page), so they
can't silently drift from the tool. This guide is the recipe for writing one. For a
worked reference, read docs/tutorials/guestbook.md and its
asset folder docs/tutorials/guestbook/.
The governing rule: the filesystem is the source of truth; markdown transcludes it. You never paste the same content twice.
File layout¶
docs/tutorials/
<name>.md # the tutorial page
<name>/ # its assets, staged into the sandbox at check time
<name>.feat.yaml # whole files the reader creates
edits/
commands.yaml.diff # edits to generated files (unified diffs)
...
just tutorials-check copies docs/tutorials/<name>/ into a throwaway directory and runs
the page's shell steps there. So reference assets by the relative path they'll have in
that sandbox (edits/commands.yaml.diff, <name>.feat.yaml), not by their
docs/tutorials/... path.
The three kinds of code block¶
Pick the block type by intent. byexample is run with -l shell, so it only executes
```shell blocks and ignores every other fence — that's what lets the other two
render freely without being run.
1. Commands — ```shell (executed + checked)¶
```shell
$ dizzy generate definitions guestbook.feat.yaml .
Generated def/ stubs and libconfig.yaml. Next steps:
<...>
```
- Lines starting with
$are commands;>continues the previous line (a heredoc). - Everything below a command up to the next
$/blank is its expected output, matched exactly. <...>matches "anything here" — use it for output you don't want to pin (long blocks, variable tails).- Keep output deterministic. Timestamps, UUIDs, and absolute paths will fail the
match — wildcard them with
<...>, or avoid printing them. lscolumnises under byexample's pty; usels -1for one-per-line output.
2. New files — ```yaml / ```python (displayed, copyable)¶
Store the file as an asset and transclude it, so it renders with highlighting, a title, and a copy button:
```yaml title="guestbook.feat.yaml"
description: Guestbook — visitors sign, signatures are stored and listed
# A write intent: "please record this signature"
commands:
sign_guestbook: A visitor wants to leave a signature
# An immutable fact: "a signature was recorded"
events:
guestbook_signed: A visitor signed the guestbook
# Business logic: handle the command, emit the fact
procedures:
record_signature:
description: Validate the signature and record it as a fact
command: sign_guestbook
emits:
- guestbook_signed
# A read-optimized schema (its classes are authored in def/models/guestbook.yaml)
models:
guestbook:
description: Stored guestbook signatures
adapters:
- sqla
# Build read state: when a signature happens, write it into the guestbook model
projections:
signature_store:
description: Persist each signature into the guestbook model
event: guestbook_signed
model: guestbook
adapter: sqla
# Read state back out
queries:
list_signatures:
description: List all guestbook signatures, newest first
model: guestbook
adapter: sqla
```
Because the asset is staged into the sandbox, the file is already on disk — no cat
needed. Anchor it with a cheap executed check so Step N still validates:
```shell
$ head -n 1 guestbook.feat.yaml
description: Guestbook — visitors sign, signatures are stored and listed
```
3. Edits to generated files — ```diff (red/green, applied)¶
Show the change to a scaffolded file as a unified diff (renders red/green) and apply the same patch in a shell step:
```diff
--- a/def/commands.yaml
+++ b/def/commands.yaml
@@ -8,4 +8,10 @@ imports:
classes:
sign_guestbook:
description: A visitor wants to leave a signature
- attributes: {}
+ attributes:
+ visitor_name:
+ range: string
+ required: true
+ message:
+ range: string
+ required: true
```
```shell
$ git apply edits/commands.yaml.diff
$ cat def/commands.yaml
... filled-in content ...
```
A diff makes clear the file didn't start empty — the context lines show the
generated scaffold, and - attributes: {} → + attributes: … shows what you added.
Generating a diff that applies cleanly¶
A patch must match the scaffolder's output exactly, so derive it from a real run rather
than writing it by hand. The reliable recipe: in a throwaway directory, scaffold, commit
that baseline, make your edit, and dump the diff. git diff emits a/ … b/ … headers
that git apply consumes:
cd "$(mktemp -d)"
cp /path/to/your.feat.yaml .
dizzy generate definitions your.feat.yaml .
git init -q && git add -A && git commit -qm scaffold
# edit def/commands.yaml (fill in the attributes) ...
git diff def/commands.yaml > commands.yaml.diff # → docs/tutorials/<name>/edits/
Two phases mirror the tutorial: capture def/**.yaml edits against the scaffold from
generate definitions, and capture src/*.py edits against the stubs from
generate libraries (run generate static then generate libraries, commit, then edit
and diff).
Tidy the patch for rendering: strip git's diff --git/index preamble so the snippet is
a plain unified diff (keep everything from the first --- line). git apply accepts
that form and works in a bare directory with no .git. If the scaffolder's output ever
changes, just tutorials-check fails — regenerate the diff.
Wiring it up¶
- Transclusion already works:
pymdownx.snippetsis configured withbase_path: [docs], and*.diffis inexclude_docsso patches aren't copied into the site as orphan files (snippets still read them from disk). - Add the page to the nav in
mkdocs.ymlunderTutorials:and link it fromdocs/tutorials/index.md. - byexample ships in the
docsdependency-group — nothing to install.
Run it¶
just tutorials-check # every docs/tutorials/*.md, in a sandbox
just docs-serve # preview rendering (red/green diffs, copy buttons)
tutorials-check exits non-zero on any mismatch. Confirm your tutorial actually guards
against drift: change an expected output (or break a patch) and watch it go red, then
revert.
Gotchas¶
- Asset paths are sandbox-relative.
edits/x.diff, notdocs/tutorials/<name>/edits/x.diff. - Snippets vs. raw view. In the rendered site the diff/yaml shows inline; in raw
markdown (e.g. GitHub) readers see the
--8<--directive. The mkdocs site is the canonical view. - Determinism is the whole game. If a step's output depends on time, randomness, or
the network, wildcard it with
<...>or don't print it — otherwise the check is flaky.