@supersuit/hyperspec 0.8.0 → 0.9.1

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 (37) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +46 -5
  3. package/SPEC.md +2 -2
  4. package/WRITING.md +369 -25
  5. package/bin/hyperspec.mjs +185 -13
  6. package/examples/writing/course/part-1.md +17 -0
  7. package/examples/writing/course/part-2.md +17 -0
  8. package/examples/writing/course.hyperspec.md +1 -0
  9. package/examples/writing/essay/judge/panel-buyer.packet.json +118 -0
  10. package/examples/writing/essay/judge/panel-expert.packet.json +114 -0
  11. package/examples/writing/essay/judge/panel-novice.packet.json +114 -0
  12. package/examples/writing/essay/judge/panel-skeptic.packet.json +114 -0
  13. package/examples/writing/essay/sample-verdicts/panel-buyer.verdict.json +11 -0
  14. package/examples/writing/essay/sample-verdicts/panel-expert.verdict.json +13 -0
  15. package/examples/writing/essay/sample-verdicts/panel-novice.verdict.json +11 -0
  16. package/examples/writing/essay/sample-verdicts/panel-skeptic.verdict.json +13 -0
  17. package/examples/writing/story/judge/panel-buyer.packet.json +118 -0
  18. package/examples/writing/story/judge/panel-expert.packet.json +114 -0
  19. package/examples/writing/story/judge/panel-novice.packet.json +114 -0
  20. package/examples/writing/story/judge/panel-skeptic.packet.json +114 -0
  21. package/examples/writing/story/sample-verdicts/panel-buyer.verdict.json +13 -0
  22. package/examples/writing/story/sample-verdicts/panel-expert.verdict.json +11 -0
  23. package/examples/writing/story/sample-verdicts/panel-novice.verdict.json +11 -0
  24. package/examples/writing/story/sample-verdicts/panel-skeptic.verdict.json +11 -0
  25. package/package.json +1 -1
  26. package/src/evidence.mjs +74 -0
  27. package/src/judge.mjs +50 -66
  28. package/src/judges/index.mjs +7 -1
  29. package/src/judges/panel.mjs +135 -0
  30. package/src/segments.mjs +44 -0
  31. package/src/stations/index.mjs +3 -1
  32. package/src/stations/quotes.mjs +26 -2
  33. package/src/stations/sequence.mjs +149 -2
  34. package/src/stations/triage.mjs +20 -0
  35. package/src/triage.mjs +401 -0
  36. package/src/writing-fields.mjs +48 -1
  37. package/src/writing.mjs +5 -1
package/WRITING.md CHANGED
@@ -249,6 +249,7 @@ writing:
249
249
  files: # optional: its files in reading order; a * in a file name matches
250
250
  - course/part-*.md
251
251
  outline: course/outline.md # optional: the outline that promises each unit's terms
252
+ quiz: Check yourself # optional: the quiz heading; turns on the quiz rules
252
253
  check:
253
254
  station: structure and length check
254
255
  source: form decision
@@ -280,6 +281,14 @@ writing:
280
281
  station: every factual claim points at a source span
281
282
  source: sourcing pass
282
283
  author: agent:claude
284
+ quotes: # optional; not a block, so no check, source or author
285
+ examples: true # true | false | a list of phrasings; see quotes under Checking a draft
286
+ panel: # optional: the panel's readers; the audience's reader joins as buyer
287
+ - id: skeptic # a lower-case slug, unique, never buyer
288
+ who: a manager who has run one-on-ones for years and doubts that a list is the problem
289
+ knows: # optional
290
+ - one-on-one
291
+ lens: what the essay asserts without showing
283
292
  characters: # required when fiction: true; ids unique
284
293
  - id: ines
285
294
  entity: world/ines.json # optional; if given, the file must exist
@@ -341,7 +350,7 @@ Each row lists what the writing profile adds to that test. The core conditions i
341
350
 
342
351
  | Test | A writing spec fails it when |
343
352
  |---|---|
344
- | 1 every decision is accounted for | a required block is missing and not deferred; a required field is missing; a closed-set value is outside its set (`trust`, `reader`, `change.kind`, the shape of `identity`, `unsourced_claim`); `identity: character:<id>` names a character that is not in `writing.characters`; `fiction` is present and is anything other than `true` or `false`; two materials, two spine claims or two characters share an id; `form.length.min` or `max` is not a whole number of at least 1, or `min` is greater than `max`; `spine.claims` has fewer than three or more than seven distinct claims; a character has no knowledge entry, or an entry lacks `by` or `knows`; a material has no text, or no `segments` field, or its segments file is missing, malformed, labels a segment outside the seven (`unlabeled` included), repeats a segment id, or has segments that overlap or leave text uncovered; `dna.scope_dir` is present and is a placeholder; with `dna.scope_dir`, its `scope.md` is missing, unreadable or lacks a field, its writer, form, audience or purpose differs from the spec's, or its `goldens/` folder is missing or empty, or holds a golden that cannot be read, whose frontmatter never closes, or that has no passage; `audience.terms`, when present, holds a non-string entry or has no real entries at all; `form.sequence` is present and is not a map, or has `unit`, `terms_section`, `teaser` or `outline` empty or a placeholder, or has `files`, `sections` or `knows` that is not a list of real entries. A `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
353
+ | 1 every decision is accounted for | a required block is missing and not deferred; a required field is missing; a closed-set value is outside its set (`trust`, `reader`, `change.kind`, the shape of `identity`, `unsourced_claim`); `identity: character:<id>` names a character that is not in `writing.characters`; `fiction` is present and is anything other than `true` or `false`; two materials, two spine claims or two characters share an id; `form.length.min` or `max` is not a whole number of at least 1, or `min` is greater than `max`; `spine.claims` has fewer than three or more than seven distinct claims; a character has no knowledge entry, or an entry lacks `by` or `knows`; a material has no text, or no `segments` field, or its segments file is missing, malformed, labels a segment outside the seven (`unlabeled` included), repeats a segment id, or has segments that overlap or leave text uncovered; `dna.scope_dir` is present and is a placeholder; with `dna.scope_dir`, its `scope.md` is missing, unreadable or lacks a field, its writer, form, audience or purpose differs from the spec's, or its `goldens/` folder is missing or empty, or holds a golden that cannot be read, whose frontmatter never closes, or that has no passage; `audience.terms`, when present, holds a non-string entry or has no real entries at all; `form.sequence` is present and is not a map, or has `unit`, `terms_section`, `teaser` or `outline` empty or a placeholder, or has `files`, `sections` or `knows` that is not a list of real entries, or `quiz` empty or a placeholder; `quotes` is present and is not a map, or its `examples` is not `true`, `false` or a list of real phrasings; `panel` is present and is not a list of readers, or a reader has no `id`, `who` or `lens`, an `id` that is not a lower-case slug, an `id` used twice or `buyer`, or `knows` that is not a list of real entries. A `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
345
354
  | 2 every requirement can fail | `goal.conditions` lists fewer than five or more than ten distinct ids, lists an id twice, or names an id that is not a top-level requirement |
