@supersuit/hyperspec 0.1.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +105 -4
  3. package/SPEC.md +205 -13
  4. package/WRITING.md +426 -0
  5. package/bin/hyperspec.mjs +322 -2
  6. package/examples/minimal.hyperspec.md +2 -2
  7. package/examples/recipe/doctor.mjs +11 -0
  8. package/examples/recipe/essay.hyperspec.md +50 -0
  9. package/examples/recipe/factory.mjs +29 -0
  10. package/examples/recipe/materials/call-2.md +2 -0
  11. package/examples/recipe/materials/call.md +3 -0
  12. package/examples/recipe/materials/notes.md +3 -0
  13. package/examples/recipe/runner.mjs +20 -0
  14. package/examples/recipe/runs.jsonl +0 -0
  15. package/examples/recipe/stages.mjs +31 -0
  16. package/examples/writing/essay/goldens/close.md +2 -0
  17. package/examples/writing/essay/goldens/opening.md +2 -0
  18. package/examples/writing/essay/materials/interview-notes.md +12 -0
  19. package/examples/writing/essay/materials/team-survey.md +7 -0
  20. package/examples/writing/essay/materials/voice-memo.md +18 -0
  21. package/examples/writing/essay/runs.jsonl +0 -0
  22. package/examples/writing/essay.hyperspec.md +217 -0
  23. package/examples/writing/story/goldens/dialogue.md +3 -0
  24. package/examples/writing/story/goldens/opening.md +3 -0
  25. package/examples/writing/story/materials/bakery-visit.md +8 -0
  26. package/examples/writing/story/materials/notes.md +14 -0
  27. package/examples/writing/story/materials/scene-list.md +7 -0
  28. package/examples/writing/story/runs.jsonl +0 -0
  29. package/examples/writing/story.hyperspec.md +285 -0
  30. package/examples/writing/style-rules.md +19 -0
  31. package/package.json +8 -2
  32. package/runs.jsonl +0 -0
  33. package/src/blobs.mjs +77 -0
  34. package/src/compare.mjs +189 -0
  35. package/src/fsutil.mjs +33 -0
  36. package/src/hash.mjs +26 -0
  37. package/src/placeholder.mjs +20 -0
  38. package/src/profiles.mjs +50 -0
  39. package/src/recipe.mjs +164 -0
  40. package/src/regenerate.mjs +318 -0
  41. package/src/reproduce.mjs +156 -0
  42. package/src/rules.mjs +58 -19
  43. package/src/score.mjs +7 -1
  44. package/src/template.mjs +15 -3
  45. package/src/writer.mjs +125 -0
  46. package/src/writing-fields.mjs +317 -0
  47. package/src/writing-template.mjs +192 -0
  48. package/src/writing.mjs +181 -0
