@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.
- package/CHANGELOG.md +93 -0
- package/README.md +76 -9
- package/SPEC.md +2 -2
- package/WRITING.md +463 -18
- package/bin/hyperspec.mjs +137 -6
- package/examples/writing/course/claims.jsonl +0 -0
- package/examples/writing/course/goldens/lesson.md +1 -0
- package/examples/writing/course/materials/brief.md +9 -0
- package/examples/writing/course/materials/brief.md.segments.jsonl +6 -0
- package/examples/writing/course/outline.md +11 -0
- package/examples/writing/course/part-1.md +64 -0
- package/examples/writing/course/part-2.md +57 -0
- package/examples/writing/course/runs.jsonl +0 -0
- package/examples/writing/course.hyperspec.md +206 -0
- package/examples/writing/essay/judge/panel-buyer.packet.json +118 -0
- package/examples/writing/essay/judge/panel-expert.packet.json +114 -0
- package/examples/writing/essay/judge/panel-novice.packet.json +114 -0
- package/examples/writing/essay/judge/panel-skeptic.packet.json +114 -0
- package/examples/writing/essay/sample-verdicts/panel-buyer.verdict.json +11 -0
- package/examples/writing/essay/sample-verdicts/panel-expert.verdict.json +13 -0
- package/examples/writing/essay/sample-verdicts/panel-novice.verdict.json +11 -0
- package/examples/writing/essay/sample-verdicts/panel-skeptic.verdict.json +13 -0
- package/examples/writing/story/judge/panel-buyer.packet.json +118 -0
- package/examples/writing/story/judge/panel-expert.packet.json +114 -0
- package/examples/writing/story/judge/panel-novice.packet.json +114 -0
- package/examples/writing/story/judge/panel-skeptic.packet.json +114 -0
- package/examples/writing/story/sample-verdicts/panel-buyer.verdict.json +13 -0
- package/examples/writing/story/sample-verdicts/panel-expert.verdict.json +11 -0
- package/examples/writing/story/sample-verdicts/panel-novice.verdict.json +11 -0
- package/examples/writing/story/sample-verdicts/panel-skeptic.verdict.json +11 -0
- package/package.json +1 -1
- package/src/check.mjs +25 -7
- package/src/evidence.mjs +74 -0
- package/src/judge.mjs +50 -66
- package/src/judges/index.mjs +7 -1
- package/src/judges/panel.mjs +135 -0
- package/src/sequence-draft.mjs +75 -0
- package/src/stations/index.mjs +5 -1
- package/src/stations/links.mjs +11 -3
- package/src/stations/quotes.mjs +26 -2
- package/src/stations/sequence.mjs +388 -0
- package/src/stations/triage.mjs +20 -0
- package/src/triage.mjs +401 -0
- package/src/writing-fields.mjs +81 -0
- 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)
|
|
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.
|