@supersuit/hyperspec 0.4.0 → 0.6.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 +143 -0
- package/README.md +50 -2
- package/SPEC.md +5 -5
- package/WRITING.md +510 -17
- package/bin/hyperspec.mjs +177 -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/claims.jsonl +9 -0
- package/examples/writing/essay/draft.md +82 -0
- package/examples/writing/essay/materials/interview-notes.md.segments.jsonl +2 -2
- package/examples/writing/essay.hyperspec.md +32 -13
- package/examples/writing/story/claims.jsonl +9 -0
- package/examples/writing/story/draft.md +267 -0
- package/examples/writing/story.hyperspec.md +20 -5
- package/package.json +1 -1
- package/src/check.mjs +245 -0
- package/src/dna.mjs +474 -0
- package/src/stations/claims.mjs +150 -0
- package/src/stations/dna.mjs +126 -0
- package/src/stations/form.mjs +115 -0
- package/src/stations/index.mjs +27 -0
- package/src/stations/links.mjs +275 -0
- package/src/stations/private.mjs +117 -0
- package/src/stations/quotes.mjs +168 -0
- package/src/stations/terms.mjs +131 -0
- package/src/stations/util.mjs +99 -0
- package/src/writing-exports.mjs +8 -3
- package/src/writing-fields.mjs +197 -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/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
|
-
|
|
66
|
-
|
|
67
|
-
scope
|
|
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
|
|
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
|
|
@@ -204,6 +209,8 @@ writing:
|
|
|
204
209
|
wants: a plan for the first meeting
|
|
205
210
|
reads_on: a phone, in the ten minutes before the meeting
|
|
206
211
|
reader: person # person | agent
|
|
212
|
+
terms: # optional: terms the piece uses that the reader may not know
|
|
213
|
+
- skip-level
|
|
207
214
|
check:
|
|
208
215
|
station: term check against knows
|
|
209
216
|
rubric: simulated reader reports where it got lost
|
|
@@ -312,6 +319,17 @@ cannot pass a presence check. A placeholder is a whole value, trimmed and in any
|
|
|
312
319
|
marks, or an ellipsis, optionally followed by a trailing `.`, `:` or `!`. Real text that starts
|
|
313
320
|
with one of those, such as `TODO: write the opening`, counts as present, and so does `none`.
|
|
314
321
|
|
|
322
|
+
`audience.terms` is checked by the `terms` station in `hyperspec check`, and that check is a
|
|
323
|
+
**mechanical proxy, not an understanding of meaning**: it looks for a definition-SHAPED phrase
|
|
324
|
+
near the term's first appearance (the word `is`, `means`, `refers to`, a colon within a few words,
|
|
325
|
+
or an immediate parenthetical), not for whether that phrase defines the term. A sentence
|
|
326
|
+
like "A hyperspec is mentioned here" reads as a definition of "hyperspec" by this rule, because
|
|
327
|
+
`is` immediately follows the word, even though nothing about the term is explained. This is
|
|
328
|
+
deliberate and known, not a bug to fix later in this station: reading for meaning is a judgment
|
|
329
|
+
call, and hyperspec's deterministic stations do not make judgment calls. A later release adds a
|
|
330
|
+
simulated-reader station that reads for meaning instead of shape; `terms` stays the fast,
|
|
331
|
+
mechanical first pass.
|
|
332
|
+
|
|
315
333
|
## The test mapping
|
|
316
334
|
|
|
317
335
|
Each row lists what the writing profile adds to that test. The core conditions in
|
|
@@ -319,12 +337,12 @@ Each row lists what the writing profile adds to that test. The core conditions i
|
|
|
319
337
|
|
|
320
338
|
| Test | A writing spec fails it when |
|
|
321
339
|
|---|---|
|
|
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` |
|
|
340
|
+
| 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; `audience.terms`, when present, holds a non-string entry or has no real entries at all. A `stance` outside the four is a warning, and so is `unsourced_claim: warn` |
|
|
323
341
|
| 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
342
|
| 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) |
|
|
343
|
+
| 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` |
|
|
344
|
+
| 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 |
|
|
345
|
+
| 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
346
|
| 7 a stranger can resume it | `writing.progress` exists. An unknown `profile:` is a warning |
|
|
329
347
|
| 8 its adopters can push back on it | nothing further; the core rule applies |
|
|
330
348
|
| 9 it improves itself | nothing further; the core rule applies |
|
|
@@ -527,6 +545,470 @@ Two more options, `displayPath` and `materialDisplayPath`, set how the two files
|
|
|
527
545
|
messages (lint passes the paths as the spec wrote them); by default the paths are printed as
|
|
528
546
|
given. `MATERIAL_LABELS` is the seven labels, in the order of the table above.
|
|
529
547
|
|
|
548
|
+
## Scoped DNA
|
|
549
|
+
|
|
550
|
+
A writer does not have one voice, so hyperspec does not keep one. A writer's DNA is kept per
|
|
551
|
+
**scope**, a form, an audience and a purpose together, and each scope is a folder holding its own
|
|
552
|
+
goldens and its own measurements.
|
|
553
|
+
|
|
554
|
+
The reason is a leak. A passage can be exactly right for one kind of writing and wrong for
|
|
555
|
+
another. The short, warm sentences that make a text message land read as thin in a theology
|
|
556
|
+
essay, and the long, qualified sentences that make the essay careful read as evasive on a landing
|
|
557
|
+
page. Pool every golden a writer has into one set and an agent learns the moves of each kind of
|
|
558
|
+
writing and carries them into the others. Filed by scope, a golden feeds only work that shares
|
|
559
|
+
its scope, so the moves it teaches stay where they are right. Retrieval is by scope, never by
|
|
560
|
+
"best writing overall".
|
|
561
|
+
|
|
562
|
+
Scoped DNA is optional in this release. A spec that names no scope folder lints exactly as it did
|
|
563
|
+
in 0.4.
|
|
564
|
+
|
|
565
|
+
### The scope folder
|
|
566
|
+
|
|
567
|
+
```text
|
|
568
|
+
dna/essay-new-managers-teach/
|
|
569
|
+
scope.md writer, form, audience, purpose, optional notes
|
|
570
|
+
goldens/
|
|
571
|
+
README.md the golden file shape; never read as a golden
|
|
572
|
+
close.md one golden per file
|
|
573
|
+
opening.md
|
|
574
|
+
status.md
|
|
575
|
+
features.json written by dna measure, never by hand
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
`scope.md` carries the scope in its frontmatter; its body is free text for people:
|
|
579
|
+
|
|
580
|
+
```markdown
|
|
581
|
+
---
|
|
582
|
+
writer: example-author
|
|
583
|
+
form: essay
|
|
584
|
+
audience: new managers
|
|
585
|
+
purpose: teach
|
|
586
|
+
---
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
`writer`, `form`, `audience` and `purpose` are required, and `notes` is optional. Name the folder
|
|
590
|
+
after its scope so a person can tell scopes apart at a glance. hyperspec reads the scope from
|
|
591
|
+
`scope.md`, never from the folder's name.
|
|
592
|
+
|
|
593
|
+
`goldens/` must be a real folder inside the scope. A `goldens/` that resolves somewhere else, such
|
|
594
|
+
as a symlink to another scope's goldens, would carry that scope's passages into this one under
|
|
595
|
+
this scope's name, so `dna measure` refuses it and lint fails it under test 5, both naming where it
|
|
596
|
+
leads. A whole scope folder reached through a symlink is fine, because `scope.md` travels with
|
|
597
|
+
it.
|
|
598
|
+
|
|
599
|
+
### A golden
|
|
600
|
+
|
|
601
|
+
A golden is a real passage the writer marked as right, one per file in `goldens/`. Every `.md`
|
|
602
|
+
file directly in `goldens/` is a golden except `README.md`, which is for notes to people and is
|
|
603
|
+
never read as a golden. A subfolder, a file with another extension, and a symlink sitting in the
|
|
604
|
+
folder are not read either. This is the essay example's opening:
|
|
605
|
+
|
|
606
|
+
```markdown
|
|
607
|
+
---
|
|
608
|
+
why: one plain claim in the first sentence, then two short sentences that turn it into something to do
|
|
609
|
+
approved_by: example-author
|
|
610
|
+
source: first draft of this essay's opening paragraph, marked golden on the review page
|
|
611
|
+
approved_on: "2026-09-18"
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
Your first one-on-one with a new report is the only meeting on your calendar where they should
|
|
615
|
+
set the agenda. Everything else you run. This one you hand over.
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
- **`why`** (required) names the move the passage teaches. A golden without its reason teaches
|
|
619
|
+
the surface: an agent copies its length, its words and its rhythm. The reason teaches the move,
|
|
620
|
+
which carries over to a passage that shares none of those.
|
|
621
|
+
- **`approved_by`** (required) is the person who approved it, as a slug. Golden means a human
|
|
622
|
+
approved it, so an approver that starts `agent:` is refused. An agent may propose a golden;
|
|
623
|
+
only a person makes one.
|
|
624
|
+
- **`source`** (required) says where the passage came from: a draft, an earlier piece, a review
|
|
625
|
+
page. It lets someone check that the passage is real and find the context it was written in.
|
|
626
|
+
- **`approved_on`** (optional) is the date it was approved.
|
|
627
|
+
- **The body** is the passage, verbatim. Whitespace before and after it is dropped, and nothing
|
|
628
|
+
inside it is changed.
|
|
629
|
+
|
|
630
|
+
A placeholder counts as missing in every one of these fields, as it does everywhere in a
|
|
631
|
+
hyperspec (see [The schema](#the-schema)).
|
|
632
|
+
|
|
633
|
+
### Starting a scope
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
mkdir -p dna
|
|
637
|
+
npx @supersuit/hyperspec dna init dna/essay-new-managers-teach --writer example-author --form essay --audience "new managers" --purpose teach
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`hyperspec dna init <scope-dir> --writer W --form F --audience A --purpose P` writes `scope.md`
|
|
641
|
+
and a `goldens/` folder holding only a README on the golden file shape. All four flags are
|
|
642
|
+
required. It refuses to overwrite an existing `scope.md`, never replaces a `goldens/README.md`
|
|
643
|
+
that is already there, and exits 2 with a plain message on a missing flag, a flag whose value is a
|
|
644
|
+
placeholder, or a scope folder whose parent folder does not exist. Then add one file per golden.
|
|
645
|
+
|
|
646
|
+
### Measuring a scope
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
npx @supersuit/hyperspec dna measure dna/essay-new-managers-teach
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
```
|
|
653
|
+
dna/essay-new-managers-teach: measured 3 goldens
|
|
654
|
+
word_count 118, sentence length mean 13.111 median 15 p90 25
|
|
655
|
+
signature words: first, report, tracker
|
|
656
|
+
wrote dna/essay-new-managers-teach/features.json
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`hyperspec dna measure <scope-dir> [--json]` reads every golden, checks each one's own fields, and
|
|
660
|
+
writes `<scope-dir>/features.json`. If the scope or any golden fails a check (no `why`, an agent
|
|
661
|
+
approver, no passage, a `goldens/` folder that resolves outside the scope), it prints the findings,
|
|
662
|
+
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`
|
|
663
|
+
prints the same result as JSON. The same goldens always produce the same bytes.
|
|
664
|
+
|
|
665
|
+
`features.json` holds `dna` (the version of this format, `"0.1"`), `scope` (the four fields from
|
|
666
|
+
`scope.md`), `goldens` (each golden's path inside the folder and the SHA-256 of its file, sorted
|
|
667
|
+
by path) and `features`.
|
|
668
|
+
|
|
669
|
+
### What is measured
|
|
670
|
+
|
|
671
|
+
Every feature is a count or a ratio computed from the goldens' text. None of them is a judgment:
|
|
672
|
+
a number says how the writer writes in this scope, never whether the writing is good, and
|
|
673
|
+
hyperspec calls no model to get it. Words are pooled across every golden in the scope, so their
|
|
674
|
+
order changes nothing. Paragraphs and sentences are split exactly as `segments init` splits them,
|
|
675
|
+
so the two never disagree about where a boundary falls. A word is a run of letters, digits and
|
|
676
|
+
apostrophes, lowercased. Every number is rounded to three decimal places, and a rate is per 1000
|
|
677
|
+
words.
|
|
678
|
+
|
|
679
|
+
| Feature | What it counts | What it is for |
|
|
680
|
+
|---|---|---|
|
|
681
|
+
| `word_count` | words across every golden | how much text the other numbers rest on; a scope of a few dozen words measures loosely |
|
|
682
|
+
| `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 |
|
|
683
|
+
| `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 |
|
|
684
|
+
| `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 |
|
|
685
|
+
| `contraction_rate` | words with an apostrophe between two letters | how conversational the writer is in this scope |
|
|
686
|
+
| `first_person_singular_rate` | I, me, my, mine, myself | how much the writer speaks as themselves |
|
|
687
|
+
| `first_person_plural_rate` | we, us, our, ours, ourselves | how much the writer speaks as a group, or alongside the reader |
|
|
688
|
+
| `second_person_rate` | you, your, yours, yourself, yourselves | how directly the writer addresses the reader |
|
|
689
|
+
| `mean_word_length` | characters per word | plain words or long ones |
|
|
690
|
+
| `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 |
|
|
691
|
+
|
|
692
|
+
### When the scope changes
|
|
693
|
+
|
|
694
|
+
`features.json` is current only when it is exactly what `dna measure` would write from the scope
|
|
695
|
+
as it reads now: the same goldens, pinned by the SHA-256 of each file; the same four fields as
|
|
696
|
+
`scope.md`; the format version `"0.1"`; and the same numbers. Add a golden, remove one, change any
|
|
697
|
+
byte of one (its passage or its frontmatter), edit `scope.md`, or edit a number by hand, and lint
|
|
698
|
+
fails the scope as stale under test 6. The finding names what differs: each golden added, removed
|
|
699
|
+
or changed, each scope field that changed, an unknown version, or each feature whose number no
|
|
700
|
+
longer matches a fresh measurement. Run `dna measure` again. The hash is over bytes, so a
|
|
701
|
+
line-ending conversion counts as a change, as it does for a segments file (see
|
|
702
|
+
[When a material changes](#when-a-material-changes)).
|
|
703
|
+
|
|
704
|
+
### Naming the scope in a spec
|
|
705
|
+
|
|
706
|
+
`writing.dna.scope_dir` points a writing spec at its scope folder, relative to the spec like every
|
|
707
|
+
other path. The essay example's `dna` block:
|
|
708
|
+
|
|
709
|
+
```yaml
|
|
710
|
+
dna:
|
|
711
|
+
writer: example-author
|
|
712
|
+
scope_dir: dna/essay-new-managers-teach
|
|
713
|
+
scope:
|
|
714
|
+
form: essay
|
|
715
|
+
audience: new managers
|
|
716
|
+
purpose: teach
|
|
717
|
+
rules: style-rules.md
|
|
718
|
+
goldens:
|
|
719
|
+
- path: dna/essay-new-managers-teach/goldens/opening.md
|
|
720
|
+
why: one plain claim in the first sentence, then two short sentences that turn it into something to do
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
`scope_dir` is optional. Without it, `dna` lints exactly as it did in 0.4: each golden the spec
|
|
724
|
+
lists needs a path to a file and a `why`, and no scope folder is read. With it, lint also checks
|
|
725
|
+
that:
|
|
726
|
+
|
|
727
|
+
- `scope.md`'s writer equals `dna.writer`, and its form, audience and purpose equal `dna.scope`,
|
|
728
|
+
compared trimmed and ignoring case (test 1);
|
|
729
|
+
- every golden the spec lists is one of the scope's goldens: after following any symlink, a `.md`
|
|
730
|
+
file directly in the scope's own `goldens/` folder, other than `README.md` (test 5). A golden
|
|
731
|
+
from another scope is a leak, the exact thing a scope exists to prevent, and so is a passage in
|
|
732
|
+
a subfolder, in `README.md` or in another kind of file, which would feed the spec without ever
|
|
733
|
+
being checked or measured;
|
|
734
|
+
- every golden in the folder, listed in the spec or not, has a `why` (test 6), an `approved_by`
|
|
735
|
+
that names a person and a `source` (test 4), and a passage (test 1);
|
|
736
|
+
- `features.json` exists and is current, as [When the scope changes](#when-the-scope-changes)
|
|
737
|
+
defines it (test 6).
|
|
738
|
+
|
|
739
|
+
The spec still gives each golden it lists a `why`, as in 0.4; the essay example keeps it the same
|
|
740
|
+
as the golden file's own. A `scope_dir` that is present but a placeholder, such as `TODO`, fails
|
|
741
|
+
test 1 on its own, so a skeleton cannot pass by leaving it unfilled.
|
|
742
|
+
|
|
743
|
+
### Findings
|
|
744
|
+
|
|
745
|
+
Every scoped-DNA finding id starts `writing-dna-` and fails the test in its row. Messages name the
|
|
746
|
+
scope folder as the spec wrote it (or as it was given to `dna measure`) and each golden by its
|
|
747
|
+
path inside the folder, so the output is the same on every machine. `<field>` is `writer`,
|
|
748
|
+
`form`, `audience` or `purpose`.
|
|
749
|
+
|
|
750
|
+
| Id | Test | Fails when |
|
|
751
|
+
|---|---|---|
|
|
752
|
+
| `writing-dna-scope-dir` | 1 | `writing.dna.scope_dir` is present and is a placeholder |
|
|
753
|
+
| `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) |
|
|
754
|
+
| `writing-dna-scope-missing` | 1 | `scope.md` does not exist, cannot be read, or its frontmatter does not parse |
|
|
755
|
+
| `writing-dna-scope-file-<field>` | 1 | `scope.md` has no such field |
|
|
756
|
+
| `writing-dna-scope-mismatch-<field>` | 1 | `scope.md` and the spec disagree on that field |
|
|
757
|
+
| `writing-dna-goldens-missing` | 1 | the `goldens/` folder does not exist or cannot be read |
|
|
758
|
+
| `writing-dna-goldens-empty` | 1 | `goldens/` holds no golden |
|
|
759
|
+
| `writing-dna-goldens-outside` | 5 | `goldens/` resolves to a folder outside the scope, such as a symlink to another scope's goldens |
|
|
760
|
+
| `writing-dna-golden-unreadable` | 1 | a golden file cannot be read |
|
|
761
|
+
| `writing-dna-golden-frontmatter` | 1 | a golden's frontmatter opens with `---` and never closes |
|
|
762
|
+
| `writing-dna-golden-empty` | 1 | a golden has no passage |
|
|
763
|
+
| `writing-dna-golden-approved-by` | 4 | a golden has no `approved_by` |
|
|
764
|
+
| `writing-dna-golden-approved-by-agent` | 4 | a golden's `approved_by` starts `agent:`, in any case |
|
|
765
|
+
| `writing-dna-golden-source` | 4 | a golden has no `source` |
|
|
766
|
+
| `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 |
|
|
767
|
+
| `writing-dna-golden-why` | 6 | a golden has no `why` |
|
|
768
|
+
| `writing-dna-features-missing` | 6 | the scope has no `features.json`, or it is not valid JSON |
|
|
769
|
+
| `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 |
|
|
770
|
+
|
|
771
|
+
`dna measure` raises the ids that come from the folder alone: every row except `scope-dir`,
|
|
772
|
+
`scope-<field>`, `scope-mismatch-<field>`, `golden-leak` and the two `features-` rows, which
|
|
773
|
+
need a spec to compare against. Lint raises all of them.
|
|
774
|
+
|
|
775
|
+
### Reading a scope from your own tool
|
|
776
|
+
|
|
777
|
+
A tool of your own, such as a review page that files goldens, can read a scope and measure it the
|
|
778
|
+
way `dna measure` does:
|
|
779
|
+
|
|
780
|
+
```js
|
|
781
|
+
import { readScope, measureFeatures } from "@supersuit/hyperspec/writing";
|
|
782
|
+
|
|
783
|
+
const { scope, goldens, findings } = readScope("dna/essay-new-managers-teach");
|
|
784
|
+
const features = measureFeatures(goldens.map((g) => g.text));
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
`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
|
|
788
|
+
`scope.md` cannot be read at all), every golden it could read, with its `path`, `why`,
|
|
789
|
+
`approved_by`, `source`, `approved_on`, `text` and `sha256`, and findings in the shape lint
|
|
790
|
+
reports. `displayDir` sets how the folder is named in messages. `measureFeatures` takes an array
|
|
791
|
+
of passages and returns the `features` object `dna measure` writes. It reads no file and returns
|
|
792
|
+
the same object for the same passages.
|
|
793
|
+
|
|
794
|
+
### The worked example
|
|
795
|
+
|
|
796
|
+
The essay in [`examples/writing/`](examples/writing/) takes its voice from
|
|
797
|
+
`dna/essay-new-managers-teach/`: three goldens, each with its `why`, a person's approval and its
|
|
798
|
+
source, and a `features.json` that `dna measure` wrote. A test measures the folder again on every
|
|
799
|
+
release and requires the same bytes, so the example cannot drift from the tool. The short story
|
|
800
|
+
beside it lists its goldens in the spec with no scope folder, the 0.4 shape, which still passes.
|
|
801
|
+
|
|
802
|
+
## Checking a draft
|
|
803
|
+
|
|
804
|
+
Once a spec lints clean and a draft exists, `check` runs the spec's deterministic stations
|
|
805
|
+
against the draft:
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
npx @supersuit/hyperspec check essay.hyperspec.md --draft essay/draft.md
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
It lints the spec first. A spec that fails lint, or is blocked on an open decision, runs no
|
|
812
|
+
station and exits with lint's own code, because a draft cannot be checked against a spec that is
|
|
813
|
+
not ready. Then it runs seven stations in a fixed order and prints one line for each: `pass`,
|
|
814
|
+
`fail` with its findings, or `skip` with the reason. A warning prints under its station and never
|
|
815
|
+
fails it. This is the essay example's draft:
|
|
816
|
+
|
|
817
|
+
```
|
|
818
|
+
form: pass
|
|
819
|
+
terms: pass
|
|
820
|
+
claims: pass
|
|
821
|
+
quotes: pass
|
|
822
|
+
private: pass
|
|
823
|
+
dna: pass
|
|
824
|
+
warn [station-dna-drift] first_person_singular_rate is 22.892 in the draft; the scope's goldens measure 0, band 0 to 5
|
|
825
|
+
fix: Bring first_person_singular_rate back inside the band, or, if the scope no longer describes this writer, re-measure it with better goldens.
|
|
826
|
+
links: pass
|
|
827
|
+
verdict: one-shot
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
`--only form,terms` runs just those stations, still in the fixed order, and its ledger line is
|
|
831
|
+
marked partial (see [The runs ledger](#the-runs-ledger)). `--json` prints the whole result, every
|
|
832
|
+
finding included; a spec that is not ready prints lint's result with `lintBlocked: true` instead,
|
|
833
|
+
and a usage error prints `{ "spec", "draft", "error" }`. A finding names the draft line it points
|
|
834
|
+
at where there is one, quotes at most 80 characters of the draft, and never prints an absolute
|
|
835
|
+
path. A UTF-8 byte order mark at the start of the draft is ignored.
|
|
836
|
+
|
|
837
|
+
Exit codes: **0** every station that ran passed (a skip or a warning does not fail it); **1** a
|
|
838
|
+
station failed; **2** usage: no spec path, no `--draft`, a draft that cannot be read, a spec
|
|
839
|
+
without `profile: writing`, or an `--only` that names no known station; and lint's own **1** or
|
|
840
|
+
**3** when the spec is not ready.
|
|
841
|
+
|
|
842
|
+
Every station is a plain function of the spec and the draft. None of them calls a model, and none
|
|
843
|
+
of them touches the network. What each one checks, and what it cannot:
|
|
844
|
+
|
|
845
|
+
### form
|
|
846
|
+
|
|
847
|
+
Counts the draft's words, by the same word definition `dna measure` uses, against
|
|
848
|
+
`form.length`. Only `unit: words` is measured; any other unit skips the whole station rather than
|
|
849
|
+
checking half of it. Every `required_parts` entry must appear as an ATX heading (`#` to
|
|
850
|
+
`######`, indented at most three spaces, closing `#`s allowed) whose text equals the part,
|
|
851
|
+
ignoring case, or as a line that starts with the part and a colon, for the fields a form fills in
|
|
852
|
+
place (`To:`, `Subject:`). An underlined (Setext) heading does not count, and neither does
|
|
853
|
+
anything inside a code block. It cannot tell whether the section under a heading does what the
|
|
854
|
+
part is for, so write `required_parts` as the headings the piece will carry,
|
|
855
|
+
as both examples do.
|
|
856
|
+
|
|
857
|
+
| Id | Severity | Meaning |
|
|
858
|
+
|---|---|---|
|
|
859
|
+
| `station-form-length` | fail | the word count is outside `form.length`; the message gives the count and the range |
|
|
860
|
+
| `station-form-required-part-<part>` | fail | a required part appears as neither a heading nor a `part:` line |
|
|
861
|
+
|
|
862
|
+
### terms
|
|
863
|
+
|
|
864
|
+
Reads the optional `audience.terms`: the words the piece uses that its reader may not know. For
|
|
865
|
+
each term not also in `audience.knows`, it finds the term's first appearance (whole word, ignoring
|
|
866
|
+
case) and looks for a definition in that sentence or the next: the term followed within six words
|
|
867
|
+
by `is`, `means` or `refers to`, a colon among those words, or a parenthesis right after the term.
|
|
868
|
+
This is a mechanical proxy for a definition, not a reading of one: "A hyperspec is mentioned here"
|
|
869
|
+
passes. A term the draft never uses is not flagged, code blocks and inline code are ignored, and
|
|
870
|
+
with no `terms` list the station skips.
|
|
871
|
+
|
|
872
|
+
| Id | Severity | Meaning |
|
|
873
|
+
|---|---|---|
|
|
874
|
+
| `station-terms-undefined-<term>` | fail | the term's first appearance has no definition in that sentence or the next |
|
|
875
|
+
|
|
876
|
+
### claims
|
|
877
|
+
|
|
878
|
+
Reads the claims ledger at `sources.ledger`: JSONL, one claim per line, each with the claim's
|
|
879
|
+
`text` exactly as the draft says it, a `source`, and optionally a `span`, the words in the source
|
|
880
|
+
that support it. The examples cite a segment as the source, the same `material#segment` form the
|
|
881
|
+
spine uses:
|
|
882
|
+
|
|
883
|
+
```jsonl
|
|
884
|
+
{"text":"11 of 41 said at least one of their one-on-ones in the last quarter was mostly project status.","source":"survey#s4","span":"11 of 41 said at least one of their one-on-ones in the last quarter was mostly project status."}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
Every claim's text must still appear in the draft word for word, with whitespace and quote
|
|
888
|
+
characters normalized and case kept; otherwise the ledger is stale. Every claim needs a real
|
|
889
|
+
source; one without fails, or warns under `unsourced_claim: warn`. A missing ledger fails. The
|
|
890
|
+
station does not decide what counts as a factual claim: the ledger is the list of claims, so a
|
|
891
|
+
factual sentence left out of it passes unseen. Nor does it read the source to see whether it says
|
|
892
|
+
what the claim says.
|
|
893
|
+
|
|
894
|
+
| Id | Severity | Meaning |
|
|
895
|
+
|---|---|---|
|
|
896
|
+
| `station-claims-ledger-missing` | fail | the ledger file does not exist or cannot be read |
|
|
897
|
+
| `station-claims-json-line-<n>` | fail | ledger line n is not JSON, not an object, or has no `text` |
|
|
898
|
+
| `station-claims-stale` | fail | the claim on a ledger line no longer appears in the draft |
|
|
899
|
+
| `station-claims-unsourced` | fail, or warn under `unsourced_claim: warn` | the claim on a ledger line has no source, or only a placeholder; points at the draft line where the claim appears |
|
|
900
|
+
|
|
901
|
+
### quotes
|
|
902
|
+
|
|
903
|
+
Every span in double quotation marks, straight or curly, of four words or more must appear word
|
|
904
|
+
for word in a `quote` or `story` segment of a marked material. Quote characters and whitespace
|
|
905
|
+
are normalized, case is kept, and a comma or period just inside the closing mark is dropped,
|
|
906
|
+
because that punctuation is the writer's; a `?` or `!` is kept, because adding one changes what
|
|
907
|
+
was said. Shorter spans are not checked, since two or three quoted words are as often a title as
|
|
908
|
+
a quotation. A private segment is never a source for a quote. When the sentence around a quote
|
|
909
|
+
names a speaker, the quote must come from a quote segment with that `speaker`. A speaker is named
|
|
910
|
+
by the full `speaker` value, hyphens read as spaces, or by its first word, so `maria-lopez` is
|
|
911
|
+
named by "Maria Lopez" and by "Maria". The first word alone counts only when it has two or more
|
|
912
|
+
letters and is not a common function word such as "the", so a speaker recorded as "the manager
|
|
913
|
+
interviewed" is named only by all three words. Attribution needs a declared speaker: a name that
|
|
914
|
+
is no segment's `speaker` attributes nothing, so start a `speaker` with the person's name, as
|
|
915
|
+
the essay example does with `dana, an engineering manager`. A spec with `fiction: true` skips
|
|
916
|
+
the station: a character's dialogue is invented rather than quoted from a material, and a later
|
|
917
|
+
release checks it against each character's own lines.
|
|
918
|
+
|
|
919
|
+
| Id | Severity | Meaning |
|
|
920
|
+
|---|---|---|
|
|
921
|
+
| `station-quotes-unmatched` | fail | a quoted span is in no quote or story segment |
|
|
922
|
+
| `station-quotes-misattributed` | fail | the sentence names a speaker, and the span is in no quote segment by that speaker |
|
|
923
|
+
|
|
924
|
+
### private
|
|
925
|
+
|
|
926
|
+
No run of eight or more consecutive words from any `private` segment may appear in the draft,
|
|
927
|
+
compared by words with case and punctuation ignored. A private segment of four to seven words is
|
|
928
|
+
checked whole. One under four words is not checked, because two or three words match ordinary
|
|
929
|
+
prose; the station reports how many it skipped, as one warning that never quotes them. It cannot
|
|
930
|
+
catch a paraphrase, or a leak shorter than the run.
|
|
931
|
+
|
|
932
|
+
| Id | Severity | Meaning |
|
|
933
|
+
|---|---|---|
|
|
934
|
+
| `station-private-leak` | fail | the draft repeats a run from a private segment; names the material, the segment and the run |
|
|
935
|
+
| `station-private-short-skipped` | warn | private segments under four words were not checked; gives the count |
|
|
936
|
+
|
|
937
|
+
### dna
|
|
938
|
+
|
|
939
|
+
Runs when `dna.scope_dir` is set and its `features.json` is current. It measures the draft the
|
|
940
|
+
way `dna measure` measures goldens and compares the sentence length mean, both paragraph length
|
|
941
|
+
means, every per-1000-word punctuation rate, and the contraction and person rates with the
|
|
942
|
+
scope's. For a scope value v, a draft value outside v ÷ 1.5 to the larger of v × 1.5 and v + 5 is
|
|
943
|
+
reported with both values. An em dash in a draft whose scope has none is its own finding,
|
|
944
|
+
pointing at the first one. Both
|
|
945
|
+
are warnings and the station never fails: it measures, and whether a draft sounds like its writer
|
|
946
|
+
is a judgment. The essay's warning is an example of what to read: its goldens are instructions in
|
|
947
|
+
the second person, and the essay tells the author's own story in the first. With no `scope_dir`,
|
|
948
|
+
or a `features.json` that is missing or stale, the station skips and says which.
|
|
949
|
+
|
|
950
|
+
| Id | Severity | Meaning |
|
|
951
|
+
|---|---|---|
|
|
952
|
+
| `station-dna-drift` | warn | a feature is outside its band; gives the draft's value, the scope's and the band |
|
|
953
|
+
| `station-dna-em-dash` | warn | the draft uses em dashes and the scope's goldens use none |
|
|
954
|
+
|
|
955
|
+
### links
|
|
956
|
+
|
|
957
|
+
Every Markdown link (inline, reference, collapsed and shortcut) and every bare URL. An `http` or
|
|
958
|
+
`https` URL must parse and name a host, a `mailto:` link must carry an address, and any other
|
|
959
|
+
scheme fails. A relative link must resolve to a file, relative to the draft's own folder; the
|
|
960
|
+
part after `#` is not checked. A link that starts with `/` is relative to a site root the station
|
|
961
|
+
cannot see, so it warns. A full or collapsed reference, `[text][label]` or `[label][]`, needs a
|
|
962
|
+
definition for its label. A bare `[label]` is a link only when that label has a definition;
|
|
963
|
+
otherwise it is ordinary text, as Markdown renders it, so an editorial `[sic]`, a task list's
|
|
964
|
+
`[x]` and a numbered note `[1]` pass. Code blocks and inline code are ignored. It never touches
|
|
965
|
+
the network, so it cannot tell you a URL is live.
|
|
966
|
+
|
|
967
|
+
| Id | Severity | Meaning |
|
|
968
|
+
|---|---|---|
|
|
969
|
+
| `station-links-malformed` | fail | an http or https URL with no host (a bare `https://` included), or a `mailto:` with no address |
|
|
970
|
+
| `station-links-bad-scheme` | fail | a scheme other than http, https or mailto |
|
|
971
|
+
| `station-links-broken-relative` | fail | a relative link names no file beside the draft |
|
|
972
|
+
| `station-links-root-relative` | warn | a link starting with `/`, which cannot be resolved without the site |
|
|
973
|
+
| `station-links-undefined-reference` | fail | a full or collapsed reference link whose label has no definition |
|
|
974
|
+
|
|
975
|
+
### Any station
|
|
976
|
+
|
|
977
|
+
| Id | Severity | Meaning |
|
|
978
|
+
|---|---|---|
|
|
979
|
+
| `station-<name>-crashed` | fail | the station threw; the message is the error's, with any absolute path shortened; the other stations and the ledger line still run |
|
|
980
|
+
|
|
981
|
+
### The runs ledger
|
|
982
|
+
|
|
983
|
+
Each `check` appends one line to the spec's `improvement.ledger`, the same file lint's test 9
|
|
984
|
+
reads:
|
|
985
|
+
|
|
986
|
+
```json
|
|
987
|
+
{"at":"2026-09-29T13:21:37.330Z","kind":"check","draft":"essay/draft.md","draft_sha256":"<sha256 of the draft>","spec_sha256":"<sha256 of the spec>","stations":{"form":"pass","terms":"pass","claims":"pass","quotes":"pass","private":"pass","dna":"pass","links":"pass"},"verdict":"one-shot"}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
`draft` is the draft's path relative to the spec's folder, however you spelled it, so one draft
|
|
991
|
+
has one history. `draft_sha256` and `spec_sha256` hash the two files' bytes; the files the spec
|
|
992
|
+
names (materials, the claims ledger, a scope folder) are not hashed, so "changed" below means the
|
|
993
|
+
draft or the spec. `stations` holds each station's status.
|
|
994
|
+
|
|
995
|
+
A run with `--only` is partial: its line carries `partial: true`, its verdict is `not-improved`
|
|
996
|
+
with the reason `partial run: <stations>`, and later verdicts ignore it, so a subset never claims
|
|
997
|
+
the verdict for the whole draft. A full run is compared with the most recent earlier full line for
|
|
998
|
+
the same draft:
|
|
999
|
+
|
|
1000
|
+
- **one-shot**: there is none, and every station passes.
|
|
1001
|
+
- **improved**: that line failed and every station passes now; `change` names exactly the
|
|
1002
|
+
stations that failed then and pass now.
|
|
1003
|
+
- **not-improved** otherwise, with a `reason` that says which case it is: `failing stations: ...`
|
|
1004
|
+
on a first check that fails; `no change since the last passing check`; `draft changed; every
|
|
1005
|
+
station still passes` (or `spec changed`, or `spec and draft changed`); `still failing: ...`,
|
|
1006
|
+
after `no change since the last check;` or after what changed, when every failing station failed
|
|
1007
|
+
last time too; `failing stations: ...` after what changed when a station fails that passed last
|
|
1008
|
+
time; and `stations that failed last time now skip: ...` when a spec change stopped them running.
|
|
1009
|
+
|
|
1010
|
+
A ledger path that leads outside the spec's folder is not written, and `check` prints a warning.
|
|
1011
|
+
|
|
530
1012
|
## Deferring a block
|
|
531
1013
|
|
|
532
1014
|
A block can be deferred, never silently missing. A required block that is absent fails test 1
|
|
@@ -561,8 +1043,10 @@ placeholder. `dna`, `persona`, `audience` and `goal` also carry an open decision
|
|
|
561
1043
|
says what you have to answer before the placeholder means anything. `--form` sets both `kind:`
|
|
562
1044
|
and `writing.form.name`, and defaults to `essay`. The material item names
|
|
563
1045
|
`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.
|
|
565
|
-
|
|
1046
|
+
materials keeps failing until a real material is marked. `dna` shows `scope_dir: TODO`, which
|
|
1047
|
+
fails until it names a scope folder (see [Scoped DNA](#scoped-dna)) or is deleted, since the
|
|
1048
|
+
field is optional. `--fiction` sets `fiction: true` and adds one character with the same
|
|
1049
|
+
treatment. The skeleton never passes: it lints `fail`, with
|
|
566
1050
|
`writing: 1/9 blocks complete` (or `0/9` with `--fiction`), until the placeholders and the open
|
|
567
1051
|
decisions are replaced with real content.
|
|
568
1052
|
|
|
@@ -577,7 +1061,8 @@ Two complete specs ship in [`examples/writing/`](examples/writing/), each with e
|
|
|
577
1061
|
names:
|
|
578
1062
|
|
|
579
1063
|
- `essay.hyperspec.md`: an essay for new managers on running a first one-on-one. Three materials
|
|
580
|
-
at three trust levels,
|
|
1064
|
+
at three trust levels, its voice from the scope folder `dna/essay-new-managers-teach/` with three
|
|
1065
|
+
annotated goldens and their measured features, a four-claim spine.
|
|
581
1066
|
- `story.hyperspec.md`: a short story, `fiction: true`, narrated by one of its two characters.
|
|
582
1067
|
Each character has speech rules, a knowledge timeline by scene, and golden and rejected lines
|
|
583
1068
|
in a voice you can tell apart from the other's.
|
|
@@ -587,11 +1072,19 @@ the field it needs, and every spine claim cites the segments that support it. Ea
|
|
|
587
1072
|
keeps the boundaries `segments init` wrote, in paragraph mode for prose and sentence mode for
|
|
588
1073
|
bulleted notes, so you can re-run it and compare.
|
|
589
1074
|
|
|
590
|
-
Both lint `pass (9/9)` with `writing: 9/9 blocks complete` and no findings.
|
|
591
|
-
|
|
1075
|
+
Both lint `pass (9/9)` with `writing: 9/9 blocks complete` and no findings. Each also ships a
|
|
1076
|
+
draft written to it, `essay/draft.md` and `story/draft.md`, with its claims ledger beside it, and
|
|
1077
|
+
both drafts pass every station of `check`: the essay with one dna warning, described under
|
|
1078
|
+
[dna](#dna), and the story with dna skipped, since it names no scope folder, and quotes skipped,
|
|
1079
|
+
since it is fiction. A test lints both
|
|
1080
|
+
specs and checks both drafts on every release, so they cannot drift from the tool.
|
|
592
1081
|
|
|
593
1082
|
## What later versions add
|
|
594
1083
|
|
|
595
|
-
This release is the schema, its lint,
|
|
596
|
-
|
|
597
|
-
|
|
1084
|
+
This release is the schema, its lint, marked materials, scoped DNA, and `check` with seven
|
|
1085
|
+
deterministic stations. Next come the judgment stations: the simulated reader, the blind lineup,
|
|
1086
|
+
the persona judge and the doctor. hyperspec calls no model, so `check` will write each one as a
|
|
1087
|
+
packet, the draft and the rubric and the materials the judge needs, for an outside judge to fill
|
|
1088
|
+
in, and read the filled packet back as a station result. After that, a learn step that reads the
|
|
1089
|
+
runs ledger for the stations that keep failing and the changes that made them pass, so a fix
|
|
1090
|
+
lands in the spec or the skill that wrote the draft rather than in one draft.
|