@supersuit/hyperspec 0.3.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.
@@ -74,7 +74,7 @@ examples:
74
74
  - path: essay/goldens/opening.md
75
75
  why: the claim lands in the first sentence, and the second sentence turns it into an instruction
76
76
  resume:
77
- next_action: mark the three materials into labeled segments, then outline the four spine claims against the form's required parts
77
+ next_action: outline the four spine claims against the form's required parts, citing the segments each claim points at
78
78
  feedback:
79
79
  issues: https://github.com/SupersuitUp/hyperspec/issues
80
80
  fork: MIT; fork it for your own purposes
@@ -85,24 +85,27 @@ writing:
85
85
  items:
86
86
  - id: voice-memo
87
87
  path: essay/materials/voice-memo.md
88
+ segments: essay/materials/voice-memo.md.segments.jsonl
88
89
  produced_by: example-author
89
90
  captured: "2026-09-12"
90
91
  how: voice memo, transcribed
91
92
  trust: raw
92
93
  - id: interview
93
94
  path: essay/materials/interview-notes.md
95
+ segments: essay/materials/interview-notes.md.segments.jsonl
94
96
  produced_by: example-author
95
97
  captured: "2026-09-15"
96
98
  how: notes taken during a call, reviewed by the person interviewed
97
99
  trust: considered
98
100
  - id: survey
99
101
  path: essay/materials/team-survey.md
102
+ segments: essay/materials/team-survey.md.segments.jsonl
100
103
  produced_by: example-author
101
104
  captured: "2026-05-30"
102
105
  how: survey summary, figures checked against the raw export by a second person
103
106
  trust: verified
104
107
  check:
105
- station: every segment of every material carries a label from the closed set
108
+ station: every segment of every material carries a label from the closed set, matches its source verbatim, and the markings are current
106
109
  source: capture step
107
110
  author: agent:claude
108
111
  dna:
@@ -186,16 +189,16 @@ writing:
186
189
  claims:
187
190
  - id: c1
188
191
  text: the first one-on-one is the one meeting where the report should set the agenda
