25 KiB
25 KiB
Notes
- User has 2 months of prior exercises in this dir (ch1–9 of the Rust Book: variables, functions, control flow, ownership, slices, structs, enums, modules, collections, error handling) but reports forgetting all of it. Treat ch1–9 as "exposure, not mastery" until a lesson or diagnostic proves otherwise.
- Goal: backend/CLI job. Bias future lessons toward
Result/error handling, traits/generics, and eventually a backend framework (axum/tokio) over embedded topics. - Preference confirmed 2026-08-28: diagnostic-first — find actual gaps before building recap lessons, rather than re-teaching everything blind.
get-dependecies/has an unusedtrpl = "0.2.0"dependency (the async-book helper crate) — signals prior intent to reach ch17 (async), not yet attempted.- Lesson format rule (2026-08-28): typing-first. User forgot how to write code, not just what it means — every lesson needs hands on keyboard in a real cargo project,
cargo run/cargo testas the feedback loop. See LR-0001. - User asked to cover topics the diagnostic marked "solid" too. Do not skip them — fold them in as supporting material in typing lessons rather than dedicating lessons to them.
- Do not let the user copy-paste lesson code. State the no-paste rule explicitly in each lesson.
- Always link
reference/rust-syntax.htmlfrom lessons; forgotten syntax was eating the working memory needed for concepts. - Verify claimed compiler error text by actually running it before shipping a lesson (done for E0004 non-exhaustive, E0382 moved value in lesson 0002).
- Env: rustc/cargo 1.96.1 installed and working. Chrome opens lesson files via
nohup google-chrome --new-window "file://$(realpath …)" & disown(plainwslviewblocks the shell;rm -rfis blocked by policy — usemktemp -dfor scratch projects). - Next lesson candidates, in priority order: (1) reading compiler errors fluently, (2) Modules & Paths (0/1, and
restauran/learn-modulesexist as material), (3) enums +Option/Resultmodelling, (4) structs+impl by building something, (5) collections drill. - Format shift (lesson 0003): user finished 0002, reported still "feeling like lacking", and asked for spec-driven work — a project brief with acceptance tests, NOT step-by-step syntax guidance, plus syntax examples they can consult. Self-identified weak: packages, enums, structs. Deliver specs + test suites from here; put syntax help in a different domain so it cannot be pasted, and collapse hints behind
<details>so they choose the difficulty. - Always verify a shipped test suite is passable by writing a private reference impl in a temp dir first (done for 0003: 17/17). Never ship an unproven spec.
cargo new <name> --libdoes NOT createtests/— instructions must includemkdir tests.- Two-lesson rule (2026-09-02): for any topic the user names as weak, ship a concept lesson (reading, real compiler output, from zero) BEFORE the project lesson. 0003 shipped project-only and was unusable; 0004 backfilled it. See LR-0002.
- Specs need prose. Signatures alone are not a specification. Every project brief needs: what the program is for, what its data is, what its commands are — in plain language, written so the type choices (struct vs enum) fall out of the description. Then signatures.
- Diagnostic scores overstate ability: recognition-style questions pass without design ability. Self-reported weakness > quiz score. (Struct 2/2, Enum 1/2 on 0001, yet both needed teaching from zero.)
- Teaching demos: use a domain different from the project domain (café for 0004 vs tasks for 0003) so nothing is pasteable. Capture real
cargo run/cargo buildoutput including deliberate errors — never write error text from memory. - 0003 result (2026-09-02): 17/17 green, idiomatic code (closures,
iter_mut,?, private field +&[Task]accessor). Structs/enums/packages are now produced, not just recognised — stop teaching them as weak. See LR-0003. - Untested prose is undone prose. The spec's stderr/exit-1 requirement was the one thing the 17 integration tests could not reach, and it is the one thing that was not done (
println!for errors, exit 0,.unwrap()panic ondone 9). Either test it or make it the next lesson's drill. - Drill format that works: operate on the crate the user already wrote, one shell-level check per step (
cargo run --quiet -- fly ; echo $?). No new test file, and their real project improves. - Next lesson candidates after 0005: (1) 0006 project —
String→TaskErrorenum withDisplay/Error/Fromacross all four files, plus persistence to a file (fs,io::Error, realFromconversion) — the natural project for traits; (2) collections + iterators drill (HashMap,filter/map/collect), still untouched since the 1/2 diagnostic; (3) lifetimes, only once traits are solid; (4) then axum/serde, where traits pay off. - Dark mode is the house style (2026-09-02, user request).
assets/style.cssis dark by default and every lesson/reference doc gets it by linking that one file — so a new lesson needs no colour work, just<link rel="stylesheet" href="../assets/style.css">. Never inline colours or a per-lesson palette. Panels usevar(--panel)/var(--panel-hover);@media printre-declares the whole variable block as ink-on-paper so printouts stay readable. - Typography (2026-09-02, user request): the old serif stack was unreadable — none of "Iowan Old Style"/Palatino/Georgia/Charter is installed on this machine, so it fell back to DejaVu Serif. Now a humanist sans stack (
Inter,Ubuntu Sans,system-ui, …) at1.2rem/1.68line-height, measure narrowed to40em, code at0.87eminDejaVu Sans Mono. Only fonts actually installed here: Ubuntu Sans, DejaVu Sans, Liberation Sans, DejaVu Sans Mono, Ubuntu Mono — check withfc-list : familybefore naming a font. - Quiz authoring checklist (bug found 2026-09-02). A recall question without
<button class="reveal-btn">Show answer</button>renders dead:initRecallinassets/quiz.jsbails out if reveal/answer/grade are not all present, so the Got it / Missed it buttons never appear. Four questions in 0005 shipped that way. Before shipping any lesson with a quiz, click every question in the browser — do not eyeball the HTML. quiz.jsmarks a graded recall withopt-correct/opt-incorrect, which the stylesheet did not define — Got it / Missed it gave no colour feedback in any lesson. Fixed inassets/style.css; the graded choice now turns green/red and the other button dims to 0.45 opacity.- 0005 drill result (2026-09-03): all three checks green.
Display for Taskcorrect,unwrapgone,run() -> Result<(), String>,Command::parse(args)?with the match onCommandvalues (fixed after review),eprintln!+process::exit(1), exit 1 on both failure paths, 17/17 still pass. The stderr/exit-1 contract missed in 0003 is now produced. Remaining nits are cosmetic only (&argson a&[String]param,matchwhereif let Errwould do). Traits/Display/?are produced, not just recognised — 0006 can assume them. - Concrete material for 0006 (used):
"id not found"is returned by bothcommand.rs(no id argument given) andstore.rs(id not in the store) — two different failures, identical prose. Their own defect, not an invented example. Enums scored 0/1 on the 0005 recall for exactly this reason. - 0006 shipped (2026-09-03):
TaskErrorenum +Display+Error::source()+From<ParseIntError>acrosserror.rs/command.rs/store.rs/main.rs, driven by a shippedtests/errors.rs(7 tests). Reference impl verified in a temp dir first: 17 + 7 = 24 green. Deliberately NOT in 0006: file persistence (that is 0007, andFrom<io::Error>is easier onceTaskErrorexists). - New in 0006 beyond 0005's recipe:
Error::source(), plus the std rule that a wrapped cause goes in eithersource()orDisplay, never both. Cited from the std page, and the tests enforce it (BadIdDisplay says "task id must be a number"; theParseIntErrorsentence is only reachable viasource()). - Real compiler output captured for 0006 (never from memory):
E0004non-exhaustive after adding a variant,E0432unresolved import whenpub mod error;is missing,E0308leftoverStringerror,E0271?with noFromimpl (the annotated-target variant ofE0277, exactly as 0005 predicted),E0369missingPartialEqseen from the test file. reference/book-coverage.html(new, 2026-09-03): ch1–21 mapped to Read / Produced / Gap, chapter list taken verbatim from the book'sSUMMARY.md. Answers "am I missing something important?" without guessing. Keep it updated after every lesson — it is now the thing that picks the next lesson. Named gaps: ch8 collections/HashMap, ch11 writing own tests, ch13 iterator chains, ch10.3 lifetimes, ch16/17 concurrency+async.- Lesson order after 0006: (1) 0007 persistence —
fs,io::Error,From<io::Error>,FromStrto read a task back from a line; (2) 0008 collections + iterators (HashMap,filter/map/collect, first hand-written generic fn); (3) 0009 writing your own tests (ch11) — the user has consumed 24 of my tests and written zero; (4) lifetimes as reading practice; then serde → axum → async. - Quiz checklist ran for 0006 (2026-09-03): served the workspace over
python3 -m http.server(Chrome/Playwright blocksfile://), clicked all 6 questions via Playwright — 4 recalls have reveal+grade, 2 MCQs mark correct/incorrect, summary and Copy report work, only console error is a missing favicon. Do this for every lesson; it is three tool calls. - Style additions for 0006:
blockquote(verbatim primary-source quotes) and.gap(red cell in the coverage table) now live inassets/style.css. Both print correctly because the print block re-declares the variables. - 0006 drill result (2026-09-03): 24/24 green (17 spec + 7 errors),
tests/errors.rsbyte-identical to the shipped spec.TaskErrorhas all 7 variants,Displaycorrect,source()returns theParseIntErroronly forBadId,From<ParseIntError>written. ButFromis never exercised:command.rshand-matchesid.parse()withErr(e) => return Err(TaskError::BadId(e))in bothdoneandremove, duplicated, so the?conversion the lesson taught is dead code. Also unchanged from 0005:match/Ok(_) => ()inmaininstead ofif let Err. Traits are produced; the gap is reaching for?+Frominstead of manual matching. - 0007 shipped (2026-09-03): files +
FromStr.fs::read_to_string/fs::write,io::Error+ErrorKind::NotFoundmatch guard,From<io::Error>(the secondFrom, which answers the user's own question),FromStr for Taskwith an associatedtype Err,env::varwith a default, and a hand-writtenPartialEqbecauseio::Erroris notPartialEq. Reference impl verified in/tmp/ref7first: 17 + 7 + 8 = 32 green, spec shipped aslessons/0007-persist-spec.rs. - Real compiler output captured for 0007 by running it:
E0369(derivePartialEqover anio::Errorfield),E0277(?with noFrom<io::Error>),E0277(Task: FromStrnot satisfied),E0046(missingtype Err),E0119(conflictingFromimpls). Never from memory. - Drill steps need a mechanical check, not prose (third repeat — see LR-0005). 0006 asked in prose for the two
match id.parse()blocks to collapse to?; it did not happen. 0007 makes it step 0 withgrep -c "match id.parse" src/command.rs→0. - The user asks mechanism questions after each lesson (this session: can two
Fromimpls exist, where does the wrapped message go, must a dev walksource()by hand). Answer with runnable evidence — a scratch binary in/tmpand its real output beats prose, and takes one tool call. - Deliberately left un-idiomatic in 0007 so 0008 has the user's own code to rewrite: the
forloop inStore::loadthat pushes into aVecis acollect::<Result<Vec<_>, _>>()waiting to happen. - 0008 shipped (2026-09-04): iterators +
HashMap+ first generic function. Reference impl verified in/tmp/ref8first: 17 + 7 + 8 + 14 = 46 green,cargo clippy --all-targetsclean apart from one pre-existingDefault for Storesuggestion. Spec shipped aslessons/0008-collections-spec.rs(14 tests, 162 lines). - What 0008 makes the user produce:
Store::loadrewritten ascollect::<Result<Vec<Task>, TaskError>>()?(the loop 0007 left behind),count_by_priorityon aHashMap<Priority, usize>,remove_completedviaVec::retain,titles_withas afilter+map+collectchain,findvspositiondistinguished, andfn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize> where K: Eq + Hash, F: Fn(&T) -> Kwritten from the signature up in a newstats.rs. - Real compiler output captured for 0008 by running it (never from memory):
unused Map that must be used+ "iterators are lazy and do nothing unless consumed" (no consumer),E0283type annotations needed on a barecollect(),E0277Priority: Eq/Priority: Hashnot satisfied at thetallycall site,E0502aniter_mut()borrow held across aself.books.len()read,E0507cannot movetask.priorityout from behind&Task,E0004non-exhaustivematchafter addingStats/Clear. - The
Copyderive is the interesting one. Keying aHashMapby an enum field read through&Tforces the user to choose between.clone(),#[derive(Clone, Copy)], and keying by&str— a real ownership decision with three defensible answers, not a syntax lookup. It is quiz question 4 (Ownership) for that reason. - 0008 ends with a runnable payoff, not just green tests: two new CLI commands (
stats,clear) wired throughCommand/main, so theHashMapand theretainare visible from the terminal. Real session captured in the lesson verbatim (stats→high 1 / medium 1 / low 1,clear→cleared 1 completed, exit 0). - Reference doc grew three sections, all anchored and linked from 0008:
#iterators(the one-method trait, the three ways in, adapters vs consumers,collecttargets,lines()vssplit('\n'), the four errors),#hashmap-keys(Eq + Hash, theentryidiom, arbitrary order,BTreeMapas the sorted alternative), and a generic-function block in#traits(monomorphisation, thewhereclause as a two-way contract,Fn/FnMut/FnOnce). - Quiz checklist ran for 0008 (2026-09-04): served over
python3 -m http.server 8899, clicked all 6 questions through Playwright — 2 MCQs mark correct/incorrect, 4 recalls reveal + grade, summary reports6 of 6 answered, 6 correctbroken down by topic, every local link and#anchorresolves, only console error is the missing favicon. - Interleaving in the 0008 quiz is deliberate: only 2 of 6 questions are about iterators. The rest revisit
HashMapkeys, theCopy/cloneownership choice, and trait bounds — spaced retrieval of 0005/0006/0007 material inside a new lesson. - Coverage map after 0008: ch8 and ch13 flip Gap/Partial → Produced, ch10.1 Read → Produced. One core gap remains: ch11, writing your own tests — the user has now run 46 of my tests and written zero
#[test]. That is 0009, and the map's "order that follows" section says so. Then lifetimes (ch10.3) as reading practice — 0008 had the user write one unknowingly, sincetitles_withreturnsVec<&str>borrowed from&selfand elision hid the annotation. - Nothing deliberately left un-idiomatic in 0008 — 0009 supplies its own material, because the target is the tests the user writes rather than the code they refactor.
- Lesson format rule (2026-09-04, user-reported): define the demo domain's data model before the first example. 0008 shipped with
Book,Shelf,book(), andshelf_of()used across ten snippets and defined nowhere — the user could not tell what fields a book had, so every example needed guessing. Fixed by adding a "The demo domain, in full" section before Part 1: both type definitions, both helpers, the literal four-bookVec, and an explicit note thatshelf(lowercase) is theVec<Book>whileShelf(capitalised) is the enum. Every future lesson with a demo domain does this first. - Related trap the same complaint exposed: one variable name must have one element type. 0008 used
shelffor both aVec<Book>(closure|b|) and aVec<&str>(closure|t|), because the two error examples came from a different scratch file. Fixed by re-running/tmp/demo8/examples/{e1,e2}.rswith atitlesbinding and re-capturing real rustc output — never hand-edit a variable name inside quoted compiler output, re-run it. - Layout rule (2026-09-04, user-decided): narrow prose column, small code font, and let the few verbatim blocks scroll. 13 of 28 code blocks in 0008 were scrolling horizontally, because prose was capped at
max-width: 40emwhile verbatim rustc output runs to ~105 characters. Two fixes were tried and rejected: (1) lettingprebreak out of the prose column with negative margins — the user said code hanging past the paragraph above it looks broken; (2) widening the body to65remso prose and code share one edge — the user said the long measure strained their eyes to read. Final:body { max-width: 40em }is back,predrops tofont-size: 0.73em(0.62em in print), and the handful of blocks still too wide scroll on the wheel. - What that buys: the column fits ~81 characters of code. Every authored line in every lesson and in
rust-syntax.htmlis now trimmed to fit it — the pass that did it lives in the session log, but the rule going forward is simply: keep authored code and its trailing//comment inside 81 characters. - What still scrolls, and why it must: verbatim rustc output only — 83 to 103 characters,
E0502/E0277/E0507/E0432, the^^^^and-----spans. Those cannot be wrapped or hand-shortened, because the underlines have to stay column-aligned or they point at the wrong token. Measured at 1385px with layout settled:rust-syntax4/51, 0005 3/34, 0007 2/22, 0008 4/28, and 0/N on 0001-0004 and 0006. All quizzes still click through. - Measure
preoverflow after a settle delay. A bareiframe.onloadread reported 22/51 onrust-syntaxwhere the settled read gives 4/51 — the scrollbar geometry is not final at load. - Keep terminal sessions short with a shell helper (
run(){ ... }), as 0008 does. 0007 repeated the fullTASKS_FILE=t.txt cargo run -q --manifest-path ...on every line and was the one block still too wide; re-recorded with arun()helper — re-run the session, never retype it by hand. - Lesson format rule (2026-09-04, user-reported): a lesson must read top to bottom, and no name may appear before it is defined. 0008 put
tally(self.tasks(), |task| task.priority)inside the Part 5 error output, three sections before Part 6 definestally— the user could not read the error, because the call site it pointed at was meaningless. I did not notice, because I wrote the lesson as a whole and read it as a whole; the user reads it in one direction, once. Captured compiler output is verbatim and must never be hand-edited (see theshelf/titlestrap above), so the fix is always to introduce the name earlier — a signature plus a one-line gloss before the first sighting — never to alter the quote or reorder the parts. Ship check: read the lesson in order and list every identifier at its first appearance; if it is not defined above that point, it is a bug. This is the demo-domain rule one level up: that one covers the demo's data model, this one covers every other name, including ones the drill will define later. - The "specs need prose" rule (line 18) covers drill steps too — enforcement failure found 2026-09-05. 0008 Step 3 shipped as two bare signatures (
titles_with,remove_completed) plus one note about&str, and the user was blocked: a signature says what goes in and out, never what the function is for or what its edge cases are. I had the behaviour, it was in the six spec tests — order preserved, emptyVecnot an error, ids not renumbered, counter untouched,0is a legal answer — and I left the user to reverse-engineer prose out ofassert_eq!. Fixed by writing those out. Rule going forward: every function a drill asks for gets a plain-language sentence on what it is for, plus the edge cases its tests pin down, written from the test file at authoring time. A signature is never the specification. Step 5 of the same drill is the pattern that works — the captured terminal session specifies both new commands by example, so no prose was needed there. - 0008 drill result (2026-09-05): 46/46 green, library produced, CLI half missed three requirements.
tallywritten from the signature,loadas onecollect::<Result<Vec<Task>, TaskError>>()?,retain, thefilter/map/collectchain — all correct. Butstatsprinted high/low/medium (main.rslooped[Priority::High, Low, Medium]),clearprinted nothing and dropped theusize, and nothing tested either, becauserunlives insrc/main.rswhere notests/file can import it. Third repeat of "untested prose is undone prose" (LR-0003, LR-0005), first time the cause is structural. See LR-0006. - 0009 shipped (2026-09-05): writing your own tests (ch11). Demo domain is
/tmp/heating— a thermostat with a private field, a private helper, one panicking constructor and oneResultsetter, so the page can show#[should_panic]and aResult-returning test against the same type. All 28 code/output blocks captured by running them; no new spec file, because the skill is producing assertions rather than satisfying mine. - New grading device:
lessons/0009-mutants.sh. Copies the crate to a temp dir, applies onesedmutation, runs the user's tests, restores nothing (the copy is thrown away) — six mutations covering stats order, the zero-count line, the clear count, theDisplayline,Status::parse'sin-progressarm, andCommand::parse's case folding. Verified to discriminate:0 killed, 6 survivedagainst only the 46 shipped tests,6 killed, 0 survivedagainst the reference impl in/tmp/ref9. Pre-drill run on the user's crate:0 killed, 3 survived, 3 skipped(three mutations targetsrc/cli.rs, which the drill creates). Drill's finishing condition is the report, not a test count — reuse this device for any lesson where the deliverable is tests. - The 0009 refactor is the point, not scaffolding:
runmoves fromsrc/main.rstosrc/cli.rsand takesout: &mut impl Write, somainpassesio::stdout().lock()and a test passesVec<u8>and asserts on the bytes. That single parameter is what makes the three 0008 CLI defects testable, and it is the same move that makes an axum handler testable later. Reference impl/tmp/ref9: 4 unit + 6 cli + 14 + 7 + 3 + 8 + 17 = 59 green, no warnings, CLI session verified end to end (statsin high/medium/low order,cleared 1 completed,done 9→ exit 1). - Deliberately NOT in 0009: doc tests (
///examples, ch14),#[bench],assert_cmd/predicatesfor subprocess testing,proptest, andcargo-mutants(the real version of the shipped script). Named in a "then stop" section so the user knows they exist and why they are not next. - Test names drive the width of captured output. Five of 28 blocks in 0009 still scroll, all of them on cargo's own 93–97 character
test result:line, which cannot be shortened. Four more were scrolling only because my demo test names were 34–40 characters long (a_room_below_the_target_is_heating,every_legal_target_survives_a_round_trip); renaming them to ~17–30 characters and re-running the captures fixed those blocks. Never hand-edit the quoted output to fit — rename in the source and re-run (same rule as theshelf/titlestrap, NOTES line 55). - Quiz checklist ran for 0009 (2026-09-05): served over
python3 -m http.server 8899, clicked all 6 questions through Playwright — 3 MCQs mark correct/incorrect, 3 recalls reveal + grade, summary reports6 of 6 answered, 6 correctsplit Tests 3/3, Traits 1/1, Collections 1/1, Modules 1/1; every local link and#anchorresolves (including the newrust-syntax.html#tests); console clean, zero errors this time (the favicon 404 is gone because the page is served, not opened fromfile://). - Reference doc gained
#tests(the attribute pair, the three macros and what each failure prints,should_panicvs aResulttest, the unit-vs-integration access table, thetests/common/mod.rsspelling, the writer-injection shape, the runner flags,E0433/E0603/E0616) and its title/subtitle now say ch. 1–13. Coverage map: ch8, ch10.1 and ch13 flip to Produced after the 0008 drill; ch11 is "Taught — drill pending"; the "order that follows" section now names lifetimes (10.3) and patterns (19) as the remainder, then serde → axum → async. - After the 0009 drill the book's core has no Gap left except ch10.3 lifetimes (reading practice) and ch19 patterns (one page). Next real decision is serde vs a first axum service — ask the user which, since both are now reachable.