@@ -0,0 +1,217 @@
1
+ ---
2
+ hyperspec: "0.1"
3
+ title: Hand your first one-on-one to the person you manage
4
+ kind: essay
5
+ profile: writing
6
+ decisions:
7
+ - id: kind
8
+ state: decided
9
+ value: an essay of 700 to 1,100 words for a newsletter read by people in their first year of managing
10
+ source: essay/materials/voice-memo.md
11
+ author: example-author
12
+ chosen_by: human
13
+ - id: agenda-card
14
+ state: decided
15
+ value: the essay ends on the three questions, written so a reader can copy them onto a card
16
+ source: essay/materials/voice-memo.md, the three questions
17
+ author: example-author
18
+ chosen_by: human
19
+ - id: publish-venue
20
+ state: delegated
21
+ rule: publish where the audience block's reads_on line says the reader already is, and nowhere else
22
+ source: publishing checklist
23
+ author: agent:claude
24
+ chosen_by: agent
25
+ requirements:
26
+ - id: r1
27
+ text: the opening line tells the reader who sets the agenda of a first one-on-one
28
+ fails_when: a reader shown only the first two sentences cannot say who should set the agenda
29
+ check:
30
+ rubric: show the simulated reader the first two sentences and ask who sets the agenda; pass only on "the report"
31
+ source: essay/goldens/opening.md
32
+ author: example-author
33
+ - id: r2
34
+ text: the three questions appear word for word as the voice memo states them
35
+ fails_when: any of the three questions differs from essay/materials/voice-memo.md by a word
36
+ check:
37
+ station: verbatim match of each question against the voice memo
38
+ source: essay/materials/voice-memo.md
39
+ author: example-author
40
+ - id: r3
41
+ text: every survey figure in the draft matches the verified survey summary
42
+ fails_when: a figure in the draft has no entry in the claims ledger pointing at essay/materials/team-survey.md, or differs from it
43
+ check:
44
+ station: every factual claim in the ledger points at a source span
45
+ source: sourcing pass
46
+ author: agent:claude
47
+ - id: r4
48
+ text: the draft argues only the four claims in the spine, in order
49
+ fails_when: a paragraph advances a point that traces to none of c1 to c4, or c3 lands before c2
50
+ check:
51
+ rubric: map each paragraph to a spine claim; fail on any paragraph that maps to none or out of order
52
+ source: spine interview
53
+ author: example-author
54
+ - id: r5
55
+ text: the draft tells the reader what to do with silence in the meeting
56
+ fails_when: the draft never says to wait after asking a question
57
+ check:
58
+ rubric: ask the simulated reader what to do after asking the first question; pass only on "wait"
59
+ source: essay/materials/interview-notes.md
60
+ author: example-author
61
+ - id: r6
62
+ text: the draft stays inside its length envelope
63
+ fails_when: the word count is under 700 or over 1,100
64
+ check:
65
+ station: word count against form.length
66
+ source: form decision
67
+ author: example-author
68
+ rejects:
69
+ - a list of more than three questions
70
+ - advice to use the one-on-one for project status
71
+ - any claim about what most managers do that the survey does not support
72
+ - the walking one-on-one aside from the voice memo
73
+ examples:
74
+ - path: essay/goldens/opening.md
75
+ why: the claim lands in the first sentence, and the second sentence turns it into an instruction
76
+ resume:
77
+ next_action: mark the three materials into labeled segments, then outline the four spine claims against the form's required parts
78
+ feedback:
79
+ issues: https://github.com/SupersuitUp/hyperspec/issues
80
+ fork: MIT; fork it for your own purposes
81
+ improvement:
82
+ ledger: essay/runs.jsonl
83
+ writing:
84
+ materials:
85
+ items:
86
+ - id: voice-memo
87
+ path: essay/materials/voice-memo.md
88
+ produced_by: example-author
89
+ captured: "2026-09-12"
90
+ how: voice memo, transcribed
91
+ trust: raw
92
+ - id: interview
93
+ path: essay/materials/interview-notes.md
94
+ produced_by: example-author
95
+ captured: "2026-09-15"
96
+ how: notes taken during a call, reviewed by the person interviewed
97
+ trust: considered
98
+ - id: survey
99
+ path: essay/materials/team-survey.md
100
+ produced_by: example-author
101
+ captured: "2026-05-30"
102
+ how: survey summary, figures checked against the raw export by a second person
103
+ trust: verified
104
+ check:
105
+ station: every segment of every material carries a label from the closed set
106
+ source: capture step
107
+ author: agent:claude
108
+ dna:
109
+ writer: example-author
110
+ scope:
111
+ form: essay
112
+ audience: new managers
113
+ purpose: teach
114
+ rules: style-rules.md
115
+ goldens:
116
+ - path: essay/goldens/opening.md
117
+ why: one plain claim, then a second sentence that turns it into something to do
118
+ - path: essay/goldens/close.md
119
+ why: ends on an instruction and gives the reason for it in the same sentence
120
+ check:
121
+ rubric: blind lineup within this scope; a judge shown the generated opening beside the two goldens cannot pick it out
122
+ source: goldens marked on the review page
123
+ author: example-author
124
+ persona:
125
+ identity: self
126
+ stance: mentor
127
+ may_assert:
128
+ - what the author did in their own first one-on-ones and what happened
129
+ - the three questions the author uses now
130
+ will_not_say:
131
+ - a claim about what most managers do, beyond the survey's own figures
132
+ - the name of anyone on the author's team
133
+ facts_from: sources
134
+ check:
135
+ rubric: persona-consistency judge; the mentor stance holds, and no fact appears that is not in the claims ledger
136
+ source: persona interview
137
+ author: example-author
138
+ audience:
139
+ who: someone in their first three months of managing, who was promoted from the team they now lead
140
+ funnel_now: has a first one-on-one with a new report on the calendar this week
141
+ knows:
142
+ - one-on-one
143
+ - report
144
+ - tracker
145
+ believes_now: a one-on-one is where a manager catches up on how the work is going
146
+ wants: a plan for the first meeting that will not waste either person's half hour
147
+ reads_on: a phone, in the ten minutes before the meeting
148
+ reader: person
149
+ check:
150
+ station: term check against knows; any other term is defined on first use
151
+ rubric: simulated reader reports where it got lost and where it stopped reading
152
+ source: audience interview
153
+ author: example-author
154
+ goal:
155
+ from: plans to run the first one-on-one from their own list
156
+ to: hands the first one-on-one to the report and asks the three questions
157
+ next_if_worked: copies the three questions into their calendar invite
158
+ change:
159
+ kind: action
160
+ text: the reader asks the three questions in their next one-on-one and waits after each
161
+ conditions: [r1, r2, r3, r4, r5, r6]
162
+ check:
163
+ rubric: the doctor grades the draft against every condition; the simulated reader is asked whether it would copy the questions now
164
+ source: goal interview
165
+ author: example-author
166
+ form:
167
+ name: essay
168
+ length:
169
+ min: 700
170
+ max: 1100
171
+ unit: words
172
+ required_parts:
173
+ - an opening that states the claim
174
+ - the story of the author's first one-on-one
175
+ - the three questions
176
+ - what to do with the answers
177
+ - a close the reader can act on
178
+ stations:
179
+ - the three questions render as a numbered list
180
+ check:
181
+ station: structure and length check against required_parts and length
182
+ source: form decision
183
+ author: example-author
184
+ spine:
185
+ kind: primer
186
+ claims:
187
+ - id: c1
188
+ text: the first one-on-one is the one meeting where the report should set the agenda
189
+ materials: [voice-memo, interview]
190
+ - id: c2
191
+ text: status belongs in the tracker, and a one-on-one spent on it teaches the manager nothing new
192
+ materials: [voice-memo, survey]
193
+ - id: c3
194
+ text: three questions are enough to hand the meeting over
195
+ materials: [voice-memo]
196
+ - id: c4
197
+ text: the answer worth having comes after a silence the manager does not fill
198
+ materials: [interview]
199
+ check:
200
+ rubric: each claim lands, in order, and the draft argues nothing outside the chain
201
+ source: spine interview
202
+ author: example-author
203
+ sources:
204
+ ledger: essay/claims.jsonl
205
+ unsourced_claim: fail
206
+ check:
207
+ station: every factual claim in the ledger points at a source span; every quote matches its source verbatim
208
+ source: sourcing pass
209
+ author: agent:claude
210
+ fiction: false
211
+ ---
212
+
213
+ # Hand your first one-on-one to the person you manage
214
+
215
+ A worked example of the writing profile: an essay for new managers, specified before a word of
216
+ it is drafted. Every file this spec names ships beside it. `essay/claims.jsonl` does not exist
217
+ yet, because the claims ledger is written during drafting.
@@ -0,0 +1,3 @@
1
+ "You're late," she said. I wasn't. She knew I wasn't.
2
+ "The bus," I said anyway, because that is how we start.
3
+ "Flour first. Then you can talk."
@@ -0,0 +1,3 @@
1
+ The alarm at the bakery goes at 3:20, but Ines is always there first, so I have never once
2
+ heard it. What I hear is the mixer. It has a knock in it, a slow one, like it is trying to
3
+ remember something.
@@ -0,0 +1,8 @@
1
+ Notes from a morning spent at a working bakery, 3:30 to 7:30, with the owner's permission.
2
+ Considered: the owner read these notes and corrected the proofing times.
3
+
4
+ - First mix at 3:45. Rye sourdough proofs about three hours at room temperature in winter.
5
+ - Trays are turned halfway through the bake because the back left of a deck oven runs hot.
6
+ - The doors open to customers at 7:00. The first person in is usually a regular.
7
+ - The owner does not talk while shaping. Talking happens at the mixer and at the till.
8
+ - Flour is weighed, never scooped. Water temperature is checked every batch.
@@ -0,0 +1,14 @@
1
+ Author's notes for the story, typed over two evenings. Raw thinking.
2
+
3
+ A bakery on its last morning before the sale closes. Two people: the owner, who has run it for
4
+ thirty-one years, and the apprentice she took on at sixteen, now nineteen. She has not told him
5
+ it is sold. He has not told her he got into a baking school in another city and leaves in the
6
+ autumn. Each thinks they are protecting the other.
7
+
8
+ The story is four scenes, one morning, 3:40 to 7:00. The oven has a noise. The rye is the thing
9
+ she has never let him do alone.
10
+
11
+ What it is about, I think: a craft outlives the room it was practiced in. And: people who love
12
+ each other in a work setting say it through the work.
13
+
14
+ The last line should be about bread, never about feelings.
@@ -0,0 +1,7 @@
1
+ Scene list, agreed with the editor before drafting. Considered.
2
+
3
+ - scene-1, 3:40: Theo arrives. Ines is already mixing. The oven noise. No one says anything real.
4
+ - scene-2, 4:30: shaping. Ines lets Theo shape the rye for the first time, and does not say why.
5
+ - scene-3, 5:50: the bake. Ines tells Theo the bakery is sold, while turning a tray.
6
+ - scene-4, 6:55: before the doors open. Theo tells Ines about the school. She hands him the
7
+ starter jar.
File without changes
@@ -0,0 +1,285 @@
1
+ ---
2
+ hyperspec: "0.1"
3
+ title: The Rye
4
+ kind: short story
5
+ profile: writing
6
+ decisions:
7
+ - id: point-of-view
8
+ state: decided
9
+ value: first person, told by Theo, in the past tense
10
+ source: story/materials/notes.md
11
+ author: example-author
12
+ chosen_by: human
13
+ - id: time-span
14
+ state: decided
15
+ value: one morning, 3:40 to 7:00, in the four scenes of the scene list and in that order
16
+ source: story/materials/scene-list.md
17
+ author: example-author
18
+ chosen_by: human
19
+ - id: bakery-name
20
+ state: delegated
21
+ rule: the bakery is never named; it is always "the bakery" or "Ines's"
22
+ source: editor's note on the scene list
23
+ author: agent:claude
24
+ chosen_by: agent
25
+ requirements:
26
+ - id: r1
27
+ text: the two voices cannot be confused
28
+ fails_when: a judge shown ten lines of dialogue with the speaker hidden names the wrong speaker for more than one of them
29
+ check:
30
+ rubric: blind attribution test across both characters, ten lines drawn at random from the draft
31
+ source: story/materials/notes.md
32
+ author: example-author
33
+ - id: r2
34
+ text: no one says what they could not know yet
35
+ fails_when: Theo mentions the sale before scene-3, or Ines mentions the school before scene-4
36
+ check:
37
+ station: knowledge-leak check of every line against each character's knowledge timeline
38
+ source: story/materials/scene-list.md
39
+ author: example-author
40
+ - id: r3
41
+ text: the bakery's process matches a real working morning
42
+ fails_when: a proofing time, the tray turn, or the opening time differs from story/materials/bakery-visit.md
43
+ check:
44
+ station: every process detail in the claims ledger points at a span of the bakery visit notes
45
+ source: story/materials/bakery-visit.md
46
+ author: agent:claude
47
+ - id: r4
48
+ text: the story is the four scenes of the scene list, in order
49
+ fails_when: the draft has more or fewer than four scenes, or they run out of the scene list's order
50
+ check:
51
+ station: structure check against story/materials/scene-list.md
52
+ source: story/materials/scene-list.md
53
+ author: example-author
54
+ - id: r5
55
+ text: the last line is about bread
56
+ fails_when: the final sentence names a feeling, or does not mention bread, dough, flour or the starter
57
+ check:
58
+ rubric: read the final sentence; fail if it names an emotion or mentions none of bread, dough, flour or the starter
59
+ source: story/materials/notes.md
60
+ author: example-author
61
+ - id: r6
62
+ text: the story stays inside its length envelope
63
+ fails_when: the word count is under 2,500 or over 4,000
64
+ check:
65
+ station: word count against form.length
66
+ source: editor's brief
67
+ author: example-author
68
+ rejects:
69
+ - either character saying out loud that they love or will miss the other
70
+ - a flashback outside the one morning
71
+ - a scene where the oven noise is explained
72
+ - an ending that tells the reader what the starter jar means
73
+ examples:
74
+ - path: story/goldens/dialogue.md
75
+ why: three short lines carry a ritual both characters know, without either of them naming it
76
+ resume:
77
+ next_action: mark the three materials into labeled segments, then draft scene-1 from the scene list with Theo's knowledge as of scene-1
78
+ feedback:
79
+ issues: https://github.com/SupersuitUp/hyperspec/issues
80
+ fork: MIT; fork it for your own purposes
81
+ improvement:
82
+ ledger: story/runs.jsonl
83
+ writing:
84
+ materials:
85
+ items:
86
+ - id: notes
87
+ path: story/materials/notes.md
88
+ produced_by: example-author
89
+ captured: "2026-08-20"
90
+ how: typed notes
91
+ trust: raw
92
+ - id: bakery-visit
93
+ path: story/materials/bakery-visit.md
94
+ produced_by: example-author
95
+ captured: "2026-08-28"
96
+ how: notes taken on site, corrected by the bakery owner afterwards
97
+ trust: considered
98
+ - id: scene-list
99
+ path: story/materials/scene-list.md
100
+ produced_by: example-author
101
+ captured: "2026-09-02"
102
+ how: scene list agreed with the editor
103
+ trust: considered
104
+ check:
105
+ station: every segment of every material carries a label from the closed set
106
+ source: capture step
107
+ author: agent:claude
108
+ dna:
109
+ writer: example-author
110
+ scope:
111
+ form: short story
112
+ audience: literary magazine readers
113
+ purpose: move
114
+ rules: style-rules.md
115
+ goldens:
116
+ - path: story/goldens/opening.md
117
+ why: the narrator's voice arrives through one concrete sound, and the sentence about the mixer tells you how he sees the world
118
+ - path: story/goldens/dialogue.md
119
+ why: the narration between lines says only what Theo notices, never what Ines feels
120
+ check:
121
+ rubric: blind lineup within this scope; a judge shown a generated passage beside the two goldens cannot pick it out
122
+ source: goldens marked on the review page
123
+ author: example-author
124
+ persona:
125
+ identity: character:theo
126
+ stance: witness
127
+ may_assert:
128
+ - what Theo sees, hears and does in the bakery that morning
129
+ - what Theo knows as of the current scene, per his knowledge timeline
130
+ will_not_say:
131
+ - what Ines is thinking or feeling
132
+ - anything Theo does not know yet at that point in the morning
133
+ facts_from: sources
134
+ check:
135
+ rubric: persona-consistency judge; Theo's narration stays a witness's, and no process detail appears that is not in the claims ledger
136
+ source: persona interview
137
+ author: example-author
138
+ audience:
139
+ who: readers of a quarterly literary magazine who read short fiction in print
140
+ funnel_now: has turned to the story in the magazine without knowing the author
141
+ knows:
142
+ - bakery
143
+ - apprentice
144
+ - sourdough
145
+ believes_now: nothing yet about this author or these two people
146
+ wants: a story they can finish in one sitting and keep thinking about
147
+ reads_on: print, in one sitting of about fifteen minutes
148
+ reader: person
149
+ check:
150
+ station: term check against knows; proof, starter and deck oven are made plain by context on first use
151
+ rubric: simulated reader reports where it got lost and where it stopped reading
152
+ source: editor's brief
153
+ author: example-author
154
+ goal:
155
+ from: has never read the author
156
+ to: finishes the story and remembers the starter jar
157
+ next_if_worked: looks for the author's other stories
158
+ change:
159
+ kind: feeling
160
+ text: the reader feels the handover of the starter jar as the moment the two say what neither says aloud
161
+ conditions: [r1, r2, r3, r4, r5, r6]
162
+ check:
163
+ rubric: the doctor grades the draft against every condition; the simulated reader is asked what the starter jar meant and whether it would look for the author's next story
164
+ source: goal interview
165
+ author: example-author
166
+ form:
167
+ name: short story
168
+ length:
169
+ min: 2500
170
+ max: 4000
171
+ unit: words
172
+ required_parts:
173
+ - four scenes, in the scene list's order
174
+ - a last line about bread
175
+ stations:
176
+ - continuity against the scene list
177
+ - knowledge-leak check, per character, per scene
178
+ check:
179
+ station: structure and length check against required_parts and length
180
+ source: editor's brief
181
+ author: example-author
182
+ spine:
183
+ kind: story
184
+ claims:
185
+ - id: c1
186
+ text: each of them hides their news to protect the other, and the hiding is the thing they share
187
+ materials: [notes, scene-list]
188
+ - id: c2
189
+ text: people who love each other at work say it through the work
190
+ materials: [notes, bakery-visit]
191
+ - id: c3
192
+ text: a craft outlives the room it was practiced in
193
+ materials: [notes, scene-list]
194
+ check:
195
+ rubric: each claim lands, in order, through what the characters do, and the story argues nothing outside the chain
196
+ source: spine interview
197
+ author: example-author
198
+ sources:
199
+ ledger: story/claims.jsonl
200
+ unsourced_claim: fail
201
+ check:
202
+ station: every process detail in the ledger points at a source span; every quote matches its source verbatim
203
+ source: sourcing pass
204
+ author: agent:claude
205
+ characters:
206
+ - id: ines
207
+ speech:
208
+ uses:
209
+ - instructions in the imperative
210
+ - numbers, weights and times
211
+ - first names, for customers
212
+ never:
213
+ - an apology in words
214
+ - a sentence about her own feelings
215
+ - a question she does not need answered
216
+ rhythm: short, flat sentences, often without a subject; silence where another person would reassure
217
+ wants: to leave the bakery in hands she trusts, without having to say that she is leaving it
218
+ fears: that Theo will stay in a trade with no bakery to do it in, because of her
219
+ hides: that she sold the bakery two weeks ago
220
+ knowledge:
221
+ - by: scene-1
222
+ knows: the sale closes on Friday, and the new owners will not keep it a bakery
223
+ - by: scene-4
224
+ knows: Theo has a place at a baking school in another city and leaves in the autumn
225
+ relationships:
226
+ - to: theo
227
+ how: gives him instructions where another person would give praise
228
+ - to: customers
229
+ how: warm and unhurried, asks after their families by name
230
+ arc_state: has let go of the bakery, and has not yet let go of Theo
231
+ golden_lines:
232
+ - Flour first. Then you can talk.
233
+ - Left side runs hot. Turn them at eight minutes.
234
+ - Sold means sold. Shape the rye.
235
+ rejected_lines:
236
+ - I'm so sorry I didn't tell you sooner, Theo.
237
+ - This place has been my whole life, you know?
238
+ - Would you like to try the rye today?
239
+ check:
240
+ rubric: blind attribution test, knowledge-leak check against the timeline, consistency against golden and rejected lines
241
+ source: story/materials/notes.md
242
+ author: example-author
243
+ - id: theo
244
+ speech:
245
+ uses:
246
+ - questions he already knows the answer to
247
+ - hedges such as "I mean" and "kind of"
248
+ - a joke when he is nervous
249
+ never:
250
+ - a flat instruction
251
+ - a word of baking jargon he has not heard Ines use first
252
+ rhythm: long sentences that double back on themselves and end in a question
253
+ wants: Ines to tell him he is ready, in words, once
254
+ fears: that leaving for school is a betrayal of the person who taught him
255
+ hides: that he has a place at a baking school in another city and leaves in the autumn
256
+ knowledge:
257
+ - by: scene-1
258
+ knows: the oven has made the noise since March, and he leaves for school in the autumn
259
+ - by: scene-3
260
+ knows: the bakery is sold, and the new owners will not keep it a bakery
261
+ relationships:
262
+ - to: ines
263
+ how: asks questions to keep her talking, and never interrupts her while she shapes
264
+ arc_state: still acting the apprentice while privately already leaving
265
+ golden_lines:
266
+ - Okay but like, if the oven's made that noise since March, is it a noise, or is that just how the oven talks now?
267
+ - I can do the rye. I mean, I think I can do the rye. I did it Tuesday, kind of.
268
+ - So is that a yes, or is that the face you make when it's a yes?
269
+ rejected_lines:
270
+ - Shape the rye.
271
+ - I have been accepted to a culinary program and will be leaving in the autumn.
272
+ - Left side runs hot, turn them early.
273
+ check:
274
+ rubric: blind attribution test, knowledge-leak check against the timeline, consistency against golden and rejected lines
275
+ source: story/materials/notes.md
276
+ author: example-author
277
+ fiction: true
278
+ ---
279
+
280
+ # The Rye
281
+
282
+ A worked example of the writing profile for fiction: a short story with two characters, each
283
+ specified well enough that an agent can write their dialogue and a judge can tell them apart.
284
+ Every file this spec names ships beside it. `story/claims.jsonl` does not exist yet, because the
285
+ claims ledger is written during drafting.
@@ -0,0 +1,19 @@
1
+ # Style rules
2
+
3
+ The always-on layer. These apply to everything this writer publishes, whatever the form, the
4
+ audience or the purpose. Goldens change with the scope; these do not.
5
+
6
+ ## Never
7
+
8
+ - No em dashes. Use a comma, a colon, or a new sentence.
9
+ - No sentence that sets up a claim only to knock down a weaker one first. State the claim.
10
+ - No adjective standing in for evidence. If a sentence says a thing is important, it shows why.
11
+ - No filler openers ("In today's world", "It goes without saying").
12
+ - No exclamation marks outside quoted dialogue.
13
+
14
+ ## Always
15
+
16
+ - The first line of a piece states what it is about.
17
+ - A number carries its source, or it does not go in.
18
+ - A quotation is word for word, or it is not in quotation marks.
19
+ - One idea per paragraph. A paragraph that needs a second idea is two paragraphs.
package/package.json CHANGED
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "name": "@supersuit/hyperspec",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A hyperspec is a spec written for an agent: every decision accounted for, every requirement failable and checked, every field traced. The standard and its linter.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "hyperspec": "bin/hyperspec.mjs"
8
8
  },
