@supersuit/hyperspec 0.5.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 +75 -0
- package/README.md +25 -1
- package/SPEC.md +4 -4
- package/WRITING.md +237 -8
- package/bin/hyperspec.mjs +69 -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 +17 -6
- 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 +4 -1
- 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-fields.mjs +26 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
A spec can now check a draft. Until this release hyperspec could tell you whether a writing spec
|
|
6
|
+
was ready; it could not tell you whether the piece written from it met the spec. `hyperspec
|
|
7
|
+
check` runs seven deterministic stations against a draft: the length and required parts, the
|
|
8
|
+
terms the reader needs defined, the claims ledger, quotations, private material, the writer's
|
|
9
|
+
measured style, and links. None of them calls a model or touches the network, so the same draft
|
|
10
|
+
and spec always give the same answer. Each run leaves one line in the spec's runs ledger saying
|
|
11
|
+
whether the draft passed first time, improved, or did not, and why.
|
|
12
|
+
|
|
13
|
+
**No behavior change for lint.** Every spec without `writing.audience.terms` passes and fails
|
|
14
|
+
exactly as it did in 0.5.0, and no lint finding id changed. That field is the one schema
|
|
15
|
+
addition, and it is optional.
|
|
16
|
+
|
|
17
|
+
- `hyperspec check <spec> --draft <file> [--json] [--only a,b]` needs a writing spec
|
|
18
|
+
(`profile: writing`) and lints it first: a spec that fails lint, or is blocked on an open
|
|
19
|
+
decision, runs no station and exits with lint's code. Then it runs every station in a fixed
|
|
20
|
+
order, `form, terms, claims, quotes, private, dna, links`, and prints each one's `pass`, `fail`
|
|
21
|
+
with findings, or `skip` with the reason. Warnings print under their station and never fail it.
|
|
22
|
+
Exit 0 when every station that ran passed, 1 when one failed, 2 on usage (no spec, no
|
|
23
|
+
`--draft`, an unreadable draft, a spec without the writing profile, an `--only` that names no
|
|
24
|
+
known station); under `--json` a usage error is one document, `{ spec, draft, error }`.
|
|
25
|
+
`--only` runs a subset, still in the fixed order. A finding names the draft line it points at
|
|
26
|
+
where there is one, quotes at most 80 characters of the draft, and never prints an absolute
|
|
27
|
+
path. A byte order mark at the start of the draft is ignored. A station that throws becomes one
|
|
28
|
+
failing finding, `station-<name>-crashed`, and the rest still run.
|
|
29
|
+
- `form`: word count against `writing.form.length` (only `unit: words` is measured; another unit
|
|
30
|
+
skips the station), and every `required_parts` entry present as an ATX heading of that text
|
|
31
|
+
(indented up to three spaces, closing `#`s allowed), or as a line starting `part:`, outside
|
|
32
|
+
code blocks.
|
|
33
|
+
- `terms`: every term in the new optional `writing.audience.terms`, other than those in `knows`,
|
|
34
|
+
is defined at its first appearance: in that sentence or the next, the term followed within six
|
|
35
|
+
words by `is`, `means`, `refers to` or a colon, or by a parenthesis. A mechanical proxy for a
|
|
36
|
+
definition, and documented as one. No `terms` list: skip.
|
|
37
|
+
- `claims`: every claim in the JSONL ledger at `writing.sources.ledger` (`text`, `source`,
|
|
38
|
+
optional `span`) still appears in the draft word for word, and has a real source; unsourced
|
|
39
|
+
claims warn instead under `unsourced_claim: warn`, and point at the draft line where the claim
|
|
40
|
+
appears. A missing ledger fails. The ledger is the list of claims: the station does not decide
|
|
41
|
+
what counts as one.
|
|
42
|
+
- `quotes`: every double-quoted span of four words or more appears word for word in a `quote` or
|
|
43
|
+
`story` segment of a marked material, never a private one; when the sentence names a quote
|
|
44
|
+
segment's speaker, by the full name or by its first word (when that word has two or more
|
|
45
|
+
letters and is not a common function word such as "the"), the span must come from that
|
|
46
|
+
speaker. A spec with `fiction: true` skips the station, since a character's dialogue is
|
|
47
|
+
invented rather than quoted.
|
|
48
|
+
- `private`: no run of eight words from a `private` segment appears in the draft. Segments of four
|
|
49
|
+
to seven words are checked whole; shorter ones are counted in one warning and never quoted.
|
|
50
|
+
- `dna`: with a current `writing.dna.scope_dir`, the draft is measured the way goldens are and
|
|
51
|
+
each feature compared with the scope's, from v ÷ 1.5 to the larger of v × 1.5 and v + 5; drift,
|
|
52
|
+
and an em dash where the scope has none (pointing at the first one), are warnings. No scope
|
|
53
|
+
folder: skip.
|
|
54
|
+
- `links`: inline, reference and bare links are well-formed http, https or mailto (a bare
|
|
55
|
+
`https://` with no host fails); relative links resolve to a file beside the draft; a `/` link
|
|
56
|
+
warns, since there is no site root to resolve it against; a full or collapsed reference needs
|
|
57
|
+
its definition. A bare `[label]` is a link only when its label is defined, so `[sic]`, `[x]` and
|
|
58
|
+
`[1]` are text. No network access.
|
|
59
|
+
- The runs ledger: each check appends `{ at, kind: "check", draft, draft_sha256, spec_sha256,
|
|
60
|
+
stations, verdict }` to `improvement.ledger`, with `draft` relative to the spec's folder. A run
|
|
61
|
+
with `--only` adds `partial: true`, is `not-improved` with the reason `partial run: <stations>`,
|
|
62
|
+
and is ignored by later verdicts. A full run is compared with the last full line for the same
|
|
63
|
+
draft: `one-shot` when there is none and every station passes; `improved` when that line failed
|
|
64
|
+
and every station passes now, with `change` naming exactly the stations that failed then and
|
|
65
|
+
pass now; otherwise `not-improved`, with a `reason` that says which case it is, such as "no
|
|
66
|
+
change since the last passing check" or "spec changed; failing stations: terms". The lines keep
|
|
67
|
+
lint's test 9 passing.
|
|
68
|
+
- New optional field `writing.audience.terms`: a list of real strings when present (test 1,
|
|
69
|
+
`writing-audience-terms`, naming a scalar that is not a list, an empty list, and each entry that
|
|
70
|
+
is not a string, is empty or is a placeholder). Absent, nothing changes.
|
|
71
|
+
- The worked examples each ship a draft that passes every station, with its claims ledger:
|
|
72
|
+
`examples/writing/essay/draft.md` and `examples/writing/story/draft.md`. Both specs now list
|
|
73
|
+
`audience.terms`, and their `required_parts` are the drafts' headings. The essay's interview
|
|
74
|
+
quotes now name their speaker `dana, an engineering manager`, and the essay links the survey
|
|
75
|
+
summary it cites. A test runs `check` on both. WRITING.md gains "Checking a draft": each station, what it cannot check, a findings table
|
|
76
|
+
per station held by a test to the ids the stations raise, and the ledger line.
|
|
77
|
+
|
|
3
78
|
## 0.5.0 (2026-09-29)
|
|
4
79
|
|
|
5
80
|
Scoped writer DNA. A writer does not have one voice: the same person writes differently for a
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ improvement ledger. Every test is defined in [SPEC.md](SPEC.md).
|
|
|
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
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
30
|
| `hyperspec dna measure <scope-dir>` | Check every golden in a scope and write its measured features. |
|
|
31
|
+
| `hyperspec check <spec> --draft <file> [--only a,b]` | Run a writing spec's deterministic stations against a draft. |
|
|
31
32
|
| `hyperspec recipe check <output-or-recipe>` | Check that a recipe records everything the standard asks for. |
|
|
32
33
|
| `hyperspec recipe approve <recipe> --by <slug>` | Record who approved the output. |
|
|
33
34
|
| `hyperspec reproduce <recipe> [--restore]` | Re-check every hash the recipe recorded. Never runs a model. |
|
|
@@ -42,6 +43,10 @@ Every command except `init`, `segments init` and `dna init` takes `--json`. `hyp
|
|
|
42
43
|
test fails, 3 when every test passes but a decision is still open (blocked), and 2 on a usage
|
|
43
44
|
error or a file that cannot be read, has broken frontmatter, or is not a hyperspec.
|
|
44
45
|
|
|
46
|
+
`hyperspec check` exits 0 when every station it ran passed, 1 when one failed, and 2 on a usage
|
|
47
|
+
error, a draft that cannot be read, or a spec without `profile: writing`. A spec that is not ready to check against exits with lint's
|
|
48
|
+
own code, 1 or 3, and no station runs.
|
|
49
|
+
|
|
45
50
|
The recipe commands use the same numbers: 0 ok, 1 a check failed or the child regressed, 2
|
|
46
51
|
usage or unreadable input, 3 pending, when `regenerate` has stages waiting for a runner.
|
|
47
52
|
`regenerate` also exits 1 when a stage it ran reported a failing verdict, or none. A stage it
|
|
@@ -132,7 +137,7 @@ The skeleton fails until every placeholder is real and its four open questions (
|
|
|
132
137
|
who speaks, who reads, what changes) are answered. The blocks, every field, and which test
|
|
133
138
|
each rule reports under are in [WRITING.md](WRITING.md). Two complete specs that pass with
|
|
134
139
|
nothing to warn ship in `examples/writing/`: an essay for new managers, and a short story with
|
|
135
|
-
two characters whose voices a judge can tell apart.
|
|
140
|
+
two characters whose voices a judge can tell apart. Each comes with a draft written to it.
|
|
136
141
|
|
|
137
142
|
### Marking materials
|
|
138
143
|
|
|
@@ -176,6 +181,25 @@ it did in 0.4. The folder shape, every feature, and every finding are in
|
|
|
176
181
|
[WRITING.md](WRITING.md#scoped-dna); `readScope` and `measureFeatures` are exported from
|
|
177
182
|
`@supersuit/hyperspec/writing`.
|
|
178
183
|
|
|
184
|
+
### Checking a draft
|
|
185
|
+
|
|
186
|
+
Once a draft exists, `check` holds it to its spec with seven stations, none of which calls a
|
|
187
|
+
model or touches the network: `form` (length and required parts), `terms` (every word in the new
|
|
188
|
+
optional `writing.audience.terms` is defined where it first appears), `claims` (the claims
|
|
189
|
+
ledger still matches the draft, and every claim has a source), `quotes` (in nonfiction, every quotation of four
|
|
190
|
+
words or more is word for word in a marked quote), `private` (no run of eight words from a
|
|
191
|
+
private segment), `dna` (the draft's measured style beside its scope's, as warnings) and `links`
|
|
192
|
+
(well-formed, and relative links resolve).
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx @supersuit/hyperspec check essay.hyperspec.md --draft essay/draft.md
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
It lints the spec first, prints each station's pass, fail or skip, and appends one line to the
|
|
199
|
+
spec's runs ledger with a verdict. Both examples ship a draft that passes. What each station
|
|
200
|
+
checks and cannot check, and every finding, are in
|
|
201
|
+
[WRITING.md](WRITING.md#checking-a-draft).
|
|
202
|
+
|
|
179
203
|
## The format
|
|
180
204
|
|
|
181
205
|
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.
|
|
100
|
+
next_action: collect adopter issues on 0.6, the check command included, and cut 0.7 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.
|
|
112
|
+
**Version 0.6.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). 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.
|
|
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. The writing profile also has an optional `writing.audience.terms`, the words a piece uses that its reader may not know: when present it must list real terms (test 1), and `hyperspec check` reads it to require each term's definition where the draft first uses it. Without it nothing changes.
|
|
224
224
|
|
|
225
225
|
## Exit codes
|
|
226
226
|
|
|
@@ -236,7 +236,7 @@ A profile adds its own conditions to these rows. The writing profile's are in [W
|
|
|
236
236
|
Every run of a skill that works from a hyperspec writes one line to the ledger named in `improvement.ledger`, one JSON object per line, with a `verdict`:
|
|
237
237
|
|
|
238
238
|
- **one-shot**: no intervention, nothing to learn.
|
|
239
|
-
- **improved**: the skill, the spec template, or a component library changed, and the line carries `change`, naming what changed.
|
|
239
|
+
- **improved**: the skill, the spec template, or a component library changed, and the line carries `change`, naming what changed. On a `kind: "check"` line, which `hyperspec check` writes, it means the draft now passes every station after the last full check of it failed, and `change` names those stations.
|
|
240
240
|
- **not-improved**: nothing changed, and the line carries `reason`, a reason a later session can argue with, such as "the correction was about this piece only" or "the fix belongs to a shipped skill and was filed as an issue".
|
|
241
241
|
|
|
242
242
|
Silence is not a verdict. A run that learned nothing has to say so and why, and a ledger line with none of the three verdicts fails the ninth test.
|
package/WRITING.md
CHANGED
|
@@ -209,6 +209,8 @@ writing:
|
|
|
209
209
|
wants: a plan for the first meeting
|
|
210
210
|
reads_on: a phone, in the ten minutes before the meeting
|
|
211
211
|
reader: person # person | agent
|
|
212
|
+
terms: # optional: terms the piece uses that the reader may not know
|
|
213
|
+
- skip-level
|
|
212
214
|
check:
|
|
213
215
|
station: term check against knows
|
|
214
216
|
rubric: simulated reader reports where it got lost
|
|
@@ -317,6 +319,17 @@ cannot pass a presence check. A placeholder is a whole value, trimmed and in any
|
|
|
317
319
|
marks, or an ellipsis, optionally followed by a trailing `.`, `:` or `!`. Real text that starts
|
|
318
320
|
with one of those, such as `TODO: write the opening`, counts as present, and so does `none`.
|
|
319
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
|
+
|
|
320
333
|
## The test mapping
|
|
321
334
|
|
|
322
335
|
Each row lists what the writing profile adds to that test. The core conditions in
|
|
@@ -324,7 +337,7 @@ Each row lists what the writing profile adds to that test. The core conditions i
|
|
|
324
337
|
|
|
325
338
|
| Test | A writing spec fails it when |
|
|
326
339
|
|---|---|
|
|
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` |
|
|
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` |
|
|
328
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 |
|
|
329
342
|
| 3 every requirement names its check | a block or a character has no `check` with a `station` or a `rubric` |
|
|
330
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` |
|
|
@@ -786,6 +799,216 @@ source, and a `features.json` that `dna measure` wrote. A test measures the fold
|
|
|
786
799
|
release and requires the same bytes, so the example cannot drift from the tool. The short story
|
|
787
800
|
beside it lists its goldens in the spec with no scope folder, the 0.4 shape, which still passes.
|
|
788
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
|
+
|
|
789
1012
|
## Deferring a block
|
|
790
1013
|
|
|
791
1014
|
A block can be deferred, never silently missing. A required block that is absent fails test 1
|
|
@@ -849,13 +1072,19 @@ the field it needs, and every spine claim cites the segments that support it. Ea
|
|
|
849
1072
|
keeps the boundaries `segments init` wrote, in paragraph mode for prose and sentence mode for
|
|
850
1073
|
bulleted notes, so you can re-run it and compare.
|
|
851
1074
|
|
|
852
|
-
Both lint `pass (9/9)` with `writing: 9/9 blocks complete` and no findings.
|
|
853
|
-
|
|
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.
|
|
854
1081
|
|
|
855
1082
|
## What later versions add
|
|
856
1083
|
|
|
857
|
-
This release is the schema, its lint, marked materials,
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
the
|
|
861
|
-
|
|
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.
|
package/bin/hyperspec.mjs
CHANGED
|
@@ -16,11 +16,25 @@ import { splitSegments } from "../src/segments.mjs";
|
|
|
16
16
|
import { sha256 } from "../src/hash.mjs";
|
|
17
17
|
import { readScope, measureFeatures, writeFeatures, scopeTemplate, GOLDENS_README } from "../src/dna.mjs";
|
|
18
18
|
import { str } from "../src/placeholder.mjs";
|
|
19
|
+
import { runCheck } from "../src/check.mjs";
|
|
19
20
|
|
|
20
21
|
const HELP = `hyperspec <command> [options]
|
|
21
22
|
|
|
22
23
|
lint <file...> [--json] score each hyperspec against the nine tests
|
|
23
24
|
exit 0 pass, 1 a test fails, 3 blocked on an open decision, 2 usage
|
|
25
|
+
check <spec> --draft <file> [--json] [--only a,b]
|
|
26
|
+
needs a writing spec (profile: writing); lints it first (a spec
|
|
27
|
+
that does not pass lint, or is blocked, exits with lint's own code
|
|
28
|
+
and runs no station: a draft cannot be checked against a spec that
|
|
29
|
+
is not ready); then runs every deterministic station (or the
|
|
30
|
+
--only subset, by name) against the draft, printing pass, fail
|
|
31
|
+
(with findings) or skip (with a reason) per station; appends one
|
|
32
|
+
line to the spec's improvement.ledger with a verdict: one-shot,
|
|
33
|
+
improved, or not-improved with a reason (an --only run is partial:
|
|
34
|
+
not-improved, and ignored by later verdicts)
|
|
35
|
+
exit 0 every run station passed, 1 a station failed, 2 usage
|
|
36
|
+
(including a missing draft file, a spec without the writing
|
|
37
|
+
profile, or an --only that names no known station)
|
|
24
38
|
init <file> [--title T] [--kind K] write a new hyperspec skeleton (refuses to overwrite)
|
|
25
39
|
init <file> --profile writing [--title T] [--form F] [--fiction]
|
|
26
40
|
write a writing-profile skeleton: every required block (materials,
|
|
@@ -282,6 +296,61 @@ if (cmd === "lint") {
|
|
|
282
296
|
process.exit(worst);
|
|
283
297
|
}
|
|
284
298
|
|
|
299
|
+
if (cmd === "check") {
|
|
300
|
+
const parsed = parseArgs(argv.slice(1), { valueFlags: ["--draft", "--only"], boolFlags: ["--json"] });
|
|
301
|
+
if (parsed.error) { console.error(parsed.error); process.exit(2); }
|
|
302
|
+
const [specPath] = parsed.positionals;
|
|
303
|
+
const json = parsed.values["--json"];
|
|
304
|
+
// A usage error exits 2: a plain message on stderr, or under --json one document on stdout,
|
|
305
|
+
// { spec, draft, error }, the way lint --json reports a file it could not read.
|
|
306
|
+
const usage = (error) => {
|
|
307
|
+
if (json) console.log(JSON.stringify({ spec: specPath ?? null, draft: parsed.values["--draft"] ?? null, error }, null, 2));
|
|
308
|
+
else console.error(error);
|
|
309
|
+
process.exit(2);
|
|
310
|
+
};
|
|
311
|
+
if (!specPath) usage("check needs a spec path");
|
|
312
|
+
if (!parsed.values["--draft"]) usage("check needs --draft <file>");
|
|
313
|
+
let only;
|
|
314
|
+
if (parsed.values["--only"] !== undefined) {
|
|
315
|
+
only = parsed.values["--only"].split(",").map((s) => s.trim()).filter(Boolean);
|
|
316
|
+
if (!only.length) usage("--only names no station");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = runCheck(specPath, parsed.values["--draft"], { only });
|
|
320
|
+
|
|
321
|
+
// Usage errors (an unreadable spec, a spec without the writing profile, an unknown --only name,
|
|
322
|
+
// a missing draft file): exit 2, as above.
|
|
323
|
+
if (result.usage) usage(result.error);
|
|
324
|
+
|
|
325
|
+
if (result.lintBlocked) {
|
|
326
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
327
|
+
else {
|
|
328
|
+
const r = result.lintScore;
|
|
329
|
+
console.log(`${result.specPath}: ${r.status} (${r.passed}/9)${r.open.length ? `, open: ${r.open.join(", ")}` : ""}`);
|
|
330
|
+
for (const t of r.tests) if (!t.pass) console.log(` ✗ ${t.n}. ${t.name}`);
|
|
331
|
+
for (const f of result.lintFindings) console.log(` ${f.severity === "fail" ? "fail" : "warn"} [${f.test}] ${f.message}\n fix: ${f.fix}`);
|
|
332
|
+
console.log("no stations run: the spec is not ready (run `hyperspec lint` on it for details)");
|
|
333
|
+
}
|
|
334
|
+
process.exit(result.code);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (json) {
|
|
338
|
+
console.log(JSON.stringify(result, null, 2));
|
|
339
|
+
} else {
|
|
340
|
+
for (const s of result.stations) {
|
|
341
|
+
if (s.status === "skip") { console.log(`${s.station}: skip (${s.reason})`); continue; }
|
|
342
|
+
console.log(`${s.station}: ${s.status}`);
|
|
343
|
+
for (const finding of s.findings) console.log(` ${finding.severity === "fail" ? "fail" : "warn"} [${finding.id}] ${finding.message}${typeof finding.line === "number" ? ` (line ${finding.line})` : ""}\n fix: ${finding.fix}`);
|
|
344
|
+
}
|
|
345
|
+
if (result.verdict) {
|
|
346
|
+
const detail = result.verdictDetail.change ?? result.verdictDetail.reason;
|
|
347
|
+
console.log(`verdict: ${result.verdict}${detail ? ` (${detail})` : ""}`);
|
|
348
|
+
}
|
|
349
|
+
if (result.ledgerWarning) console.log(`warn: ${result.ledgerWarning}`);
|
|
350
|
+
}
|
|
351
|
+
process.exit(result.code);
|
|
352
|
+
}
|
|
353
|
+
|
|
285
354
|
// Generic flag/positional parser for the recipe verbs below. A value-taking flag (valueFlags,
|
|
286
355
|
// repeatableFlags) never swallows a following --flag as its value (missing value is an error, not
|
|
287
356
|
// a silent grab); a bool flag never eats the next token as a positional; any --flag not declared
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{"text":"29 of 41 people said their most useful one-on-one in the last quarter was one where they brought the first topic.","source":"survey#s3","span":"29 of 41 said their most useful one-on-one in the last quarter was one where they brought the first topic."}
|
|
2
|
+
{"text":"That is how an engineering manager I interviewed, eight years into the job, runs hers.","source":"interview#s4","span":"She keeps a shared document per person; they add items before the meeting, and she adds hers last, at the bottom."}
|
|
3
|
+
{"text":"Her rule is short: the report owns the agenda.","source":"interview#s3","span":"Her rule: the report owns the agenda."}
|
|
4
|
+
{"text":"My first one-on-one as a manager was a disaster, and the reason was simple: I ran it.","source":"voice-memo#s2","span":"My first one-on-one as a manager was a disaster, and the reason was simple: I ran it."}
|
|
5
|
+
{"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."}
|
|
6
|
+
{"text":"What is taking more of your energy than it should?","source":"voice-memo#s4","span":"What is taking more of your energy than it should?"}
|
|
7
|
+
{"text":"What do you want to be doing more of in six months?","source":"voice-memo#s4","span":"What do you want to be doing more of in six months?"}
|
|
8
|
+
{"text":"What should I stop doing, or start doing, that would make your week easier?","source":"voice-memo#s4","span":"What should I stop doing, or start doing, that would make your week easier?"}
|
|
9
|
+
{"text":"The most common free-text request in the survey, in 9 responses, was to be asked what they want to work on next.","source":"survey#s5","span":"The most common free-text request, in 9 responses: \"ask me what I want to work on next.\""}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Hand your first one-on-one to the person you manage
|
|
2
|
+
|
|
3
|
+
## Who sets the agenda
|
|
4
|
+
|
|
5
|
+
Your first one-on-one with a new report is the only meeting on your calendar where they should
|
|
6
|
+
set the agenda. Everything else you run. This one you hand over.
|
|
7
|
+
|
|
8
|
+
If you walk in with a list, you have told your report what the meeting is for, and it is for
|
|
9
|
+
you. They will answer your list politely and leave. You will know nothing you did not know when
|
|
10
|
+
you sat down, and so will they.
|
|
11
|
+
|
|
12
|
+
My team ran a [survey](materials/team-survey.md) this spring. 29 of 41 people said their most useful one-on-one in the last
|
|
13
|
+
quarter was one where they brought the first topic. The meeting that worked for them was the one
|
|
14
|
+
they started.
|
|
15
|
+
|
|
16
|
+
So give them the start. The simplest way to do it is a running agenda: one shared document per
|
|
17
|
+
person, kept for as long as you manage them. Your report adds items before each meeting, and you
|
|
18
|
+
add yours last, at the bottom. That is how an engineering manager I interviewed, eight years into
|
|
19
|
+
the job, runs hers. Her rule is short: the report owns the agenda.
|
|
20
|
+
|
|
21
|
+
## My first one-on-one
|
|
22
|
+
|
|
23
|
+
My first one-on-one as a manager was a disaster, and the reason was simple: I ran it. I had a
|
|
24
|
+
list, and I went down the list. Project status, blockers, the thing from Tuesday. Thirty minutes
|
|
25
|
+
later my report said thanks and left, and I had learned nothing I could not have read in the
|
|
26
|
+
tracker.
|
|
27
|
+
|
|
28
|
+
What I ran was a status meeting, which is a meeting spent reading out loud what the tracker
|
|
29
|
+
already says. Your report wrote those updates. Asking them to recite the updates to you teaches
|
|
30
|
+
you nothing new, and it spends the one half hour a week that belongs to them.
|
|
31
|
+
|
|
32
|
+
The survey shows the cost from their side. 11 of 41 said at least one of their one-on-ones in the
|
|
33
|
+
last quarter was mostly project status. Read the tracker before you walk in, and leave status
|
|
34
|
+
there.
|
|
35
|
+
|
|
36
|
+
## The three questions
|
|
37
|
+
|
|
38
|
+
Here is what I ask now, in this order, and then I let the report take over:
|
|
39
|
+
|
|
40
|
+
1. What is taking more of your energy than it should?
|
|
41
|
+
2. What do you want to be doing more of in six months?
|
|
42
|
+
3. What should I stop doing, or start doing, that would make your week easier?
|
|
43
|
+
|
|
44
|
+
Three is enough to hand the meeting over. The first asks about this week. The second asks about
|
|
45
|
+
the next six months. The third asks about you, and it is the one your report will not raise
|
|
46
|
+
without being asked. A fourth question starts to look like your list again, and the list is what
|
|
47
|
+
you came to give up.
|
|
48
|
+
|
|
49
|
+
Ask them in the same words every time. Your report will learn them, and after a few weeks they
|
|
50
|
+
will walk in with answers already half formed. That is the point of fixing the words: the meeting
|
|
51
|
+
starts on their topic before you have said anything at all.
|
|
52
|
+
|
|
53
|
+
The second question is the one my own team asked for. The most common free-text request in the
|
|
54
|
+
survey, in 9 responses, was to be asked what they want to work on next.
|
|
55
|
+
|
|
56
|
+
After the third question, stop talking.
|
|
57
|
+
|
|
58
|
+
## What to do with the answers
|
|
59
|
+
|
|
60
|
+
Wait after each question, longer than you want to. Dana, the engineering manager I interviewed,
|
|
61
|
+
puts it in three short sentences: "Wait. Count to five. The real answer is the second one." The first
|
|
62
|
+
answer your report gives is the tidy one, the version they could give anyone. The second is what
|
|
63
|
+
they came in with, and you only hear it if you let the silence run.
|
|
64
|
+
|
|
65
|
+
Write down what they say, in their words, at the top of the running agenda. That list is where
|
|
66
|
+
your next one-on-one starts, so the meeting stays theirs the week after too.
|
|
67
|
+
|
|
68
|
+
Do not try to fix everything in the room. Pick one thing you can act on this week, say what you
|
|
69
|
+
will do, and do it before you meet again. The rest stays on the running agenda until it is done
|
|
70
|
+
or your report takes it off.
|
|
71
|
+
|
|
72
|
+
And keep your own urgent items out of it. She told me: "If I have something urgent, it is not a
|
|
73
|
+
one-on-one topic. I send it the day it happens." Your report should never have to wait a week to
|
|
74
|
+
hear something you needed them to know on Monday.
|
|
75
|
+
|
|
76
|
+
## Before the meeting
|
|
77
|
+
|
|
78
|
+
Open the invite and delete your list from it. Ask your report to add the first item to the
|
|
79
|
+
running agenda instead.
|
|
80
|
+
|
|
81
|
+
So write the three questions on a card. Ask the first one. Then wait, longer than feels polite,
|
|
82
|
+
because the first answer is the one they rehearsed and the second one is the one you came for.
|