@supersuit/hyperspec 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +71 -8
  3. package/SPEC.md +2 -2
  4. package/WRITING.md +680 -21
  5. package/bin/hyperspec.mjs +188 -4
  6. package/examples/writing/course/claims.jsonl +0 -0
  7. package/examples/writing/course/goldens/lesson.md +1 -0
  8. package/examples/writing/course/materials/brief.md +9 -0
  9. package/examples/writing/course/materials/brief.md.segments.jsonl +6 -0
  10. package/examples/writing/course/outline.md +11 -0
  11. package/examples/writing/course/part-1.md +47 -0
  12. package/examples/writing/course/part-2.md +40 -0
  13. package/examples/writing/course/runs.jsonl +0 -0
  14. package/examples/writing/course.hyperspec.md +205 -0
  15. package/examples/writing/essay/judge/doctor.packet.json +108 -0
  16. package/examples/writing/essay/judge/lineup.packet.json +64 -0
  17. package/examples/writing/essay/judge/persona.packet.json +73 -0
  18. package/examples/writing/essay/judge/reader.packet.json +93 -0
  19. package/examples/writing/essay/learn/first-draft.md +84 -0
  20. package/examples/writing/essay/learn/learn.packet.json +106 -0
  21. package/examples/writing/essay/sample-verdicts/doctor.verdict.json +43 -0
  22. package/examples/writing/essay/sample-verdicts/learn.verdict.json +30 -0
  23. package/examples/writing/essay/sample-verdicts/lineup.verdict.json +6 -0
  24. package/examples/writing/essay/sample-verdicts/persona.verdict.json +4 -0
  25. package/examples/writing/essay/sample-verdicts/reader.verdict.json +7 -0
  26. package/examples/writing/essay.hyperspec.md +6 -1
  27. package/examples/writing/story/judge/attribution.packet.json +194 -0
  28. package/examples/writing/story/judge/doctor.packet.json +108 -0
  29. package/examples/writing/story/judge/knowledge.packet.json +77 -0
  30. package/examples/writing/story/judge/persona.packet.json +73 -0
  31. package/examples/writing/story/judge/reader.packet.json +94 -0
  32. package/examples/writing/story/sample-verdicts/attribution.verdict.json +81 -0
  33. package/examples/writing/story/sample-verdicts/doctor.verdict.json +43 -0
  34. package/examples/writing/story/sample-verdicts/knowledge.verdict.json +4 -0
  35. package/examples/writing/story/sample-verdicts/persona.verdict.json +20 -0
  36. package/examples/writing/story/sample-verdicts/reader.verdict.json +16 -0
  37. package/examples/writing/story.hyperspec.md +7 -3
  38. package/package.json +1 -1
  39. package/src/check.mjs +96 -132
  40. package/src/draft.mjs +26 -0
  41. package/src/judge.mjs +386 -0
  42. package/src/judges/attribution.mjs +360 -0
  43. package/src/judges/doctor.mjs +126 -0
  44. package/src/judges/index.mjs +31 -0
  45. package/src/judges/knowledge.mjs +111 -0
  46. package/src/judges/lineup.mjs +272 -0
  47. package/src/judges/persona.mjs +137 -0
  48. package/src/judges/reader.mjs +111 -0
  49. package/src/learn.mjs +422 -0
  50. package/src/ledger.mjs +108 -0
  51. package/src/sentences.mjs +81 -0
  52. package/src/sequence-draft.mjs +75 -0
  53. package/src/stations/claims.mjs +44 -39
  54. package/src/stations/index.mjs +3 -1
  55. package/src/stations/links.mjs +11 -3
  56. package/src/stations/quotes.mjs +6 -4
  57. package/src/stations/sequence.mjs +275 -0
  58. package/src/writing-fields.mjs +34 -0
  59. package/src/writing.mjs +1 -1
package/bin/hyperspec.mjs CHANGED
@@ -17,13 +17,17 @@ import { sha256 } from "../src/hash.mjs";
17
17
  import { readScope, measureFeatures, writeFeatures, scopeTemplate, GOLDENS_README } from "../src/dna.mjs";
18
18
  import { str } from "../src/placeholder.mjs";
19
19
  import { runCheck } from "../src/check.mjs";
