Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec System

Status: Current Last modified: 2026-08-21 13:12 EDT

spec/ is the source of truth for what CHAT is and for what chatter rejects. Tests, fixtures and error documentation are GENERATED from it. You change the spec; you do not hand-edit what it produces.

This chapter is the reference: what the spec files contain, what each field does, and what checks them. To make a change, follow Spec Workflow.

Start here: ask the system

Before reading further, run:

just spec-status

It reports, derived from the same code the gates use rather than from prose: how many specs exist and what status they declare, how many examples are verified, how many are deferred, how many assert nothing at all, the state of CLAN CHECK parity, and which gate checks which artifact. If this page and that command ever disagree, the command is right.

The two kinds of spec

Construct specs, spec/constructs/

A valid CHAT fragment and the tree it must parse to.

# languages_single

@Languages header with single language code

## Input

```languages_header
@Languages:	eng
```

## Expected CST

```cst
(languages_header
  (languages_prefix)
  ...
)
```

## Metadata

- **Level**: header
- **Category**: header

The Input fence label (languages_header, main_tier, utterance, standalone_word, …) names a template in spec/tools/templates/ that wraps the fragment in a complete CHAT file, because tree-sitter parses documents rather than fragments. A label with no matching .tera template is an error; add the template.

Error specs, spec/errors/

Invalid CHAT, and the codes it must produce. Everything declared lives in +++ TOML frontmatter; everything published as prose lives in the body.

+++
code = 'E207'
name = 'Unknown scoped annotation marker'
kind = 'Invalidity'
status = 'implemented'

[[example]]
source = 'E2xx_word_errors/E207_multiple_form_types.cha'
level = 'word'
claim = 'violates'
chat = '''
@UTF8
@Begin
@Languages:	eng
@Participants:	CHI Target_Child
@ID:	eng|corpus|CHI|||||Target_Child|||
*CHI:	word@zz .
@End
'''
+++

## Description

Unknown scoped annotation marker.

What every field actually does

The fields are not decoration. Each one changes what is checked. The authoritative list, with types, is talkbank_spec_vocabulary::frontmatter, which refuses an unrecognised key at load; this table says what each field DOES, which a type cannot.

FieldEffect
codeThe code the spec is about; names the generated tests.
nameThe spec’s short name, published as the page’s title.
kindThe DiagnosticKind axis.
statusWhether examples are verified or deferred, see below.
status_noteA human’s adjudication of the code’s current state. Prose, published nowhere, read by people.
example.chatThe input itself, a whole CHAT file. Required: an example without one is not an example.
example.sourceThe fixture the example came from. Its stem NAMES the transcript, see below.
example.title, example.notesProse about this example, read by people.
example.claimWhat the example asserts: violates, legal, or subsumed_by <code(s)>. REQUIRED, and both halves are enforced (absences included), see below.
example.levelWhere THIS example’s fault is (word, tier, utterance, header, file). Required per example: a code like E519 is violated at header level in one example and at utterance level in another, so the fault site is a fact about the example, not the code. The page’s Level line renders the distinct set.

Two prose sections are published as well as read by humans:

SectionEffect
## DescriptionPublished verbatim, markdown and paragraph breaks intact, as the page’s description. Required.
## CHAT RulePublished verbatim as the page’s ## CHAT Rule section: what CHAT requires, and therefore what a maintainer must write instead. Optional; a spec without one publishes no such section.
## Expected Behavior, ## NotesProse for whoever opens the spec file. Read by no tool.

Write the RULE in ## CHAT Rule, not a bare manual link. The pages exist so a data maintainer can fix a file without reading the validator’s source.

There is no longer a rule about where a field sits

