@supersuit/hyperspec 0.2.0 → 0.3.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 +60 -0
- package/README.md +31 -2
- package/SPEC.md +22 -10
- package/WRITING.md +426 -0
- package/bin/hyperspec.mjs +45 -2
- 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/team-survey.md +7 -0
- package/examples/writing/essay/materials/voice-memo.md +18 -0
- package/examples/writing/essay/runs.jsonl +0 -0
- package/examples/writing/essay.hyperspec.md +217 -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 +8 -0
- package/examples/writing/story/materials/notes.md +14 -0
- package/examples/writing/story/materials/scene-list.md +7 -0
- package/examples/writing/story/runs.jsonl +0 -0
- package/examples/writing/story.hyperspec.md +285 -0
- package/examples/writing/style-rules.md +19 -0
- package/package.json +3 -2
- package/src/placeholder.mjs +20 -0
- package/src/profiles.mjs +50 -0
- package/src/rules.mjs +25 -13
- package/src/score.mjs +7 -1
- package/src/template.mjs +4 -1
- package/src/writing-fields.mjs +317 -0
- package/src/writing-template.mjs +192 -0
- package/src/writing.mjs +181 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
The writing profile. A piece of writing can now carry a hyperspec that names everything an
|
|
6
|
+
agent would otherwise fill with the average: what the piece is made from and how far each
|
|
7
|
+
source can be trusted, whose voice it is for this form, audience and purpose, who it speaks as,
|
|
8
|
+
who reads it, the one change it is for, what kind of thing it is, the claims it argues in order,
|
|
9
|
+
where every fact comes from, and in fiction how each character speaks and what they know by
|
|
10
|
+
each scene. `hyperspec lint` checks all of it under the same nine tests, and
|
|
11
|
+
`hyperspec init --profile writing` lays every block out for you to fill in.
|
|
12
|
+
|
|
13
|
+
**Behavior change for existing specs:** a placeholder now counts as missing, everywhere, in
|
|
14
|
+
specs with no profile too. A placeholder is a whole value, trimmed and in any case, of `todo`,
|
|
15
|
+
`tbd`, `fixme`, `xxx`, `placeholder`, `<placeholder>`, `n/a`, a run of dashes, a run of question
|
|
16
|
+
marks, or an ellipsis, optionally followed by a trailing `.`, `:` or `!`. A spec that passed
|
|
17
|
+
0.2.0 with `source: TODO` or `source: n/a` on a decision now fails that test. Real text that
|
|
18
|
+
starts with one of those, such as `TODO: write the opening`, still counts as present, and so
|
|
19
|
+
does `none`.
|
|
20
|
+
|
|
21
|
+
- `profile: writing` opts a spec in; its blocks live under a top-level `writing:` map:
|
|
22
|
+
`materials`, `dna`, `persona`, `audience`, `goal`, `form`, `spine`, `sources`, and
|
|
23
|
+
`characters`, which is required when `fiction: true`. Every block carries a `check`, a
|
|
24
|
+
`source` and an `author`. The core format still applies in full.
|
|
25
|
+
- No tenth test. Every writing finding reports under one of the nine, with an id starting
|
|
26
|
+
`writing-`, and the score stays out of nine. `lint` prints one more line,
|
|
27
|
+
`writing: <k>/9 blocks complete`, and `--json` carries it as `profile` on each file.
|
|
28
|
+
- A missing block fails test 1 unless a decision with the id `writing-<block>` defers it:
|
|
29
|
+
`open`, which blocks the spec like any open decision, or `delegated` with a `rule`. A
|
|
30
|
+
deferred block does not count as complete.
|
|
31
|
+
- Progress is never stored: a `writing.progress` key fails test 7, because saved progress goes
|
|
32
|
+
stale the first time a session dies mid-arc. Progress is read off the folder instead.
|
|
33
|
+
- Field rules for every block, each under the test it belongs to: closed sets for `fiction`
|
|
34
|
+
(absent means false), `trust`, `reader`, `change.kind`, the shape of `persona.identity` and
|
|
35
|
+
`unsourced_claim`; unique ids for materials, spine claims and characters; a length envelope
|
|
36
|
+
of whole numbers of at least 1 (test 1); five to ten distinct `goal.conditions` naming real
|
|
37
|
+
requirements, each listed once (test 2); spine claims that name real materials (test 4);
|
|
38
|
+
`persona.facts_from: sources` and a non-empty `will_not_say` (test 5); a `why` on every
|
|
39
|
+
golden, material and golden paths that exist, and golden and rejected lines for every
|
|
40
|
+
character with no line in both (test 6). A repeated id counts once toward a minimum. A
|
|
41
|
+
`stance` outside peer, mentor, witness and guide is a warning.
|
|
42
|
+
- A character needs speech rules (what they say and never say), wants, fears, what they hide,
|
|
43
|
+
an arc state, a knowledge timeline, and golden and rejected lines. `relationships` and a
|
|
44
|
+
pointer to a character `entity` file are optional.
|
|
45
|
+
- `hyperspec init <file> --profile writing [--title T] [--form F] [--fiction]` writes a
|
|
46
|
+
skeleton with every required block in schema order, every field a placeholder, and open
|
|
47
|
+
decisions for the four blocks that need your judgment first (dna, persona, audience, goal).
|
|
48
|
+
It fails lint until the placeholders are replaced. `init` exits 2 with a plain message on a
|
|
49
|
+
`--profile` with no value or one it does not know, `--fiction` or `--form` without
|
|
50
|
+
`--profile writing`, `--kind` with it, and a folder that does not exist.
|
|
51
|
+
- `lint` warns under test 7 on a `profile` it does not know, and checks none of its rules. A
|
|
52
|
+
name every object inherits, such as `constructor`, is an unknown profile like any other.
|
|
53
|
+
- WRITING.md documents the profile: the ten components, the schema with every field, the test
|
|
54
|
+
mapping, the closed sets, the seven materials labels and what each may be used as, and
|
|
55
|
+
deferral. It ships in the package.
|
|
56
|
+
- `examples/writing/` ships two complete specs, an essay and a two-character short story, with
|
|
57
|
+
every file they name. Both pass with no findings, and a test keeps them that way.
|
|
58
|
+
- SPEC.md gains a Profiles section and states the placeholder words in its test-to-field map.
|
|
59
|
+
- The illustrative specs and recipe in SPEC.md and `examples/minimal.hyperspec.md` name a
|
|
60
|
+
placeholder author, `example-author`, and a placeholder factory, `my-factory`.
|
|
61
|
+
- Requires `@supersuit/superskill` 0.2.2, whose YAML reader reads an inline map (`scope: { form: essay, purpose: persuade }`), a bare inline map as a list item, and an inline list followed by a comment. On 0.2.1 those came back as text, so a correct spec written in that compact style failed. A test lints the essay example rewritten in that style.
|
|
62
|
+
|
|
3
63
|
## 0.2.0 (2026-09-28)
|
|
4
64
|
|
|
5
65
|
Recipes. Every output a factory makes can now carry a recipe beside it: what made it, from
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ improvement ledger. Every test is defined in [SPEC.md](SPEC.md).
|
|
|
24
24
|
|---|---|
|
|
25
25
|
| `hyperspec lint <file...> [--json]` | Score each hyperspec against the nine tests. |
|
|
26
26
|
| `hyperspec init <file> [--title T] [--kind K]` | Write a new hyperspec skeleton. Refuses to overwrite an existing file. |
|
|
27
|
+
| `hyperspec init <file> --profile writing [--title T] [--form F] [--fiction]` | Write a writing-spec skeleton, every block shown with placeholders. |
|
|
27
28
|
| `hyperspec recipe check <output-or-recipe>` | Check that a recipe records everything the standard asks for. |
|
|
28
29
|
| `hyperspec recipe approve <recipe> --by <slug>` | Record who approved the output. |
|
|
29
30
|
| `hyperspec reproduce <recipe> [--restore]` | Re-check every hash the recipe recorded. Never runs a model. |
|
|
@@ -103,11 +104,39 @@ recipe.finish();
|
|
|
103
104
|
The schema, the stage key, the runner and doctor contracts, and every exit code are in
|
|
104
105
|
[SPEC.md](SPEC.md#recipes).
|
|
105
106
|
|
|
107
|
+
## Writing specs
|
|
108
|
+
|
|
109
|
+
A piece of writing gets its own profile. Add `profile: writing` to a hyperspec and it gains nine
|
|
110
|
+
blocks that name what an agent would otherwise fill with the average: the materials it draws on
|
|
111
|
+
and how far each can be trusted, the writer's voice scoped to this form, audience and purpose,
|
|
112
|
+
who the piece speaks as, who reads it and what they already know, the one change the piece is
|
|
113
|
+
for, its form, the claims it argues in order, where every fact comes from, and in fiction every
|
|
114
|
+
character who speaks. Findings still report under the nine tests, and `lint` adds one line:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
essay.hyperspec.md: pass (9/9)
|
|
118
|
+
writing: 9/9 blocks complete
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Start one with every block laid out and waiting:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx @supersuit/hyperspec init essay.hyperspec.md --profile writing --title "Your title"
|
|
125
|
+
npx @supersuit/hyperspec init story.hyperspec.md --profile writing --form "short story" --fiction
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The skeleton fails until every placeholder is real and its four open questions (whose voice,
|
|
129
|
+
who speaks, who reads, what changes) are answered. The blocks, every field, and which test
|
|
130
|
+
each rule reports under are in [WRITING.md](WRITING.md). Two complete specs that pass with
|
|
131
|
+
nothing to warn ship in `examples/writing/`: an essay for new managers, and a short story with
|
|
132
|
+
two characters whose voices a judge can tell apart.
|
|
133
|
+
|
|
106
134
|
## The format
|
|
107
135
|
|
|
108
136
|
A hyperspec is a markdown file with a YAML frontmatter block: `decisions`, `requirements`,
|
|
109
|
-
`rejects`, `examples`, `resume`, `feedback`, and `improvement
|
|
110
|
-
standard, including what makes each of the nine tests fail, is in
|
|
137
|
+
`rejects`, `examples`, `resume`, `feedback`, and `improvement`, plus an optional `profile`.
|
|
138
|
+
The full field-by-field standard, including what makes each of the nine tests fail, is in
|
|
139
|
+
[SPEC.md](SPEC.md).
|
|
111
140
|
|
|
112
141
|
## Install
|
|
113
142
|
|
package/SPEC.md
CHANGED
|
@@ -39,6 +39,12 @@ decisions:
|
|
|
39
39
|
source: SPEC.md, section "Recipes"
|
|
40
40
|
author: agent:claude
|
|
41
41
|
chosen_by: agent
|
|
42
|
+
- id: profiles
|
|
43
|
+
state: decided
|
|
44
|
+
value: a profile adds rules for one kind of work under the nine tests; it never adds a tenth test, every finding it raises names one of the nine, and the score stays out of nine
|
|
45
|
+
source: WRITING.md, section "The writing profile"
|
|
46
|
+
author: agent:claude
|
|
47
|
+
chosen_by: agent
|
|
42
48
|
requirements:
|
|
43
49
|
- id: r1
|
|
44
50
|
text: lint exits 0 only when all nine tests pass and nothing is open
|
|
@@ -91,7 +97,7 @@ examples:
|
|
|
91
97
|
- path: examples/minimal.hyperspec.md
|
|
92
98
|
why: the smallest spec that passes all nine tests
|
|
93
99
|
resume:
|
|
94
|
-
next_action: collect adopter issues on 0.
|
|
100
|
+
next_action: collect adopter issues on 0.3, the writing profile included, and cut 0.4 from them
|
|
95
101
|
feedback:
|
|
96
102
|
issues: https://github.com/SupersuitUp/hyperspec/issues
|
|
97
103
|
fork: MIT; fork it for your own purposes and say so in your SPEC
|
|
@@ -103,7 +109,7 @@ improvement:
|
|
|
103
109
|
|
|
104
110
|
A person writing for another person leaves most of the specification unsaid, because the other person fills the gaps from shared context. An agent has none of that context, so it fills every gap with the average, and the average is what reads as middling. Hyperspecification is writing down the gaps. It is a level of detail that would feel like overkill between two people and is exactly enough for an agent: every decision the agent would otherwise guess is either decided, delegated with the rule for deciding it, or marked open, so the work stops instead of guessing.
|
|
105
111
|
|
|
106
|
-
**Version 0.
|
|
112
|
+
**Version 0.3.0** (2026-09-29)
|
|
107
113
|
|
|
108
114
|
## What makes a spec a hyperspec
|
|
109
115
|
|
|
@@ -161,7 +167,7 @@ decisions:
|
|
|
161
167
|
state: decided # decided | delegated | open
|
|
162
168
|
value: the operator, reading on a phone
|
|
163
169
|
source: interview A2 # where it came from
|
|
164
|
-
author:
|
|
170
|
+
author: example-author # a person slug, or agent:<model>
|
|
165
171
|
chosen_by: human # human | agent
|
|
166
172
|
- id: length
|
|
167
173
|
state: delegated
|
|
@@ -182,7 +188,7 @@ requirements:
|
|
|
182
188
|
check:
|
|
183
189
|
rubric: ask the simulated reader to define the term; pass only on a correct definition
|
|
184
190
|
source: design doc, audience block
|
|
185
|
-
author:
|
|
191
|
+
author: example-author
|
|
186
192
|
rejects:
|
|
187
193
|
- hype words about AI
|
|
188
194
|
examples:
|
|
@@ -200,7 +206,7 @@ improvement:
|
|
|
200
206
|
|
|
201
207
|
## The test-to-field map
|
|
202
208
|
|
|
203
|
-
Each row lists every condition under which `hyperspec lint` fails that test. A warning never fails a test. A value that is only a YAML comment (`source: # TODO`),
|
|
209
|
+
Each row lists every condition under which `hyperspec lint` fails that test. A warning never fails a test. A value that is only a YAML comment (`source: # TODO`), `null`, `~`, or a placeholder counts as missing. A placeholder is a whole value, trimmed and in any case, of `todo`, `tbd`, `fixme`, `xxx`, `placeholder`, `<placeholder>`, `n/a`, a run of dashes, a run of question marks, or an ellipsis, optionally followed by a trailing `.`, `:` or `!`. Real text that starts with one of those (`TODO: write the opening`) counts as present, and so do `none` and a quoted value that happens to start with `#` (`source: "# literal"`).
|
|
204
210
|
|
|
205
211
|
| Test | Fails when |
|
|
206
212
|
|---|---|
|
|
@@ -210,7 +216,7 @@ Each row lists every condition under which `hyperspec lint` fails that test. A w
|
|
|
210
216
|
| 4 every field says where it came from and who wrote it | a decision or requirement without `source` or `author`; a decision whose `chosen_by` is not human or agent |
|
|
211
217
|
| 5 negative space is specified | `rejects` missing or empty; a `rejects` item that is not a plain string |
|
|
212
218
|
| 6 examples outrank adjectives | `examples` missing or empty; an example without `path` or `why`; a `path` that is not an http(s) URL and does not exist, is a folder, or is the spec itself, read relative to the spec or as an absolute path |
|
|
213
|
-
| 7 a stranger can resume it | `resume.next_action` missing; a `next_action` that is only a no-action word (`continue`, `follow up`, `tbd`, `todo`, `keep going`, `pick it back up`, `n/a`, `none`); a `next_action` that says `as discussed` or `as mentioned earlier` or `above`. Those pointers in the body are a warning, and so
|
|
219
|
+
| 7 a stranger can resume it | `resume.next_action` missing; a `next_action` that is only a no-action word (`continue`, `follow up`, `tbd`, `todo`, `keep going`, `pick it back up`, `n/a`, `none`); a `next_action` that says `as discussed` or `as mentioned earlier` or `above`. Those pointers in the body are a warning, and so are a `hyperspec` version and a `profile` this linter does not know |
|
|
214
220
|
| 8 its adopters can push back on it | `feedback.issues` or `feedback.fork` missing |
|
|
215
221
|
| 9 it improves itself | `improvement.ledger` missing; a ledger path that exists and is not a readable file; if the ledger file exists, a line that is not a JSON object, a `verdict` outside one-shot, improved or not-improved, `improved` without `change`, `not-improved` without `reason`. A declared ledger that does not exist yet is a warning |
|
|
216
222
|
|
|
@@ -233,6 +239,12 @@ Every run of a skill that works from a hyperspec writes one line to the ledger n
|
|
|
233
239
|
|
|
234
240
|
Silence is not a verdict. A run that learned nothing has to say so and why, and a ledger line with none of the three verdicts fails the ninth test.
|
|
235
241
|
|
|
242
|
+
## Profiles
|
|
243
|
+
|
|
244
|
+
A profile adds the rules for one kind of work on top of the nine tests. A spec opts in with a top-level `profile:` naming it. A profile never adds a tenth test: every finding it raises reports under one of the nine, with an id that starts with the profile's name, and the score stays out of nine. `lint` prints one more line for a profiled spec, how many of the profile's blocks are complete. A `profile` this linter does not know is a warning under test 7, and none of its rules are checked.
|
|
245
|
+
|
|
246
|
+
One profile ships: `writing`, for essays, chapters, letters, stories and anything else an agent drafts for a person to read. Its blocks, its fields, which test each rule reports under, and `hyperspec init --profile writing` are in [WRITING.md](WRITING.md).
|
|
247
|
+
|
|
236
248
|
## Recipes
|
|
237
249
|
|
|
238
250
|
A hyperspec says what the work must be. A recipe records what one output was made from, so the output can be checked, made again with one change, and graded against the version before it. hyperspec never calls a model. Every step that needs one is a command the caller supplies: a runner for stages and a doctor for grading.
|
|
@@ -246,8 +258,8 @@ A recipe sits beside its output as `<output>.recipe.json`: JSON with a two-space
|
|
|
246
258
|
"recipe": "0.1",
|
|
247
259
|
"created": "2026-09-28T18:00:00.000Z",
|
|
248
260
|
"output": { "path": "essay.md", "sha256": "<hex>" },
|
|
249
|
-
"factory": { "name": "
|
|
250
|
-
"spec": { "path": "essay.hyperspec.md", "sha256": "<hex>", "authors": { "audience": "
|
|
261
|
+
"factory": { "name": "my-factory", "version": "0.3.0" },
|
|
262
|
+
"spec": { "path": "essay.hyperspec.md", "sha256": "<hex>", "authors": { "audience": "example-author", "length": "agent:claude" } },
|
|
251
263
|
"inputs": [
|
|
252
264
|
{ "name": "call", "path": "materials/call.md", "sha256": "<hex>", "order": 1 }
|
|
253
265
|
],
|
|
@@ -261,8 +273,8 @@ A recipe sits beside its output as `<output>.recipe.json`: JSON with a two-space
|
|
|
261
273
|
"verdict": { "station": "outline-has-claim-chain", "pass": true, "note": "" }
|
|
262
274
|
}
|
|
263
275
|
],
|
|
264
|
-
"clicker": "
|
|
265
|
-
"approver": "
|
|
276
|
+
"clicker": "example-author",
|
|
277
|
+
"approver": "example-author",
|
|
266
278
|
"parent": null,
|
|
267
279
|
"change": null
|
|
268
280
|
}
|
package/WRITING.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
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
|
+
[Materials labels](#materials-labels)).
|
|
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 materials
|
|
108
|
+
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
|
+
produced_by: example-author
|
|
163
|
+
captured: "2026-09-12"
|
|
164
|
+
how: voice memo, transcribed
|
|
165
|
+
trust: raw # raw | considered | verified
|
|
166
|
+
check:
|
|
167
|
+
station: every segment of every material carries a label from the closed set
|
|
168
|
+
source: capture step
|
|
169
|
+
author: agent:claude
|
|
170
|
+
dna:
|
|
171
|
+
writer: example-author
|
|
172
|
+
scope:
|
|
173
|
+
form: essay
|
|
174
|
+
audience: new managers
|
|
175
|
+
purpose: teach
|
|
176
|
+
rules: style-rules.md # your style rules file, the always-on layer
|
|
177
|
+
goldens: # at least one
|
|
178
|
+
- path: goldens/opening.md
|
|
179
|
+
why: one plain claim, then a second sentence that turns it into something to do
|
|
180
|
+
check:
|
|
181
|
+
rubric: blind lineup within this scope
|
|
182
|
+
source: goldens marked on the review page
|
|
183
|
+
author: example-author
|
|
184
|
+
persona:
|
|
185
|
+
identity: self # self | role:<name> | character:<id>
|
|
186
|
+
stance: mentor # peer | mentor | witness | guide; anything else warns
|
|
187
|
+
may_assert:
|
|
188
|
+
- what the author did in their own first one-on-ones
|
|
189
|
+
will_not_say:
|
|
190
|
+
- the name of anyone on the author's team
|
|
191
|
+
facts_from: sources # must be exactly "sources"
|
|
192
|
+
check:
|
|
193
|
+
rubric: persona-consistency judge
|
|
194
|
+
source: persona interview
|
|
195
|
+
author: example-author
|
|
196
|
+
audience:
|
|
197
|
+
who: someone in their first three months of managing
|
|
198
|
+
funnel_now: has a first one-on-one on the calendar this week
|
|
199
|
+
knows:
|
|
200
|
+
- one-on-one
|
|
201
|
+
- report
|
|
202
|
+
believes_now: a one-on-one is where a manager catches up on the work
|
|
203
|
+
wants: a plan for the first meeting
|
|
204
|
+
reads_on: a phone, in the ten minutes before the meeting
|
|
205
|
+
reader: person # person | agent
|
|
206
|
+
check:
|
|
207
|
+
station: term check against knows
|
|
208
|
+
rubric: simulated reader reports where it got lost
|
|
209
|
+
source: audience interview
|
|
210
|
+
author: example-author
|
|
211
|
+
goal:
|
|
212
|
+
from: plans to run the meeting from their own list
|
|
213
|
+
to: hands the meeting to the report
|
|
214
|
+
next_if_worked: copies the three questions into the invite
|
|
215
|
+
change:
|
|
216
|
+
kind: action # belief | action | feeling
|
|
217
|
+
text: the reader asks the three questions and waits
|
|
218
|
+
conditions: # 5 to 10 distinct ids of top-level requirements, each once
|
|
219
|
+
- r1
|
|
220
|
+
- r2
|
|
221
|
+
- r3
|
|
222
|
+
- r4
|
|
223
|
+
- r5
|
|
224
|
+
check:
|
|
225
|
+
rubric: grade the draft against every condition
|
|
226
|
+
source: goal interview
|
|
227
|
+
author: example-author
|
|
228
|
+
form:
|
|
229
|
+
name: essay # open set
|
|
230
|
+
length: # whole numbers, at least 1, min no more than max
|
|
231
|
+
min: 700
|
|
232
|
+
max: 1100
|
|
233
|
+
unit: words
|
|
234
|
+
required_parts:
|
|
235
|
+
- an opening that states the claim
|
|
236
|
+
- the three questions
|
|
237
|
+
- a close
|
|
238
|
+
stations: # may be empty
|
|
239
|
+
- the three questions render as a numbered list
|
|
240
|
+
check:
|
|
241
|
+
station: structure and length check
|
|
242
|
+
source: form decision
|
|
243
|
+
author: example-author
|
|
244
|
+
spine:
|
|
245
|
+
kind: primer # open set
|
|
246
|
+
claims: # 3 to 7, in order, each with its own id
|
|
247
|
+
- id: c1
|
|
248
|
+
text: the first one-on-one is the one meeting the report should set the agenda for
|
|
249
|
+
materials: # ids from materials.items; voice-memo#segment is accepted
|
|
250
|
+
- voice-memo
|
|
251
|
+
- id: c2
|
|
252
|
+
text: status belongs in the tracker
|
|
253
|
+
materials:
|
|
254
|
+
- voice-memo
|
|
255
|
+
- id: c3
|
|
256
|
+
text: three questions are enough to hand the meeting over
|
|
257
|
+
materials:
|
|
258
|
+
- voice-memo
|
|
259
|
+
check:
|
|
260
|
+
rubric: each claim lands, in order, and nothing is argued outside the chain
|
|
261
|
+
source: spine interview
|
|
262
|
+
author: example-author
|
|
263
|
+
sources:
|
|
264
|
+
ledger: claims.jsonl # need not exist before drafting
|
|
265
|
+
unsourced_claim: fail # fail | warn; warn is reported as a warning
|
|
266
|
+
check:
|
|
267
|
+
station: every factual claim points at a source span
|
|
268
|
+
source: sourcing pass
|
|
269
|
+
author: agent:claude
|
|
270
|
+
characters: # required when fiction: true; ids unique
|
|
271
|
+
- id: ines
|
|
272
|
+
entity: world/ines.json # optional; if given, the file must exist
|
|
273
|
+
speech:
|
|
274
|
+
uses:
|
|
275
|
+
- instructions in the imperative
|
|
276
|
+
never:
|
|
277
|
+
- an apology in words
|
|
278
|
+
rhythm: short, flat sentences # optional
|
|
279
|
+
wants: to leave the bakery in hands she trusts
|
|
280
|
+
fears: that he will stay in a trade with no bakery to do it in
|
|
281
|
+
hides: that she sold the bakery two weeks ago
|
|
282
|
+
knowledge: # at least one; each entry needs by and knows
|
|
283
|
+
- by: scene-1
|
|
284
|
+
knows: the sale closes on Friday
|
|
285
|
+
relationships: # optional
|
|
286
|
+
- to: theo
|
|
287
|
+
how: gives him instructions where another person would give praise
|
|
288
|
+
arc_state: has let go of the bakery, and has not yet let go of him
|
|
289
|
+
golden_lines: # at least one
|
|
290
|
+
- Flour first. Then you can talk.
|
|
291
|
+
rejected_lines: # at least one, and none of them also golden
|
|
292
|
+
- I'm so sorry I didn't tell you sooner.
|
|
293
|
+
check:
|
|
294
|
+
rubric: blind attribution test, knowledge-leak check, consistency against golden and rejected lines
|
|
295
|
+
source: notes.md
|
|
296
|
+
author: example-author
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Every block, and every character, carries `check` (with `station:` for a deterministic check or
|
|
300
|
+
`rubric:` for what a grader applies, the same shape a requirement's check has), `source` and
|
|
301
|
+
`author`. `characters` is a list, so there each entry carries its own.
|
|
302
|
+
|
|
303
|
+
Write every map in block style, one key per line, as above. The YAML reader hyperspec uses reads
|
|
304
|
+
a flow list such as `[r1, r2]`, but it reads an inline map such as `{ station: ... }` as a plain
|
|
305
|
+
string, and a flow list followed by a comment on the same line as a string too.
|
|
306
|
+
|
|
307
|
+
A placeholder counts as missing, here and everywhere else in a hyperspec, so a scaffolded field
|
|
308
|
+
cannot pass a presence check. A placeholder is a whole value, trimmed and in any case, of `todo`,
|
|
309
|
+
`tbd`, `fixme`, `xxx`, `placeholder`, `<placeholder>`, `n/a`, a run of dashes, a run of question
|
|
310
|
+
marks, or an ellipsis, optionally followed by a trailing `.`, `:` or `!`. Real text that starts
|
|
311
|
+
with one of those, such as `TODO: write the opening`, counts as present, and so does `none`.
|
|
312
|
+
|
|
313
|
+
## The test mapping
|
|
314
|
+
|
|
315
|
+
Each row lists what the writing profile adds to that test. The core conditions in
|
|
316
|
+
[SPEC.md](SPEC.md#the-test-to-field-map) still apply alongside them.
|
|
317
|
+
|
|
318
|
+
| Test | A writing spec fails it when |
|
|
319
|
+
|---|---|
|
|
320
|
+
| 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 `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
|
|
321
|
+
| 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 |
|
|
322
|
+
| 3 every requirement names its check | a block or a character has no `check` with a `station` or a `rubric` |
|
|
323
|
+
| 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` |
|
|
324
|
+
| 5 negative space is specified | `persona.will_not_say` is empty; `persona.facts_from` is anything other than `sources` |
|
|
325
|
+
| 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) |
|
|
326
|
+
| 7 a stranger can resume it | `writing.progress` exists. An unknown `profile:` is a warning |
|
|
327
|
+
| 8 its adopters can push back on it | nothing further; the core rule applies |
|
|
328
|
+
| 9 it improves itself | nothing further; the core rule applies |
|
|
329
|
+
|
|
330
|
+
## Closed sets
|
|
331
|
+
|
|
332
|
+
| Field | Allowed values |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `materials.items[].trust` | `raw`, `considered`, `verified` |
|
|
335
|
+
| `persona.identity` | `self`, `role:<name>`, `character:<id>` |
|
|
336
|
+
| `persona.stance` | `peer`, `mentor`, `witness`, `guide`; any other value warns |
|
|
337
|
+
| `persona.facts_from` | `sources` |
|
|
338
|
+
| `audience.reader` | `person`, `agent` |
|
|
339
|
+
| `goal.change.kind` | `belief`, `action`, `feeling` |
|
|
340
|
+
| `sources.unsourced_claim` | `fail`, `warn` |
|
|
341
|
+
| `fiction` | `true`, `false`; absent means `false` |
|
|
342
|
+
|
|
343
|
+
`form.name` and `spine.kind` are open: name the form and the kind of argument in your own words.
|
|
344
|
+
|
|
345
|
+
## Materials labels
|
|
346
|
+
|
|
347
|
+
Before a material is used, it is split into segments, and each segment gets one of seven labels.
|
|
348
|
+
The label decides what the segment may become in the draft.
|
|
349
|
+
|
|
350
|
+
| Label | Means | May be used as |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| `claim` | a statement of fact about the world | only with a source, or as the author's own claim said as such |
|
|
353
|
+
| `story` | something that happened, told by someone who was there | testimony, with the teller named |
|
|
354
|
+
| `quote` | words someone said, verbatim | quoted exactly, never paraphrased inside quotation marks |
|
|
355
|
+
| `stance` | an opinion or conviction | the author's position |
|
|
356
|
+
| `question` | something open | a prompt for the interview, never an assertion |
|
|
357
|
+
| `aside` | true but off the thread | held back unless the spine needs it |
|
|
358
|
+
| `private` | not for this audience | never used; kept for context |
|
|
359
|
+
|
|
360
|
+
The linter defines the set once, as `MATERIAL_LABELS` in `src/writing.mjs`. This release checks
|
|
361
|
+
the materials list itself; it does not yet read segment files or check their labels. A spine
|
|
362
|
+
claim may point at a segment as `m1#segment`, and today only the material id before the `#` is
|
|
363
|
+
checked.
|
|
364
|
+
|
|
365
|
+
## Deferring a block
|
|
366
|
+
|
|
367
|
+
A block can be deferred, never silently missing. A required block that is absent fails test 1
|
|
368
|
+
unless a decision with the id `writing-<block>` stands in for it:
|
|
369
|
+
|
|
370
|
+
```yaml
|
|
371
|
+
decisions:
|
|
372
|
+
- id: writing-audience
|
|
373
|
+
state: open
|
|
374
|
+
question: who reads this, and what do they already believe?
|
|
375
|
+
source: kickoff call
|
|
376
|
+
author: agent:claude
|
|
377
|
+
chosen_by: agent
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
- **`open`** defers the block to a question only a person can answer. The spec is blocked on it,
|
|
381
|
+
exactly like any open decision: `lint` exits 3 once every test passes.
|
|
382
|
+
- **`delegated` with a `rule`** defers the block to a standing rule the agent follows. The spec
|
|
383
|
+
can pass. A delegated decision with no `rule` defers nothing, so the missing block still fails.
|
|
384
|
+
|
|
385
|
+
Either way the deferred block does not count toward `writing: k/9 blocks complete`.
|
|
386
|
+
|
|
387
|
+
## Starting a writing spec
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npx @supersuit/hyperspec init essay.hyperspec.md --profile writing --title "Your title" --form essay
|
|
391
|
+
npx @supersuit/hyperspec init story.hyperspec.md --profile writing --form "short story" --fiction
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The skeleton shows every required block in schema order with every field present as a `TODO`
|
|
395
|
+
placeholder. `dna`, `persona`, `audience` and `goal` also carry an open decision whose question
|
|
396
|
+
says what you have to answer before the placeholder means anything. `--form` sets both `kind:`
|
|
397
|
+
and `writing.form.name`, and defaults to `essay`. `--fiction` sets `fiction: true` and adds one
|
|
398
|
+
character with the same treatment. The skeleton never passes: it lints `fail`, with
|
|
399
|
+
`writing: 1/9 blocks complete` (or `0/9` with `--fiction`), until the placeholders and the open
|
|
400
|
+
decisions are replaced with real content.
|
|
401
|
+
|
|
402
|
+
`init` refuses, with exit 2 and a plain message, anything it would otherwise have to ignore:
|
|
403
|
+
`--profile` with no value or one this linter does not know, `--fiction` or `--form` without
|
|
404
|
+
`--profile writing`, `--kind` with it (the form sets the kind), and a file in a folder that does
|
|
405
|
+
not exist.
|
|
406
|
+
|
|
407
|
+
## Worked examples
|
|
408
|
+
|
|
409
|
+
Two complete specs ship in [`examples/writing/`](examples/writing/), each with every file it
|
|
410
|
+
names:
|
|
411
|
+
|
|
412
|
+
- `essay.hyperspec.md`: an essay for new managers on running a first one-on-one. Three materials
|
|
413
|
+
at three trust levels, scoped DNA with two annotated goldens, a four-claim spine.
|
|
414
|
+
- `story.hyperspec.md`: a short story, `fiction: true`, narrated by one of its two characters.
|
|
415
|
+
Each character has speech rules, a knowledge timeline by scene, and golden and rejected lines
|
|
416
|
+
in a voice you can tell apart from the other's.
|
|
417
|
+
|
|
418
|
+
Both lint `pass (9/9)` with `writing: 9/9 blocks complete` and no findings. A test runs them on
|
|
419
|
+
every release, so they cannot drift from the linter.
|
|
420
|
+
|
|
421
|
+
## What later versions add
|
|
422
|
+
|
|
423
|
+
This release is the schema and its lint. Later versions build on it in order: marking materials
|
|
424
|
+
(a brain dump or transcript in, labeled segments out, with the labels above enforced), scoped
|
|
425
|
+
DNA with annotated goldens filed by form, audience and purpose, and the stations themselves,
|
|
426
|
+
running the checks each block names and grading drafts against the goal.
|