20
+ import { prepareJudges, recordJudgment } from "../src/judge.mjs";
21
+ import { prepareLearn, recordLearn, tallyLine } from "../src/learn.mjs";
20
22
 
21
23
  const HELP = `hyperspec <command> [options]
22
24
 
23
25
  lint <file...> [--json] score each hyperspec against the nine tests
24
26
  exit 0 pass, 1 a test fails, 3 blocked on an open decision, 2 usage
25
- check <spec> --draft <file> [--json] [--only a,b]
26
- needs a writing spec (profile: writing); lints it first (a spec
27
+ check <spec> [--draft <file>] [--json] [--only a,b]
28
+ needs a writing spec (profile: writing), and --draft unless the
29
+ spec lists writing.form.sequence.files, whose files, joined in
30
+ reading order, are then the draft; lints it first (a spec
27
31
  that does not pass lint, or is blocked, exits with lint's own code
28
32
  and runs no station: a draft cannot be checked against a spec that
29
33
  is not ready); then runs every deterministic station (or the
@@ -35,6 +39,53 @@ const HELP = `hyperspec <command> [options]
35
39
  exit 0 every run station passed, 1 a station failed, 2 usage
36
40
  (including a missing draft file, a spec without the writing
37
41
  profile, or an --only that names no known station)
42
+ judge prepare <spec> --draft <file> --out <dir> [--only a,b] [--force] [--json]
43
+ needs a writing spec that passes lint (exits with lint's own code
44
+ otherwise); writes one <station>.packet.json per applicable
45
+ judgment station (or the --only subset) into <dir>: the rubric from
46
+ the spec, fixed instructions, the inputs and the exact verdict
47
+ shape, for an outside judge to fill; hyperspec never calls a model;
48
+ stations: doctor, lineup (a blind voice lineup against the DNA
49
+ scope's goldens, whose answer goes to lineup.key.json for a person
50
+ to read; record rebuilds it and never reads the file), reader,
51
+ persona (against the claims ledger), and for fiction attribution (a
52
+ blind speaker test on the dialogue lines whose speaker the draft
53
+ names; the answers go to attribution.key.json, likewise rebuilt)
54
+ and knowledge (each character's knowledge timeline);
55
+ the same spec and draft give byte-identical packets; refuses to
56
+ overwrite an existing packet without --force
57
+ exit 0 written, 1 a station could not build its packet (the rest
58
+ are written), 2 usage (a missing or non-folder --out, an existing
59
+ packet without --force, an unknown --only name), or lint's own code
60
+ judge record <packet> --verdict <file> [--json]
61
+ validate the judge's verdict against its packet (every evidence
62
+ span must appear in the draft; a draft or spec changed since the
63
+ packet is stale), derive the station's status, and append one line
64
+ to the spec's improvement.ledger, as check does; an invalid or stale
65
+ verdict appends nothing; run it from the folder prepare ran in, since
66
+ the packet keeps the spec and draft paths as they were given
67
+ exit 0 the station passed, 1 it failed or the verdict is invalid
68
+ or stale, 2 usage
69
+ learn prepare <spec> --first <draft> --approved <draft> --out <dir> [--force] [--json]
70
+ needs a writing spec that passes lint (exits with lint's own code
71
+ otherwise); diffs the first draft a factory produced against the
72
+ draft a person approved, sentence by sentence, and writes
73
+ <dir>/learn.packet.json: each edit as a hunk (E1..En: deleted,
74
+ inserted or replaced, with both texts), the spec's block names plus
75
+ none, fixed instructions and the verdict shape, for an outside judge
76
+ to name the block that should have prevented each edit; the same
77
+ files give a byte-identical packet; refuses to overwrite it without
78
+ --force
79
+ exit 0 written, 2 usage
80
+ learn record <packet> --verdict <file> [--json]
81
+ rebuild the packet from the files on disk (a changed file is stale,
82
+ an edited packet is refused), validate the verdict (every hunk id
83
+ exactly once, a block the spec has or none, a why), print the edits
84
+ per block and one next move for the block with the most, and append
85
+ one learn line to the spec's improvement.ledger (not-improved: the
86
+ spec has not changed yet); learn never edits the spec
87
+ exit 0 recorded, 1 the verdict is invalid or stale (nothing
88
+ appended), 2 usage
38
89
  init <file> [--title T] [--kind K] write a new hyperspec skeleton (refuses to overwrite)