This section used to say Expected Error Codes must precede the fence, because the loader read the content before the ```chat block and a spec that put the line below it declared nothing while reading, to a human, as fully specified. Two of E757’s examples did exactly that, and the loader grew a guard that refused the placement.

Phase 1b deleted the rule and the guard together: an example is one value that carries its own input, so there is no fence for a field to be on the wrong side of. It is recorded here because it is the clearest example in this system of a type removing a rule rather than a document restating one.

Every example carries a CLAIM, and absences are assertable

Since R2 (2026-08-21) each example declares one of:

claim = 'violates'                         # the spec's code MUST appear
claim = 'legal'                            # the spec's code MUST NOT appear
claim = { subsumed_by = 'E316' }           # E316 appears; this code does not
claim = { subsumed_by = ['E246', 'E249'] } # all listed appear; this code does not

The claim is REQUIRED: an example that asserts nothing is unwritable, which retired the self-demonstration gate and its 36-entry baseline outright, plus the zero-ratchet test whose own docstring had named exactly this retirement (“nothing in a type stops the next spec omitting it”).

Extra emitted codes are still fine (one malformed line legitimately raises several diagnostics); the exact per-stage sets are the observation snapshot’s business. What changed is the NEGATIVE half: legal and the own-code-absent part of subsumed_by are assertions the old subset check could not express at all, and this page used to say so (“a spec cannot be used to assert that a code is NOT emitted”). A spec whose examples are all subsumed_by is the parser-specificity worklist, verifiable against the snapshot rather than merely recorded; coverage --errors lists it.

There is no layer field, and the runner is total

Until R4 (2026-08-21) every spec declared layer = 'parser' | 'validation', and the field decided what a generated test could SEE: a parser-layer spec got a string-based test inspecting parse diagnostics only, a validation-layer spec got a fixture. Declaring a validation-layer code in a parser-layer spec therefore produced a test that could never see it, which the E342/E390 case demonstrated in production.

R4 deleted the field and the failure mode together, in three moves:

  • every example is a fixture, and the fixture runner has always collected BOTH stages’ codes against a real file, so there is no stage a declared code can hide in (five examples’ codes are genuinely SPLIT across stages, which no per-stage harness could assert);
  • the string-based error tests are gone, being strictly weaker than the fixture runner plus the observation snapshot;
  • which stage catches a rule is an OBSERVATION, recorded per example in spec/observations/example-diagnostics.json. The authored field disagreed with the observation on 17 examples on the day it was measured.

Tree-sitter corpus membership, which the field used to route, is derived from the snapshot instead: an example joins iff it produced parse-stage diagnostics, so there is structure to pin.

status decides whether an example is checked at all

ValueEffect
implementedExamples are verified.
not_implementedExamples are DEFERRED, not checked, and generated tests carry #[ignore].
deprecated, unreachable_from_chatDeferred, same as above.
absentREFUSED: the spec fails to load, naming the file.

Status used to default to implemented when the bullet was missing, so the file said nothing and the loader invented an answer. On 2026-08-11 that was true of 104 of 238 specs. All of them now declare it explicitly and the default is gone, so implemented in a spec file means somebody decided it.

Changing a spec from not_implemented to implemented un-#[ignore]s its generated tests, and those tests may never have run. Regenerate and run them in the same change.

source names the transcript

Some CHAT rules are about the file’s own name: E531 requires the @Media header’s filename to match the transcript’s stem. The example runner therefore names each transcript after the stem of its source, and an example with no source is anonymous, so those rules do not run for it.

This field was parsed by nothing until 2026-08-11, which is why E531’s spec could not be verified and was reported as failing rather than as untestable.

The observation snapshot

spec/observations/example-diagnostics.json (generated, gated) records, for every example of every spec, the exact diagnostic codes the current binary produces, split by the stage (parse or validation) that emitted them. It covers every spec regardless of status, because an observation is not an assertion: for an unimplemented rule the honest record is “nothing fires”.

It is the corpus differential aimed at the spec suite: a diff in this file is a review event, and every changed entry is adjudicated INTENDED (the behaviour change was the point; commit the regenerated snapshot in the same change) or UNINTENDED (a regression; fix the code, never the snapshot). It is also what makes a subsumed by claim verifiable and what the layer-of-capture question is answered from, observed rather than authored.

What is generated, and by what

One command regenerates everything committed: just spec-gen. Its registry (spec/tools/src/artifacts.rs, plus the half in spec/runtime-tools that needs the live ErrorCode enum) is the only place a destination is written down, and the same list drives writing, checking and the gate.

ArtifactCommitted atDirectory ownership
example-diagnostics observation snapshotspec/observations/the whole directory, cleared on every run
tree-sitter corpus testsgrammar/test/corpus/generated/the whole directory, cleared on every run
generated Rust test bodiescrates/talkbank-parser-tests/tests/integration/generated/only the files it produces
published error documentationdocs/errors/the whole directory, cleared on every run
validation fixture corpus + manifestcrates/talkbank-parser-tests/tests/error_corpus/validation_errors/the whole directory, cleared on every run
DiagnosticKind registrycrates/talkbank-model/src/errors/only the files it produces
the book’s artifact tablebook/src/architecture/generated/only the files it produces

That table is itself generated from the registry, and the currency gate keeps it true. The hand-written one it replaced listed five generators when the tree held eighteen binaries, and named four separate commands that had by then become one.

One generator sits outside it, deliberately: gen_form_markers has its own registry and its own drift gate (just form-markers-gen).

docs/errors/*.md used to be described here as “an optional local reference nothing commits”. That was false when written: the directory has been tracked since 2026-06-23, 226 files of it. It is now a registry artifact like any other, so just spec-gen writes it and just spec-check compares it, and the standalone gen_error_docs binary that wrote it outside the gate is deleted.

Two registries under spec/ own closed vocabularies and generate every site that names them: spec/symbols/symbol_registry.json (just symbols-gen) and spec/form_markers/form_marker_registry.json (just form-markers-gen). Each has its own README and its own drift gate.

Generated and hand-written tests live in separate trees. grammar/test/corpus/generated/ is wiped in full on every run and refuses to clear a directory lacking its .generated-output-dir marker; grammar/test/corpus/manual/ is never written by a generator. Both were once one tree, which destroyed 1,468 lines of hand-mined corpus tests twice in three days.

What checks what

GateChecksNeeds
every_generated_artifact_is_currentevery committed generated artifact against what the specs produce now
error_spec_codesevery example emits the codes it declares
manifest_agrees_with_clan_referenceparity manifest against check.cpp
generated_form_marker_sites_are_currentform-marker outputs against the registry
generated_symbol_sets_are_currentsymbol-set outputs against the registrynode
clan_check_groundingfixtures against the REAL CLAN binaryCLAN, CHATTER_CLAN_RUN

The first four run in CI under cargo test --manifest-path spec/Cargo.toml --workspace. clan_check_grounding is #[ignore]d and catches UPSTREAM drift; refresh-unix-clan.sh runs it after a successful CLAN sync, which is the moment it matters.

CLAN CHECK parity

CHECK is a decades-old approximation and a QUESTION LIST, never a specification. For each of its error codes the question is whether the construct it rejects actually fails to make sense, answered against spec/, the grammar and real corpus data.

Every code carries a verdict in crates/talkbank-parser-tests/tests/check_parity/manifest.json:

  • parity, chatter rejects it too;
  • divergence, chatter deliberately accepts it, with the reason recorded;
  • no_obligation, CLAN cannot emit it (commented out, no emission path, unreachable in file mode, or GUI-only), with the reason as a typed value.

just spec-status prints the current counts. CHECK’s silence is not authority: when upstream retired error 76 in the 2026-08-07 bundle, chatter KEPT its rule, because the changelog showed enforcement being abandoned rather than a linguistic question being decided.

  • Why the Spec System Looks Like That, which answers the questions this page raises and does not settle: what _auto means, why an E202 spec can carry an example expecting E316, and why eleven codes have two spec files. Read it before concluding that a spec file means what it appears to mean.
  • Spec Workflow, how to make a change.
  • Testing, the wider test strategy.
  • Grammar Governance, the grammar side.