346
355
  | 3 every requirement names its check | a block or a character has no `check` with a `station` or a `rubric` |
347
356
  | 4 every field says where it came from and who wrote it | a block or a character has no `source` or no `author`; a spine claim names no materials, or names a material id that is not in `materials.items`, or a segment that is not in that material's segments file; a segment's text does not match its material word for word; a material changed after it was marked; a claim segment has no `source` and no `own`, a story no `teller`, a quote no `speaker`; with `dna.scope_dir`, a golden in the scope has no `approved_by`, an approver that starts `agent:`, or no `source` |
@@ -388,14 +397,15 @@ writing spec cannot pass until every material it draws on is marked.
388
397
  npx @supersuit/hyperspec segments init materials/voice-memo.md --id voice-memo
389
398
  ```
390
399
 
391
- `hyperspec segments init <material> --id <mid> [--out <file>] [--by paragraph|sentence]` writes
392
- `<material>.segments.jsonl`, or the path `--out` names. `--by paragraph`, the default, makes one
400
+ `hyperspec segments init <material> --id <mid> [--out <file>] [--by paragraph|sentence] [--keep <old>]`
401
+ writes `<material>.segments.jsonl`, or the path `--out` names. `--by paragraph`, the default, makes one
393
402
  segment per paragraph. `--by sentence` makes one per sentence, and a new line that opens on a list
394
403
  marker (`-`, `*`, `+`, `1.` or `1)`, then a space) also starts a segment, so each bullet in a set
395
404
  of notes stands on its own. Every segment starts as `unlabeled`, which lint never accepts. `init`
396
- refuses to overwrite a file that exists, and exits 2 on a material that does not exist or a
397
- `--by` it does not know, a material with nothing in it, and an `--out` folder that does not
398
- exist.
405
+ refuses to overwrite a file that exists, unless `--keep` names that file (see
406
+ [When a material changes](#when-a-material-changes)), and exits 2 on a material that does not
407
+ exist or a `--by` it does not know, a material with nothing in it, and an `--out` folder that does
408
+ not exist.
399
409
 
400
410
  Then name the file on the material item, as `segments:` beside `path:`, and label every segment.
401
411
  You may also move a boundary by hand, splitting one segment in two or joining two, as long as
@@ -473,9 +483,31 @@ fails test 1.
473
483
 
474
484
  The header's `sha256` pins the material as it was when it was marked. If the material changes,
475
485
  lint fails the segments file as stale (test 4), because its offsets and labels describe text that
476
- is no longer there. Mark it again: run `segments init` with `--out` to a new file, point the
477
- material item at it, and label every segment, carrying labels over from the old file wherever the
478
- text did not change.
486
+ is no longer there. Mark it again, keeping the labels of every stretch of text that did not change:
487
+
488
+ ```bash
489
+ npx @supersuit/hyperspec segments init materials/voice-memo.md --id voice-memo --keep materials/voice-memo.md.segments.jsonl
490
+ ```
491
+
492
+ `--keep <old>` splits the material as it reads now, and every segment whose text, trimmed, a
493
+ segment in `<old>` has keeps that segment's `id`, its `label` and every other key it carries
494
+ (`own`, `source`, `teller`, `speaker`, anything added by hand), with its `start` and `end` taken
495
+ from the new split. The `id` is kept because the spine cites segments by id, so a citation keeps
496
+ pointing at the words it pointed at. Old segments with the same text are used in order, each once.
497
+ A segment no old one matches is new or changed text: it starts `unlabeled`, gets the next `s<n>` id
498
+ no old segment used, and is listed with the start of its text, so only those need a label.
499
+ `--keep` may name the file being written, which is how a material is re-marked in place; with no
500
+ `--keep`, or one naming another file, an existing file is still never overwritten. It exits 2 when
501
+ the `--keep` file cannot be read, has a line that is not a JSON object, or marks a material other
502
+ than `--id`.
503
+
504
+ After the author's framing line, s4, is edited to "and I kept the note in my wallet.", that prints:
505
+
506
+ ```text
507
+ 5 segments written to materials/voice-memo.md.segments.jsonl, 4 labels carried from materials/voice-memo.md.segments.jsonl, 1 to label:
508
+ s6 "My first manager said this to me in my second week, and I ke..."
509
+ Label each one (claim, story, quote, stance, question, aside, private), then run hyperspec lint on the spec.
510
+ ```
479
511
 
480
512
  The hash is over the file's bytes, so a change nobody would call an edit still counts. Converting
481
513
  line endings is the common one: a material marked with LF endings reads as stale once an editor
@@ -814,7 +846,7 @@ npx @supersuit/hyperspec check essay.hyperspec.md --draft essay/draft.md
814
846
 
815
847
  It lints the spec first. A spec that fails lint, or is blocked on an open decision, runs no
816
848
  station and exits with lint's own code, because a draft cannot be checked against a spec that is
817
- not ready. Then it runs eight stations in a fixed order and prints one line for each: `pass`,
849
+ not ready. Then it runs nine stations in a fixed order and prints one line for each: `pass`,
818
850
  `fail` with its findings, or `skip` with the reason. A warning prints under its station and never
819
851
  fails it. This is the essay example's draft:
820
852
 
@@ -829,6 +861,7 @@ dna: pass
829
861
  fix: Bring first_person_singular_rate back inside the band, or, if the scope no longer describes this writer, re-measure it with better goldens.
830
862
  links: pass
831
863
  sequence: skip (the spec declares no writing.form.sequence)
864
+ triage: skip (no triage file at essay/triage.jsonl yet; a recorded panel verdict or a triage import starts one)
832
865
  verdict: one-shot
833
866
  ```
