@supersuit/hyperspec 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +53 -3
  3. package/SPEC.md +24 -10
  4. package/WRITING.md +597 -0
  5. package/bin/hyperspec.mjs +94 -3
  6. package/examples/minimal.hyperspec.md +2 -2
  7. package/examples/writing/essay/goldens/close.md +2 -0
  8. package/examples/writing/essay/goldens/opening.md +2 -0
  9. package/examples/writing/essay/materials/interview-notes.md +12 -0
  10. package/examples/writing/essay/materials/interview-notes.md.segments.jsonl +10 -0
  11. package/examples/writing/essay/materials/team-survey.md +7 -0
  12. package/examples/writing/essay/materials/team-survey.md.segments.jsonl +6 -0
  13. package/examples/writing/essay/materials/voice-memo.md +18 -0
  14. package/examples/writing/essay/materials/voice-memo.md.segments.jsonl +7 -0
  15. package/examples/writing/essay/runs.jsonl +0 -0
  16. package/examples/writing/essay.hyperspec.md +220 -0
  17. package/examples/writing/story/goldens/dialogue.md +3 -0
  18. package/examples/writing/story/goldens/opening.md +3 -0
  19. package/examples/writing/story/materials/bakery-visit.md +9 -0
  20. package/examples/writing/story/materials/bakery-visit.md.segments.jsonl +13 -0
  21. package/examples/writing/story/materials/notes.md +16 -0
  22. package/examples/writing/story/materials/notes.md.segments.jsonl +7 -0
  23. package/examples/writing/story/materials/scene-list.md +7 -0
  24. package/examples/writing/story/materials/scene-list.md.segments.jsonl +14 -0
  25. package/examples/writing/story/runs.jsonl +0 -0
  26. package/examples/writing/story.hyperspec.md +288 -0
  27. package/examples/writing/style-rules.md +19 -0
  28. package/package.json +4 -2
  29. package/src/blobs.mjs +1 -1
  30. package/src/compare.mjs +6 -6
  31. package/src/fsutil.mjs +1 -1
  32. package/src/labels.mjs +6 -0
  33. package/src/placeholder.mjs +20 -0
  34. package/src/profiles.mjs +50 -0
  35. package/src/reproduce.mjs +5 -5
  36. package/src/rules.mjs +25 -13
  37. package/src/score.mjs +7 -1
  38. package/src/segments.mjs +407 -0
  39. package/src/template.mjs +4 -1
  40. package/src/writing-exports.mjs +6 -0
  41. package/src/writing-fields.mjs +418 -0
  42. package/src/writing-template.mjs +199 -0
  43. package/src/writing.mjs +181 -0
