@supersuit/hyperspec 0.7.0 → 0.9.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 (45) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +76 -9
  3. package/SPEC.md +2 -2
  4. package/WRITING.md +463 -18
  5. package/bin/hyperspec.mjs +137 -6
  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 +64 -0
  12. package/examples/writing/course/part-2.md +57 -0
  13. package/examples/writing/course/runs.jsonl +0 -0
  14. package/examples/writing/course.hyperspec.md +206 -0
  15. package/examples/writing/essay/judge/panel-buyer.packet.json +118 -0
  16. package/examples/writing/essay/judge/panel-expert.packet.json +114 -0
  17. package/examples/writing/essay/judge/panel-novice.packet.json +114 -0
  18. package/examples/writing/essay/judge/panel-skeptic.packet.json +114 -0
  19. package/examples/writing/essay/sample-verdicts/panel-buyer.verdict.json +11 -0
  20. package/examples/writing/essay/sample-verdicts/panel-expert.verdict.json +13 -0
  21. package/examples/writing/essay/sample-verdicts/panel-novice.verdict.json +11 -0
  22. package/examples/writing/essay/sample-verdicts/panel-skeptic.verdict.json +13 -0
  23. package/examples/writing/story/judge/panel-buyer.packet.json +118 -0
  24. package/examples/writing/story/judge/panel-expert.packet.json +114 -0
  25. package/examples/writing/story/judge/panel-novice.packet.json +114 -0
  26. package/examples/writing/story/judge/panel-skeptic.packet.json +114 -0
  27. package/examples/writing/story/sample-verdicts/panel-buyer.verdict.json +13 -0
  28. package/examples/writing/story/sample-verdicts/panel-expert.verdict.json +11 -0
  29. package/examples/writing/story/sample-verdicts/panel-novice.verdict.json +11 -0
  30. package/examples/writing/story/sample-verdicts/panel-skeptic.verdict.json +11 -0
  31. package/package.json +1 -1
  32. package/src/check.mjs +25 -7
  33. package/src/evidence.mjs +74 -0
  34. package/src/judge.mjs +50 -66
  35. package/src/judges/index.mjs +7 -1
  36. package/src/judges/panel.mjs +135 -0
  37. package/src/sequence-draft.mjs +75 -0
  38. package/src/stations/index.mjs +5 -1
  39. package/src/stations/links.mjs +11 -3
  40. package/src/stations/quotes.mjs +26 -2
  41. package/src/stations/sequence.mjs +388 -0
  42. package/src/stations/triage.mjs +20 -0
  43. package/src/triage.mjs +401 -0
  44. package/src/writing-fields.mjs +81 -0
  45. package/src/writing.mjs +5 -1
package/bin/hyperspec.mjs CHANGED
@@ -19,13 +19,17 @@ import { str } from "../src/placeholder.mjs";
19
19
  import { runCheck } from "../src/check.mjs";
20
20
  import { prepareJudges, recordJudgment } from "../src/judge.mjs";
21
21
  import { prepareLearn, recordLearn, tallyLine } from "../src/learn.mjs";
22
+ import { triageContext, triageState, answerFinding, importReview, replyText } from "../src/triage.mjs";
23
+ import { sourceAt } from "../src/sequence-draft.mjs";
22
24
 
23
25
  const HELP = `hyperspec <command> [options]
24
26
 
25
27
  lint <file...> [--json] score each hyperspec against the nine tests
26
28
  exit 0 pass, 1 a test fails, 3 blocked on an open decision, 2 usage
27
- check <spec> --draft <file> [--json] [--only a,b]
28
- needs a writing spec (profile: writing); lints it first (a spec
29
+ check <spec> [--draft <file>] [--json] [--only a,b]
30
+ needs a writing spec (profile: writing), and --draft unless the
31
+ spec lists writing.form.sequence.files, whose files, joined in
32
+ reading order, are then the draft; lints it first (a spec
29
33
  that does not pass lint, or is blocked, exits with lint's own code
30
34
  and runs no station: a draft cannot be checked against a spec that
31
35
  is not ready); then runs every deterministic station (or the
@@ -49,7 +53,10 @@ const HELP = `hyperspec <command> [options]
49
53
  persona (against the claims ledger), and for fiction attribution (a
50
54
  blind speaker test on the dialogue lines whose speaker the draft
51
55
  names; the answers go to attribution.key.json, likewise rebuilt)