39
90
  init <file> --profile writing [--title T] [--form F] [--fiction]
40
91
  write a writing-profile skeleton: every required block (materials,
@@ -309,7 +360,6 @@ if (cmd === "check") {
309
360
  process.exit(2);
310
361
  };
311
362
  if (!specPath) usage("check needs a spec path");
312
- if (!parsed.values["--draft"]) usage("check needs --draft <file>");
313
363
  let only;
314
364
  if (parsed.values["--only"] !== undefined) {
315
365
  only = parsed.values["--only"].split(",").map((s) => s.trim()).filter(Boolean);
@@ -340,7 +390,7 @@ if (cmd === "check") {
340
390
  for (const s of result.stations) {
341
391
  if (s.status === "skip") { console.log(`${s.station}: skip (${s.reason})`); continue; }
342
392
  console.log(`${s.station}: ${s.status}`);
343
- for (const finding of s.findings) console.log(` ${finding.severity === "fail" ? "fail" : "warn"} [${finding.id}] ${finding.message}${typeof finding.line === "number" ? ` (line ${finding.line})` : ""}\n fix: ${finding.fix}`);
393
+ for (const finding of s.findings) console.log(` ${finding.severity === "fail" ? "fail" : "warn"} [${finding.id}] ${finding.message}${typeof finding.line === "number" ? ` (${finding.file ? `${finding.file} ` : ""}line ${finding.line})` : ""}\n fix: ${finding.fix}`);
344
394
  }
345
395
  if (result.verdict) {
346
396
  const detail = result.verdictDetail.change ?? result.verdictDetail.reason;
@@ -351,6 +401,140 @@ if (cmd === "check") {
351
401
  process.exit(result.code);
352
402
  }
353
403
 
404
+ if (cmd === "judge") {
405
+ const sub = argv[1];
406
+ const printFinding = (f) => console.log(` ${f.severity === "fail" ? "fail" : "warn"} [${f.id}] ${f.message}${typeof f.line === "number" ? ` (line ${f.line})` : ""}\n fix: ${f.fix}`);
407
+
408
+ if (sub === "prepare") {
409
+ const parsed = parseArgs(argv.slice(2), { valueFlags: ["--draft", "--out", "--only"], boolFlags: ["--force", "--json"] });
410
+ if (parsed.error) { console.error(parsed.error); process.exit(2); }
411
+ const [specPath] = parsed.positionals;
412
+ const json = parsed.values["--json"];
413
+ const usage = (error) => {
414
+ if (json) console.log(JSON.stringify({ spec: specPath ?? null, draft: parsed.values["--draft"] ?? null, out: parsed.values["--out"] ?? null, error }, null, 2));
415
+ else console.error(error);
416
+ process.exit(2);
417
+ };
418
+ if (!specPath) usage("judge prepare needs a spec path");
419
+ if (!parsed.values["--draft"]) usage("judge prepare needs --draft <file>");
420
+ if (!parsed.values["--out"]) usage("judge prepare needs --out <dir>");
421
+ let only;
422
+ if (parsed.values["--only"] !== undefined) {
423
+ only = parsed.values["--only"].split(",").map((x) => x.trim()).filter(Boolean);
424
+ if (!only.length) usage("--only names no judge");
425
+ }
426
+ const result = prepareJudges(specPath, parsed.values["--draft"], parsed.values["--out"], { only, force: parsed.values["--force"] });
427
+ if (result.usage) usage(result.error);
428
+ if (result.lintBlocked) printLintBlocked(result, json, "no packets written");
429
+ if (json) console.log(JSON.stringify(result, null, 2));
430
+ else {
431
+ for (const w of result.written) console.log(w.path);
432
+ for (const s of result.skipped) console.log(`${s.station}: skip (${s.reason})`);
433
+ for (const f of result.crashed) { console.log(`${f.station}: no packet`); printFinding(f); }
434
+ }
435
+ process.exit(result.code);
436
+ }
437
+
438
+ if (sub === "record") {
439
+ const parsed = parseArgs(argv.slice(2), { valueFlags: ["--verdict"], boolFlags: ["--json"] });
440
+ if (parsed.error) { console.error(parsed.error); process.exit(2); }
441
+ const [packetPath] = parsed.positionals;
442
+ const json = parsed.values["--json"];
443
+ const usage = (error) => {
444
+ if (json) console.log(JSON.stringify({ packet: packetPath ?? null, verdict: parsed.values["--verdict"] ?? null, error }, null, 2));
445
+ else console.error(error);
446
+ process.exit(2);
447
+ };
448
+ if (!packetPath) usage("judge record needs a packet path");
449
+ if (!parsed.values["--verdict"]) usage("judge record needs --verdict <file>");
450
+ const result = recordJudgment(packetPath, parsed.values["--verdict"]);
451
+ if (result.usage) usage(result.error);
452
+ if (json) console.log(JSON.stringify(result, null, 2));
453
+ else if (result.invalid) {
454
+ console.log(`${result.station}: ${result.stale ? "stale packet" : "invalid verdict"}, nothing recorded`);
455
+ for (const f of result.findings) printFinding(f);
456
+ } else {
457
+ console.log(`${result.station}: ${result.status}`);
458
+ if (result.summary) console.log(` ${result.summary}`);
459
+ for (const f of result.findings) printFinding(f);
460
+ if (result.verdict) {
461
+ const detail = result.verdictDetail.change ?? result.verdictDetail.reason;
462
+ console.log(`verdict: ${result.verdict}${detail ? ` (${detail})` : ""}`);
463
+ }
464
+ if (result.ledgerWarning) console.log(`warn: ${result.ledgerWarning}`);
465
+ }
466
+ process.exit(result.code);
467
+ }
468
+
469
+ console.error(`unknown judge subcommand: ${sub ?? "(none)"}\n\n${HELP}`);
470
+ process.exit(2);
471
+ }
472
+
473
+ if (cmd === "learn") {
474
+ const sub = argv[1];
475
+ const printFinding = (f) => console.log(` ${f.severity === "fail" ? "fail" : "warn"} [${f.id}] ${f.message}\n fix: ${f.fix}`);
476
+
477
+ if (sub === "prepare") {
478
+ const parsed = parseArgs(argv.slice(2), { valueFlags: ["--first", "--approved", "--out"], boolFlags: ["--force", "--json"] });
479
+ if (parsed.error) { console.error(parsed.error); process.exit(2); }
480
+ const [specPath] = parsed.positionals;
481
+ const json = parsed.values["--json"];
482
+ const opts = { first: parsed.values["--first"], approved: parsed.values["--approved"], out: parsed.values["--out"], force: parsed.values["--force"] };
483
+ const result = prepareLearn(specPath, opts);
484
+ if (result.usage) {
485
+ if (json) console.log(JSON.stringify({ spec: specPath ?? null, first: opts.first ?? null, approved: opts.approved ?? null, out: opts.out ?? null, error: result.error }, null, 2));
486
+ else console.error(result.error);
487
+ process.exit(2);
488
+ }
489
+ if (result.lintBlocked) printLintBlocked(result, json, "no packet written");
490
+ if (json) console.log(JSON.stringify(result, null, 2));
491
+ else console.log(`${result.path}\n${result.summary}`);
492
+ process.exit(result.code);
493
+ }
494
+
495
+ if (sub === "record") {
496
+ const parsed = parseArgs(argv.slice(2), { valueFlags: ["--verdict"], boolFlags: ["--json"] });
497
+ if (parsed.error) { console.error(parsed.error); process.exit(2); }
498
+ const [packetPath] = parsed.positionals;
499
+ const json = parsed.values["--json"];
500
+ const result = recordLearn(packetPath, parsed.values["--verdict"]);
501
+ if (result.usage) {
502
+ if (json) console.log(JSON.stringify({ packet: packetPath ?? null, verdict: parsed.values["--verdict"] ?? null, error: result.error }, null, 2));
503
+ else console.error(result.error);
504
+ process.exit(2);
505
+ }
506
+ if (json) console.log(JSON.stringify(result, null, 2));
507
+ else if (result.invalid) {
508
+ console.log(`learn: ${result.stale ? "stale packet" : "invalid verdict"}, nothing recorded`);
509
+ for (const f of result.findings) printFinding(f);
510
+ } else {
511
+ console.log(`learn: ${result.edits} edit${result.edits === 1 ? "" : "s"} classified`);
512
+ for (const [block, c] of Object.entries(result.tally)) console.log(` ${block} ${tallyLine(c)}`);
513
+ console.log(`next move: ${result.next}`);
514
+ if (result.verdict) console.log(`verdict: ${result.verdict} (${result.reason})`);
515
+ if (result.ledgerWarning) console.log(`warn: ${result.ledgerWarning}`);
516
+ }
517
+ process.exit(result.code);
518
+ }
519
+
520
+ console.error(`unknown learn subcommand: ${sub ?? "(none)"}\n\n${HELP}`);
521
+ process.exit(2);
522
+ }
523
+
524
+ // A spec that does not lint clean: print what lint would, say nothing was written, and exit with
525
+ // lint's own code. Shared by judge prepare and learn prepare.
526
+ function printLintBlocked(result, json, nothingWritten) {
527
+ if (json) console.log(JSON.stringify(result, null, 2));
528
+ else {
529
+ const r = result.lintScore;
530
+ console.log(`${result.specPath}: ${r.status} (${r.passed}/9)${r.open.length ? `, open: ${r.open.join(", ")}` : ""}`);
531
+ for (const t of r.tests) if (!t.pass) console.log(` ✗ ${t.n}. ${t.name}`);
532
+ for (const f of result.lintFindings) console.log(` ${f.severity === "fail" ? "fail" : "warn"} [${f.test}] ${f.message}\n fix: ${f.fix}`);
533
+ console.log(`${nothingWritten}: the spec is not ready (run \`hyperspec lint\` on it for details)`);
534
+ }
535
+ process.exit(result.code);
536
+ }
537
+
354
538
  // Generic flag/positional parser for the recipe verbs below. A value-taking flag (valueFlags,
355
539
  // repeatableFlags) never swallows a following --flag as its value (missing value is an error, not
356
540
  // a silent grab); a bool flag never eats the next token as a positional; any --flag not declared
File without changes
@@ -0,0 +1 @@
1
+ Mix the flour and the water with your hand until no dry flour is left. It will look wrong. It is supposed to look wrong. Leave it for half an hour and come back.
@@ -0,0 +1,9 @@
1
+ Course brief, written by the author after teaching the class twice.
2
+
3
+ Nobody who has never baked needs a recipe first. They need four words they do not have yet: dough, starter, proof and crumb. Every class that went badly went badly because I used one of those words before I had said what it meant.
4
+
5
+ The order matters more than the recipes. Water and flour first, then what makes it rise, then shaping, then the oven. Each lesson should stand only on the ones before it.
6
+
7
+ Each lesson ends with one thing to do in a real kitchen, because nobody learns bread by reading about it.
8
+
9
+ I once taught shaping before proofing and half the room shaped dough that had not risen. I never did it again.
@@ -0,0 +1,6 @@
1
+ {"material":"brief","path":"course/materials/brief.md","sha256":"7fc69cebae3218131d8e920af1408b42505ebfe2ccb4f72e9eb9c9ac9fb48b24"}
2
+ {"id":"s1","start":0,"end":67,"label":"aside","text":"Course brief, written by the author after teaching the class twice."}
3
+ {"id":"s2","start":69,"end":299,"label":"claim","own":true,"text":"Nobody who has never baked needs a recipe first. They need four words they do not have yet: dough, starter, proof and crumb. Every class that went badly went badly because I used one of those words before I had said what it meant."}
4
+ {"id":"s3","start":301,"end":471,"label":"claim","own":true,"text":"The order matters more than the recipes. Water and flour first, then what makes it rise, then shaping, then the oven. Each lesson should stand only on the ones before it."}
5
+ {"id":"s4","start":473,"end":578,"label":"stance","text":"Each lesson ends with one thing to do in a real kitchen, because nobody learns bread by reading about it."}
6
+ {"id":"s5","start":580,"end":690,"label":"story","teller":"example-author","text":"I once taught shaping before proofing and half the room shaped dough that had not risen. I never did it again."}
@@ -0,0 +1,11 @@
1
+ # Bread from zero: the outline
2
+
3
+ ## Part 1: Dough
4
+
5
+ 1. **Flour and water.** What happens when the two meet. *Terms: dough, hydration.*
6
+ 2. **What makes it rise.** A living culture you keep in a jar. *Terms: starter, proof.*
7
+
8
+ ## Part 2: The bake
9
+
10
+ 3. **Shaping.** Turning a slack mass into a loaf that holds itself up. *Terms: bench rest, surface tension.*
11
+ 4. **The oven.** What heat does in the first ten minutes, and how to read the inside. *Terms: oven spring, crumb.*
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: "Bread from zero, Part 1: Dough"
3
+ ---
4
+
5
+ # Bread from zero
6
+
7
+ ## Part 1: Dough
8
+
9
+ This course assumes you have never baked a loaf. Every word it needs is defined in the lesson that first uses it.
10
+
11
+ ---
12
+
13
+ ## Lesson 1: Flour and water
14
+
15
+ **After this lesson you can:** mix a dough and say how wet it is.
16
+
17
+ **New terms:**
18
+ - **Dough:** flour and water mixed until no dry flour is left.
19
+ - **Hydration:** how much water a dough holds for its flour, as a share of the flour's weight. Five hundred grams of flour and three hundred and fifty of water is seventy percent.
20
+
21
+ Put five hundred grams of flour in a bowl and pour in three hundred and fifty grams of water. Mix with your hand until nothing dry is left. That is a dough.
22
+
23
+ It will look wrong: shaggy, sticky, nothing like bread. Leave it covered for half an hour. The water is still working its way into the flour, and when you come back the dough will be smoother without your having done anything.
24
+
25
+ The higher the hydration, the stickier the dough and the more open the bread. Seventy percent is a forgiving place to start. Lesson 4 shows you what hydration does inside a finished loaf.
26
+
27
+ **Try this:** mix one dough at sixty percent and one at seventy-five, and press a finger into each after half an hour.
28
+
29
+ ---
30
+
31
+ ## Lesson 2: What makes it rise
32
+
33
+ **After this lesson you can:** keep a starter alive and tell when a dough has risen enough.
34
+
35
+ **New terms:**
36
+ - **Starter:** flour and water left to ferment, kept in a jar and fed every day. It is what makes the dough rise.
37
+ - **Proof:** the rise itself, the hours a dough spends growing before it is shaped and baked.
38
+
39
+ Mix fifty grams of flour and fifty of water in a jar, loosely covered. Feed it the same again every day. In about a week it will double within hours of a feed and smell sour. It is alive, and it is ready.
40
+
41
+ Add a spoon of starter to the dough from Lesson 1 and leave it somewhere warm. The dough is now proofing. It is done when it has grown by half and a finger pressed into it leaves a dent that fills back slowly.
42
+
43
+ **Try this:** start a starter today, and mark the jar with tape at its height after each feed.
44
+
45
+ ---
46
+
47
+ **Next, Part 2: The bake.** Shaping a loaf that holds itself up, and what happens to the crumb in the oven.
@@ -0,0 +1,40 @@
1
+ ---
2
+ title: "Bread from zero, Part 2: The bake"
3
+ ---
4
+
5
+ # Bread from zero
6
+
7
+ ## Part 2: The bake
8
+
9
+ Part 1 made a dough and made it rise. This part turns it into bread.
10
+
11
+ ---
12
+
13
+ ## Lesson 3: Shaping
14
+
15
+ **After this lesson you can:** shape a proofed dough into a loaf that holds itself up.
16
+
17
+ **New terms:**
18
+ - **Bench rest:** twenty minutes a dough sits on the counter between a rough shape and the final one, so it relaxes enough to be shaped again.
19
+ - **Surface tension:** the tight skin you pull across the top of a loaf, which is what lets it stand instead of spreading.
20
+ - **Dough** (from Lesson 1): flour and water mixed until no dry flour is left.
21
+
22
+ Tip the proofed dough onto the counter and fold it into a rough ball. Give it a bench rest. Then flip it, fold the far edge to the middle, and roll it toward you, pulling the top tight as you go. That tightness is surface tension, and a loaf without it spreads in the oven.
23
+
24
+ **Try this:** shape two loaves, one pulled tight and one left loose, and bake them side by side after Lesson 4.
25
+
26
+ ---
27
+
28
+ ## Lesson 4: The oven
29
+
30
+ **After this lesson you can:** bake a loaf and read what its inside tells you.
31
+
32
+ **New terms:**
33
+ - **Oven spring:** the last rise a loaf takes in the first ten minutes of heat, before the crust sets.
34
+ - **Crumb:** the inside of a baked loaf: the holes, their size, and how they are spread.
35
+
36
+ Heat the oven as hot as it goes, with a heavy pot inside. Put the loaf in the pot, cover it, and bake for twenty minutes, then uncover it and bake until it is dark. The covered minutes are the oven spring: steam keeps the crust soft while the loaf grows.
37
+
38
+ Let it cool for an hour before you cut it. Then read the crumb. Big uneven holes mean a well proofed, high hydration dough. A tight, even crumb with a dense band at the bottom means the proof was too short.
39
+
40
+ **Try this:** cut the two loaves from Lesson 3 and compare their crumb.
File without changes
@@ -0,0 +1,205 @@
1
+ ---
2
+ hyperspec: "0.1"
3
+ title: "Bread from zero: a four-lesson course"
4
+ kind: course
5
+ profile: writing
6
+ decisions:
7
+ - id: kind
8
+ state: decided
9
+ value: a course of four lessons in two parts, for someone who has never baked a loaf
10
+ source: course/materials/brief.md
11
+ author: example-author
12
+ chosen_by: human
13
+ - id: lesson-shape
14
+ state: decided
15
+ value: every lesson opens with what the reader can do after it and the terms it defines, and ends with one thing to do in a kitchen
16
+ source: course/materials/brief.md, the paragraph on ending each lesson
17
+ author: example-author
18
+ chosen_by: human
19
+ - id: part-files
20
+ state: delegated
21
+ rule: one file per part, named part-<n>.md, so a new part is picked up by the files pattern without editing the spec
22
+ source: course layout
23
+ author: agent:claude
24
+ chosen_by: agent
25
+ requirements:
26
+ - id: r1
27
+ text: no lesson uses a term before the lesson that defines it
28
+ fails_when: the sequence station reports a term used before it is defined
29
+ check:
30
+ station: sequence station, order guard
31
+ source: course/materials/brief.md
32
+ author: example-author
33
+ - id: r2
34
+ text: every term is defined in exactly one lesson
35
+ fails_when: the sequence station reports a term defined twice
36
+ check:
37
+ station: sequence station, defined-once guard
38
+ source: lesson-shape decision
39
+ author: example-author
40
+ - id: r3
41
+ text: every lesson carries its three sections
42
+ fails_when: a lesson has no "After this lesson you can", "New terms" or "Try this" section
43
+ check:
44
+ station: sequence station, sections guard
45
+ source: lesson-shape decision
46
+ author: example-author
47
+ - id: r4
48
+ text: each lesson defines the terms the outline promises for it
49
+ fails_when: the outline promises a term in a lesson that does not define it
50
+ check:
51
+ station: sequence station, outline guard
52
+ source: course/outline.md
53
+ author: example-author
54
+ - id: r5
55
+ text: every Try this can be done in a home kitchen in one day, apart from the starter, which takes a week
56
+ fails_when: a Try this needs equipment beyond a bowl, a scale, a jar, an oven and a heavy pot
57
+ check:
58
+ rubric: list what each Try this needs; fail on anything outside that list
59
+ source: course/materials/brief.md
60
+ author: example-author
61
+ rejects:
62
+ - a recipe before the reader has the words to follow it
63
+ - a term used before the lesson that defines it
64
+ examples:
65
+ - path: course/goldens/lesson.md
66
+ why: an instruction, then the reader's likely worry named plainly, then what to do next
67
+ resume:
68
+ next_action: bake the course's two loaves from Lesson 3 and photograph their crumb for Lesson 4
69
+ feedback:
70
+ issues: https://github.com/SupersuitUp/hyperspec/issues
71
+ fork: MIT; fork it for your own purposes
72
+ improvement:
73
+ ledger: course/runs.jsonl
74
+ writing:
75
+ materials:
76
+ items:
77
+ - id: brief
78
+ path: course/materials/brief.md
79
+ segments: course/materials/brief.md.segments.jsonl
80
+ produced_by: example-author
81
+ captured: "2026-09-20"
82
+ how: written after teaching the class twice
83
+ trust: considered
84
+ check:
85
+ station: every segment of every material carries a label from the closed set, matches its source verbatim, and the markings are current
86
+ source: capture step
87
+ author: agent:claude
88
+ dna:
89
+ writer: example-author
90
+ scope:
91
+ form: course
92
+ audience: first-time bakers
93
+ purpose: teach
94
+ rules: style-rules.md
95
+ goldens:
96
+ - path: course/goldens/lesson.md
97
+ why: an instruction, then the reader's likely worry named plainly, then what to do next
98
+ check:
99
+ rubric: blind lineup within this scope
100
+ source: goldens marked on the review page
101
+ author: example-author
102
+ persona:
103
+ identity: self
104
+ stance: guide
105
+ may_assert:
106
+ - what the author saw go wrong when teaching the class
107
+ will_not_say:
108
+ - a bake time or temperature for an oven the author has not used
109
+ facts_from: sources
110
+ check:
111
+ rubric: persona-consistency judge
112
+ source: persona interview
113
+ author: example-author
114
+ audience:
115
+ who: someone who has never baked a loaf of bread
116
+ funnel_now: has bought flour and has never used it for bread
117
+ knows:
118
+ - flour
119
+ - oven
120
+ believes_now: bread takes a recipe and a lot of skill
121
+ wants: to bake one good loaf
122
+ reads_on: a tablet propped up in the kitchen
123
+ reader: person
124
+ check:
125
+ station: the sequence station defines every term before it is used
126
+ rubric: simulated reader reports where it got lost
127
+ source: audience interview
128
+ author: example-author
129
+ goal:
130
+ from: has never baked bread
131
+ to: bakes a loaf and reads its crumb
132
+ next_if_worked: starts a starter the day they finish Lesson 2
133
+ change:
134
+ kind: action
135
+ text: the reader bakes their first loaf
136
+ conditions: [r1, r2, r3, r4, r5]
137
+ check:
138
+ rubric: the doctor grades the course against every condition
139
+ source: goal interview
140
+ author: example-author
141
+ form:
142
+ name: course
143
+ length:
144
+ min: 400
145
+ max: 1500
146
+ unit: words
147
+ required_parts:
148
+ - "Part 1: Dough"
149
+ - "Part 2: The bake"
150
+ stations:
151
+ - the sequence station
152
+ sequence:
153
+ unit: Lesson
154
+ files:
155
+ - course/part-*.md
156
+ sections:
157
+ - After this lesson you can
158
+ - New terms
159
+ - Try this
160
+ terms_section: New terms
161
+ outline: course/outline.md
162
+ teaser: Next,
163
+ check:
164
+ station: structure and length, then the sequence station
165
+ source: form decision
166
+ author: example-author
167
+ spine:
168
+ kind: primer
169
+ claims:
170
+ - id: c1
171
+ text: a first-time baker needs four words before any recipe
172
+ materials: [brief#s2]
173
+ - id: c2
174
+ text: the order is water and flour, then the rise, then shaping, then the oven
175
+ materials: [brief#s3]
176
+ - id: c3
177
+ text: each lesson ends with something to do in a real kitchen
178
+ materials: [brief#s4, brief#s5]
179
+ check:
180
+ rubric: each claim lands, in order
181
+ source: spine interview
182
+ author: example-author
183
+ sources:
184
+ ledger: course/claims.jsonl
185
+ unsourced_claim: fail
186
+ check:
187
+ station: every factual claim in the ledger points at a source span
188
+ source: sourcing pass
189
+ author: agent:claude
190
+ fiction: false
191
+ ---
192
+
193
+ # Bread from zero
194
+
195
+ A worked example of a sequential work: a course of four lessons in two parts, one file per part.
196
+ The spec lists the parts as `course/part-*.md`, so `check` needs no `--draft`: the parts, joined
197
+ in order, are the draft.
198
+
199
+ ```bash
200
+ npx @supersuit/hyperspec check course.hyperspec.md
201
+ ```
202
+
203
+ The `sequence` station holds the course to what a reader of Lesson 3 depends on: Lessons 1 and 2
204
+ defined every word it uses. The outline in `course/outline.md` promises the terms each lesson
205
+ defines, and the station checks the lessons keep that promise.