@supersuit/hyperspec 0.4.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 +68 -0
- package/README.md +25 -1
- package/SPEC.md +4 -4
- package/WRITING.md +279 -15
- package/bin/hyperspec.mjs +108 -2
- 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.hyperspec.md +15 -7
- package/package.json +1 -1
- package/src/dna.mjs +471 -0
- package/src/writing-exports.mjs +8 -3
- package/src/writing-fields.mjs +173 -1
- package/src/writing-template.mjs +8 -0
- 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, readFileSync } 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";
|
|
@@ -14,6 +14,8 @@ import { regenerate } from "../src/regenerate.mjs";
|
|
|
14
14
|
import { compare } from "../src/compare.mjs";
|
|
15
15
|
import { splitSegments } from "../src/segments.mjs";
|
|
16
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";
|
|
17
19
|
|
|
18
20
|
const HELP = `hyperspec <command> [options]
|
|
19
21
|
|
|
@@ -43,6 +45,23 @@ const HELP = `hyperspec <command> [options]
|
|
|
43
45
|
with nothing in it, an --out folder that does not exist, or a --by
|
|
44
46
|
outside paragraph/sentence
|
|
45
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
|
+
|
|
46
65
|
recipe check <output-or-recipe> [--json]
|
|
47
66
|
check a recipe's completeness (a path not ending .recipe.json
|
|
48
67
|
means <path>.recipe.json)
|
|
@@ -146,6 +165,93 @@ if (cmd === "segments") {
|
|
|
146
165
|
process.exit(2);
|
|
147
166
|
}
|
|
148
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
|
+
|
|
149
255
|
if (cmd === "lint") {
|
|
150
256
|
const json = argv.includes("--json");
|
|
151
257
|
// Lint takes one flag, --json, and no flag takes a value, so a flag is dropped on its own and
|
|
@@ -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.
|
|
@@ -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,8 +71,8 @@ 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
77
|
next_action: outline the four spine claims against the form's required parts, citing the segments each claim points at
|
|
78
78
|
feedback:
|
|
@@ -110,18 +110,21 @@ writing:
|
|
|
110
110
|
author: agent:claude
|
|
111
111
|
dna:
|
|
112
112
|
writer: example-author
|
|
113
|
+
scope_dir: dna/essay-new-managers-teach
|
|
113
114
|
scope:
|
|
114
115
|
form: essay
|
|
115
116
|
audience: new managers
|
|
116
117
|
purpose: teach
|
|
117
118
|
rules: style-rules.md
|
|
118
119
|
goldens:
|
|
119
|
-
- path: essay/goldens/opening.md
|
|
120
|
-
why: one plain claim, then
|
|
121
|
-
- 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
|
|
122
125
|
why: ends on an instruction and gives the reason for it in the same sentence
|
|
123
126
|
check:
|
|
124
|
-
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
|
|
125
128
|
source: goldens marked on the review page
|
|
126
129
|
author: example-author
|
|
127
130
|
persona:
|
|
@@ -218,3 +221,8 @@ fiction: false
|
|
|
218
221
|
A worked example of the writing profile: an essay for new managers, specified before a word of
|
|
219
222
|
it is drafted. Every file this spec names ships beside it. `essay/claims.jsonl` does not exist
|
|
220
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.
|
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": {
|