@supersuit/hyperspec 0.3.0 → 0.5.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 +127 -0
- package/README.md +46 -1
- package/SPEC.md +5 -3
- package/WRITING.md +475 -40
- package/bin/hyperspec.mjs +157 -3
- package/examples/writing/dna/essay-new-managers-teach/features.json +56 -0
- package/examples/writing/dna/essay-new-managers-teach/goldens/README.md +14 -0
- package/examples/writing/dna/essay-new-managers-teach/goldens/close.md +9 -0
- package/examples/writing/dna/essay-new-managers-teach/goldens/opening.md +9 -0
- package/examples/writing/dna/essay-new-managers-teach/goldens/status.md +10 -0
- package/examples/writing/dna/essay-new-managers-teach/scope.md +11 -0
- package/examples/writing/essay/materials/interview-notes.md.segments.jsonl +10 -0
- package/examples/writing/essay/materials/team-survey.md.segments.jsonl +6 -0
- package/examples/writing/essay/materials/voice-memo.md.segments.jsonl +7 -0
- package/examples/writing/essay.hyperspec.md +24 -13
- package/examples/writing/story/materials/bakery-visit.md +1 -0
- package/examples/writing/story/materials/bakery-visit.md.segments.jsonl +13 -0
- package/examples/writing/story/materials/notes.md +2 -0
- package/examples/writing/story/materials/notes.md.segments.jsonl +7 -0
- package/examples/writing/story/materials/scene-list.md.segments.jsonl +14 -0
- package/examples/writing/story.hyperspec.md +8 -5
- package/package.json +2 -1
- package/src/blobs.mjs +1 -1
- package/src/compare.mjs +6 -6
- package/src/dna.mjs +471 -0
- package/src/fsutil.mjs +1 -1
- package/src/labels.mjs +6 -0
- package/src/reproduce.mjs +5 -5
- package/src/segments.mjs +407 -0
- package/src/writing-exports.mjs +11 -0
- package/src/writing-fields.mjs +279 -6
- package/src/writing-template.mjs +16 -1
- package/src/writing.mjs +4 -4
- package/examples/writing/essay/goldens/close.md +0 -2
- package/examples/writing/essay/goldens/opening.md +0 -2
package/bin/hyperspec.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { existsSync, statSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve, join } from "node:path";
|
|
4
4
|
import { loadSpec } from "../src/load.mjs";
|
|
5
5
|
import { lintSpec } from "../src/rules.mjs";
|
|
6
6
|
import { score, exitCode } from "../src/score.mjs";
|
|
@@ -12,6 +12,10 @@ import { approve } from "../src/writer.mjs";
|
|
|
12
12
|
import { reproduce } from "../src/reproduce.mjs";
|
|
13
13
|
import { regenerate } from "../src/regenerate.mjs";
|
|
14
14
|
import { compare } from "../src/compare.mjs";
|
|
15
|
+
import { splitSegments } from "../src/segments.mjs";
|
|
16
|
+
import { sha256 } from "../src/hash.mjs";
|
|
17
|
+
import { readScope, measureFeatures, writeFeatures, scopeTemplate, GOLDENS_README } from "../src/dna.mjs";
|
|
18
|
+
import { str } from "../src/placeholder.mjs";
|
|
15
19
|
|
|
16
20
|
const HELP = `hyperspec <command> [options]
|
|
17
21
|
|
|
@@ -30,6 +34,34 @@ const HELP = `hyperspec <command> [options]
|
|
|
30
34
|
writing, --kind with it (use --form), or a folder that does not
|
|
31
35
|
exist
|
|
32
36
|
|
|
37
|
+
segments init <material> --id <mid> [--out <file>] [--by paragraph|sentence]
|
|
38
|
+
split a material into candidate segments, written as JSONL to
|
|
39
|
+
<material>.segments.jsonl by default; every segment starts label:
|
|
40
|
+
unlabeled, never valid in lint; label each one by hand (claim,
|
|
41
|
+
story, quote, stance, question, aside, private), then run hyperspec
|
|
42
|
+
lint on the spec; --by sentence also starts a segment at each list
|
|
43
|
+
item (-, *, +, 1. or 1) then a space); refuses to overwrite an
|
|
44
|
+
existing file (exit 2); exit 2 for a missing material, a material
|
|
45
|
+
with nothing in it, an --out folder that does not exist, or a --by
|
|
46
|
+
outside paragraph/sentence
|
|
47
|
+
|
|
48
|
+
dna init <scope-dir> --writer W --form F --audience A --purpose P
|
|
49
|
+
write a new writer-DNA scope: scope.md (writer, form, audience,
|
|
50
|
+
purpose) and an empty goldens/ folder holding a README on the
|
|
51
|
+
golden file shape (why, approved_by, source, optional approved_on;
|
|
52
|
+
the body is the passage, verbatim); refuses to overwrite an
|
|
53
|
+
existing scope.md; exit 2 on a missing parent folder, a missing
|
|
54
|
+
flag, or a flag value that looks like a placeholder (todo, ..., a
|
|
55
|
+
bare -), never a stack trace
|
|
56
|
+
dna measure <scope-dir> [--json]
|
|
57
|
+
read every golden in <scope-dir>/goldens/, check its required
|
|
58
|
+
fields, and write <scope-dir>/features.json: deterministic style
|
|
59
|
+
features measured from the goldens' text, never judged and never
|
|
60
|
+
run through a model
|
|
61
|
+
exit 0 wrote features.json, 1 a golden (or the scope itself) fails
|
|
62
|
+
a required-field check, so a hollow golden is never measured into
|
|
63
|
+
the DNA, 2 usage
|
|
64
|
+
|
|
33
65
|
recipe check <output-or-recipe> [--json]
|
|
34
66
|
check a recipe's completeness (a path not ending .recipe.json
|
|
35
67
|
means <path>.recipe.json)
|
|
@@ -98,6 +130,128 @@ if (cmd === "init") {
|
|
|
98
130
|
process.exit(0);
|
|
99
131
|
}
|
|
100
132
|
|
|
133
|
+
if (cmd === "segments") {
|
|
134
|
+
const sub = argv[1];
|
|
135
|
+
|
|
136
|
+
if (sub === "init") {
|
|
137
|
+
const parsed = parseArgs(argv.slice(2), { valueFlags: ["--id", "--out", "--by"] });
|
|
138
|
+
if (parsed.error) { console.error(parsed.error); process.exit(2); }
|
|
139
|
+
const [material] = parsed.positionals;
|
|
140
|
+
if (!material) { console.error("segments init needs a material path"); process.exit(2); }
|
|
141
|
+
if (!parsed.values["--id"]) { console.error("segments init needs --id <material id>"); process.exit(2); }
|
|
142
|
+
const id = parsed.values["--id"];
|
|
143
|
+
const by = parsed.values["--by"] ?? "paragraph";
|
|
144
|
+
if (by !== "paragraph" && by !== "sentence") { console.error(`--by must be paragraph or sentence, not "${by}"`); process.exit(2); }
|
|
145
|
+
let materialStat;
|
|
146
|
+
try { materialStat = statSync(material); } catch { materialStat = null; }
|
|
147
|
+
if (!materialStat || !materialStat.isFile()) { console.error(`material not found: ${material}`); process.exit(2); }
|
|
148
|
+
const out = parsed.values["--out"] ?? `${material}.segments.jsonl`;
|
|
149
|
+
if (existsSync(out)) { console.error(`refusing to overwrite ${out}`); process.exit(2); }
|
|
150
|
+
const outFolder = dirname(resolve(out));
|
|
151
|
+
if (!existsSync(outFolder) || !statSync(outFolder).isDirectory()) { console.error(`folder does not exist: ${dirname(out)}; create it first`); process.exit(2); }
|
|
152
|
+
|
|
153
|
+
const buf = readFileSync(material);
|
|
154
|
+
const text = buf.toString("utf8");
|
|
155
|
+
if (!/\S/.test(text)) { console.error(`nothing to mark: ${material} has no text`); process.exit(2); }
|
|
156
|
+
const segments = splitSegments(text, { by });
|
|
157
|
+
const header = { material: id, path: material, sha256: sha256(buf) };
|
|
158
|
+
const lines = [JSON.stringify(header), ...segments.map((s) => JSON.stringify(s))];
|
|
159
|
+
writeFileSync(out, `${lines.join("\n")}\n`);
|
|
160
|
+
console.log(`${segments.length} segments written to ${out}. Label every segment (claim, story, quote, stance, question, aside, private), then run hyperspec lint on the spec.`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.error(`unknown segments subcommand: ${sub}\n\n${HELP}`);
|
|
165
|
+
process.exit(2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (cmd === "dna") {
|
|
169
|
+
const sub = argv[1];
|
|
170
|
+
|
|
171
|
+
if (sub === "init") {
|
|
172
|
+
const parsed = parseArgs(argv.slice(2), { valueFlags: ["--writer", "--form", "--audience", "--purpose"] });
|
|
173
|
+
if (parsed.error) { console.error(parsed.error); process.exit(2); }
|
|
174
|
+
const [scopeDir] = parsed.positionals;
|
|
175
|
+
if (!scopeDir) { console.error("dna init needs a scope-dir path"); process.exit(2); }
|
|
176
|
+
for (const flagName of ["--writer", "--form", "--audience", "--purpose"]) {
|
|
177
|
+
if (!parsed.values[flagName]) { console.error(`dna init needs ${flagName} <value>`); process.exit(2); }
|
|
178
|
+
}
|
|
179
|
+
// A placeholder-looking value (todo, tbd, ..., ???, ...) is caught here rather than left
|
|
180
|
+
// for the next `dna measure` to catch on scope.md's own fields; str() is the one place that
|
|
181
|
+
// pattern is defined (src/placeholder.mjs), reused rather than re-checked.
|
|
182
|
+
for (const flagName of ["--writer", "--form", "--audience", "--purpose"]) {
|
|
183
|
+
if (!str(parsed.values[flagName])) {
|
|
184
|
+
console.error(`dna init ${flagName} "${parsed.values[flagName]}" looks like a placeholder; give it real content`);
|
|
185
|
+
process.exit(2);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const writer = parsed.values["--writer"];
|
|
189
|
+
const form = parsed.values["--form"];
|
|
190
|
+
const audience = parsed.values["--audience"];
|
|
191
|
+
const purpose = parsed.values["--purpose"];
|
|
192
|
+
|
|
193
|
+
const scopeAbs = resolve(scopeDir);
|
|
194
|
+
const parent = dirname(scopeAbs);
|
|
195
|
+
if (!existsSync(parent) || !statSync(parent).isDirectory()) {
|
|
196
|
+
console.error(`the folder ${dirname(scopeDir)} does not exist; create it first`);
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
if (existsSync(scopeAbs) && !statSync(scopeAbs).isDirectory()) {
|
|
200
|
+
console.error(`${scopeDir} is not a directory`);
|
|
201
|
+
process.exit(2);
|
|
202
|
+
}
|
|
203
|
+
// scopeMdPath (absolute, resolved from the cwd) is for filesystem operations only. Every
|
|
204
|
+
// message uses scopeMdDisplay, built from scopeDir exactly as given (relative, if that is how
|
|
205
|
+
// the operator typed it): a path the operator did not resolve themselves must never appear
|
|
206
|
+
// resolved in output, the same rule every other finding in this linter already follows.
|
|
207
|
+
const scopeMdPath = join(scopeAbs, "scope.md");
|
|
208
|
+
const scopeMdDisplay = join(scopeDir, "scope.md");
|
|
209
|
+
if (existsSync(scopeMdPath)) { console.error(`refusing to overwrite ${scopeMdDisplay}`); process.exit(2); }
|
|
210
|
+
|
|
211
|
+
mkdirSync(join(scopeAbs, "goldens"), { recursive: true });
|
|
212
|
+
writeFileSync(scopeMdPath, scopeTemplate({ writer, form, audience, purpose }));
|
|
213
|
+
// An operator's own goldens/README.md is theirs: write the guidance only where none exists.
|
|
214
|
+
try {
|
|
215
|
+
writeFileSync(join(scopeAbs, "goldens", "README.md"), GOLDENS_README, { flag: "wx" });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (err.code !== "EEXIST") throw err;
|
|
218
|
+
}
|
|
219
|
+
console.log(`wrote ${scopeMdDisplay} and ${scopeDir}/goldens/. Add goldens, then run: hyperspec dna measure ${scopeDir}`);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (sub === "measure") {
|
|
224
|
+
const parsed = parseArgs(argv.slice(2), { boolFlags: ["--json"] });
|
|
225
|
+
if (parsed.error) { console.error(parsed.error); process.exit(2); }
|
|
226
|
+
const [scopeDir] = parsed.positionals;
|
|
227
|
+
if (!scopeDir) { console.error("dna measure needs a scope-dir path"); process.exit(2); }
|
|
228
|
+
const json = parsed.values["--json"];
|
|
229
|
+
|
|
230
|
+
const { scope, goldens, findings } = readScope(scopeDir);
|
|
231
|
+
const hasFail = findings.some((x) => x.severity === "fail");
|
|
232
|
+
if (hasFail) {
|
|
233
|
+
if (json) console.log(JSON.stringify({ scope: scopeDir, findings }, null, 2));
|
|
234
|
+
else for (const x of findings) console.log(`fail [${x.test}] ${x.message}\n fix: ${x.fix}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const features = measureFeatures(goldens.map((g) => g.text));
|
|
239
|
+
const { path: featuresPath } = writeFeatures(scopeDir, { scope, goldens, features });
|
|
240
|
+
if (json) {
|
|
241
|
+
console.log(JSON.stringify({ scope: scopeDir, goldens: goldens.length, features, wrote: featuresPath }, null, 2));
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`${scopeDir}: measured ${goldens.length} golden${goldens.length === 1 ? "" : "s"}`);
|
|
244
|
+
console.log(` word_count ${features.word_count}, sentence length mean ${features.sentence_length.mean} median ${features.sentence_length.median} p90 ${features.sentence_length.p90}`);
|
|
245
|
+
console.log(` signature words: ${features.signature_words.join(", ") || "(none)"}`);
|
|
246
|
+
console.log(`wrote ${featuresPath}`);
|
|
247
|
+
}
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.error(`unknown dna subcommand: ${sub}\n\n${HELP}`);
|
|
252
|
+
process.exit(2);
|
|
253
|
+
}
|
|
254
|
+
|
|
101
255
|
if (cmd === "lint") {
|
|
102
256
|
const json = argv.includes("--json");
|
|
103
257
|
// Lint takes one flag, --json, and no flag takes a value, so a flag is dropped on its own and
|
|
@@ -359,7 +513,7 @@ if (cmd === "compare") {
|
|
|
359
513
|
if (json) console.log(JSON.stringify(result, null, 2));
|
|
360
514
|
|
|
361
515
|
// Both a usage error and a check-failed error (missing output, escaping path, ...) map to
|
|
362
|
-
// 2 here
|
|
516
|
+
// 2 here: compare's own ok:false without a usage flag is still "could not grade", i.e. an
|
|
363
517
|
// unreadable-input class failure, not a graded-but-worse-1 class one.
|
|
364
518
|
if (!result.ok) {
|
|
365
519
|
if (!json) console.error(result.error);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dna": "0.1",
|
|
3
|
+
"scope": {
|
|
4
|
+
"writer": "example-author",
|
|
5
|
+
"form": "essay",
|
|
6
|
+
"audience": "new managers",
|
|
7
|
+
"purpose": "teach"
|
|
8
|
+
},
|
|
9
|
+
"goldens": [
|
|
10
|
+
{
|
|
11
|
+
"path": "goldens/close.md",
|
|
12
|
+
"sha256": "a90291714fc3bd79a16c613deb536b34c4ef577e56083c0c6b17142fbc4b871f"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "goldens/opening.md",
|
|
16
|
+
"sha256": "395c7c37d604e255a07603276bac0d2f0915bc80d9acea45e3a30a6fe248b44e"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "goldens/status.md",
|
|
20
|
+
"sha256": "891c34d9134380e5c752e69036ec9707729b7e30c9f2b3f2789b8d3ee3f311ed"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"features": {
|
|
24
|
+
"word_count": 118,
|
|
25
|
+
"sentence_length": {
|
|
26
|
+
"mean": 13.111,
|
|
27
|
+
"median": 15,
|
|
28
|
+
"p90": 25
|
|
29
|
+
},
|
|
30
|
+
"paragraph_length": {
|
|
31
|
+
"mean_sentences": 3,
|
|
32
|
+
"mean_words": 39.333
|
|
33
|
+
},
|
|
34
|
+
"rates_per_1000_words": {
|
|
35
|
+
"comma": 33.898,
|
|
36
|
+
"semicolon": 0,
|
|
37
|
+
"colon": 8.475,
|
|
38
|
+
"em_dash": 0,
|
|
39
|
+
"en_dash": 0,
|
|
40
|
+
"exclamation": 0,
|
|
41
|
+
"question_mark": 0,
|
|
42
|
+
"parentheses": 0,
|
|
43
|
+
"quotation_marks": 0
|
|
44
|
+
},
|
|
45
|
+
"contraction_rate": 0,
|
|
46
|
+
"first_person_singular_rate": 0,
|
|
47
|
+
"first_person_plural_rate": 0,
|
|
48
|
+
"second_person_rate": 59.322,
|
|
49
|
+
"mean_word_length": 3.831,
|
|
50
|
+
"signature_words": [
|
|
51
|
+
"first",
|
|
52
|
+
"report",
|
|
53
|
+
"tracker"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Goldens
|
|
2
|
+
|
|
3
|
+
Each file in this folder except this one is a golden: a passage the writer marked as right,
|
|
4
|
+
filed under this scope.
|
|
5
|
+
|
|
6
|
+
Frontmatter:
|
|
7
|
+
- why (required): what makes it golden, the move it teaches.
|
|
8
|
+
- approved_by (required): a person slug. Golden means a human approved it; agent:* is refused.
|
|
9
|
+
- source (required): where the passage came from.
|
|
10
|
+
- approved_on (optional): a date.
|
|
11
|
+
|
|
12
|
+
The body is the passage, verbatim.
|
|
13
|
+
|
|
14
|
+
Run `hyperspec dna measure <scope-dir>` once every golden here has why, approved_by and source.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
why: ends on an instruction and gives the reason for it in the same sentence
|
|
3
|
+
approved_by: example-author
|
|
4
|
+
source: first draft of this essay's closing paragraph, marked golden on the review page
|
|
5
|
+
approved_on: "2026-09-18"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
So write the three questions on a card. Ask the first one. Then wait, longer than feels polite,
|
|
9
|
+
because the first answer is the one they rehearsed and the second one is the one you came for.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
why: one plain claim in the first sentence, then two short sentences that turn it into something to do
|
|
3
|
+
approved_by: example-author
|
|
4
|
+
source: first draft of this essay's opening paragraph, marked golden on the review page
|
|
5
|
+
approved_on: "2026-09-18"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Your first one-on-one with a new report is the only meeting on your calendar where they should
|
|
9
|
+
set the agenda. Everything else you run. This one you hand over.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
why: states what to stop doing, then where that thing already lives, then what the freed half hour is for, one sentence each
|
|
3
|
+
approved_by: example-author
|
|
4
|
+
source: an earlier newsletter essay for new managers by the same writer, its middle section
|
|
5
|
+
approved_on: "2026-09-18"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
If you can read it in the tracker, do not ask for it in the room. Status already has a home,
|
|
9
|
+
and your report is the one who put it there. Spend the half hour on what the tracker cannot
|
|
10
|
+
hold: how the work looks from their side of it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{"material":"interview","path":"essay/materials/interview-notes.md","sha256":"3c4fd3ff19435027b1bb9584e648128d59feb3184f67eeaf6b09ed9fa8bcfaeb"}
|
|
2
|
+
{"id":"s1","start":0,"end":118,"label":"aside","text":"Interview notes, a thirty-minute call with an engineering manager of eight years, taken by the\nauthor during the call."}
|
|
3
|
+
{"id":"s2","start":119,"end":199,"label":"aside","text":"Considered: the manager reviewed these notes afterwards and corrected two\nlines."}
|
|
4
|
+
{"id":"s3","start":201,"end":240,"label":"claim","source":"interview with an engineering manager of eight years","text":"- Her rule: the report owns the agenda."}
|
|
5
|
+
{"id":"s4","start":241,"end":356,"label":"claim","source":"interview with an engineering manager of eight years","text":"She keeps a shared document per person; they add items\n before the meeting, and she adds hers last, at the bottom."}
|
|
6
|
+
{"id":"s5","start":357,"end":448,"label":"quote","speaker":"the engineering manager interviewed","text":"- \"If I have something urgent, it is not a one-on-one topic. I send it the day it happens.\""}
|
|
7
|
+
{"id":"s6","start":449,"end":617,"label":"claim","source":"interview with an engineering manager of eight years","text":"- The first one-on-one with a new report is always the same: she asks how they like to receive\n feedback, in writing or out loud, right away or at the end of the week."}
|
|
8
|
+
{"id":"s7","start":618,"end":673,"label":"claim","source":"interview with an engineering manager of eight years","text":"- Her warning: new managers treat silence as a problem."}
|
|
9
|
+
{"id":"s8","start":674,"end":733,"label":"quote","speaker":"the engineering manager interviewed","text":"\"Wait. Count to five. The real answer is\n the second one.\""}
|
|
10
|
+
{"id":"s9","start":734,"end":812,"label":"claim","source":"interview with an engineering manager of eight years","text":"- She cancels a one-on-one only when the report asks her to, never on her own."}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"material":"survey","path":"essay/materials/team-survey.md","sha256":"ae2c821b1731ad02eb3280c28872aa9d2163728b868f262554d0cbfd3da19b97"}
|
|
2
|
+
{"id":"s1","start":0,"end":139,"label":"aside","text":"Summary of an internal survey the author's team ran in the spring, 41 responses, figures checked\nagainst the raw export by a second person."}
|
|
3
|
+
{"id":"s2","start":140,"end":149,"label":"aside","text":"Verified."}
|
|
4
|
+
{"id":"s3","start":151,"end":261,"label":"claim","source":"team survey, spring, 41 responses, figures checked against the raw export","text":"- 29 of 41 said their most useful one-on-one in the last quarter was one where they brought the\n first topic."}
|
|
5
|
+
{"id":"s4","start":262,"end":358,"label":"claim","source":"team survey, spring, 41 responses, figures checked against the raw export","text":"- 11 of 41 said at least one of their one-on-ones in the last quarter was mostly project status."}
|
|
6
|
+
{"id":"s5","start":359,"end":449,"label":"claim","source":"team survey, spring, 41 responses, figures checked against the raw export","text":"- The most common free-text request, in 9 responses: \"ask me what I want to work on next.\""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{"material":"voice-memo","path":"essay/materials/voice-memo.md","sha256":"c1e07e7d6886f0f3abe638500b614a94883b66f259d272b213f7fe63e70afdc8"}
|
|
2
|
+
{"id":"s1","start":0,"end":87,"label":"aside","text":"Voice memo transcript, recorded by the author on a walk, lightly cleaned. Raw thinking."}
|
|
3
|
+
{"id":"s2","start":89,"end":385,"label":"story","teller":"example-author","text":"My first one-on-one as a manager was a disaster, and the reason was simple: I ran it. I had a\nlist. I went down the list. Project status, blockers, the thing from Tuesday. Thirty minutes\nlater my report said \"cool, thanks\" and left, and I had learned nothing I could not have read in\nthe tracker."}
|
|
4
|
+
{"id":"s3","start":387,"end":572,"label":"claim","own":true,"text":"What I wish someone had told me: the first one-on-one is the only meeting where they get to set\nthe agenda. If you set it, you have told them what the meeting is for, and it is for you."}
|
|
5
|
+
{"id":"s4","start":574,"end":799,"label":"claim","own":true,"text":"The three questions I use now. What is taking more of your energy than it should? What do you\nwant to be doing more of in six months? What should I stop doing, or start doing, that would\nmake your week easier? Then I shut up."}
|
|
6
|
+
{"id":"s5","start":801,"end":897,"label":"stance","text":"Status goes in the tracker. If a one-on-one is a status meeting, cancel it and read the tracker."}
|
|
7
|
+
{"id":"s6","start":899,"end":1037,"label":"aside","text":"Aside, probably not for this piece: my second manager used to walk the one-on-ones outside. I\nliked it but I do not think it is the point."}
|
|
@@ -28,7 +28,7 @@ requirements:
|
|
|
28
28
|
fails_when: a reader shown only the first two sentences cannot say who should set the agenda
|
|
29
29
|
check:
|
|
30
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
|
|
31
|
+
source: dna/essay-new-managers-teach/goldens/opening.md
|
|
32
32
|
author: example-author
|
|
33
33
|
- id: r2
|
|
34
34
|
text: the three questions appear word for word as the voice memo states them
|
|
@@ -71,10 +71,10 @@ rejects:
|
|
|
71
71
|
- any claim about what most managers do that the survey does not support
|
|
72
72
|
- the walking one-on-one aside from the voice memo
|
|
73
73
|
examples:
|
|
74
|
-
- path: essay/goldens/opening.md
|
|
75
|
-
why: the claim lands in the first sentence, and the
|
|
74
|
+
- path: dna/essay-new-managers-teach/goldens/opening.md
|
|
75
|
+
why: the claim lands in the first sentence, and the two short sentences after it turn it into an instruction
|
|
76
76
|
resume:
|
|
77
|
-
next_action:
|
|
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,40 +85,46 @@ 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:
|
|
109
112
|
writer: example-author
|
|
113
|
+
scope_dir: dna/essay-new-managers-teach
|
|
110
114
|
scope:
|
|
111
115
|
form: essay
|
|
112
116
|
audience: new managers
|
|
113
117
|
purpose: teach
|
|
114
118
|
rules: style-rules.md
|
|
115
119
|
goldens:
|
|
116
|
-
- path: essay/goldens/opening.md
|
|
117
|
-
why: one plain claim, then
|
|
118
|
-
- path: essay/goldens/
|
|
120
|
+
- path: dna/essay-new-managers-teach/goldens/opening.md
|
|
121
|
+
why: one plain claim in the first sentence, then two short sentences that turn it into something to do
|
|
122
|
+
- path: dna/essay-new-managers-teach/goldens/status.md
|
|
123
|
+
why: states what to stop doing, then where that thing already lives, then what the freed half hour is for, one sentence each
|
|
124
|
+
- path: dna/essay-new-managers-teach/goldens/close.md
|
|
119
125
|
why: ends on an instruction and gives the reason for it in the same sentence
|
|
120
126
|
check:
|
|
121
|
-
rubric: blind lineup within this scope; a judge shown
|
|
127
|
+
rubric: blind lineup within this scope; a judge shown a generated passage beside the scope's three goldens cannot pick it out
|
|
122
128
|
source: goldens marked on the review page
|
|
123
129
|
author: example-author
|
|
124
130
|
persona:
|
|
@@ -186,16 +192,16 @@ writing:
|
|
|
186
192
|
claims:
|
|
187
193
|
- id: c1
|
|
188
194
|
text: the first one-on-one is the one meeting where the report should set the agenda
|
|
189
|
-
materials: [voice-memo, interview]
|
|
195
|
+
materials: [voice-memo#s3, interview#s3]
|
|
190
196
|
- id: c2
|
|
191
197
|
text: status belongs in the tracker, and a one-on-one spent on it teaches the manager nothing new
|
|
192
|
-
materials: [voice-memo, survey]
|
|
198
|
+
materials: [voice-memo#s2, voice-memo#s5, survey#s4]
|
|
193
199
|
- id: c3
|
|
194
200
|
text: three questions are enough to hand the meeting over
|
|
195
|
-
materials: [voice-memo]
|
|
201
|
+
materials: [voice-memo#s4]
|
|
196
202
|
- id: c4
|
|
197
203
|
text: the answer worth having comes after a silence the manager does not fill
|
|
198
|
-
materials: [interview]
|
|
204
|
+
materials: [interview#s7, interview#s8]
|
|
199
205
|
check:
|
|
200
206
|
rubric: each claim lands, in order, and the draft argues nothing outside the chain
|
|
201
207
|
source: spine interview
|
|
@@ -215,3 +221,8 @@ fiction: false
|
|
|
215
221
|
A worked example of the writing profile: an essay for new managers, specified before a word of
|
|
216
222
|
it is drafted. Every file this spec names ships beside it. `essay/claims.jsonl` does not exist
|
|
217
223
|
yet, because the claims ledger is written during drafting.
|
|
224
|
+
|
|
225
|
+
The writer's voice for this piece comes from a scope folder, `dna/essay-new-managers-teach/`:
|
|
226
|
+
the goldens this writer approved for essays that teach new managers, and the features measured
|
|
227
|
+
from them. Another essay by the same writer, for the same readers and the same purpose, would
|
|
228
|
+
point at the same folder.
|
|
@@ -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:
|
|
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
|
+
"version": "0.5.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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
166
|
-
// stdin
|
|
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 }),
|