834
867
 
@@ -925,6 +958,22 @@ the station: a character's dialogue is invented rather than quoted from a materi
925
958
  `attribution` judge tests it against each character's own lines instead (see
926
959
  [Judging a draft](#judging-a-draft)).
927
960
 
961
+ A primer shows its reader what to type or say, and "write the update for Dana" in quotation marks
962
+ is an example of a request, not a quotation of anyone. No material holds it, so the station would
963
+ fail it. `writing.quotes.examples` says which quoted spans are examples:
964
+
965
+ ```yaml
966
+ quotes:
967
+ examples: true
968
+ ```
969
+
970
+ With `true`, a span whose sentence names no speaker is read as an example and passes unmatched. A
971
+ quotation that names its speaker is still held to the materials, so `Dana said "..."` must still be
972
+ in one of Dana's quote segments. With a list, only the phrasings listed pass, each compared the
973
+ way a span is matched, and every other span is checked as before. The list is the stricter
974
+ choice: under `true`, a made-up quotation that names no one passes too. `false`, or no `quotes`
975
+ key, checks every span.
976
+
928
977
  | Id | Severity | Meaning |
929
978
  |---|---|---|
930
979
  | `station-quotes-unmatched` | fail | a quoted span is in no quote or story segment |
@@ -985,8 +1034,9 @@ the network, so it cannot tell you a URL is live.
985
1034
 
986
1035
  Runs when the spec declares `form.sequence`, and holds a work read in order to what its reader
987
1036
  depends on: every lesson carries its sections, every term is defined once, no lesson uses a term
988
- before the lesson that defines it, and each lesson defines what the outline promises. How a unit is
989
- read, and each guard, are under [Sequential works](#sequential-works). With no `form.sequence` the
1037
+ before the lesson that defines it, each lesson defines what the outline promises, and, when the
1038
+ spec names a quiz heading, the quizzes test every term in order. How a unit and a quiz are read,
1039
+ and each guard, are under [Sequential works](#sequential-works). With no `form.sequence` the
990
1040
  station skips. It reads words, never meaning: a term used in another sense still counts as a use.
991
1041
 
992
1042
  | Id | Severity | Meaning |
@@ -999,6 +1049,33 @@ station skips. It reads words, never meaning: a term used in another sense still
999
1049
  | `station-sequence-outline-unreadable` | fail | `outline` is set and the file cannot be read |
1000
1050
  | `station-sequence-outline-unkept` | fail | the outline promises a term in a unit that does not define it |
1001
1051
  | `station-sequence-forward-pointer` | warn | a unit mentions a later unit by number, once per pair |
1052
+ | `station-sequence-quiz-missing` | fail | `quiz` is set and the draft has no quiz heading |
1053
+ | `station-sequence-quiz-untagged` | fail | a numbered line in a quiz names no unit it tests |
1054
+ | `station-sequence-quiz-answer` | fail | a question has no answer on its quiz's answers line, or its answer is not one of its options |
1055
+ | `station-sequence-quiz-used-before-defined` | fail | a question uses a term a later unit than its tagged one defines; points at the question |
1056
+ | `station-sequence-quiz-untested` | fail | no question uses a defined term, or its simple plural; points at the defining unit |
1057
+
1058
+ ### triage
1059
+
1060
+ Runs when the spec's triage file exists (`triage.jsonl`, beside the runs ledger), and holds every
1061
+ finding in it to its answer: a finding nobody answered fails, a `taken` or `already-true` answer
1062
+ fails unless the passage it quotes is in the draft as it is now, and a `kept` answer fails without
1063
+ its reason. An `open` finding is a warning, since it is a decision for the operator and the draft
1064
+ can ship while it waits. So is a `kept` or `open` answer about a passage that has since left the
1065
+ draft. How findings arrive and how they are answered are under [Triage](#triage-1). With no triage
1066
+ file the station skips. It checks that an answer's evidence is in the draft, never whether the
1067
+ passage does what the finding asked.
1068
+
1069
+ | Id | Severity | Meaning |
1070
+ |---|---|---|
1071
+ | `station-triage-unreadable` | fail | a line of the triage file is not a finding: not JSON, or no `finding_id` or `text` |
1072
+ | `station-triage-untriaged` | fail | a finding has no disposition; names it, at its evidence's line |
1073
+ | `station-triage-disposition` | fail | a finding's disposition is not `taken`, `kept`, `already-true` or `open` |
1074
+ | `station-triage-evidence-missing` | fail | a `taken` or `already-true` answer quotes no passage of at least three words |
1075
+ | `station-triage-evidence-not-found` | fail | a `taken` or `already-true` answer quotes a passage that is not in the draft |
1076
+ | `station-triage-reason-missing` | fail | a `kept` answer gives no reason |
1077
+ | `station-triage-open` | warn | a finding is open, a decision still to make |
1078
+ | `station-triage-stale` | warn | a `kept` or `open` answer is about a passage that is no longer in the draft |
1002
1079
 
1003
1080
  ### Any station
1004
1081
 
@@ -1012,7 +1089,7 @@ Each `check` appends one line to the spec's `improvement.ledger`, the same file
1012
1089
  reads:
1013
1090
 
1014
1091
  ```json
1015
- {"at":"2026-09-29T13:21:37.330Z","kind":"check","draft":"essay/draft.md","draft_sha256":"<sha256 of the draft>","spec_sha256":"<sha256 of the spec>","stations":{"form":"pass","terms":"pass","claims":"pass","quotes":"pass","private":"pass","dna":"pass","links":"pass","sequence":"skip"},"verdict":"one-shot"}
1092
+ {"at":"2026-09-29T13:21:37.330Z","kind":"check","draft":"essay/draft.md","draft_sha256":"<sha256 of the draft>","spec_sha256":"<sha256 of the spec>","stations":{"form":"pass","terms":"pass","claims":"pass","quotes":"pass","private":"pass","dna":"pass","links":"pass","sequence":"skip","triage":"skip"},"verdict":"one-shot"}
1016
1093
  ```
1017
1094
 
1018
1095
  `draft` is the draft's path relative to the spec's folder, however you spelled it, so one draft
@@ -1064,6 +1141,7 @@ four that have defaults written out:
1064
1141
  terms_section: New terms
1065
1142
  outline: course/outline.md
1066
1143
  teaser: Next,
1144
+ quiz: Check yourself
1067
1145
  ```
1068
1146
 
1069
1147
  | Key | Default | What it is |
@@ -1075,6 +1153,7 @@ four that have defaults written out:
1075
1153
  | `outline` | none | an outline whose numbered items promise each unit's terms as `*Terms: a, b.*` |
1076
1154
  | `knows` | none | words a unit may use before a unit defines them, beside `audience.knows` |
1077
1155
  | `teaser` | `Next,` | a closing line starting `**<teaser>` names what is coming; it and everything after it in the unit are exempt from the order guard and the pointer warning |
1156
+ | `quiz` | none | the heading each quiz starts with; set, it turns on the quiz rules (see How a quiz is read) |
1078
1157
 
1079
1158
  ### How a unit is read
1080
1159
 
@@ -1090,11 +1169,41 @@ marks and any parenthetical dropped. An item that says `(from Lesson 3)` reminds
1090
1169
  earlier term and defines nothing.
1091
1170
 
1092
1171
  A use is a whole-word match, ignoring case, where a hyphen joins a word: "context-aware" does not
1093
- use "context", and "skills" does not use "skill". Code is not prose: fenced blocks and inline code
1172
+ use "context", and "skills" does not use "skill". A word inside a longer defined term is not a
1173
+ use of the shorter one: with "level" and "thinking level" both defined, "thinking level" and
1174
+ "thinking levels" use only "thinking level", while "level" on its own still uses "level". The quiz
1175
+ rules read a question the same way. Code is not prose: fenced blocks and inline code
1094
1176
  never count as a use, so `@supersuit/superskill` does not use "supersuit". A unit's own terms
1095
1177
  section is not a use either. Listing a word in `knows` or `audience.knows` is a decision that the
1096
1178
  reader already has it, and it is the only way a unit may use a word before the unit that defines it.
1097
1179
 
1180
+ ### How a quiz is read
1181
+
1182
+ With `quiz` set, the station also holds the work's quizzes to the promise a quiz makes: it tests
1183
+ what the lessons taught, and only what the reader has been taught by the lesson it names. A quiz
1184
+ starts at a heading that begins with `quiz`, in any case, and runs to the next heading of its level
1185
+ or above or the end of its file, so a quiz between two lessons belongs to neither. These are the
1186
+ first two questions of the course example's first quiz, under its heading `## Check yourself: Part 1`:
1187
+
1188
+ ```markdown
1189
+ 1. *(Lesson 1)* Five hundred grams of flour and four hundred of water: what is the hydration?
1190
+ - a) Forty percent
1191
+ - b) Eighty percent
1192
+ 2. *(Lesson 1)* When is flour and water a dough?
1193
+ - a) When no dry flour is left
1194
+ - b) When it has doubled
1195
+ ```
1196
+
1197
+ A question is a numbered line tagged `*(<unit> N)*`, the unit it tests. It runs to the next
1198
+ numbered line, and its options are the indented `- a) ` lines inside it. A quiz ends with one
1199
+ `**Answers:**` line of number and letter pairs, `**Answers:** 1 b · 2 a`, in any separator. Every
1200
+ term a unit defines must be used by some question, where a simple plural counts ("doughs" tests
1201
+ "dough"); no question, options included, may use a term a later unit defines than the one it is
1202
+ tagged with, except a word in `knows`; every numbered line in a quiz must carry a tag; and every
1203
+ question's answer must name one of its options. Code in a question is not a use, as in a lesson.
1204
+ The rules and the question shape are those of the checker the first book written this way used,
1205
+ promoted here so every sequential work gets them.
1206
+
1098
1207
  ### Checking a sequence
1099
1208
 
1100
1209
  With `files` listed, `check` needs no `--draft`: the files, joined in reading order, are the draft,
@@ -1117,6 +1226,7 @@ sequence: pass
1117
1226
  fix: Keep it a pointer ("more in Lesson 4"): Lesson 1 must make sense to a reader who has not read Lesson 4.
1118
1227
  warn [station-sequence-forward-pointer] Lesson 3 points forward to Lesson 4 (course/part-2.md line 24)
1119
1228
  fix: Keep it a pointer ("more in Lesson 4"): Lesson 3 must make sense to a reader who has not read Lesson 4.
1229
+ triage: skip (no triage file at course/triage.jsonl yet; a recorded panel verdict or a triage import starts one)
1120
1230
  verdict: one-shot
1121
1231
  ```
1122
1232
 
@@ -1131,7 +1241,8 @@ complaint. A pointer to a later unit is a warning, never a failure: "more in Les
1131
1241
  long as the lesson makes sense without it.
1132
1242
 
1133
1243
  It cannot tell whether a definition is good, whether a term is used in the sense it was defined in,
1134
- or whether a quiz tests what the lessons taught. The judges still take one `--draft` file.
1244
+ or whether a question that uses a term tests understanding of it rather than only naming it. The
1245
+ judges still take one `--draft` file.
1135
1246
 
1136
1247
  ## Judging a draft
1137
1248
 
@@ -1156,6 +1267,10 @@ essay/judge/lineup.packet.json
1156
1267
  essay/judge/lineup.key.json
1157
1268
  essay/judge/reader.packet.json
1158
1269
  essay/judge/persona.packet.json
1270
+ essay/judge/panel-skeptic.packet.json
1271
+ essay/judge/panel-novice.packet.json
1272
+ essay/judge/panel-expert.packet.json
1273
+ essay/judge/panel-buyer.packet.json
1159
1274
  attribution: skip (the spec is not fiction; attribution applies only with fiction: true)
1160
1275
  knowledge: skip (the spec is not fiction; knowledge applies only with fiction: true)
1161
1276
  ```
@@ -1163,8 +1278,9 @@ knowledge: skip (the spec is not fiction; knowledge applies only with fiction: t
1163
1278
  Like `check`, it lints the spec first: a spec that fails lint, or is blocked on an open
1164
1279
  decision, gets no packet, and `prepare` exits with lint's own code. Then it writes one
1165
1280
  `<station>.packet.json` for each station that applies, in a fixed order (`doctor, lineup,
1166
- reader, persona, attribution, knowledge`), and prints a `skip` line with the reason for each
1167
- that does not. `--only doctor,reader` prepares just those. The `--out` folder must already
1281
+ reader, persona, attribution, knowledge, panel`), and prints a `skip` line with the reason for each
1282
+ that does not. The panel writes one packet per reader, `panel-<reader>.packet.json`. `--only
1283
+ doctor,reader` prepares just those. The `--out` folder must already
1168
1284
  exist. `prepare` refuses to overwrite any file it would write, naming every one, and then writes
1169
1285
  nothing; `--force` replaces them. The worked examples ship the packets this writes, so add
1170
1286
  `--force` to write them again there. The same spec and draft, with the same goldens and claims
@@ -1219,7 +1335,8 @@ it cannot find the spec and exits 2.
1219
1335
  ### The evidence rule
1220
1336
 
1221
1337
  Every verdict field that cites the draft (the doctor's `evidence`, the reader's `lost_at` and
1222
- `stopped_at`, the persona's `breaks`, the knowledge `leaks`) is a span copied from the draft. A
1338
+ `stopped_at`, the persona's `breaks`, the knowledge `leaks`, every panel item) is a span copied
1339
+ from the draft. A
1223
1340
  span counts only when:
1224
1341
 
1225
1342
  - it has at least three words, where a word is a run of letters and digits (so "It's" is two);
@@ -1452,6 +1569,59 @@ them by order. Write `by` as something a reader of the draft can find.
1452
1569
  | `judge-knowledge-leak` | fail | a character knows something too early, at its evidence's line |
1453
1570
  | `judge-knowledge-character-unknown` | invalid | a leak names a character with no timeline in the packet |
1454
1571
 
1572
+ ### panel
1573
+
1574
+ The draft read by several readers at once, each through their own lens: the pressure test a
1575
+ draft gets by hand before it ships, made part of the run. Every other station asks one question
1576
+ with a right answer; the panel asks each reader what works, what to improve, what is missing and
1577
+ what to remove, and each thing they would change is a finding somebody has to answer (see
1578
+ [Triage](#triage-1)). Applies when `writing.audience` is written and its check has a rubric, the
1579
+ reader station's own condition, because one of the readers is always the audience's own.
1580
+
1581
+ The readers are the ones `writing.panel` lists, each with an `id`, `who`, what they already `knows`
1582
+ and the `lens` they read for:
1583
+
1584
+ ```yaml
1585
+ panel:
1586
+ - id: skeptic
1587
+ who: a manager who has run one-on-ones for years and doubts that a list is the problem
1588
+ lens: what the essay asserts without showing
1589
+ ```
1590
+
1591
+ With no `panel` declared, three readers read it: a `skeptic`, who doubts the central claim and
1592
+ reads for what is asserted without support; a `novice`, new to the subject, reading for every
1593
+ term, step or assumption left unexplained; and an `expert` in the subject, reading for what is
1594
+ wrong, out of date or oversimplified. Declared or not, the audience's own reader joins last as
1595
+ `buyer`, built from `audience.who`, `audience.knows` and `audience.wants`: a panel that never
1596
+ includes the person the piece is for tests everything except whether it works for them.
1597
+
1598
+ There is one packet per reader. Inputs: `reader` (`id`, `who`, `knows`, `lens`) and the `draft`;
1599
+ the rubric is the audience block's. The verdict is `{ good, improve, missing, remove }`, four
1600
+ lists, any of them empty, where every item is `{ evidence, note }`: a span copied from the draft,
1601
+ and what and why in a sentence. A missing item quotes the passage nearest where the missing thing
1602
+ belongs. The panel never fails a draft. Each improve, missing and remove item is a warning at its
1603
+ evidence's line, and goes to the spec's triage file as a finding to answer; what works is counted
1604
+ and never triaged, since there is nothing to answer. The summary line counts all four and the
1605
+ findings added:
1606
+
1607
+ ```
1608
+ panel: pass
1609
+ skeptic: 1 good, 1 to improve, 0 missing, 0 to remove; 1 added to triage (story/triage.jsonl)
1610
+ warn [judge-panel-improve] skeptic would improve this: The red ring is planted hard; a doubting reader sees the sale coming a scene before Ines says it. (line 63)
1611
+ fix: Answer it in the triage file: take it, keep the passage with a reason, show it is already true, or leave it open for a decision.
1612
+ verdict: one-shot
1613
+ ```
1614
+
1615
+ Each reader has its own history in the runs ledger: a panel judge line carries `reader` after
1616
+ `station`, and is compared only with that reader's earlier lines. Recording the same verdict twice
1617
+ adds nothing to triage, since a finding's id is a hash of what it says.
1618
+
1619
+ | Id | Kind | Meaning |
1620
+ |---|---|---|
1621
+ | `judge-panel-improve` | warn | the reader would change this passage, and why |
1622
+ | `judge-panel-missing` | warn | the reader needs something the draft does not give, near this passage |
1623
+ | `judge-panel-remove` | warn | the reader would cut this passage, and why |
1624
+
1455
1625
  ### Any judgment station
1456
1626
 
1457
1627
  | Id | Kind | Meaning |
@@ -1522,10 +1692,181 @@ copy on every release.
1522
1692
  | story | persona | fail | three process details (the deck oven's heat-up time, the rolls' bake time, how the starter is fed) are in no claim, and the rubric allows none outside the ledger |
1523
1693
  | story | attribution | pass | all 19 lines named right by voice alone |
1524
1694
  | story | knowledge | pass | neither character knows anything early |
1695
+ | essay | panel-skeptic | pass | the claim rests on a count; "three is enough" is asserted, and who answered the survey is never said |
1696
+ | essay | panel-novice | pass | the one term a newcomer lacks is defined where it appears; the running agenda needs a first step |
1697
+ | essay | panel-expert | pass | the follow-through is right; say what to do when the silence runs on, and cut the opening disaster story |
1698
+ | essay | panel-buyer | pass | it ends on the card; it misses what to do when the report says nothing, at the same passage as the expert |
1699
+ | story | panel-skeptic | pass | the letter is carried by what Theo does not do; the red ring round Friday gives the sale away |
1700
+ | story | panel-novice | pass | the deck oven is shown where it is named; the proving cabinet is not |
1701
+ | story | panel-expert | pass | the flour is weighed, as in a real bakery; the starter is never shown being fed |
1702
+ | story | panel-buyer | pass | the first line of Ines's speech holds the reader; the red ring again, and a radio that goes nowhere |
1525
1703
 
1526
1704
  Both examples pass every station of `check`. Each failure here is something no deterministic
1527
1705
  station can see.
1528
1706
 
1707
+ ## Triage
1708
+
1709
+ A panel verdict, or an outside review, is a list of findings, and a finding is only useful once
1710
+ someone has decided what to do about it. Triage is where that happens: every finding lands in one
1711
+ file, each gets one of four answers, `check` holds every answer to the draft as it is now, and a
1712
+ reply to the reviewer is written from the answers. Four answers, because a finding ends in one of
1713
+ four places:
1714
+
1715
+ | Disposition | Means | Needs |
1716
+ |---|---|---|
1717
+ | `taken` | the draft now does what the finding asked | `--evidence`: the passage of the current draft that does it |
1718
+ | `kept` | the passage stays as it is, on purpose | `--reason`: why |
1719
+ | `already-true` | the draft already did it | `--evidence`: the passage that does it |
1720
+ | `open` | a decision for the operator | `--reason`, optionally: what is to decide |
1721
+
1722
+ Evidence follows [the evidence rule](#the-evidence-rule) a judge's quotes follow, against the
1723
+ draft as it is when the answer is given. Nothing in triage reads meaning: it holds an answer's
1724
+ evidence to the draft word for word, and the person answering decides whether the passage does
1725
+ what the finding asked.
1726
+
1727
+ ### The triage file
1728
+
1729
+ Findings live in `triage.jsonl` beside the spec's runs ledger (`improvement.ledger`), so the
1730
+ story example's is `story/triage.jsonl`. Recording a panel verdict adds the reader's improve,
1731
+ missing and remove items to it, and `triage import` adds an outside review's. Each line is one
1732
+ finding:
1733
+
1734
+ ```json
1735
+ {"finding_id":"panel-skeptic-aeb09835","source":"panel","reader":"skeptic","kind":"improve","text":"The red ring is planted hard; a doubting reader sees the sale coming a scene before Ines says it.","evidence":"somebody had drawn a ring round Friday in red pen","draft_sha256":"fdf8dd1ee311a376e45cca31b50f0b01f1157a889cb332620cbd178e9c510306","disposition":null,"answer":null}
1736
+ ```
1737
+
1738
+ `finding_id` is `panel-<reader>-` or `import-` and eight characters of a hash of what the finding
1739
+ says, so the same finding recorded twice is one finding. `source` is `panel` or where an imported
1740
+ review came from, `reader` the panel reader or the review's heading, `kind` one of `improve`,
1741
+ `missing` and `remove`, or `note` for an imported point under no such label. `evidence` is the
1742
+ passage the finding is about, and `draft_sha256` the draft it was raised against. `disposition` and
1743
+ `answer` are null until the finding is answered; then `answer` holds `evidence` or `reason` as
1744
+ given, the draft's `draft_sha256` and the time, `at`.
1745
+
1746
+ ### Answering a finding
1747
+
1748
+ Record the four panel samples of the story (see [panel](#panel)), then answer each finding:
1749
+
1750
+ ```bash
1751
+ npx @supersuit/hyperspec triage answer story.hyperspec.md panel-novice-47ce4719 already-true --draft story/draft.md --evidence "Ines will not put it in the proving cabinet. She says the cabinet is for the white."
1752
+ npx @supersuit/hyperspec triage answer story.hyperspec.md panel-buyer-23aee52f kept --draft story/draft.md --reason "the radio nobody turns on is the silence the bench scene depends on"
1753
+ npx @supersuit/hyperspec triage answer story.hyperspec.md panel-expert-ef15b7b2 kept --draft story/draft.md --reason "the starter is the one thing Theo is never allowed to touch, so he never sees it fed"
1754
+ npx @supersuit/hyperspec triage answer story.hyperspec.md panel-skeptic-aeb09835 open --draft story/draft.md --reason "how early to plant the sale is the author's call"
1755
+ npx @supersuit/hyperspec triage answer story.hyperspec.md panel-buyer-57810b16 open --draft story/draft.md --reason "the same call as the skeptic's"
1756
+ npx @supersuit/hyperspec triage status story.hyperspec.md --draft story/draft.md
1757
+ ```
1758
+
1759
+ Each answer prints `<finding>: <disposition> (story/triage.jsonl)`. `status` counts the answers,
1760
+ lists the passages two or more readers raised findings about, and runs the triage station's rules:
1761
+
1762
+ ```
1763
+ story/triage.jsonl: 5 findings; 0 taken, 2 kept, 1 already true, 2 open, 0 not answered
1764
+ shared by two or more readers:
1765
+ line 63: "somebody had drawn a ring round Friday in red pen"
1766
+ skeptic (improve): The red ring is planted hard; a doubting reader sees the sale coming a scene before Ines says it.
1767
+ buyer (improve): I guessed Friday before the reveal, which took some of the weight out of the oven scene.
1768
+ triage: pass
1769
+ warn [station-triage-open] panel-skeptic-aeb09835 (skeptic, improve) is open: "The red ring is planted hard; a doubting reader sees the sale coming a scene be…" (how early to plant the sale is the author's call) (line 63)
1770
+ fix: A decision for the operator; answer it once it is made.
1771
+ warn [station-triage-open] panel-buyer-57810b16 (buyer, improve) is open: "I guessed Friday before the reveal, which took some of the weight out of the ov…" (the same call as the skeptic's) (line 63)
1772
+ fix: A decision for the operator; answer it once it is made.
1773
+ ```
1774
+
1775
+ The shared passages are the synthesis of a panel: where two or more readers point at overlapping
1776
+ passages of the draft, the passage is worth reading first. It groups by where, never by meaning,
1777
+ so two readers who say the same thing about different passages are not grouped.
1778
+
1779
+ `answer` refuses, and writes nothing, when a `taken` or `already-true` answer's evidence is missing,
1780
+ under three words or not in the draft, or a `kept` answer has no reason. It may be given again: the
1781
+ last answer stands. Like `check`, every triage command takes `--draft`, or reads the files of a
1782
+ spec that lists `writing.form.sequence.files`, and names a finding's line by the file that holds
1783
+ it.
1784
+
1785
+ ### Importing an outside review
1786
+
1787
+ A review written anywhere else, as markdown or plain text, comes in as findings with `triage
1788
+ import`, so it gets the same answers and the same check as the panel. This is a short review of the
1789
+ story:
1790
+
1791
+ ```markdown
1792
+ #### The magazine's editor
1793
+
1794
+ **Good:** the opening puts the reader in the kitchen at once.
1795
+
1796
+ **Improve:**
1797
+
1798
+ - The oven noise comes twice; the second, "It made the noise while I was weighing the second batch", could go.
1799
+
1800
+ **Missing:**
1801
+
1802
+ - Theo never says what he wants: "I did not look at the coat. I looked at the dough." carries it, but only just.
1803
+
1804
+ #### A first-time reader
1805
+
1806
+ - Remove: the line about "the bakery closing on a Tuesday" felt out of place.
1807
+ ```
1808
+
1809
+ ```bash
1810
+ npx @supersuit/hyperspec triage import story.hyperspec.md story/review.md --draft story/draft.md --source "the editor's review"
1811
+ ```
1812
+
1813
+ ```
1814
+ the editor's review: 3 findings added to triage (story/triage.jsonl); 1 item of praise, not triaged
1815
+ warn [triage-import-quote-not-found] a finding quotes text that is not in the current draft: "the bakery closing on a Tuesday" (the line about "the bakery closing on a Tuesday" felt out o…)
1816
+ fix: The review may have read another copy of the draft. Check the finding against the draft as it is now before answering it.
1817
+ ```
1818
+
1819
+ A heading names the reader of the points under it. A label (`Good`, `Improve`, `Improvement`,
1820
+ `Missing` or `Remove`, as a heading, a bold line, or a word and a colon starting a line or a list
1821
+ item) sets the kind of what follows. Each list item, with its indented lines, is one finding, and
1822
+ so is a paragraph under a label; an introduction under no label is not. What is good is counted
1823
+ and left out, since there is nothing to answer. A finding's evidence is the first quoted span, in
1824
+ double quotation marks or a `>` quotation block, of three words or more that is in the draft. A
1825
+ finding that quotes only text the draft does not hold is imported with no evidence and a warning:
1826
+ a review of a stale or partial copy is the usual cause, and the finding is still answered.
1827
+ `--source` names the review, by default its file's path, so a reply can be written to it alone.
1828
+
1829
+ ### Replying to the reviewer
1830
+
1831
+ ```bash
1832
+ npx @supersuit/hyperspec triage answer story.hyperspec.md import-6c60f92a kept --draft story/draft.md --reason "the second time is when Theo starts to listen to it"
1833
+ npx @supersuit/hyperspec triage answer story.hyperspec.md import-919ea916 already-true --draft story/draft.md --evidence "I did not look at the coat. I looked at the dough."
1834
+ npx @supersuit/hyperspec triage answer story.hyperspec.md import-ad3d7fe9 kept --draft story/draft.md --reason "no line about a Tuesday closing is in the draft; the review read another copy"
1835
+ npx @supersuit/hyperspec triage reply story.hyperspec.md --draft story/draft.md --source "the editor's review"
1836
+ ```
1837
+
1838
+ ```
1839
+ Thank you for the review. Here is what happened to each point.
1840
+
1841
+ Kept as it is:
1842
+ - The magazine's editor: The oven noise comes twice; the second, "It made the noise while I was weighing the second batch", could go. Why: the second time is when Theo starts to listen to it
1843
+ - A first-time reader: the line about "the bakery closing on a Tuesday" felt out of place. Why: no line about a Tuesday closing is in the draft; the review read another copy
1844
+
1845
+ Already in the draft:
1846
+ - The magazine's editor: Theo never says what he wants: "I did not look at the coat. I looked at the dough." carries it, but only just. It is here: "I did not look at the coat. I looked at the dough."
1847
+ ```
1848
+
1849
+ `reply` prints plain text, one line per finding, under Taken, Kept as it is, Already in the draft
1850
+ and Still open, leaving out an empty heading, and anything not answered yet under a last heading
1851
+ of its own. It never sends anything: it is a draft for the operator to read and send, or not. It
1852
+ exits 1, with a line on stderr, when the triage station would fail, so a reply with a gap in it is
1853
+ not sent by accident. `--source` limits it to one review; without it every finding is included.
1854
+
1855
+ Exit codes: `status` **0** when the triage station would pass, **1** when it would fail; `answer`
1856
+ **0** written, **1** refused; `import` **0** imported, **1** the review holds no finding; `reply`
1857
+ **0** ready, **1** not; all four **2** on usage: no spec, a spec without `profile: writing`, no
1858
+ draft, no triage file for `answer` and `reply`, an unknown finding or disposition, or a review that
1859
+ cannot be read. `--json` prints the whole result.
1860
+
1861
+ | Id | Kind | Meaning |
1862
+ |---|---|---|
1863
+ | `triage-evidence-missing` | invalid | a `taken` or `already-true` answer has no `--evidence` |
1864
+ | `triage-evidence-too-short` | invalid | the answer's evidence has fewer than three words |
1865
+ | `triage-evidence-not-found` | invalid | the answer's evidence is not in the draft |
1866
+ | `triage-reason-missing` | invalid | a `kept` answer has no `--reason` |
1867
+ | `triage-import-quote-not-found` | warn | an imported finding quotes text, and none of it is in the draft |
1868
+ | `triage-import-empty` | invalid | the review holds no finding to answer |
1869
+
1529
1870
  ## Learning from edits
1530
1871
 
1531
1872
  A factory writes a first draft, and a person edits it into the draft they approve. Every edit is
@@ -1716,8 +2057,9 @@ names:
1716
2057
  Each character has speech rules, a knowledge timeline by scene, and golden and rejected lines
1717
2058
  in a voice you can tell apart from the other's.
1718
2059
  - `course.hyperspec.md`: a four-lesson course in two part files, declared a sequence (see
1719
- [Sequential works](#sequential-works)), with an outline promising each lesson's terms. `check`
1720
- reads the parts with no `--draft` and passes them, with two forward-pointer warnings.
2060
+ [Sequential works](#sequential-works)), with an outline promising each lesson's terms and a
2061
+ quiz closing each part. `check` reads the parts with no `--draft` and passes them, with two
2062
+ forward-pointer warnings.
1721
2063
 
1722
2064
  Every material in all three is marked. Between them the essay and the story use all seven labels, each with
1723
2065
  the field it needs, and every spine claim cites the segments that support it. Each segments file
@@ -1733,17 +2075,19 @@ lints all three specs and checks their drafts on every release, so they cannot d
1733
2075
 
1734
2076
  Each also ships the packets `judge prepare` writes for its draft, in `essay/judge/` and
1735
2077
  `story/judge/`, and one sample verdict per packet in `essay/sample-verdicts/` and
1736
- `story/sample-verdicts/`, filled in by hand and marked as samples; what each found is under
1737
- [The worked examples](#the-worked-examples). The essay adds a learn pair in `essay/learn/`: a
2078
+ `story/sample-verdicts/`, filled in by hand and marked as samples, four panel readers' among
2079
+ them; what each found is under [The worked examples](#the-worked-examples), and the story's panel
2080
+ findings are triaged under [Triage](#triage-1). The essay adds a learn pair in `essay/learn/`: a
1738
2081
  first draft, the packet `learn prepare` writes comparing it with `essay/draft.md`, and a sample
1739
2082
  learn verdict (see [Learning from edits](#learning-from-edits)). A test checks that every packet
1740
2083
  is what `prepare` writes now and records every sample.
1741
2084
 
1742
2085
  ## What later versions add
1743
2086
 
1744
- This release is the schema, its lint, marked materials, scoped DNA, `check` with eight
1745
- deterministic stations (sequential works among them), six judgment stations written as packets for
1746
- an outside judge, and learn.
2087
+ This release is the schema, its lint, marked materials, scoped DNA, `check` with nine
2088
+ deterministic stations (sequential works and their quizzes among them, and triage last), seven
2089
+ judgment stations written as packets for an outside judge (the panel among them), triage, and
2090
+ learn.
1747
2091
  Next: lineups over several passages of one draft, so that one lucky pick carries less weight, and
1748
2092
  a learn step that reads the runs ledger across drafts for the stations that keep failing and the
1749
2093
  changes that made them pass, beside what one pair of drafts shows.