Judge the result, not the keystrokes
Most interactive tutorials check whether you typed the expected command. That teaches imitation. Comparing the state you produced against a goal teaches the thing itself — and it changes how you build the simulator.
The quickest way to build an interactive tutorial is to compare what the learner
typed with what you expected. It is also the quickest way to teach the wrong lesson.
Type git checkout -b feature where the answer key says git switch -c feature and you
are told you are wrong, when you did exactly the right thing. The learner concludes
the skill is memorising one spelling of a command. It isn't.
When I built the courses on this site I made one rule early and never broke it: a lesson is judged on the state you end up in, never on the commands that got you there. Every design decision downstream follows from that rule, and most of them are more interesting than they look.
A goal is a script, not a picture
The tempting way to define "the state you should end up in" is to draw it — a fixture of the expected repository, or the expected table. Fixtures rot. The day the simulator changes how it names a commit or orders a result, every hand-written fixture is silently wrong.
So a goal is never a picture. It is the setup script plus a reference solution, replayed through the same simulator the learner uses. The target is computed, not described, and it cannot drift from the engine because it is the engine.
Comparing state honestly is the hard part
"Same state" sounds trivial until you try it. Two people solving the same Git lesson create commits in a different order, so the commits get different identities. A naïve comparison fails one of them.
The fix is to compare shape. Commits that existed before the lesson started keep their names; commits the learner made are anonymous, and each one is fingerprinted by what kind of commit it is plus the fingerprints of its parents — a Merkle tree over the history. Two branches match exactly when everything behind them matches. Order of creation stops mattering; structure is all that's left.
Each course needed its own version of that idea. In SQL, a result matches when the rows match as a multiset, ignoring column and row order unless the question is about order. In the shell course, a file system matches path by path and byte by byte. In the regex course, there is no target state at all — a pattern is correct when it passes every test string. The common thread is deciding, per domain, what the learner is allowed to do differently.
Feedback is the diff
Once you compare state, feedback writes itself. The difference between where you are
and where you should be is the hint: "feature isn't where the goal has it yet",
"the result should have 7 rows, not 9", "web should publish 8080 → 80". None of it is
hand-written per lesson. It falls out of the comparison, which means it is always
accurate and never gives away more than the gap.
Par instead of a single answer
If any correct route counts, what stops the lesson becoming trivial? Par. Each lesson's
reference solution is replayed and every step that changed something is counted;
commands that only read — git log, ls, SELECT in a lesson about writes — are
free. You can solve it your way and still see that there was a shorter one.
Rewarding exploration and rewarding efficiency are not in tension if reading is free.
Test the curriculum, not just the code
The last piece is the one I would insist on in any system like this: every published solution is replayed against its goal in CI. If someone edits a lesson, or changes the engine, and a solution stops reaching its goal, the build fails. Across the courses that is well over a hundred lessons, each one proven solvable on every commit.
It caught real bugs while I was building — including a chmod that swallowed its own
file argument, and a snapshot that made two Git commands invisible to undo. A curriculum
is a program. It deserves tests like one.