9
+ "exports": {
10
+ "./recipe": "./src/writer.mjs",
11
+ "./package.json": "./package.json"
12
+ },
9
13
  "files": [
10
14
  "bin/",
11
15
  "src/",
12
16
  "examples/",
13
17
  "SPEC.md",
18
+ "WRITING.md",
19
+ "runs.jsonl",
14
20
  "README.md",
15
21
  "CHANGELOG.md",
16
22
  "LICENSE"
@@ -34,6 +40,6 @@
34
40
  "outcome-factory"
35
41
  ],
36
42
  "dependencies": {
37
- "@supersuit/superskill": "^0.2.1"
43
+ "@supersuit/superskill": "^0.2.2"
38
44
  }
39
45
  }
package/runs.jsonl ADDED
File without changes
package/src/blobs.mjs ADDED
@@ -0,0 +1,77 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { sha256 } from "./hash.mjs";
5
+
6
+ // Resolve the store root: the --store flag, then HYPERSPEC_STORE, then the
7
+ // nearest ancestor of `from` holding .hyperspec/ or .git, else from's own
8
+ // directory. `from` is normally the recipe's directory, but a file path
9
+ // works too (we walk up from its dirname).
10
+ export function storeRoot({ from, store } = {}) {
11
+ if (store) return resolve(store);
12
+ if (process.env.HYPERSPEC_STORE) return resolve(process.env.HYPERSPEC_STORE);
13
+ let dir = resolve(from);
14
+ try { if (statSync(dir).isFile()) dir = dirname(dir); } catch { /* from may not exist yet; treat it as a directory */ }
15
+ let cur = dir;
16
+ while (true) {
17
+ if (existsSync(join(cur, ".hyperspec")) || existsSync(join(cur, ".git"))) return cur;
18
+ const parent = dirname(cur);
19
+ if (parent === cur) return dir; // hit the filesystem root: fall back to the starting directory
20
+ cur = parent;
21
+ }
22
+ }
23
+
24
+ // A hash read from a recipe is a claim, and it becomes part of a filesystem path here. Anything
25
+ // but exactly 64 lowercase hex characters is refused before it touches the disk, so a crafted
26
+ // recipe cannot point a read at `../../somewhere`, a device, or a pipe that never closes.
27
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
28
+ export function isSha256(hex) {
29
+ return typeof hex === "string" && SHA256_HEX.test(hex);
30
+ }
31
+
32
+ export function blobPath(root, hex) {
33
+ if (!isSha256(hex)) {
34
+ const shown = typeof hex === "string" ? JSON.stringify(hex.length > 80 ? `${hex.slice(0, 80)}...` : hex) : String(hex);
35
+ throw new Error(`not a SHA-256 hash (64 lowercase hex characters): ${shown}`);
36
+ }
37
+ return join(root, ".hyperspec", "blobs", hex.slice(0, 2), hex);
38
+ }
39
+
40
+ // Writes only if the blob is absent; an existing blob is left untouched (never rewritten).
41
+ // Atomic: bytes land in a temp file in the same directory first, then a single renameSync
42
+ // (atomic on one filesystem) puts them at the final path. A process killed mid-write leaves
43
+ // only the orphaned temp file, never a partially-written file sitting at the content-addressed
44
+ // path — the failure mode a plain writeFileSync(path, bytes) would otherwise leave behind, and
45
+ // which nothing short of an explicit verifyBlob would ever catch afterward.
46
+ export function putBlob(root, bytes) {
47
+ const hex = sha256(bytes);
48
+ const path = blobPath(root, hex);
49
+ if (existsSync(path)) return hex;
50
+ mkdirSync(dirname(path), { recursive: true });
51
+ const tmp = `${path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
52
+ try {
53
+ writeFileSync(tmp, bytes);
54
+ if (existsSync(path)) { unlinkSync(tmp); return hex; } // another writer won the race; keep theirs
55
+ renameSync(tmp, path);
56
+ } catch (e) {
57
+ try { unlinkSync(tmp); } catch { /* nothing to clean up */ }
58
+ throw e;
59
+ }
60
+ return hex;
61
+ }
62
+
63
+ // A malformed hash names no blob, so hasBlob, getBlob and verifyBlob treat it as missing.
64
+ export function hasBlob(root, hex) {
65
+ return isSha256(hex) && existsSync(blobPath(root, hex));
66
+ }
67
+
68
+ export function getBlob(root, hex) {
69
+ if (!isSha256(hex)) return null;
70
+ try { return readFileSync(blobPath(root, hex)); } catch { return null; }
71
+ }
72
+
73
+ // True when the blob exists and its bytes re-hash to hex (catches tampering).
74
+ export function verifyBlob(root, hex) {
75
+ const bytes = getBlob(root, hex);
76
+ return bytes !== null && sha256(bytes) === hex;
77
+ }