@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.
- package/CHANGELOG.md +119 -0
- package/README.md +53 -3
- package/SPEC.md +24 -10
- package/WRITING.md +597 -0
- package/bin/hyperspec.mjs +94 -3
- package/examples/minimal.hyperspec.md +2 -2
- package/examples/writing/essay/goldens/close.md +2 -0
- package/examples/writing/essay/goldens/opening.md +2 -0
- package/examples/writing/essay/materials/interview-notes.md +12 -0
- package/examples/writing/essay/materials/interview-notes.md.segments.jsonl +10 -0
- package/examples/writing/essay/materials/team-survey.md +7 -0
- package/examples/writing/essay/materials/team-survey.md.segments.jsonl +6 -0
- package/examples/writing/essay/materials/voice-memo.md +18 -0
- package/examples/writing/essay/materials/voice-memo.md.segments.jsonl +7 -0
- package/examples/writing/essay/runs.jsonl +0 -0
- package/examples/writing/essay.hyperspec.md +220 -0
- package/examples/writing/story/goldens/dialogue.md +3 -0
- package/examples/writing/story/goldens/opening.md +3 -0
- package/examples/writing/story/materials/bakery-visit.md +9 -0
- package/examples/writing/story/materials/bakery-visit.md.segments.jsonl +13 -0
- package/examples/writing/story/materials/notes.md +16 -0
- package/examples/writing/story/materials/notes.md.segments.jsonl +7 -0
- package/examples/writing/story/materials/scene-list.md +7 -0
- package/examples/writing/story/materials/scene-list.md.segments.jsonl +14 -0
- package/examples/writing/story/runs.jsonl +0 -0
- package/examples/writing/story.hyperspec.md +288 -0
- package/examples/writing/style-rules.md +19 -0
- package/package.json +4 -2
- package/src/blobs.mjs +1 -1
- package/src/compare.mjs +6 -6
- package/src/fsutil.mjs +1 -1
- package/src/labels.mjs +6 -0
- package/src/placeholder.mjs +20 -0
- package/src/profiles.mjs +50 -0
- package/src/reproduce.mjs +5 -5
- package/src/rules.mjs +25 -13
- package/src/score.mjs +7 -1
- package/src/segments.mjs +407 -0
- package/src/template.mjs +4 -1
- package/src/writing-exports.mjs +6 -0
- package/src/writing-fields.mjs +418 -0
- package/src/writing-template.mjs +199 -0
- 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.
|