Episode 06 --- Demo Assets
Supporting build-a-domain.md. The domain built live in this episode is
todo --- the simplest possible real domain plugin, implemented at
muse/plugins/todo/plugin.py in the muse repo (started from
muse/plugins/scaffold).
Production note: this already existed before Episode 04 was recorded
If you're reading this after watching episodes out of order and wondering
why muse/plugins/todo (and even muse/plugins/chess, Episode 22's
finale plugin) already existed in the muse repo before Episode 04 was
recorded --- that's expected, not a mistake. Scripts for the whole season
get written up front, and demo code/plugins get built and tested well
ahead of the recording schedule, in whatever order is convenient --- not
necessarily episode order. Recording is the thing that happens
sequentially; script-writing and demo-building don't. Check this
directory's presence and the top-level README.md status table before
assuming something is missing or was added by mistake.
The todo domain --- one file, one task per line, content-addressed
Model: a single tracked file, todo.txt. Each task is addressed by its
own content, not by line position:
def _task_address(task: str) -> str:
digest = hashlib.sha256(task.encode("utf-8")).hexdigest()[:12]
return f"{_TASK_FILE}::{digest}"
That one design choice --- identity by content, not position --- is what makes every other method fall out cleanly, and it's the idea every later, more complex domain (MIDI, code symbols, chess) builds on.
How the six-method protocol maps onto it
| Method | What it does for todo |
|---|---|
snapshot |
Reads todo.txt off disk, hashes its raw bytes as one blob. No per-task parsing here --- just "what does the file contain right now." |
diff |
Loads both snapshots' task sets (splitting todo.txt into lines) and computes target - base (added) / base - target (removed) as set difference. Each added/removed task becomes its own AddressedInsertOp / AddressedDeleteOp, nested inside one outer PatchOp keyed on todo.txt (that outer key is what checkout actually restores from disk). |
merge |
Three-way, but does no line-based patching: merged = (base ∪ left ∪ right) − removed, where removed is anything either side deleted relative to base. Because tasks are content-addressed set elements, not line positions, two branches adding two different tasks can never conflict --- there's no "line 5 vs line 5" collision to even detect. |
drift |
muse status calls this: re-snapshot live state, diff it against the last commit, report whether anything changed. |
apply |
A no-op --- the core engine already restores todo.txt's bytes from the object store during checkout; there's no domain-specific reconstruction needed since the file is opaque bytes, not a structured format. |
schema |
Declares SetSchema(kind="set", element_type="task", identity="by_content"), merge_mode="three_way", zero sub-dimensions --- the minimum viable domain: no address-keyed merge extension, no CRDT, just the required core. |
The contrast with Git this episode is built around
Git would treat todo.txt as text and merge by line --- two people adding
a task on the same line number is a conflict even if the tasks are
unrelated. todo instead treats the file as an unordered set of
content-addressed elements, so "did the same task change" is the only
thing that can conflict --- and since there's no edit operation (only
add/remove of whole tasks), that basically never happens.
make-todo-episode06-demo.sh --- fully scripted, run it directly
./make-todo-episode06-demo.sh
Builds a disposable todo-episode06/ repo: one task committed on main,
two branches each adding a different task, both merged back with zero
conflicts. Uses muse-dev rather than plain muse --- the todo domain
ships on muse's dev branch but hadn't gone out in a released tarball
as of this recording. Swap MUSE_BIN to muse once a release includes
it.