package/WRITING.md ADDED
@@ -0,0 +1,597 @@
1
+ # The writing profile
2
+
3
+ A hyperspec for a piece of writing. An essay, a chapter, a letter, a story: anything an agent
4
+ drafts and a person reads.
5
+
6
+ Writing is where the average does the most damage. Asked for "an essay on X", an agent fills
7
+ every gap you left with the most typical choice: the typical reader, the typical argument, the
8
+ typical voice. Each choice is reasonable and the sum reads like nobody in particular wrote it.
9
+ The writing profile names the gaps a piece of writing has, so each one is filled on purpose, by
10
+ someone you can name, and checked before a draft reaches you.
11
+
12
+ A profile adds rules for one kind of work without adding a test. Every writing finding reports
13
+ under one of the nine tests in [SPEC.md](SPEC.md), with an id that starts `writing-`, and the
14
+ score is still out of nine. Everything in the core format (decisions, requirements, rejects,
15
+ examples, resume, feedback, improvement) still applies and is still linted.
16
+
17
+ ## Opting in
18
+
19
+ Add `profile: writing` at the top level of the frontmatter, and a `writing:` map holding the
20
+ blocks. `hyperspec lint` then prints one more line under the score:
21
+
22
+ ```
23
+ essay.hyperspec.md: pass (9/9)
24
+ writing: 9/9 blocks complete
25
+ ```
26
+
27
+ A block counts as complete when it is present with no failing finding. `characters` also counts
28
+ as complete when the piece is not fiction, since nothing requires it. A block deferred to a
29
+ decision (see [Deferring a block](#deferring-a-block)) does not count as complete, because
30
+ nothing has been written in it yet. `--json` carries the same count on each file as
31
+ `"profile": { "name": "writing", "complete": 9, "total": 9 }`.
32
+
33
+ A `profile:` this linter does not know is a warning under test 7, naming the profile and saying
34
+ its rules were not checked.
35
+
36
+ ## The ten components
37
+
38
+ These are the parts you cannot remove without the writing sliding back to the middle. Nine are
39
+ blocks under `writing:`. The tenth, progress, is deliberately never written down.
40
+
41
+ ### 1. Materials: what goes in, and how it is marked
42
+
43
+ Brain dumps, transcripts, interview answers, notes, prior pieces, research. Each material
44
+ records who produced it, when it was captured, how, and how far it can be trusted: `raw` for
45
+ thinking out loud, `considered` for something someone has reviewed, `verified` for something
46
+ checked against its source. A brain dump is the most valuable input and the least structured, so
47
+ marking it is the step that turns thinking into something an agent can cite. Each material is
48
+ split into segments and each segment gets a label saying what it may be used as (see
49
+ [Marking materials](#marking-materials)).
50
+
51
+ ### 2. Writer DNA: who is writing, and how they sound, for this purpose
52
+
53
+ A writer does not have one voice. The same person writes differently for a theology journal and
54
+ a landing page, so their DNA is scoped by **form, audience and purpose**, and the scope is the
55
+ whole design of this block.
56
+
57
+ - **Rules** are the one layer that applies everywhere: what the writer never does and always
58
+ does. They live in your style rules file, and every piece in every scope points at it.
59
+ - **Goldens** are real passages the writer has marked as right, each filed under its scope. A
60
+ golden feeds only work that shares that scope, so a sermon golden can never leak into a sales
61
+ email.
62
+ - **Every golden carries a note on why it is golden.** A golden without its reason teaches the
63
+ surface; the reason teaches the move.
64
+
65
+ DNA is proven by a blind lineup within its scope: a judge sees a generated passage beside real
66
+ goldens of the same kind and tries to pick it out. Every writer has their own DNA, and nobody's
67
+ scope feeds anybody else's.
68
+
69
+ ### 3. Persona: who the piece speaks as
70
+
71
+ The identity the piece speaks as (the writer as themselves, a role, or a character), its stance
72
+ toward the reader, what it may assert, and what it will not say. Research on persona prompting
73
+ finds that a persona shapes voice and alignment and can cost accuracy
74
+ ([PRISM](https://arxiv.org/html/2603.18507v1)), so here the persona governs voice and stance only.
75
+ **Facts come from marked sources, never from the persona.** That separation is a field,
76
+ `facts_from: sources`, and the linter refuses any other value.
77
+
78
+ The persona is recorded in the spec, where it belongs. The piece itself says nothing to the
79
+ reader about who is writing.
80
+
81
+ ### 4. Audience: who is reading
82
+
83
+ A named person or a specified archetype: where they are on their path right now, which words
84
+ they already have, what they believe now, what they want, where they will read it (a phone in
85
+ ninety seconds, aloud, in print), and whether the reader is a person or another agent. Checked
86
+ by a term station (every word outside their vocabulary is defined on first use) and by a
87
+ simulated reader, a model playing that exact reader, which reports where it got lost and where
88
+ it would have stopped.
89
+
90
+ ### 5. Goal: what the piece changes
91
+
92
+ The one step down the funnel: where this reader is now, where the piece moves them, and what
93
+ they do next if it worked. Then the change that step needs in them (a belief, an action or a
94
+ feeling), and five to ten requirements that would prove it happened, each written to be failable.
95
+ This is the block a grader works against, and the simulated reader is asked the one question that
96
+ matters: would you take the next step now?
97
+
98
+ ### 6. Form: what kind of thing it is
99
+
100
+ Essay, chapter, wiki article, email, text, talk, letter, story. A form supplies its required
101
+ parts, its length envelope, and any extra checks it needs: a chapter checks continuity with the
102
+ chapters around it, a wiki article checks its links, a text message checks bubble length.
103
+
104
+ ### 7. Spine: what it argues
105
+
106
+ The kind of argument (thesis, testimony, primer, letter, story; the set is open) and the claim
107
+ chain: the three to seven claims the piece has to land, in order, each pointing at the marked
108
+ segments that support it.
109
+
110
+ ### 8. Sources and claims: what lets another agent pick it up
111
+
112
+ A claims ledger beside the piece: every factual claim in the draft points at a source and the
113
+ span in it, and every quote is matched word for word against its source. A writer's own brain
114
+ dump counts as a source for their opinions and their stories, never for an outside fact. By
115
+ default a claim with no source fails the check rather than passing with a warning.
116
+
117
+ ### 9. Characters: everyone who speaks inside the work
118
+
119
+ The persona is who the piece speaks as. A character is someone who speaks inside it, and the
120
+ same rules apply one level down. Required when `fiction: true`. Each character carries:
121
+
122
+ - **Speech DNA**: the words they use, the ones they never would, and their rhythm.
123
+ - **Wants, fears, and the thing they hide**, because dialogue is someone trying to get something
124
+ while hiding something.
125
+ - **What they know, and when.** A knowledge timeline by chapter or scene, so a character never
126
+ says what they could not know yet. This is the most common way generated dialogue breaks a
127
+ story, and it is fully checkable.
128
+ - **How they speak to each person that matters**, since speech shifts by relationship.
129
+ - **Arc state**: where they are in their change at this point in the work.
130
+ - **Golden lines and rejected lines**: real lines that sound exactly like them, and lines that
131
+ sound close but wrong.
132
+
133
+ Checked by a blind attribution test (a judge sees a line with the speaker hidden and has to name
134
+ who said it), a knowledge-leak check against the timeline, and a consistency check against the
135
+ golden and rejected lines. If your characters already live as entities in a world database, the
136
+ optional `entity` field points at that record, so one record can say how a character looks and
137
+ how they speak.
138
+
139
+ ### 10. Progress: where it is
140
+
141
+ Derived from disk, never stored. Which blocks are filled, which drafts exist, which checks
142
+ passed, what the last review said, and the next action are all things a second agent can read
143
+ off the folder itself. A saved progress field goes stale the first time a session dies mid-arc,
144
+ and from then on it lies to every agent that trusts it. So the linter refuses one: a
145
+ `writing.progress` key fails test 7, and `resume.next_action` stays the only thing the spec
146
+ says about what happens next.
147
+
148
+ ## The schema
149
+
150
+ Every field below is required unless its comment says otherwise, and a required list needs at
151
+ least one entry. A path resolves relative to the spec file, the same way `examples` does, and a
152
+ path the linter checks must name a file that exists.
153
+
154
+ ```yaml
155
+ profile: writing
156
+ fiction: false # optional, true or false; absent means false. true requires writing.characters
157
+ writing:
158
+ materials:
159
+ items: # at least one
160
+ - id: voice-memo # unique across items
161
+ path: materials/voice-memo.md
162
+ segments: materials/voice-memo.md.segments.jsonl # written by hyperspec segments init, then labeled
163
+ produced_by: example-author
164
+ captured: "2026-09-12"
165
+ how: voice memo, transcribed
166
+ trust: raw # raw | considered | verified
167
+ check:
168
+ station: every segment of every material carries a label from the closed set, matches its source verbatim, and the markings are current
169
+ source: capture step
170
+ author: agent:claude
171
+ dna:
172
+ writer: example-author
173
+ scope:
174
+ form: essay
175
+ audience: new managers
176
+ purpose: teach
177
+ rules: style-rules.md # your style rules file, the always-on layer
178
+ goldens: # at least one
179
+ - path: goldens/opening.md
180
+ why: one plain claim, then a second sentence that turns it into something to do
181
+ check:
182
+ rubric: blind lineup within this scope
183
+ source: goldens marked on the review page
184
+ author: example-author
185
+ persona:
186
+ identity: self # self | role:<name> | character:<id>
187
+ stance: mentor # peer | mentor | witness | guide; anything else warns
188
+ may_assert:
189
+ - what the author did in their own first one-on-ones
190
+ will_not_say:
191
+ - the name of anyone on the author's team
192
+ facts_from: sources # must be exactly "sources"
193
+ check:
194
+ rubric: persona-consistency judge
195
+ source: persona interview
196
+ author: example-author
197
+ audience:
198
+ who: someone in their first three months of managing
199
+ funnel_now: has a first one-on-one on the calendar this week
200
+ knows:
201
+ - one-on-one
202
+ - report
203
+ believes_now: a one-on-one is where a manager catches up on the work
204
+ wants: a plan for the first meeting
205
+ reads_on: a phone, in the ten minutes before the meeting
206
+ reader: person # person | agent
207
+ check:
208
+ station: term check against knows
209
+ rubric: simulated reader reports where it got lost
210
+ source: audience interview
211
+ author: example-author
212
+ goal:
213
+ from: plans to run the meeting from their own list
214
+ to: hands the meeting to the report
215
+ next_if_worked: copies the three questions into the invite
216
+ change:
217
+ kind: action # belief | action | feeling
218
+ text: the reader asks the three questions and waits
219
+ conditions: # 5 to 10 distinct ids of top-level requirements, each once
220
+ - r1
221
+ - r2
222
+ - r3
223
+ - r4
224
+ - r5
225
+ check:
226
+ rubric: grade the draft against every condition
227
+ source: goal interview
228
+ author: example-author
229
+ form:
230
+ name: essay # open set
231
+ length: # whole numbers, at least 1, min no more than max
232
+ min: 700
233
+ max: 1100
234
+ unit: words
235
+ required_parts:
236
+ - an opening that states the claim
237
+ - the three questions
238
+ - a close
239
+ stations: # may be empty
240
+ - the three questions render as a numbered list
241
+ check:
242
+ station: structure and length check
243
+ source: form decision
244
+ author: example-author
245
+ spine:
246
+ kind: primer # open set
247
+ claims: # 3 to 7, in order, each with its own id
248
+ - id: c1
249
+ text: the first one-on-one is the one meeting the report should set the agenda for
250
+ materials: # material#segment cites one segment; never a private or question one
251
+ - voice-memo#s3
252
+ - id: c2
253
+ text: status belongs in the tracker
254
+ materials: # a bare material id cites the whole material
255
+ - voice-memo
256
+ - id: c3
257
+ text: three questions are enough to hand the meeting over
258
+ materials:
259
+ - voice-memo#s2
260
+ - voice-memo#s5
261
+ check:
262
+ rubric: each claim lands, in order, and nothing is argued outside the chain
263
+ source: spine interview
264
+ author: example-author
265
+ sources:
266
+ ledger: claims.jsonl # need not exist before drafting
267
+ unsourced_claim: fail # fail | warn; warn is reported as a warning
268
+ check:
269
+ station: every factual claim points at a source span
270
+ source: sourcing pass
271
+ author: agent:claude
272
+ characters: # required when fiction: true; ids unique
273
+ - id: ines
274
+ entity: world/ines.json # optional; if given, the file must exist
275
+ speech:
276
+ uses:
277
+ - instructions in the imperative
278
+ never:
279
+ - an apology in words
280
+ rhythm: short, flat sentences # optional
281
+ wants: to leave the bakery in hands she trusts
282
+ fears: that he will stay in a trade with no bakery to do it in
283
+ hides: that she sold the bakery two weeks ago
284
+ knowledge: # at least one; each entry needs by and knows
285
+ - by: scene-1
286
+ knows: the sale closes on Friday
287
+ relationships: # optional
288
+ - to: theo
289
+ how: gives him instructions where another person would give praise
290
+ arc_state: has let go of the bakery, and has not yet let go of him
291
+ golden_lines: # at least one
292
+ - Flour first. Then you can talk.
293
+ rejected_lines: # at least one, and none of them also golden
294
+ - I'm so sorry I didn't tell you sooner.
295
+ check:
296
+ rubric: blind attribution test, knowledge-leak check, consistency against golden and rejected lines
297
+ source: notes.md
298
+ author: example-author
299
+ ```
300
+
301
+ Every block, and every character, carries `check` (with `station:` for a deterministic check or
302
+ `rubric:` for what a grader applies, the same shape a requirement's check has), `source` and
303
+ `author`. `characters` is a list, so there each entry carries its own.
304
+
305
+ Write every map in block style, one key per line, as above. The YAML reader hyperspec uses reads
306
+ a flow list such as `[r1, r2]`, but it reads an inline map such as `{ station: ... }` as a plain
307
+ string, and a flow list followed by a comment on the same line as a string too.
308
+
309
+ A placeholder counts as missing, here and everywhere else in a hyperspec, so a scaffolded field
310
+ cannot pass a presence check. A placeholder is a whole value, trimmed and in any case, of `todo`,
311
+ `tbd`, `fixme`, `xxx`, `placeholder`, `<placeholder>`, `n/a`, a run of dashes, a run of question
312
+ marks, or an ellipsis, optionally followed by a trailing `.`, `:` or `!`. Real text that starts
313
+ with one of those, such as `TODO: write the opening`, counts as present, and so does `none`.
314
+
315
+ ## The test mapping
316
+
317
+ Each row lists what the writing profile adds to that test. The core conditions in
318
+ [SPEC.md](SPEC.md#the-test-to-field-map) still apply alongside them.
319
+
320
+ | Test | A writing spec fails it when |
321
+ |---|---|
322
+ | 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. A `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
323
+ | 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 |
324
+ | 3 every requirement names its check | a block or a character has no `check` with a `station` or a `rubric` |
325
+ | 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` |
326
+ | 5 negative space is specified | `persona.will_not_say` is empty; `persona.facts_from` is anything other than `sources`; a spine claim cites a `private` or a `question` segment |
327
+ | 6 examples outrank adjectives | a golden has no `why`; a material, `dna.rules`, golden or character `entity` path does not exist or is not a file; a character has no golden lines or no rejected lines, or has the same line in both (compared trimmed and case-folded) |
328
+ | 7 a stranger can resume it | `writing.progress` exists. An unknown `profile:` is a warning |
329
+ | 8 its adopters can push back on it | nothing further; the core rule applies |
330
+ | 9 it improves itself | nothing further; the core rule applies |
331
+
332
+ ## Closed sets
333
+
334
+ | Field | Allowed values |
335
+ |---|---|
336
+ | `materials.items[].trust` | `raw`, `considered`, `verified` |
337
+ | `persona.identity` | `self`, `role:<name>`, `character:<id>` |
338
+ | `persona.stance` | `peer`, `mentor`, `witness`, `guide`; any other value warns |
339
+ | `persona.facts_from` | `sources` |
340
+ | `audience.reader` | `person`, `agent` |
341
+ | `goal.change.kind` | `belief`, `action`, `feeling` |
342
+ | `sources.unsourced_claim` | `fail`, `warn` |
343
+ | `fiction` | `true`, `false`; absent means `false` |
344
+
345
+ `form.name` and `spine.kind` are open: name the form and the kind of argument in your own words.
346
+
347
+ ## Marking materials
348
+
349
+ A brain dump mixes things a draft may use with things it may not: a checked fact, an opinion, a
350
+ story from the author's own week, a line said in confidence. Marking tells them apart before an
351
+ agent drafts anything. Each material is split into segments, each segment gets one label saying
352
+ what it may be used as, and the spine cites segments, so every claim in the piece points at the
353
+ exact words that support it.
354
+
355
+ hyperspec never decides a label. `segments init` splits a material the same way every time, an
356
+ agent or a person labels each segment by editing the file it wrote, and `lint` checks everything
357
+ a rule can check: every segment carries a label from the closed set and the field that label
358
+ needs, matches its material word for word, and was marked against the material as it reads now.
359
+
360
+ A material item with no `segments` field fails test 1. Marking comes before specifying, so a
361
+ writing spec cannot pass until every material it draws on is marked.
362
+
363
+ ### Marking a material
364
+
365
+ ```bash
366
+ npx @supersuit/hyperspec segments init materials/voice-memo.md --id voice-memo
367
+ ```
368
+
369
+ `hyperspec segments init <material> --id <mid> [--out <file>] [--by paragraph|sentence]` writes
370
+ `<material>.segments.jsonl`, or the path `--out` names. `--by paragraph`, the default, makes one
371
+ segment per paragraph. `--by sentence` makes one per sentence, and a new line that opens on a list
372
+ marker (`-`, `*`, `+`, `1.` or `1)`, then a space) also starts a segment, so each bullet in a set
373
+ of notes stands on its own. Every segment starts as `unlabeled`, which lint never accepts. `init`
374
+ refuses to overwrite a file that exists, and exits 2 on a material that does not exist or a
375
+ `--by` it does not know, a material with nothing in it, and an `--out` folder that does not
376
+ exist.
377
+
378
+ Then name the file on the material item, as `segments:` beside `path:`, and label every segment.
379
+ You may also move a boundary by hand, splitting one segment in two or joining two, as long as
380
+ the rules under [Coverage](#coverage) still hold.
381
+
382
+ ### The segments file
383
+
384
+ JSON Lines: one object per line. Line 1 is a header, and every later line is one segment. For this
385
+ material, `materials/voice-memo.md`:
386
+
387
+ ```text
388
+ Voice memo, recorded on a walk. Raw thinking.
389
+
390
+ My first one-on-one as a manager was a disaster. I ran it from my own list.
391
+
392
+ The first one-on-one is the one meeting the report should set the agenda for.
393
+
394
+ My first manager said this to me in my second week, and I wrote it down.
395
+
396
+ "Ask what they want to talk about, then stop talking."
397
+ ```
398
+
399
+ `segments init` writes five segments, and once they are labeled the file reads:
400
+
401
+ ```jsonl
402
+ {"material":"voice-memo","path":"materials/voice-memo.md","sha256":"66c62b995a6f29c72f2a9a20c6b27deaef5d9b195bb6bab96336c06f9a8f2bfc"}
403
+ {"id":"s1","start":0,"end":45,"label":"aside","text":"Voice memo, recorded on a walk. Raw thinking."}
404
+ {"id":"s2","start":47,"end":122,"label":"story","teller":"example-author","text":"My first one-on-one as a manager was a disaster. I ran it from my own list."}
405
+ {"id":"s3","start":124,"end":201,"label":"claim","own":true,"text":"The first one-on-one is the one meeting the report should set the agenda for."}
406
+ {"id":"s4","start":203,"end":275,"label":"story","teller":"example-author","text":"My first manager said this to me in my second week, and I wrote it down."}
407
+ {"id":"s5","start":277,"end":331,"label":"quote","speaker":"the author's first manager","text":"\"Ask what they want to talk about, then stop talking.\""}
408
+ ```
409
+
410
+ The author's framing, s4, is a segment of its own, so the quote, s5, holds only the manager's
411
+ words, which is all a `quote` may hold.
412
+
413
+ - **Header.** `material` is the item's id and must match it. `path` records the material path
414
+ given to `segments init`; lint reads the material from the item's own `path`. `sha256` is the
415
+ SHA-256 of the material file's bytes when it was marked.
416
+ - **`id`** is unique within the file. `init` writes `s1`, `s2` and so on; any id works, and it is
417
+ what the spine cites.
418
+ - **`start` and `end`** are character offsets into the material's text read as UTF-8, counted as
419
+ JavaScript string indices (UTF-16 code units), with `end` exclusive.
420
+ - **`text`** is exactly the material's characters from `start` to `end`.
421
+ - **`label`**, plus the one field some labels need (below). Those fields are strings, except `own`.
422
+
423
+ ### The labels
424
+
425
+ | Label | Means | May be used as | Needs |
426
+ |---|---|---|---|
427
+ | `claim` | a statement of fact about the world | only with a source, or as the author's own claim said as such | `source`, non-empty, or `own: true` |
428
+ | `story` | something that happened, told by someone who was there | testimony, with the teller named | `teller` |
429
+ | `quote` | words someone said, verbatim | quoted exactly, never paraphrased inside quotation marks | `speaker` |
430
+ | `stance` | an opinion or conviction | the author's position | nothing more |
431
+ | `question` | something open | a prompt for the interview, never an assertion | nothing more |
432
+ | `aside` | true but off the thread | held back unless the spine needs it | nothing more |
433
+ | `private` | not for this audience | never used; kept for context | nothing more |
434
+
435
+ `own` counts when it is `true` or the string `"true"`. Any other value, `false` included, leaves
436
+ it unset, and a claim with no `source` then fails. A placeholder word such as `TODO`, `n/a` or
437
+ `???` counts as missing here as it does everywhere in a hyperspec (see [The schema](#the-schema)),
438
+ so `source: "TODO"` fails like no source at all. The same holds for the header's fields and for
439
+ segment ids.
440
+
441
+ ### Coverage
442
+
443
+ Taken in order of `start`, whatever order the lines are in, segments never overlap, and between
444
+ them they cover every character of the material that is not whitespace. Whitespace between
445
+ segments may be left out, which is what `init` does. Segment ids are unique within a file. When
446
+ text is left uncovered, the finding gives the offset of the first uncovered stretch and quotes up
447
+ to 60 characters of it. A material with no text that is not whitespace has nothing to mark and
448
+ fails test 1.
449
+
450
+ ### When a material changes
451
+
452
+ The header's `sha256` pins the material as it was when it was marked. If the material changes,
453
+ lint fails the segments file as stale (test 4), because its offsets and labels describe text that
454
+ is no longer there. Mark it again: run `segments init` with `--out` to a new file, point the
455
+ material item at it, and label every segment, carrying labels over from the old file wherever the
456
+ text did not change.
457
+
458
+ The hash is over the file's bytes, so a change nobody would call an edit still counts. Converting
459
+ line endings is the common one: a material marked with LF endings reads as stale once an editor
460
+ or a checkout setting rewrites it with CRLF. Mark a material in the line endings it will be kept
461
+ in, and if it lives in git, pin them with a `.gitattributes` line such as
462
+ `materials/** text eol=lf`.
463
+
464
+ ### Citing segments in the spine
465
+
466
+ A spine claim cites a segment as `<material>#<segment>`, such as `voice-memo#s3`. The segment has
467
+ to exist in that material's segments file (test 4). A `private` segment is never used and a
468
+ `question` is never an assertion, so a claim citing either fails test 5. A bare material id, such
469
+ as `voice-memo`, still cites the whole material; `voice-memo#`, with nothing after the `#`, is not
470
+ a bare id and fails as an unknown segment. When a claim cites a segment of a material whose
471
+ segments cannot be read at all (the material is not marked, its file is missing, or the file
472
+ holds no segments), lint says so once for that material rather than once per citation.
473
+
474
+ ### Findings
475
+
476
+ Every marking finding fails the test in its row. `<segment>` is the segment's id, or its
477
+ position when it has none; `<line>` is a line number in the segments file; `<n>` is the claim's
478
+ position in `spine.claims`, counting from 0. Every message names the material, and the segment
479
+ where there is one, and prints paths as the spec wrote them, so the output is the same on every
480
+ machine.
481
+
482
+ | Id | Test | Fails when |
483
+ |---|---|---|
484
+ | `writing-materials-unmarked` | 1 | a material item has no `segments` field |
485
+ | `writing-materials-segments-missing` | 1 | the segments file does not exist or cannot be read |
486
+ | `writing-materials-material-missing` | 1 | the material file cannot be read. Lint reports a missing material path under test 6 instead, so this comes only from `readSegments` |
487
+ | `writing-materials-empty` | 1 | the material has no text that is not whitespace |
488
+ | `writing-materials-header` | 1 | line 1 is not a JSON object, or has no `material`, `path` or `sha256` |
489
+ | `writing-materials-header-material` | 1 | the header names a different material from the item |
490
+ | `writing-materials-json-line-<line>` | 1 | a segment line is not a JSON object |
491
+ | `writing-materials-segment-id-<line>` | 1 | a segment has no id |
492
+ | `writing-materials-segment-id` | 1 | two segments share an id |
493
+ | `writing-materials-label-<segment>` | 1 | a label outside the seven, `unlabeled` included |
494
+ | `writing-materials-segment-shape-<segment>` | 1 | `start` and `end` are not whole numbers with `start` at least 0, `end` greater than `start`, and `end` no further than the material's length |
495
+ | `writing-materials-overlap` | 1 | two segments overlap |
496
+ | `writing-materials-coverage` | 1 | text that is not whitespace lies outside every segment |
497
+ | `writing-materials-text-<segment>` | 4 | `text` is not the material's characters from `start` to `end` |
498
+ | `writing-materials-stale` | 4 | the material's SHA-256 no longer matches the header |
499
+ | `writing-materials-claim-source-<segment>` | 4 | a claim has no `source` and no `own` |
500
+ | `writing-materials-story-teller-<segment>` | 4 | a story has no `teller` |
501
+ | `writing-materials-quote-speaker-<segment>` | 4 | a quote has no `speaker` |
502
+ | `writing-spine-materials-segments-unresolvable-<material>` | 4 | a claim cites a segment of a material whose segments cannot be read |
503
+ | `writing-spine-claim-<n>-materials-segment-unknown` | 4 | a claim cites a segment that is not in the file |
504
+ | `writing-spine-claim-<n>-materials-segment-private` | 5 | a claim cites a `private` segment |
505
+ | `writing-spine-claim-<n>-materials-segment-question` | 5 | a claim cites a `question` segment |
506
+
507
+ ### Reading segments from your own tool
508
+
509
+ A tool that labels materials, such as an agent's capture step or an editor, can import the label
510
+ set and the same parse-and-check lint runs:
511
+
512
+ ```js
513
+ import { MATERIAL_LABELS, readSegments } from "@supersuit/hyperspec/writing";
514
+
515
+ const { header, segments, findings } = readSegments("materials/voice-memo.md.segments.jsonl", {
516
+ materialPath: "materials/voice-memo.md",
517
+ materialId: "voice-memo",
518
+ });
519
+ ```
520
+
521
+ `readSegments` never throws. It returns the parsed header (or `null`), every segment line that
522
+ parsed as a JSON object, and findings in the shape lint reports: `test`, `id`, `severity`,
523
+ `message` and `fix`. Without `materialPath` it runs only the checks that need no material text
524
+ (the header, ids, labels and label fields); with it, it also checks verbatim text, coverage,
525
+ overlap and staleness. `materialId`, when given, has to match the header's `material`.
526
+ Two more options, `displayPath` and `materialDisplayPath`, set how the two files are named in
527
+ messages (lint passes the paths as the spec wrote them); by default the paths are printed as
528
+ given. `MATERIAL_LABELS` is the seven labels, in the order of the table above.
529
+
530
+ ## Deferring a block
531
+
532
+ A block can be deferred, never silently missing. A required block that is absent fails test 1
533
+ unless a decision with the id `writing-<block>` stands in for it:
534
+
535
+ ```yaml
536
+ decisions:
537
+ - id: writing-audience
538
+ state: open
539
+ question: who reads this, and what do they already believe?
540
+ source: kickoff call
541
+ author: agent:claude
542
+ chosen_by: agent
543
+ ```
544
+
545
+ - **`open`** defers the block to a question only a person can answer. The spec is blocked on it,
546
+ exactly like any open decision: `lint` exits 3 once every test passes.
547
+ - **`delegated` with a `rule`** defers the block to a standing rule the agent follows. The spec
548
+ can pass. A delegated decision with no `rule` defers nothing, so the missing block still fails.
549
+
550
+ Either way the deferred block does not count toward `writing: k/9 blocks complete`.
551
+
552
+ ## Starting a writing spec
553
+
554
+ ```bash
555
+ npx @supersuit/hyperspec init essay.hyperspec.md --profile writing --title "Your title" --form essay
556
+ npx @supersuit/hyperspec init story.hyperspec.md --profile writing --form "short story" --fiction
557
+ ```
558
+
559
+ The skeleton shows every required block in schema order with every field present as a `TODO`
560
+ placeholder. `dna`, `persona`, `audience` and `goal` also carry an open decision whose question
561
+ says what you have to answer before the placeholder means anything. `--form` sets both `kind:`
562
+ and `writing.form.name`, and defaults to `essay`. The material item names
563
+ `materials/TODO.md.segments.jsonl`, the file `segments init` writes for `materials/TODO.md`, so
564
+ materials keeps failing until a real material is marked. `--fiction` sets `fiction: true` and adds one
565
+ character with the same treatment. The skeleton never passes: it lints `fail`, with
566
+ `writing: 1/9 blocks complete` (or `0/9` with `--fiction`), until the placeholders and the open
567
+ decisions are replaced with real content.
568
+
569
+ `init` refuses, with exit 2 and a plain message, anything it would otherwise have to ignore:
570
+ `--profile` with no value or one this linter does not know, `--fiction` or `--form` without
571
+ `--profile writing`, `--kind` with it (the form sets the kind), and a file in a folder that does
572
+ not exist.
573
+
574
+ ## Worked examples
575
+
576
+ Two complete specs ship in [`examples/writing/`](examples/writing/), each with every file it
577
+ names:
578
+
579
+ - `essay.hyperspec.md`: an essay for new managers on running a first one-on-one. Three materials
580
+ at three trust levels, scoped DNA with two annotated goldens, a four-claim spine.
581
+ - `story.hyperspec.md`: a short story, `fiction: true`, narrated by one of its two characters.
582
+ Each character has speech rules, a knowledge timeline by scene, and golden and rejected lines
583
+ in a voice you can tell apart from the other's.
584
+
585
+ Every material in both is marked. Between them the two examples use all seven labels, each with
586
+ the field it needs, and every spine claim cites the segments that support it. Each segments file
587
+ keeps the boundaries `segments init` wrote, in paragraph mode for prose and sentence mode for
588
+ bulleted notes, so you can re-run it and compare.
589
+
590
+ Both lint `pass (9/9)` with `writing: 9/9 blocks complete` and no findings. A test runs them on
591
+ every release, so they cannot drift from the linter.
592
+
593
+ ## What later versions add
594
+
595
+ This release is the schema, its lint, and marked materials. Later versions build on it in order:
596
+ scoped DNA with annotated goldens filed by form, audience and purpose, and the stations
597
+ themselves, running the checks each block names and grading drafts against the goal.