@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.
- package/CHANGELOG.md +127 -0
- package/README.md +71 -8
- package/SPEC.md +2 -2
- package/WRITING.md +680 -21
- package/bin/hyperspec.mjs +188 -4
- 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 +47 -0
- package/examples/writing/course/part-2.md +40 -0
- package/examples/writing/course/runs.jsonl +0 -0
- package/examples/writing/course.hyperspec.md +205 -0
- package/examples/writing/essay/judge/doctor.packet.json +108 -0
- package/examples/writing/essay/judge/lineup.packet.json +64 -0
- package/examples/writing/essay/judge/persona.packet.json +73 -0
- package/examples/writing/essay/judge/reader.packet.json +93 -0
- package/examples/writing/essay/learn/first-draft.md +84 -0
- package/examples/writing/essay/learn/learn.packet.json +106 -0
- package/examples/writing/essay/sample-verdicts/doctor.verdict.json +43 -0
- package/examples/writing/essay/sample-verdicts/learn.verdict.json +30 -0
- package/examples/writing/essay/sample-verdicts/lineup.verdict.json +6 -0
- package/examples/writing/essay/sample-verdicts/persona.verdict.json +4 -0
- package/examples/writing/essay/sample-verdicts/reader.verdict.json +7 -0
- package/examples/writing/essay.hyperspec.md +6 -1
- package/examples/writing/story/judge/attribution.packet.json +194 -0
- package/examples/writing/story/judge/doctor.packet.json +108 -0
- package/examples/writing/story/judge/knowledge.packet.json +77 -0
- package/examples/writing/story/judge/persona.packet.json +73 -0
- package/examples/writing/story/judge/reader.packet.json +94 -0
- package/examples/writing/story/sample-verdicts/attribution.verdict.json +81 -0
- package/examples/writing/story/sample-verdicts/doctor.verdict.json +43 -0
- package/examples/writing/story/sample-verdicts/knowledge.verdict.json +4 -0
- package/examples/writing/story/sample-verdicts/persona.verdict.json +20 -0
- package/examples/writing/story/sample-verdicts/reader.verdict.json +16 -0
- package/examples/writing/story.hyperspec.md +7 -3
- package/package.json +1 -1
- package/src/check.mjs +96 -132
- package/src/draft.mjs +26 -0
- package/src/judge.mjs +386 -0
- package/src/judges/attribution.mjs +360 -0
- package/src/judges/doctor.mjs +126 -0
- package/src/judges/index.mjs +31 -0
- package/src/judges/knowledge.mjs +111 -0
- package/src/judges/lineup.mjs +272 -0
- package/src/judges/persona.mjs +137 -0
- package/src/judges/reader.mjs +111 -0
- package/src/learn.mjs +422 -0
- package/src/ledger.mjs +108 -0
- package/src/sentences.mjs +81 -0
- package/src/sequence-draft.mjs +75 -0
- package/src/stations/claims.mjs +44 -39
- package/src/stations/index.mjs +3 -1
- package/src/stations/links.mjs +11 -3
- package/src/stations/quotes.mjs +6 -4
- package/src/stations/sequence.mjs +275 -0
- package/src/writing-fields.mjs +34 -0
- 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)
|
|
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.
|