189
- materials: [voice-memo, interview]
192
+ materials: [voice-memo#s3, interview#s3]
190
193
  - id: c2
191
194
  text: status belongs in the tracker, and a one-on-one spent on it teaches the manager nothing new
192
- materials: [voice-memo, survey]
195
+ materials: [voice-memo#s2, voice-memo#s5, survey#s4]
193
196
  - id: c3
194
197
  text: three questions are enough to hand the meeting over
195
- materials: [voice-memo]
198
+ materials: [voice-memo#s4]
196
199
  - id: c4
197
200
  text: the answer worth having comes after a silence the manager does not fill
198
- materials: [interview]
201
+ materials: [interview#s7, interview#s8]
199
202
  check:
200
203
  rubric: each claim lands, in order, and the draft argues nothing outside the chain
201
204
  source: spine interview
@@ -6,3 +6,4 @@ Considered: the owner read these notes and corrected the proofing times.
6
6
  - The doors open to customers at 7:00. The first person in is usually a regular.
7
7
  - The owner does not talk while shaping. Talking happens at the mixer and at the till.
8
8
  - Flour is weighed, never scooped. Water temperature is checked every batch.
9
+ - Said in confidence, not for the story: the lease ends next spring, and she has not told her staff.
@@ -0,0 +1,13 @@
1
+ {"material":"bakery-visit","path":"story/materials/bakery-visit.md","sha256":"5eb09f7c0ad8836378c549392d939f0b070669e972c9e4d94e1f81f27465a19b"}
2
+ {"id":"s1","start":0,"end":90,"label":"aside","text":"Notes from a morning spent at a working bakery, 3:30 to 7:30, with the owner's permission."}
3
+ {"id":"s2","start":91,"end":163,"label":"aside","text":"Considered: the owner read these notes and corrected the proofing times."}
4
+ {"id":"s3","start":165,"end":185,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"- First mix at 3:45."}
5
+ {"id":"s4","start":186,"end":255,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"Rye sourdough proofs about three hours at room temperature in winter."}
6
+ {"id":"s5","start":256,"end":346,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"- Trays are turned halfway through the bake because the back left of a deck oven runs hot."}
7
+ {"id":"s6","start":347,"end":385,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"- The doors open to customers at 7:00."}
8
+ {"id":"s7","start":386,"end":427,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"The first person in is usually a regular."}
9
+ {"id":"s8","start":428,"end":468,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"- The owner does not talk while shaping."}
10
+ {"id":"s9","start":469,"end":514,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"Talking happens at the mixer and at the till."}
11
+ {"id":"s10","start":515,"end":549,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"- Flour is weighed, never scooped."}
12
+ {"id":"s11","start":550,"end":591,"label":"claim","source":"a morning observed at a working bakery, notes corrected by the owner","text":"Water temperature is checked every batch."}
13
+ {"id":"s12","start":592,"end":692,"label":"private","text":"- Said in confidence, not for the story: the lease ends next spring, and she has not told her staff."}
@@ -8,6 +8,8 @@ autumn. Each thinks they are protecting the other.
8
8
  The story is four scenes, one morning, 3:40 to 7:00. The oven has a noise. The rye is the thing
9
9
  she has never let him do alone.
10
10
 
11
+ Open question: does she tell him about the sale before she lets him shape the rye, or after?
12
+
11
13
  What it is about, I think: a craft outlives the room it was practiced in. And: people who love
12
14
  each other in a work setting say it through the work.
13
15
 
@@ -0,0 +1,7 @@
1
+ {"material":"notes","path":"story/materials/notes.md","sha256":"d2af0977ca30dc1f666d219e3c5d2d0f063e91d9d07f14cf703f848ca260ada8"}
2
+ {"id":"s1","start":0,"end":68,"label":"aside","text":"Author's notes for the story, typed over two evenings. Raw thinking."}
3
+ {"id":"s2","start":70,"end":405,"label":"claim","own":true,"text":"A bakery on its last morning before the sale closes. Two people: the owner, who has run it for\nthirty-one years, and the apprentice she took on at sixteen, now nineteen. She has not told him\nit is sold. He has not told her he got into a baking school in another city and leaves in the\nautumn. Each thinks they are protecting the other."}
4
+ {"id":"s3","start":407,"end":534,"label":"claim","own":true,"text":"The story is four scenes, one morning, 3:40 to 7:00. The oven has a noise. The rye is the thing\nshe has never let him do alone."}
5
+ {"id":"s4","start":536,"end":628,"label":"question","text":"Open question: does she tell him about the sale before she lets him shape the rye, or after?"}
6
+ {"id":"s5","start":630,"end":778,"label":"stance","text":"What it is about, I think: a craft outlives the room it was practiced in. And: people who love\neach other in a work setting say it through the work."}
7
+ {"id":"s6","start":780,"end":838,"label":"stance","text":"The last line should be about bread, never about feelings."}
@@ -0,0 +1,14 @@
1
+ {"material":"scene-list","path":"story/materials/scene-list.md","sha256":"8181cdb35cd1afad01809367de15b3233f24fc7bede5b7b89c022f05e3d655e0"}
2
+ {"id":"s1","start":0,"end":51,"label":"aside","text":"Scene list, agreed with the editor before drafting."}
3
+ {"id":"s2","start":52,"end":63,"label":"aside","text":"Considered."}
4
+ {"id":"s3","start":65,"end":95,"label":"claim","own":true,"text":"- scene-1, 3:40: Theo arrives."}
5
+ {"id":"s4","start":96,"end":119,"label":"claim","own":true,"text":"Ines is already mixing."}
6
+ {"id":"s5","start":120,"end":135,"label":"claim","own":true,"text":"The oven noise."}
7
+ {"id":"s6","start":136,"end":162,"label":"claim","own":true,"text":"No one says anything real."}
8
+ {"id":"s7","start":163,"end":188,"label":"claim","own":true,"text":"- scene-2, 4:30: shaping."}
9
+ {"id":"s8","start":189,"end":259,"label":"claim","own":true,"text":"Ines lets Theo shape the rye for the first time, and does not say why."}
10
+ {"id":"s9","start":260,"end":286,"label":"claim","own":true,"text":"- scene-3, 5:50: the bake."}
11
+ {"id":"s10","start":287,"end":344,"label":"claim","own":true,"text":"Ines tells Theo the bakery is sold, while turning a tray."}
12
+ {"id":"s11","start":345,"end":384,"label":"claim","own":true,"text":"- scene-4, 6:55: before the doors open."}
13
+ {"id":"s12","start":385,"end":418,"label":"claim","own":true,"text":"Theo tells Ines about the school."}
14
+ {"id":"s13","start":419,"end":451,"label":"claim","own":true,"text":"She hands him the\n starter jar."}
@@ -74,7 +74,7 @@ examples:
74
74
  - path: story/goldens/dialogue.md
75
75
  why: three short lines carry a ritual both characters know, without either of them naming it
76
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
77
+ next_action: draft scene-1 from the scene list with Theo's knowledge as of scene-1
78
78
  feedback:
79
79
  issues: https://github.com/SupersuitUp/hyperspec/issues
80
80
  fork: MIT; fork it for your own purposes
@@ -85,24 +85,27 @@ writing:
85
85
  items:
86
86
  - id: notes
87
87
  path: story/materials/notes.md
88
+ segments: story/materials/notes.md.segments.jsonl
88
89
  produced_by: example-author
89
90
  captured: "2026-08-20"
90
91
  how: typed notes
91
92
  trust: raw
92
93
  - id: bakery-visit
93
94
  path: story/materials/bakery-visit.md
95
+ segments: story/materials/bakery-visit.md.segments.jsonl
94
96
  produced_by: example-author
95
97
  captured: "2026-08-28"
96
98
  how: notes taken on site, corrected by the bakery owner afterwards
97
99
  trust: considered
98
100
  - id: scene-list
99
101
  path: story/materials/scene-list.md
102
+ segments: story/materials/scene-list.md.segments.jsonl
100
103
  produced_by: example-author
101
104
  captured: "2026-09-02"
102
105
  how: scene list agreed with the editor
103
106
  trust: considered
104
107
  check:
105
- station: every segment of every material carries a label from the closed set
108
+ station: every segment of every material carries a label from the closed set, matches its source verbatim, and the markings are current
106
109
  source: capture step
107
110
  author: agent:claude
108
111
  dna:
@@ -184,13 +187,13 @@ writing:
184
187
  claims:
185
188
  - id: c1
186
189
  text: each of them hides their news to protect the other, and the hiding is the thing they share
187
- materials: [notes, scene-list]
190
+ materials: [notes#s2, scene-list#s10, scene-list#s12]
188
191
  - id: c2
189
192
  text: people who love each other at work say it through the work
190
- materials: [notes, bakery-visit]
193
+ materials: [notes#s5, bakery-visit#s8, scene-list#s8]
191
194
  - id: c3
192
195
  text: a craft outlives the room it was practiced in
193
- materials: [notes, scene-list]
196
+ materials: [notes#s5, scene-list#s13]
194
197
  check:
195
198
  rubric: each claim lands, in order, through what the characters do, and the story argues nothing outside the chain
196
199
  source: spine interview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/hyperspec",
3
- "version": "0.3.0",
3
+ "version": "0.4.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": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "exports": {
10
10
  "./recipe": "./src/writer.mjs",
11
+ "./writing": "./src/writing-exports.mjs",
11
12
  "./package.json": "./package.json"
12
13
  },
13
14
  "files": [
package/src/blobs.mjs CHANGED
@@ -41,7 +41,7 @@ export function blobPath(root, hex) {
41
41
  // Atomic: bytes land in a temp file in the same directory first, then a single renameSync
42
42
  // (atomic on one filesystem) puts them at the final path. A process killed mid-write leaves
43
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
44
+ // path. That is the failure mode a plain writeFileSync(path, bytes) would otherwise leave behind, and
45
45
  // which nothing short of an explicit verifyBlob would ever catch afterward.
46
46
  export function putBlob(root, bytes) {
47
47
  const hex = sha256(bytes);
package/src/compare.mjs CHANGED
@@ -66,7 +66,7 @@ export function compare(childRecipePath, { doctor, parent: parentOption, spec: s
66
66
  const specHash = sha256(readFileSync(specAbs));
67
67
  // specChanged is about the PARENT's record, not the child's: it tells the caller the spec being
68
68
  // graded against now differs from what the parent was made under. Both outputs still get graded
69
- // against this one spec file either way — never each against its own.
69
+ // against this one spec file either way, never each against its own.
70
70
  const specChanged = parentRecipe.spec?.sha256 !== specHash;
71
71
  if (specChanged) warnings.push("spec changed since the parent was made; both outputs graded against the current file");
72
72
 
@@ -114,14 +114,14 @@ export function compare(childRecipePath, { doctor, parent: parentOption, spec: s
114
114
  const ledgerAbs = resolve(specDir, ledgerDecl);
115
115
  const ledgerDir = dirname(ledgerAbs);
116
116
  // The child's own `change` is null whenever it has no genealogical parent of its own (a root
117
- // recipe compared against an explicit, unrelated --parent — compare's usage check allows
117
+ // recipe compared against an explicit, unrelated --parent; compare's usage check allows
118
118
  // this: it only requires *either* the child's recorded parent.path *or* an explicit
119
119
  // override). Fall back to naming what was actually compared against, so `change` and the
120
- // not-improved `reason` below are never "after null" — both a broken persisted record and,
120
+ // not-improved `reason` below are never "after null", which would be both a broken persisted record and,
121
121
  // for the "improved" verdict, a lint failure (rules.mjs's verdict-change: `!str(v.change)`).
122
122
  const childChange = child.change ?? `compared against ${relative(ledgerDir, parentRecipeAbs)}`;
123
123
  // A compare line must satisfy test 9 ("it improves itself"), which only knows the
124
- // verdict vocabulary one-shot/improved/not-improved — never a new "compare" verdict. A
124
+ // verdict vocabulary one-shot/improved/not-improved, never a new "compare" verdict. A
125
125
  // strictly higher child score is improved (and already carries change, which doubles as
126
126
  // that verdict's required field). Equal or lower is not-improved, with a reason a later
127
127
  // session can argue with; when it's a genuine regression (strictly lower, not merely tied)
@@ -162,8 +162,8 @@ export function compare(childRecipePath, { doctor, parent: parentOption, spec: s
162
162
 
163
163
  // Runs the doctor command once, via /bin/sh -c, over one output against the one spec. stdin is
164
164
  // { output, spec } (absolute paths); the last non-empty stdout line must be JSON with a numeric
165
- // score. Nothing recipe-derived is ever interpolated into the command string — only passed on
166
- // stdin — so the same command string is reused verbatim for the parent and the child.
165
+ // score. Nothing recipe-derived is ever interpolated into the command string; it is only passed on
166
+ // stdin, so the same command string is reused verbatim for the parent and the child.
167
167
  function runDoctor(doctorCmd, outputAbs, specAbs) {
168
168
  const res = spawnSync("/bin/sh", ["-c", doctorCmd], {
169
169
  input: JSON.stringify({ output: outputAbs, spec: specAbs }),
package/src/fsutil.mjs CHANGED
@@ -2,7 +2,7 @@ import { renameSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { relative, resolve, sep } from "node:path";
4
4
 
5
- // True when `target`, resolved against `dir`, stays inside `dir` — refuses a `..`-escape and an
5
+ // True when `target`, resolved against `dir`, stays inside `dir`. It refuses a `..`-escape and an
6
6
  // absolute path pointing elsewhere. Both a relative `target` (including one that climbs out via
7
7
  // `../`) and an already-absolute `target` are handled the same way, since `path.resolve(dir,
8
8
  // target)` already treats an absolute second argument as overriding the first: either way, the
package/src/labels.mjs ADDED
@@ -0,0 +1,6 @@
1
+ // The closed vocabulary every segment of a marked material is labeled from (hyperspec 0.4). It lives in
2
+ // this leaf module, which imports nothing, so src/segments.mjs can read it without importing
3
+ // src/writing.mjs: writing.mjs imports writing-fields.mjs, which imports segments.mjs, and reading
4
+ // the labels through writing.mjs made that a cycle. writing.mjs re-exports it for callers that
5
+ // already import it from there.
6
+ export const MATERIAL_LABELS = Object.freeze(["claim", "story", "quote", "stance", "question", "aside", "private"]);
package/src/reproduce.mjs CHANGED
@@ -11,7 +11,7 @@ const MALFORMED = "recorded hash is not a SHA-256 hash (64 lowercase hex charact
11
11
  const blobWhy = (hex, why) => (present(hex) && !isSha256(hex) ? MALFORMED : why);
12
12
 
13
13
  // Spec requirement `reproduce`: replay the record and hash-check it. Never invokes a model,
14
- // never runs a command, never regenerates a byte of content — every blob this looks at already
14
+ // never runs a command, never regenerates a byte of content. Every blob this looks at already
15
15
  // exists in the store, and this only confirms the recipe's own claims about it still hold.
16
16
  //
17
17
  // Checks, in order (and every one is reported, nothing stops the walk early): every input blob
@@ -98,8 +98,8 @@ export function reproduce(recipePath, { store, restore = false } = {}) {
98
98
  // 5. The output file on disk, if present, hashes to output.sha256. Absent is vacuously fine:
99
99
  // reproduce doesn't require the file to already exist, only that it agrees when it does.
100
100
  //
101
- // The recipe is a plain JSON file on disk — exactly the kind of claim reproduce exists to
102
- // distrust — so output.path is never trusted blind. A hand-edited or corrupted recipe could
101
+ // The recipe is a plain JSON file on disk, exactly the kind of claim reproduce exists to
102
+ // distrust, so output.path is never trusted blind. A hand-edited or corrupted recipe could
103
103
  // name a path outside the recipe's own directory (a `../` climb, or an absolute path); refuse
104
104
  // before touching disk at all, rather than reading from or (worse, under --restore) writing to
105
105
  // wherever it points.
@@ -120,12 +120,12 @@ export function reproduce(recipePath, { store, restore = false } = {}) {
120
120
  }
121
121
 
122
122
  // restore: true writes the output file from its blob, but only when the output blob
123
- // itself verified (step 4) — restoring from an unverified blob would just write
123
+ // itself verified (step 4); restoring from an unverified blob would just write
124
124
  // different wrong bytes. Covers both a lost file (never existed / deleted) and an
125
125
  // edited one. The write is atomic (temp file + rename, same pattern as putBlob), so a
126
126
  // crash mid-write never leaves the file in a state that is neither the old nor the new
127
127
  // content, and a write failure is caught and reported as a failing step rather than
128
- // thrown out of reproduce() — this function always returns a structured result.
128
+ // thrown out of reproduce(): this function always returns a structured result.
129
129
  const needsRestore = !fileExists || !matches;
130
130
  if (restore && needsRestore && outputBlobOk) {
131
131
  const blob = getBlob(root, outputSha);