Spec Workflow
Status: Current Last modified: 2026-08-21 13:12 EDT
How to change spec/ and leave the repository consistent. For what the fields
MEAN, read Spec System first; this page is
the procedure.
Every command here is written out. If a step here disagrees with what the tools do, the tools are right and this page is a bug.
Before and after any spec change
just spec-status # what state the spec system is in, derived from the gates
Run it before you start, so you know what “unchanged” looks like, and again at the end. A change that moves the “deferred” or “failing” counts in the wrong direction is worth a second look.
Adding a construct spec
A construct spec is a VALID fragment plus the tree it must parse to.
1. Write the file under the right spec/constructs/ subdirectory
(header/, main_tier/, tiers/, utterance/, word/):
# my_example
Description of what this example demonstrates.
## Input
```utterance
*CHI: hello world .
```
## Expected CST
```cst
(utterance
(main_tier
...))
```
## Metadata
- **Level**: utterance
- **Category**: main_tier
The fence label (utterance here) names a template in spec/tools/templates/
that wraps the fragment into a full CHAT file. If no template matches, create
one; the generator fails rather than guessing.
2. Get the real CST rather than writing one by hand:
cd grammar && tree-sitter parse <a file containing your input>
Copy the tree, dropping byte positions and field names.
3. Regenerate and verify (see “Regenerating” below).
Adding an error spec
An error spec is INVALID CHAT plus the codes it must produce.
1. Write the file in spec/errors/, named E###_<slug>.md. Everything
declared goes in +++ TOML frontmatter; the prose goes in the body.
+++
code = 'E301'
name = 'Empty speaker code'
kind = 'Invalidity'
status = 'implemented'
[[example]]
source = 'E3xx_main_tier_errors/E301_empty_speaker.cha'
level = 'utterance'
claim = 'violates'
chat = '''
@UTF8
@Begin
@Languages: eng
@Participants: CHI Target_Child
@ID: eng|corpus|CHI|||||Target_Child|||
*: hello .
@End
'''
+++
## Description
Empty speaker code.
A misspelled or unrecognised key is a LOAD ERROR, so you find out from
just spec-check rather than from a field that silently did nothing.
Four things decide whether your spec asserts anything, and each is easy to get wrong. They are covered in full in Spec System; in short:
claimis the field that asserts, and it is REQUIRED.violates(the spec’s code must appear),legal(it must not), orsubsumed_by <code(s)>(the targets appear and the spec’s code does not). Extra emitted codes still pass; the exact per-stage sets are the snapshot’s business.- There is no
layerfield. Which stage catches a rule is observed, not declared: every example is a fixture whose runner checks both stages, and the per-stage record lives in the observation snapshot. (The field existed until R4, and deciding it wrongly produced tests that could never see their own code.) status = 'not_implemented'DEFERS the example and#[ignore]s its generated tests.statusis REQUIRED: a spec that omits it does not load. (This bullet said omitting it “defaults toimplemented” until 2026-08-21; that default was removed on 2026-08-11, because an invented answer to “is this rule live” is the kind of wrong value nothing notices.)source’s stem names the transcript, which is what rules about the file’s own name (E531) compare against.
Write the failing case first. A new error spec should fail before the rule exists; that is what proves the fixture actually triggers it.
Regenerating
One command, from anywhere in the checkout:
just spec-gen # rewrite every generated artifact from the specs
just spec-check # or ask whether the committed copies are current
It regenerates every artifact in the registry, in dependency order (the
observation snapshot first, since the tree-sitter corpus derives its
membership from it); the generated artifact
table included in the
spec-system chapter is the live list. There is nothing to choose and no path to type:
every destination is a constant in spec/tools/src/artifacts.rs, so a
generator cannot be aimed at the wrong tree.
just spec-check writes nothing and is exactly what the
every_generated_artifact_is_current gate runs, so a green check means a green
gate.
The published error-reference pages under docs/errors/ are part of
spec-gen like every other artifact, and spec-check gates them.
Never hand-edit anything under a generated/ directory. An artifact that owns
its directory wipes it wholesale and refuses to clear one lacking its
.generated-output-dir marker.
Verifying
just spec-status # the derived summary
cargo test --manifest-path spec/Cargo.toml --workspace # every spec-side gate
just test # the main workspace
If your change touched the grammar, follow the full
Grammar Workflow as well: a
grammar.js edit needs tree-sitter generate before any parser behaviour can
be trusted.
Updating a registry
Two closed vocabularies live under spec/, each generating every site that
names it. Neither is edited anywhere but its registry.
just symbols-gen # spec/symbols/symbol_registry.json
just form-markers-gen # spec/form_markers/form_marker_registry.json
flowchart TD
registry["Edit the registry JSON"]
gen["Run its generator\n(loading validates; there is no separate check step)"]
fmt["Generator runs rustfmt on Rust output"]
gate["Drift gate compares committed output\nagainst what the generator produces"]
registry --> gen --> fmt --> gate
The generators format their own Rust output deliberately: otherwise just fmt
and the generator each rewrite the same bytes and the drift gate fails forever,
with both sides correct.
Each registry’s README covers its authorities and the follow-ups its generator
cannot do:
spec/symbols/README.md,
spec/form_markers/README.md.
Common mistakes
- Editing generated files. Change the spec or the registry, then regenerate.
- Wishing for an example that asserts nothing. There is no such state:
claimis required, and an example that cannot honestly sayviolatessayssubsumed_by(the worklist) orlegal(the boundary). - Flipping
statustoimplementedwithout regenerating. The fixture manifest still carries the old status, so the runner keeps skipping what you just enabled. (A third mistake used to sit here, declaring a validation-layer code in a parser-layer spec; R4 deleted thelayerfield and with it the possibility.) - Regenerating reflexively. Regeneration is for artifacts that genuinely changed, not a substitute for deciding what the change needs.