52
- and knowledge (each character's knowledge timeline);
56
+ and knowledge (each character's knowledge timeline), and panel
57
+ (one panel-<reader>.packet.json per reader: writing.panel's, or a
58
+ skeptic, a novice and an expert, and always the audience's reader
59
+ as buyer);
53
60
  the same spec and draft give byte-identical packets; refuses to
54
61
  overwrite an existing packet without --force
55
62
  exit 0 written, 1 a station could not build its packet (the rest
@@ -63,7 +70,28 @@ const HELP = `hyperspec <command> [options]
63
70
  verdict appends nothing; run it from the folder prepare ran in, since
64
71
  the packet keeps the spec and draft paths as they were given
65
72
  exit 0 the station passed, 1 it failed or the verdict is invalid
66
- or stale, 2 usage
73
+ or stale, 2 usage; a panel verdict's improve, missing and remove
74
+ items go to triage.jsonl, beside the runs ledger
75
+ triage status <spec> [--draft <file>] [--json]
76
+ count the answers in the spec's triage.jsonl, list the passages two
77
+ or more readers share, and hold every answer to the draft (as
78
+ check's triage station does); without --draft a spec that lists
79
+ writing.form.sequence.files reads them, as check does
80
+ exit 0 it would pass, 1 it would fail, 2 usage
81
+ triage answer <spec> <finding> taken|kept|already-true|open [--evidence S] [--reason S]
82
+ [--draft <file>] [--json]
83
+ answer one finding: taken and already-true need --evidence, a
84
+ passage of the draft word for word; kept needs --reason
85
+ exit 0 written, 1 refused (nothing written), 2 usage
86
+ triage import <spec> <review> [--source S] [--draft <file>] [--json]
87
+ bring an outside review (markdown or plain text) in as findings;
88
+ a heading names the reader, Good/Improve/Missing/Remove labels set
89
+ the kind, each list item is a finding, praise is counted only
90
+ exit 0 imported, 1 the review holds nothing to answer, 2 usage
91
+ triage reply <spec> [--source S] [--draft <file>] [--json]
92
+ print a plain-text reply to the reviewer from the answers; it
93
+ never sends anything
94
+ exit 0 ready to send, 1 a finding is still unanswered, 2 usage
67
95
  learn prepare <spec> --first <draft> --approved <draft> --out <dir> [--force] [--json]
68
96
  needs a writing spec that passes lint (exits with lint's own code
69
97
  otherwise); diffs the first draft a factory produced against the
@@ -358,7 +386,6 @@ if (cmd === "check") {
358
386
  process.exit(2);
359
387
  };
360
388
  if (!specPath) usage("check needs a spec path");
361
- if (!parsed.values["--draft"]) usage("check needs --draft <file>");
362
389
  let only;
363
390
  if (parsed.values["--only"] !== undefined) {
364
391
  only = parsed.values["--only"].split(",").map((s) => s.trim()).filter(Boolean);
@@ -389,7 +416,7 @@ if (cmd === "check") {
389
416
  for (const s of result.stations) {
390
417
  if (s.status === "skip") { console.log(`${s.station}: skip (${s.reason})`); continue; }
391
418
  console.log(`${s.station}: ${s.status}`);
392
- 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}`);
419
+ 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}`);
393
420
  }
394
421
  if (result.verdict) {
395
422
  const detail = result.verdictDetail.change ?? result.verdictDetail.reason;
@@ -520,6 +547,110 @@ if (cmd === "learn") {
520
547
  process.exit(2);
521
548
  }
522
549
 
550
+ if (cmd === "triage") {
551
+ const sub = argv[1];
552
+ const flags = {
553
+ status: { valueFlags: ["--draft"], boolFlags: ["--json"] },
554
+ answer: { valueFlags: ["--draft", "--evidence", "--reason"], boolFlags: ["--json"] },
555
+ import: { valueFlags: ["--draft", "--source"], boolFlags: ["--json"] },
556
+ reply: { valueFlags: ["--draft", "--source"], boolFlags: ["--json"] },
557
+ }[sub];
558
+ if (!flags) { console.error(`unknown triage subcommand: ${sub ?? "(none)"}\n\n${HELP}`); process.exit(2); }
559
+ const parsed = parseArgs(argv.slice(2), flags);
560
+ if (parsed.error) { console.error(parsed.error); process.exit(2); }
561
+ const [specPath, ...rest] = parsed.positionals;
562
+ const json = parsed.values["--json"];
563
+ const usage = (error) => {
564
+ if (json) console.log(JSON.stringify({ spec: specPath ?? null, draft: parsed.values["--draft"] ?? null, error }, null, 2));
565
+ else console.error(error);
566
+ process.exit(2);
567
+ };
568
+ const ctx = triageContext(specPath, parsed.values["--draft"], `triage ${sub}`);
569
+ if (ctx.usage) usage(ctx.error);
570
+ const { spec, draft } = ctx;
571
+ // A finding's line, named the way check names it: the file and its own line for a sequence.
572
+ const where = (f) => {
573
+ if (typeof f.line !== "number") return "";
574
+ const src = sourceAt(draft, f.line);
575
+ return src ? ` (${src.file} line ${f.line - src.startLine + 1})` : ` (line ${f.line})`;
576
+ };
577
+ const printFinding = (f) => console.log(` ${f.severity === "fail" ? "fail" : "warn"} [${f.id}] ${f.message}${where(f)}\n fix: ${f.fix}`);
578
+
579
+ if (sub === "status") {
580
+ const state = triageState(spec, draft);
581
+ if (state.skip) {
582
+ if (json) console.log(JSON.stringify({ spec: specPath, status: "skip", reason: state.skip }, null, 2));
583
+ else console.log(`triage: skip (${state.skip})`);
584
+ process.exit(0);
585
+ }
586
+ const status = state.findings.some((f) => f.severity === "fail") ? "fail" : "pass";
587
+ if (json) console.log(JSON.stringify({ spec: specPath, path: state.decl, status, counts: state.counts, shared: state.shared, findings: state.findings, items: state.items }, null, 2));
588
+ else {
589
+ const c = state.counts;
590
+ console.log(`${state.decl}: ${state.items.length} finding${state.items.length === 1 ? "" : "s"}; ${c.taken} taken, ${c.kept} kept, ${c["already-true"]} already true, ${c.open} open, ${c.unanswered} not answered`);
591
+ if (state.shared.length) {
592
+ console.log("shared by two or more readers:");
593
+ for (const g of state.shared) {
594
+ const line = where({ line: g.line }).replace(/^ \((.*)\)$/, "$1");
595
+ console.log(` ${line}: "${g.passage}"`);
596
+ for (const f of g.findings) console.log(` ${f.reader} (${f.kind}): ${f.text}`);
597
+ }
598
+ }
599
+ console.log(`triage: ${status}`);
600
+ for (const f of state.findings) printFinding(f);
601
+ }
602
+ process.exit(status === "pass" ? 0 : 1);
603
+ }
604
+
605
+ if (sub === "answer") {
606
+ const [findingId, disposition] = rest;
607
+ if (!findingId) usage("triage answer needs a finding id");
608
+ if (!disposition) usage("triage answer needs a disposition: taken, kept, already-true or open");
609
+ const result = answerFinding(spec, draft, findingId, disposition, { evidence: parsed.values["--evidence"], reason: parsed.values["--reason"] });
610
+ if (result.usage) usage(result.error);
611
+ if (json) console.log(JSON.stringify(result, null, 2));
612
+ else if (result.invalid) {
613
+ console.log(`${findingId}: answer refused, nothing written`);
614
+ for (const f of result.findings) printFinding(f);
615
+ } else console.log(`${findingId}: ${disposition} (${result.path})`);
616
+ process.exit(result.code);
617
+ }
618
+
619
+ if (sub === "import") {
620
+ const [reviewPath] = rest;
621
+ if (!reviewPath) usage("triage import needs a review file");
622
+ let text;
623
+ try { text = readFileSync(resolve(reviewPath), "utf8"); } catch { usage(`cannot read review: ${reviewPath}`); }
624
+ const source = parsed.values["--source"] ?? reviewPath;
625
+ const result = importReview(spec, draft, text, source);
626
+ if (result.usage) usage(result.error);
627
+ if (json) console.log(JSON.stringify(result, null, 2));
628
+ else if (result.invalid) {
629
+ console.log(`${reviewPath}: nothing imported`);
630
+ for (const f of result.findings) printFinding(f);
631
+ } else {
632
+ console.log(`${source}: ${result.added} finding${result.added === 1 ? "" : "s"} added to triage (${result.path})${result.already ? `, ${result.already} already there` : ""}${result.praise ? `; ${result.praise} item${result.praise === 1 ? "" : "s"} of praise, not triaged` : ""}`);
633
+ for (const f of result.warnings) printFinding(f);
634
+ }
635
+ process.exit(result.code);
636
+ }
637
+
638
+ if (sub === "reply") {
639
+ const state = triageState(spec, draft);
640
+ if (state.skip) usage(`nothing to reply to: ${state.skip}`);
641
+ const source = parsed.values["--source"];
642
+ const reply = replyText(state.items, source);
643
+ if (!reply.count) usage(`no finding in ${state.decl} comes from ${source}`);
644
+ const failing = state.findings.filter((f) => f.severity === "fail");
645
+ if (json) console.log(JSON.stringify({ spec: specPath, source: source ?? null, text: reply.text, unanswered: reply.unanswered, ready: !failing.length }, null, 2));
646
+ else {
647
+ process.stdout.write(reply.text);
648
+ if (failing.length) console.error(`not ready to send: triage fails (${failing.length} finding${failing.length === 1 ? "" : "s"}); run hyperspec triage status`);
649
+ }
650
+ process.exit(failing.length ? 1 : 0);
651
+ }
652
+ }
653
+
523
654
  // A spec that does not lint clean: print what lint would, say nothing was written, and exit with
524
655
  // lint's own code. Shared by judge prepare and learn prepare.
525
656
  function printLintBlocked(result, json, nothingWritten) {
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,64 @@
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
+ ## Check yourself: Part 1
46
+
47
+ 1. *(Lesson 1)* Five hundred grams of flour and four hundred of water: what is the hydration?
48
+ - a) Forty percent
49
+ - b) Eighty percent
50
+ 2. *(Lesson 1)* When is flour and water a dough?
51
+ - a) When no dry flour is left
52
+ - b) When it has doubled
53
+ 3. *(Lesson 2)* What does a starter need every day?
54
+ - a) A pinch of salt
55
+ - b) A feed of flour and water
56
+ 4. *(Lesson 2)* How do you tell a proof is done?
57
+ - a) A finger dent fills back slowly
58
+ - b) The top has cracked
59
+
60
+ **Answers:** 1 b · 2 a · 3 b · 4 a
61
+
62
+ ---
63
+
64
+ **Next, Part 2: The bake.** Shaping a loaf that holds itself up, and what happens to the crumb in the oven.
@@ -0,0 +1,57 @@
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.
41
+
42
+ ## Check yourself: Part 2
43
+
44
+ 1. *(Lesson 3)* Why give a dough a bench rest?
45
+ - a) So it relaxes enough to be shaped again
46
+ - b) So it cools down
47
+ 2. *(Lesson 3)* What lets a shaped loaf stand instead of spreading?
48
+ - a) More water
49
+ - b) Surface tension
50
+ 3. *(Lesson 4)* When does oven spring happen?
51
+ - a) In the first ten minutes of heat
52
+ - b) While the loaf cools
53
+ 4. *(Lesson 4)* A tight, even crumb with a dense band at the bottom means what?
54
+ - a) The oven was too hot
55
+ - b) The proof was too short
56
+
57
+ **Answers:** 1 a · 2 b · 3 a · 4 b
File without changes
@@ -0,0 +1,206 @@
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
+ quiz: Check yourself
164
+ check:
165
+ station: structure and length, then the sequence station
166
+ source: form decision
167
+ author: example-author
168
+ spine:
169
+ kind: primer
170
+ claims:
171
+ - id: c1
172
+ text: a first-time baker needs four words before any recipe
173
+ materials: [brief#s2]
174
+ - id: c2
175
+ text: the order is water and flour, then the rise, then shaping, then the oven
176
+ materials: [brief#s3]
177
+ - id: c3
178
+ text: each lesson ends with something to do in a real kitchen
179
+ materials: [brief#s4, brief#s5]
180
+ check:
181
+ rubric: each claim lands, in order
182
+ source: spine interview
183
+ author: example-author
184
+ sources:
185
+ ledger: course/claims.jsonl
186
+ unsourced_claim: fail
187
+ check:
188
+ station: every factual claim in the ledger points at a source span
189
+ source: sourcing pass
190
+ author: agent:claude
191
+ fiction: false
192
+ ---
193
+
194
+ # Bread from zero
195
+
196
+ A worked example of a sequential work: a course of four lessons in two parts, one file per part.
197
+ The spec lists the parts as `course/part-*.md`, so `check` needs no `--draft`: the parts, joined
198
+ in order, are the draft.
199
+
200
+ ```bash
201
+ npx @supersuit/hyperspec check course.hyperspec.md
202
+ ```
203
+
204
+ The `sequence` station holds the course to what a reader of Lesson 3 depends on: Lessons 1 and 2
205
+ defined every word it uses. The outline in `course/outline.md` promises the terms each lesson
206
+ defines, and the station checks the lessons keep that promise.