@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 CHANGED
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (2026-09-29)
4
+
5
+ Scoped writer DNA. A writer does not have one voice: the same person writes differently for a
6
+ theology journal and a landing page. So a writer's voice is now kept per scope, a form, an
7
+ audience and a purpose, as a folder of goldens: real passages a person approved, each with a note
8
+ on the move it teaches and where it came from. A golden feeds only work that shares its scope, so
9
+ a passage that is right for one kind of writing never teaches its moves to another. hyperspec
10
+ measures each scope's style from its goldens (sentence and paragraph length, punctuation,
11
+ pronouns, signature words), counts and never judges, and still calls no model.
12
+
13
+ **No behavior change for existing specs.** Scoped DNA is opt-in through a new optional field,
14
+ `writing.dna.scope_dir`. A spec without it passes and fails exactly as it did in 0.4.0, and no
15
+ finding id changed: every id below is new.
16
+
17
+ - `hyperspec dna init <scope-dir> --writer W --form F --audience A --purpose P` writes a scope
18
+ folder: `scope.md` (writer, form, audience, purpose, optional notes) and a `goldens/` folder
19
+ holding a README on the golden file shape. It refuses to overwrite an existing `scope.md`, never
20
+ replaces a `goldens/README.md` that is already there, and
21
+ exits 2 with a plain message on a missing flag, a flag whose value is a placeholder, or a
22
+ parent folder that does not exist. Paths print as you gave them.
23
+ - A golden is one `.md` file directly in `goldens/`, other than `README.md`: frontmatter `why`
24
+ (the move it teaches), `approved_by` (a person; an approver starting `agent:` is refused,
25
+ because golden means a human approved it) and `source` are required, `approved_on` is optional,
26
+ and the body is the passage, verbatim. A `goldens/` folder that resolves outside its scope, such
27
+ as a symlink to another scope's goldens, is refused under test 5, naming where it leads.
28
+ - `hyperspec dna measure <scope-dir> [--json]` checks every golden and writes
29
+ `<scope-dir>/features.json`: the scope, each golden's path and SHA-256, and the features. If
30
+ the scope or any golden fails a check it writes nothing and exits 1, so a hollow or borrowed
31
+ golden is never measured in. The same goldens always produce the same bytes.
32
+ - The features: word count; sentence length in words (mean, median, 90th percentile); paragraph
33
+ length in sentences and in words; per-1000-word rates of commas, semicolons, colons, em dashes,
34
+ en dashes, exclamation marks, question marks, parentheses and quotation marks; contraction,
35
+ first-person singular, first-person plural and second-person rates; mean word length; and up to
36
+ 15 signature words. Sentences and paragraphs are split the same way `segments init` splits
37
+ them.
38
+ - With `writing.dna.scope_dir`, `lint` checks that `scope.md` matches the spec's writer, form,
39
+ audience and purpose (test 1); that every golden the spec lists is one of the scope's goldens,
40
+ after following any symlink, and never a passage in a subfolder, in `README.md` or in another
41
+ kind of file (test 5); that every golden in the folder has its `why` (test 6), a person's
42
+ approval and a source (test 4) and a passage (test 1); and that `features.json` is exactly what
43
+ `dna measure` would write now (test 6). The stale finding names what differs: goldens added,
44
+ removed or changed, a changed `scope.md` field, an unknown format version, or a number edited by
45
+ hand. A `scope_dir` that is present but a placeholder fails test 1.
46
+ - New finding ids, all starting `writing-dna-`: `scope-dir`, `scope-missing`,
47
+ `scope-file-<field>` (a scope's `scope.md` lacks writer, form, audience or purpose),
48
+ `scope-mismatch-<field>`, `goldens-missing`, `goldens-empty`, `goldens-outside`,
49
+ `golden-unreadable`, `golden-frontmatter`, `golden-empty`, `golden-approved-by`,
50
+ `golden-approved-by-agent`, `golden-source`, `golden-leak`, `golden-why`, `features-missing` and
51
+ `features-stale`. Every existing id is unchanged; in particular a spec whose own
52
+ `writing.dna.scope` lacks a field still reports `writing-dna-scope-form` (and `-audience`,
53
+ `-purpose`) as in 0.4.0. Every message names the scope folder as the spec wrote it and the
54
+ golden by its path inside the folder, never a folder on your machine. WRITING.md lists every one
55
+ with its test.
56
+ - `hyperspec init --profile writing` shows `scope_dir: TODO` in the `dna` block, which fails until
57
+ it names a scope folder or is deleted.
58
+ - Two new exports from `@supersuit/hyperspec/writing`: `readScope`, which reads a scope folder
59
+ and returns `{ scope, goldens, findings }` without throwing, and `measureFeatures`, which takes
60
+ an array of passages and returns the features `dna measure` writes.
61
+ - The essay example takes its voice from a scope folder,
62
+ `examples/writing/dna/essay-new-managers-teach/`, with three goldens and a measured
63
+ `features.json`. Its two goldens moved there from `examples/writing/essay/goldens/`, and a third
64
+ was added. The story example keeps its goldens in the spec with no scope folder, and still
65
+ passes.
66
+ - WRITING.md gains a Scoped DNA section: why a writer's DNA is scoped, the folder shape, the
67
+ golden file, both commands with their output, what each feature measures and what it is for,
68
+ staleness, naming a scope in a spec, every finding with its test, and the exports. README and
69
+ SPEC.md point at it.
70
+
3
71
  ## 0.4.0 (2026-09-29)
4
72
 
5
73
  Marking materials. Before a writing spec can pass, every material it draws on (a brain dump, a
package/README.md CHANGED
@@ -26,13 +26,15 @@ improvement ledger. Every test is defined in [SPEC.md](SPEC.md).
26
26
  | `hyperspec init <file> [--title T] [--kind K]` | Write a new hyperspec skeleton. Refuses to overwrite an existing file. |
27
27
  | `hyperspec init <file> --profile writing [--title T] [--form F] [--fiction]` | Write a writing-spec skeleton, every block shown with placeholders. |
28
28
  | `hyperspec segments init <material> --id <mid> [--out F] [--by paragraph\|sentence]` | Split a material into segments to label. Refuses to overwrite an existing file. |
29
+ | `hyperspec dna init <scope-dir> --writer W --form F --audience A --purpose P` | Start a writer-DNA scope folder. Refuses to overwrite an existing `scope.md`. |
30
+ | `hyperspec dna measure <scope-dir>` | Check every golden in a scope and write its measured features. |
29
31
  | `hyperspec recipe check <output-or-recipe>` | Check that a recipe records everything the standard asks for. |
30
32
  | `hyperspec recipe approve <recipe> --by <slug>` | Record who approved the output. |
31
33
  | `hyperspec reproduce <recipe> [--restore]` | Re-check every hash the recipe recorded. Never runs a model. |
32
34
  | `hyperspec regenerate <recipe> --out <path> --clicker <slug> <one change> [--run cmd]` | Make a child recipe from a parent and one named change, rerunning only the stages it reaches. |
33
35
  | `hyperspec compare <child-recipe> --doctor cmd` | Grade a child and its parent through one doctor against one spec. |
34
36
 
35
- Every command except `init` and `segments init` takes `--json`. `hyperspec --help` prints every flag.
37
+ Every command except `init`, `segments init` and `dna init` takes `--json`. `hyperspec --help` prints every flag.
36
38
 
37
39
  ## Exit codes
38
40
 
@@ -152,6 +154,28 @@ segment. The file format, the labels, and every finding are in
152
154
  [WRITING.md](WRITING.md#marking-materials). A tool of your own can run the same check with
153
155
  `import { readSegments } from "@supersuit/hyperspec/writing"`.
154
156
 
157
+ ### Scoped DNA
158
+
159
+ A writer sounds different in a theology essay and on a landing page, so a writer's voice is kept
160
+ per scope (a form, an audience and a purpose), and each scope is a folder of goldens. A golden is a real
161
+ passage a person approved, with a note on the move it teaches and where it came from, and it
162
+ feeds only work that shares its scope.
163
+
164
+ ```bash
165
+ mkdir -p dna
166
+ npx @supersuit/hyperspec dna init dna/essay-new-managers-teach --writer example-author --form essay --audience "new managers" --purpose teach
167
+ npx @supersuit/hyperspec dna measure dna/essay-new-managers-teach
168
+ ```
169
+
170
+ `dna init` writes `scope.md` and a `goldens/` folder holding only a README. Add one file per
171
+ golden, then `dna measure` checks each golden and writes `features.json`: sentence and paragraph length, punctuation,
172
+ pronouns and signature words, measured and never judged. Name the folder in a spec as
173
+ `writing.dna.scope_dir` and `lint` checks that the scope matches the spec, that no golden comes
174
+ from another scope, and that the measurements are current. Without `scope_dir`, a spec lints as
175
+ it did in 0.4. The folder shape, every feature, and every finding are in
176
+ [WRITING.md](WRITING.md#scoped-dna); `readScope` and `measureFeatures` are exported from
177
+ `@supersuit/hyperspec/writing`.
178
+
155
179
  ## The format
156
180
 
157
181
  A hyperspec is a markdown file with a YAML frontmatter block: `decisions`, `requirements`,
package/SPEC.md CHANGED
@@ -97,7 +97,7 @@ examples:
97
97
  - path: examples/minimal.hyperspec.md
98
98
  why: the smallest spec that passes all nine tests
99
99
  resume:
100
- next_action: collect adopter issues on 0.4, materials marking included, and cut 0.5 from them
100
+ next_action: collect adopter issues on 0.5, scoped DNA included, and cut 0.6 from them
101
101
  feedback:
102
102
  issues: https://github.com/SupersuitUp/hyperspec/issues
103
103
  fork: MIT; fork it for your own purposes and say so in your SPEC
@@ -109,7 +109,7 @@ improvement:
109
109
 
110
110
  A person writing for another person leaves most of the specification unsaid, because the other person fills the gaps from shared context. An agent has none of that context, so it fills every gap with the average, and the average is what reads as middling. Hyperspecification is writing down the gaps. It is a level of detail that would feel like overkill between two people and is exactly enough for an agent: every decision the agent would otherwise guess is either decided, delegated with the rule for deciding it, or marked open, so the work stops instead of guessing.
111
111
 
112
- **Version 0.4.0** (2026-09-29)
112
+ **Version 0.5.0** (2026-09-29)
113
113
 
114
114
  ## What makes a spec a hyperspec
115
115
 
@@ -220,7 +220,7 @@ Each row lists every condition under which `hyperspec lint` fails that test. A w
220
220
  | 8 its adopters can push back on it | `feedback.issues` or `feedback.fork` missing |
221
221
  | 9 it improves itself | `improvement.ledger` missing; a ledger path that exists and is not a readable file; if the ledger file exists, a line that is not a JSON object, a `verdict` outside one-shot, improved or not-improved, `improved` without `change`, `not-improved` without `reason`. A declared ledger that does not exist yet is a warning |
222
222
 
223
- A profile adds its own conditions to these rows. The writing profile's are in [WRITING.md](WRITING.md#the-test-mapping), including the checks on each material's segments file: every material marked, every segment labeled from the closed set and matching its material word for word, the marking current (tests 1 and 4), and no spine claim citing a private or question segment (test 5).
223
+ A profile adds its own conditions to these rows. The writing profile's are in [WRITING.md](WRITING.md#the-test-mapping), including the checks on each material's segments file: every material marked, every segment labeled from the closed set and matching its material word for word, the marking current (tests 1 and 4), and no spine claim citing a private or question segment (test 5). A writing spec that names a writer-DNA scope folder with `writing.dna.scope_dir` is also checked against it: the scope matches the spec (test 1), every golden has a person's approval and a source (test 4), no golden comes from outside the scope (test 5), and every golden has its why and the scope's measured features are current (test 6). `scope_dir` is optional, and without it nothing changes.
224
224
 
225
225
  ## Exit codes
226
226
 
@@ -245,7 +245,7 @@ Silence is not a verdict. A run that learned nothing has to say so and why, and
245
245
 
246
246
  A profile adds the rules for one kind of work on top of the nine tests. A spec opts in with a top-level `profile:` naming it. A profile never adds a tenth test: every finding it raises reports under one of the nine, with an id that starts with the profile's name, and the score stays out of nine. `lint` prints one more line for a profiled spec, how many of the profile's blocks are complete. A `profile` this linter does not know is a warning under test 7, and none of its rules are checked.
247
247
 
248
- One profile ships: `writing`, for essays, chapters, letters, stories and anything else an agent drafts for a person to read. Its blocks, its fields, which test each rule reports under, `hyperspec init --profile writing`, and marking materials with `hyperspec segments init` are in [WRITING.md](WRITING.md).
248
+ One profile ships: `writing`, for essays, chapters, letters, stories and anything else an agent drafts for a person to read. Its blocks, its fields, which test each rule reports under, `hyperspec init --profile writing`, marking materials with `hyperspec segments init`, and scoped writer DNA with `hyperspec dna init` and `hyperspec dna measure` are in [WRITING.md](WRITING.md).
249
249
 
250
250
  ## Recipes
251
251
 
package/WRITING.md CHANGED
@@ -61,10 +61,14 @@ whole design of this block.
61
61
  email.
62
62
  - **Every golden carries a note on why it is golden.** A golden without its reason teaches the
63
63
  surface; the reason teaches the move.
64
+ - **Features** are measured per scope from its goldens: sentence and paragraph length,
65
+ punctuation habits, pronouns, signature words. Two scopes of one writer measure differently,
66
+ and each keeps its own numbers.
64
67
 
65
- DNA is proven by a blind lineup within its scope: a judge sees a generated passage beside real
66
- goldens of the same kind and tries to pick it out. Every writer has their own DNA, and nobody's
67
- scope feeds anybody else's.
68
+ A scope is a folder on disk, and [Scoped DNA](#scoped-dna) covers it: its shape, the golden file,
69
+ what is measured, and how a spec names it. A later release proves DNA with a blind lineup within
70
+ its scope: a judge sees a generated passage beside real goldens of the same kind and tries to
71
+ pick it out. Every writer has their own DNA, and nobody's scope feeds anybody else's.
68
72
 
69
73
  ### 3. Persona: who the piece speaks as
70
74
 
@@ -170,14 +174,15 @@ writing:
170
174
  author: agent:claude
171
175
  dna:
172
176
  writer: example-author
177
+ scope_dir: dna/essay-new-managers-teach # optional; a scope folder (see Scoped DNA), and every golden below then lives in its goldens/
173
178
  scope:
174
179
  form: essay
175
180
  audience: new managers
176
181
  purpose: teach
177
182
  rules: style-rules.md # your style rules file, the always-on layer
178
183
  goldens: # at least one
179
- - path: goldens/opening.md
180
- why: one plain claim, then a second sentence that turns it into something to do
184
+ - path: dna/essay-new-managers-teach/goldens/opening.md
185
+ why: one plain claim in the first sentence, then two short sentences that turn it into something to do
181
186
  check:
182
187
  rubric: blind lineup within this scope
183
188
  source: goldens marked on the review page
@@ -319,12 +324,12 @@ Each row lists what the writing profile adds to that test. The core conditions i
319
324
 
320
325
  | Test | A writing spec fails it when |
321
326
  |---|---|
322
- | 1 every decision is accounted for | a required block is missing and not deferred; a required field is missing; a closed-set value is outside its set (`trust`, `reader`, `change.kind`, the shape of `identity`, `unsourced_claim`); `identity: character:<id>` names a character that is not in `writing.characters`; `fiction` is present and is anything other than `true` or `false`; two materials, two spine claims or two characters share an id; `form.length.min` or `max` is not a whole number of at least 1, or `min` is greater than `max`; `spine.claims` has fewer than three or more than seven distinct claims; a character has no knowledge entry, or an entry lacks `by` or `knows`; a material has no text, or no `segments` field, or its segments file is missing, malformed, labels a segment outside the seven (`unlabeled` included), repeats a segment id, or has segments that overlap or leave text uncovered. A `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
327
+ | 1 every decision is accounted for | a required block is missing and not deferred; a required field is missing; a closed-set value is outside its set (`trust`, `reader`, `change.kind`, the shape of `identity`, `unsourced_claim`); `identity: character:<id>` names a character that is not in `writing.characters`; `fiction` is present and is anything other than `true` or `false`; two materials, two spine claims or two characters share an id; `form.length.min` or `max` is not a whole number of at least 1, or `min` is greater than `max`; `spine.claims` has fewer than three or more than seven distinct claims; a character has no knowledge entry, or an entry lacks `by` or `knows`; a material has no text, or no `segments` field, or its segments file is missing, malformed, labels a segment outside the seven (`unlabeled` included), repeats a segment id, or has segments that overlap or leave text uncovered; `dna.scope_dir` is present and is a placeholder; with `dna.scope_dir`, its `scope.md` is missing, unreadable or lacks a field, its writer, form, audience or purpose differs from the spec's, or its `goldens/` folder is missing or empty, or holds a golden that cannot be read, whose frontmatter never closes, or that has no passage. A `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
323
328
  | 2 every requirement can fail | `goal.conditions` lists fewer than five or more than ten distinct ids, lists an id twice, or names an id that is not a top-level requirement |
324
329
  | 3 every requirement names its check | a block or a character has no `check` with a `station` or a `rubric` |
325
- | 4 every field says where it came from and who wrote it | a block or a character has no `source` or no `author`; a spine claim names no materials, or names a material id that is not in `materials.items`, or a segment that is not in that material's segments file; a segment's text does not match its material word for word; a material changed after it was marked; a claim segment has no `source` and no `own`, a story no `teller`, a quote no `speaker` |
326
- | 5 negative space is specified | `persona.will_not_say` is empty; `persona.facts_from` is anything other than `sources`; a spine claim cites a `private` or a `question` segment |
327
- | 6 examples outrank adjectives | a golden has no `why`; a material, `dna.rules`, golden or character `entity` path does not exist or is not a file; a character has no golden lines or no rejected lines, or has the same line in both (compared trimmed and case-folded) |
330
+ | 4 every field says where it came from and who wrote it | a block or a character has no `source` or no `author`; a spine claim names no materials, or names a material id that is not in `materials.items`, or a segment that is not in that material's segments file; a segment's text does not match its material word for word; a material changed after it was marked; a claim segment has no `source` and no `own`, a story no `teller`, a quote no `speaker`; with `dna.scope_dir`, a golden in the scope has no `approved_by`, an approver that starts `agent:`, or no `source` |
331
+ | 5 negative space is specified | `persona.will_not_say` is empty; `persona.facts_from` is anything other than `sources`; a spine claim cites a `private` or a `question` segment; with `dna.scope_dir`, a golden the spec lists is not one of the scope's goldens (it lives outside the scope's `goldens/` folder, is a symlink that resolves outside it, or sits in a subfolder, is `README.md` or is not a `.md` file), or the scope's `goldens/` folder resolves outside the scope |
332
+ | 6 examples outrank adjectives | a golden has no `why`; a material, `dna.rules`, golden or character `entity` path does not exist or is not a file; a character has no golden lines or no rejected lines, or has the same line in both (compared trimmed and case-folded); with `dna.scope_dir`, a golden in the scope has no `why`, or the scope's `features.json` is missing or is not what `dna measure` would write now |
328
333
  | 7 a stranger can resume it | `writing.progress` exists. An unknown `profile:` is a warning |
329
334
  | 8 its adopters can push back on it | nothing further; the core rule applies |
330
335
  | 9 it improves itself | nothing further; the core rule applies |
@@ -527,6 +532,260 @@ Two more options, `displayPath` and `materialDisplayPath`, set how the two files
527
532
  messages (lint passes the paths as the spec wrote them); by default the paths are printed as
528
533
  given. `MATERIAL_LABELS` is the seven labels, in the order of the table above.
529
534
 
535
+ ## Scoped DNA
536
+
537
+ A writer does not have one voice, so hyperspec does not keep one. A writer's DNA is kept per
538
+ **scope**, a form, an audience and a purpose together, and each scope is a folder holding its own
539
+ goldens and its own measurements.
540
+
541
+ The reason is a leak. A passage can be exactly right for one kind of writing and wrong for
542
+ another. The short, warm sentences that make a text message land read as thin in a theology
543
+ essay, and the long, qualified sentences that make the essay careful read as evasive on a landing
544
+ page. Pool every golden a writer has into one set and an agent learns the moves of each kind of
545
+ writing and carries them into the others. Filed by scope, a golden feeds only work that shares
546
+ its scope, so the moves it teaches stay where they are right. Retrieval is by scope, never by
547
+ "best writing overall".
548
+
549
+ Scoped DNA is optional in this release. A spec that names no scope folder lints exactly as it did
550
+ in 0.4.
551
+
552
+ ### The scope folder
553
+
554
+ ```text
555
+ dna/essay-new-managers-teach/
556
+ scope.md writer, form, audience, purpose, optional notes
557
+ goldens/
558
+ README.md the golden file shape; never read as a golden
559
+ close.md one golden per file
560
+ opening.md
561
+ status.md
562
+ features.json written by dna measure, never by hand
563
+ ```
564
+
565
+ `scope.md` carries the scope in its frontmatter; its body is free text for people:
566
+
567
+ ```markdown
568
+ ---
569
+ writer: example-author
570
+ form: essay
571
+ audience: new managers
572
+ purpose: teach
573
+ ---
574
+ ```
575
+
576
+ `writer`, `form`, `audience` and `purpose` are required, and `notes` is optional. Name the folder
577
+ after its scope so a person can tell scopes apart at a glance. hyperspec reads the scope from
578
+ `scope.md`, never from the folder's name.
579
+
580
+ `goldens/` must be a real folder inside the scope. A `goldens/` that resolves somewhere else, such
581
+ as a symlink to another scope's goldens, would carry that scope's passages into this one under
582
+ this scope's name, so `dna measure` refuses it and lint fails it under test 5, both naming where it
583
+ leads. A whole scope folder reached through a symlink is fine, because `scope.md` travels with
584
+ it.
585
+
586
+ ### A golden
587
+
588
+ A golden is a real passage the writer marked as right, one per file in `goldens/`. Every `.md`
589
+ file directly in `goldens/` is a golden except `README.md`, which is for notes to people and is
590
+ never read as a golden. A subfolder, a file with another extension, and a symlink sitting in the
591
+ folder are not read either. This is the essay example's opening:
592
+
593
+ ```markdown
594
+ ---
595
+ why: one plain claim in the first sentence, then two short sentences that turn it into something to do
596
+ approved_by: example-author
597
+ source: first draft of this essay's opening paragraph, marked golden on the review page
598
+ approved_on: "2026-09-18"
599
+ ---
600
+
601
+ Your first one-on-one with a new report is the only meeting on your calendar where they should
602
+ set the agenda. Everything else you run. This one you hand over.
603
+ ```
604
+
605
+ - **`why`** (required) names the move the passage teaches. A golden without its reason teaches
606
+ the surface: an agent copies its length, its words and its rhythm. The reason teaches the move,
607
+ which carries over to a passage that shares none of those.
608
+ - **`approved_by`** (required) is the person who approved it, as a slug. Golden means a human
609
+ approved it, so an approver that starts `agent:` is refused. An agent may propose a golden;
610
+ only a person makes one.
611
+ - **`source`** (required) says where the passage came from: a draft, an earlier piece, a review
612
+ page. It lets someone check that the passage is real and find the context it was written in.
613
+ - **`approved_on`** (optional) is the date it was approved.
614
+ - **The body** is the passage, verbatim. Whitespace before and after it is dropped, and nothing
615
+ inside it is changed.
616
+
617
+ A placeholder counts as missing in every one of these fields, as it does everywhere in a
618
+ hyperspec (see [The schema](#the-schema)).
619
+
620
+ ### Starting a scope
621
+
622
+ ```bash
623
+ mkdir -p dna
624
+ npx @supersuit/hyperspec dna init dna/essay-new-managers-teach --writer example-author --form essay --audience "new managers" --purpose teach
625
+ ```
626
+
627
+ `hyperspec dna init <scope-dir> --writer W --form F --audience A --purpose P` writes `scope.md`
628
+ and a `goldens/` folder holding only a README on the golden file shape. All four flags are
629
+ required. It refuses to overwrite an existing `scope.md`, never replaces a `goldens/README.md`
630
+ that is already there, and exits 2 with a plain message on a missing flag, a flag whose value is a
631
+ placeholder, or a scope folder whose parent folder does not exist. Then add one file per golden.
632
+
633
+ ### Measuring a scope
634
+
635
+ ```bash
636
+ npx @supersuit/hyperspec dna measure dna/essay-new-managers-teach
637
+ ```
638
+
639
+ ```
640
+ dna/essay-new-managers-teach: measured 3 goldens
641
+ word_count 118, sentence length mean 13.111 median 15 p90 25
642
+ signature words: first, report, tracker
643
+ wrote dna/essay-new-managers-teach/features.json
644
+ ```
645
+
646
+ `hyperspec dna measure <scope-dir> [--json]` reads every golden, checks each one's own fields, and
647
+ writes `<scope-dir>/features.json`. If the scope or any golden fails a check (no `why`, an agent
648
+ approver, no passage, a `goldens/` folder that resolves outside the scope), it prints the findings,
649
+ writes nothing and exits 1, so a hollow or borrowed golden is never measured into the DNA. It exits 0 when it wrote the file and 2 on a usage error. `--json`
650
+ prints the same result as JSON. The same goldens always produce the same bytes.
651
+
652
+ `features.json` holds `dna` (the version of this format, `"0.1"`), `scope` (the four fields from
653
+ `scope.md`), `goldens` (each golden's path inside the folder and the SHA-256 of its file, sorted
654
+ by path) and `features`.
655
+
656
+ ### What is measured
657
+
658
+ Every feature is a count or a ratio computed from the goldens' text. None of them is a judgment:
659
+ a number says how the writer writes in this scope, never whether the writing is good, and
660
+ hyperspec calls no model to get it. Words are pooled across every golden in the scope, so their
661
+ order changes nothing. Paragraphs and sentences are split exactly as `segments init` splits them,
662
+ so the two never disagree about where a boundary falls. A word is a run of letters, digits and
663
+ apostrophes, lowercased. Every number is rounded to three decimal places, and a rate is per 1000
664
+ words.
665
+
666
+ | Feature | What it counts | What it is for |
667
+ |---|---|---|
668
+ | `word_count` | words across every golden | how much text the other numbers rest on; a scope of a few dozen words measures loosely |
669
+ | `sentence_length` | words per sentence: `mean`, `median` and `p90` (nearest rank) | the writer's usual sentence, and how long their long ones run, which a mean hides |
670
+ | `paragraph_length` | per paragraph, the mean number of sentences (`mean_sentences`) and of words (`mean_words`) | how much the writer puts in one block before a break |
671
+ | `rates_per_1000_words` | commas, semicolons, colons, em dashes, en dashes, exclamation marks, question marks, parentheses (each one counted) and double quotation marks, straight or curly | punctuation habits, which carry much of how a voice sounds |
672
+ | `contraction_rate` | words with an apostrophe between two letters | how conversational the writer is in this scope |
673
+ | `first_person_singular_rate` | I, me, my, mine, myself | how much the writer speaks as themselves |
674
+ | `first_person_plural_rate` | we, us, our, ours, ourselves | how much the writer speaks as a group, or alongside the reader |
675
+ | `second_person_rate` | you, your, yours, yourself, yourselves | how directly the writer addresses the reader |
676
+ | `mean_word_length` | characters per word | plain words or long ones |
677
+ | `signature_words` | up to 15 words of four or more letters that are not common function words and appear at least twice, most frequent first, ties in alphabetical order | the vocabulary the writer returns to in this scope |
678
+
679
+ ### When the scope changes
680
+
681
+ `features.json` is current only when it is exactly what `dna measure` would write from the scope
682
+ as it reads now: the same goldens, pinned by the SHA-256 of each file; the same four fields as
683
+ `scope.md`; the format version `"0.1"`; and the same numbers. Add a golden, remove one, change any
684
+ byte of one (its passage or its frontmatter), edit `scope.md`, or edit a number by hand, and lint
685
+ fails the scope as stale under test 6. The finding names what differs: each golden added, removed
686
+ or changed, each scope field that changed, an unknown version, or each feature whose number no
687
+ longer matches a fresh measurement. Run `dna measure` again. The hash is over bytes, so a
688
+ line-ending conversion counts as a change, as it does for a segments file (see
689
+ [When a material changes](#when-a-material-changes)).
690
+
691
+ ### Naming the scope in a spec
692
+
693
+ `writing.dna.scope_dir` points a writing spec at its scope folder, relative to the spec like every
694
+ other path. The essay example's `dna` block:
695
+
696
+ ```yaml
697
+ dna:
698
+ writer: example-author
699
+ scope_dir: dna/essay-new-managers-teach
700
+ scope:
701
+ form: essay
702
+ audience: new managers
703
+ purpose: teach
704
+ rules: style-rules.md
705
+ goldens:
706
+ - path: dna/essay-new-managers-teach/goldens/opening.md
707
+ why: one plain claim in the first sentence, then two short sentences that turn it into something to do
708
+ ```
709
+
710
+ `scope_dir` is optional. Without it, `dna` lints exactly as it did in 0.4: each golden the spec
711
+ lists needs a path to a file and a `why`, and no scope folder is read. With it, lint also checks
712
+ that:
713
+
714
+ - `scope.md`'s writer equals `dna.writer`, and its form, audience and purpose equal `dna.scope`,
715
+ compared trimmed and ignoring case (test 1);
716
+ - every golden the spec lists is one of the scope's goldens: after following any symlink, a `.md`
717
+ file directly in the scope's own `goldens/` folder, other than `README.md` (test 5). A golden
718
+ from another scope is a leak, the exact thing a scope exists to prevent, and so is a passage in
719
+ a subfolder, in `README.md` or in another kind of file, which would feed the spec without ever
720
+ being checked or measured;
721
+ - every golden in the folder, listed in the spec or not, has a `why` (test 6), an `approved_by`
722
+ that names a person and a `source` (test 4), and a passage (test 1);
723
+ - `features.json` exists and is current, as [When the scope changes](#when-the-scope-changes)
724
+ defines it (test 6).
725
+
726
+ The spec still gives each golden it lists a `why`, as in 0.4; the essay example keeps it the same
727
+ as the golden file's own. A `scope_dir` that is present but a placeholder, such as `TODO`, fails
728
+ test 1 on its own, so a skeleton cannot pass by leaving it unfilled.
729
+
730
+ ### Findings
731
+
732
+ Every scoped-DNA finding id starts `writing-dna-` and fails the test in its row. Messages name the
733
+ scope folder as the spec wrote it (or as it was given to `dna measure`) and each golden by its
734
+ path inside the folder, so the output is the same on every machine. `<field>` is `writer`,
735
+ `form`, `audience` or `purpose`.
736
+
737
+ | Id | Test | Fails when |
738
+ |---|---|---|
739
+ | `writing-dna-scope-dir` | 1 | `writing.dna.scope_dir` is present and is a placeholder |
740
+ | `writing-dna-scope-<field>` | 1 | the spec's own `writing.dna.scope` has no `form`, `audience` or `purpose` (this check runs with or without `scope_dir`, and is the id 0.4 used) |
741
+ | `writing-dna-scope-missing` | 1 | `scope.md` does not exist, cannot be read, or its frontmatter does not parse |
742
+ | `writing-dna-scope-file-<field>` | 1 | `scope.md` has no such field |
743
+ | `writing-dna-scope-mismatch-<field>` | 1 | `scope.md` and the spec disagree on that field |
744
+ | `writing-dna-goldens-missing` | 1 | the `goldens/` folder does not exist or cannot be read |
745
+ | `writing-dna-goldens-empty` | 1 | `goldens/` holds no golden |
746
+ | `writing-dna-goldens-outside` | 5 | `goldens/` resolves to a folder outside the scope, such as a symlink to another scope's goldens |
747
+ | `writing-dna-golden-unreadable` | 1 | a golden file cannot be read |
748
+ | `writing-dna-golden-frontmatter` | 1 | a golden's frontmatter opens with `---` and never closes |
749
+ | `writing-dna-golden-empty` | 1 | a golden has no passage |
750
+ | `writing-dna-golden-approved-by` | 4 | a golden has no `approved_by` |
751
+ | `writing-dna-golden-approved-by-agent` | 4 | a golden's `approved_by` starts `agent:`, in any case |
752
+ | `writing-dna-golden-source` | 4 | a golden has no `source` |
753
+ | `writing-dna-golden-leak` | 5 | a golden the spec lists is not one of the scope's goldens: it lives outside the scope's `goldens/` folder, is a symlink that resolves outside it, or sits in a subfolder, is `README.md` or is not a `.md` file |
754
+ | `writing-dna-golden-why` | 6 | a golden has no `why` |
755
+ | `writing-dna-features-missing` | 6 | the scope has no `features.json`, or it is not valid JSON |
756
+ | `writing-dna-features-stale` | 6 | `features.json` is not what `dna measure` would write now: a golden was added, removed or changed, `scope.md` changed, the version is unknown, or a number differs from a fresh measurement |
757
+
758
+ `dna measure` raises the ids that come from the folder alone: every row except `scope-dir`,
759
+ `scope-<field>`, `scope-mismatch-<field>`, `golden-leak` and the two `features-` rows, which
760
+ need a spec to compare against. Lint raises all of them.
761
+
762
+ ### Reading a scope from your own tool
763
+
764
+ A tool of your own, such as a review page that files goldens, can read a scope and measure it the
765
+ way `dna measure` does:
766
+
767
+ ```js
768
+ import { readScope, measureFeatures } from "@supersuit/hyperspec/writing";
769
+
770
+ const { scope, goldens, findings } = readScope("dna/essay-new-managers-teach");
771
+ const features = measureFeatures(goldens.map((g) => g.text));
772
+ ```
773
+
774
+ `readScope` never throws for a folder path, whatever is or is not in the folder. It returns the scope's four fields and `notes` (or `null` when
775
+ `scope.md` cannot be read at all), every golden it could read, with its `path`, `why`,
776
+ `approved_by`, `source`, `approved_on`, `text` and `sha256`, and findings in the shape lint
777
+ reports. `displayDir` sets how the folder is named in messages. `measureFeatures` takes an array
778
+ of passages and returns the `features` object `dna measure` writes. It reads no file and returns
779
+ the same object for the same passages.
780
+
781
+ ### The worked example
782
+
783
+ The essay in [`examples/writing/`](examples/writing/) takes its voice from
784
+ `dna/essay-new-managers-teach/`: three goldens, each with its `why`, a person's approval and its
785
+ source, and a `features.json` that `dna measure` wrote. A test measures the folder again on every
786
+ release and requires the same bytes, so the example cannot drift from the tool. The short story
787
+ beside it lists its goldens in the spec with no scope folder, the 0.4 shape, which still passes.
788
+
530
789
  ## Deferring a block
531
790
 
532
791
  A block can be deferred, never silently missing. A required block that is absent fails test 1
@@ -561,8 +820,10 @@ placeholder. `dna`, `persona`, `audience` and `goal` also carry an open decision
561
820
  says what you have to answer before the placeholder means anything. `--form` sets both `kind:`
562
821
  and `writing.form.name`, and defaults to `essay`. The material item names
563
822
  `materials/TODO.md.segments.jsonl`, the file `segments init` writes for `materials/TODO.md`, so
564
- materials keeps failing until a real material is marked. `--fiction` sets `fiction: true` and adds one
565
- character with the same treatment. The skeleton never passes: it lints `fail`, with
823
+ materials keeps failing until a real material is marked. `dna` shows `scope_dir: TODO`, which
824
+ fails until it names a scope folder (see [Scoped DNA](#scoped-dna)) or is deleted, since the
825
+ field is optional. `--fiction` sets `fiction: true` and adds one character with the same
826
+ treatment. The skeleton never passes: it lints `fail`, with
566
827
  `writing: 1/9 blocks complete` (or `0/9` with `--fiction`), until the placeholders and the open
567
828
  decisions are replaced with real content.
568
829
 
@@ -577,7 +838,8 @@ Two complete specs ship in [`examples/writing/`](examples/writing/), each with e
577
838
  names:
578
839
 
579
840
  - `essay.hyperspec.md`: an essay for new managers on running a first one-on-one. Three materials
580
- at three trust levels, scoped DNA with two annotated goldens, a four-claim spine.
841
+ at three trust levels, its voice from the scope folder `dna/essay-new-managers-teach/` with three
842
+ annotated goldens and their measured features, a four-claim spine.
581
843
  - `story.hyperspec.md`: a short story, `fiction: true`, narrated by one of its two characters.
582
844
  Each character has speech rules, a knowledge timeline by scene, and golden and rejected lines
583
845
  in a voice you can tell apart from the other's.
@@ -592,6 +854,8 @@ every release, so they cannot drift from the linter.
592
854
 
593
855
  ## What later versions add
594
856
 
595
- This release is the schema, its lint, and marked materials. Later versions build on it in order:
596
- scoped DNA with annotated goldens filed by form, audience and purpose, and the stations
597
- themselves, running the checks each block names and grading drafts against the goal.
857
+ This release is the schema, its lint, marked materials, and scoped DNA with measured features.
858
+ Later versions build on it in order. The first compares a draft against its scope: its features
859
+ beside the scope's features, and a blind lineup in which a judge sees a generated passage among
860
+ the scope's goldens and tries to pick it out. Then the stations themselves, running the checks
861
+ each block names and grading drafts against the goal.