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

Annotations

Status: Current Last updated: 2026-08-27 18:09 EDT

A scoped annotation is a bracketed code written immediately after the thing it describes: hello [*], <the dog> [//], bobo [= toy], 0 [= ! whining]. It is scoped because it attaches to a specific construct rather than to the utterance as a whole, which is what separates it from a postcode (utterance-wide, written before the terminator) and from a dependent tier (a whole line of its own).

This chapter answers three questions the model makes precise: what can carry annotations, what it means for something to carry none, and why an annotated construct always carries at least one.

What can be annotated

Each of these constructs has exactly two spellings. The list is the count; stating a number beside it is one more thing to keep true, and this line said five above six rows.

ConstructBareAnnotated
WordWordAnnotatedWord
Group <...>GroupAnnotatedGroup
QuotationQuotationAnnotatedQuotation
Event &=laughsEventAnnotatedEvent
Action 0ActionAnnotatedAction
RetraceRetraceAnnotatedRetrace

Everything else in an utterance is a leaf that takes no scoped annotation: pauses, separators, overlap points, bullets, freecodes, and the long-feature, underline and nonvocal delimiters.

Two constructs are worth calling out because they behave unlike their neighbours. A replaced word (word [: replacement]) is ReplacedWord rather than an Annotated<Word>, because the replacement is part of the word’s identity rather than a comment on it; it carries its own annotations alongside. And a retrace’s annotations describe the retrace itself, not the material inside it, which is why a retrace opens no language scope for the words it contains.

Carrying none is a different variant, not an empty list

The bare and annotated spellings are different variants because they are different things. hello is a word; hello [*] is a word plus a claim about it. The model does not represent the first as the second with nothing in it.

This is enforced in the type rather than checked afterwards:

// The only public constructor. `None` when the list is empty.
AnnotatedContentAnnotations::new(annotations) -> Option<AnnotatedContentAnnotations>

So an annotated wrapper cannot be built without an annotation, and that Option IS the bare-versus-annotated decision. Every place the parser builds content, it reads:

match AnnotatedContentAnnotations::new(scoped) {
    None => UtteranceContent::Event(event),
    Some(scoped) => UtteranceContent::AnnotatedEvent(Annotated::new(event, scoped)),
}

TryFrom<Vec<_>> applies the same check, Deserialize rejects an empty list off the wire rather than accepting one, and there is deliberately no Default. The type also does not take the crate’s collection-newtype macro, whose take and retain can empty a collection in place.

Why this is stated so emphatically

Because the invariant was prose for a long time, and prose does not hold.

Until 2026-08-26 UtteranceContent had no bare Action, though it had a bare Event sitting two lines away in the same enum. An action with no annotations therefore had nowhere to go, and the parser wrapped every one of them in an Annotated carrying an empty list. Measured across a 106,000-file corpus that was 20,184,072 values claiming to be annotated while carrying nothing, almost all of them a bare 0 marking silence in daylong audio recordings. BracketedItem had the mirror-image gap: no bare Group, so an unannotated nested group became an AnnotatedGroup with an empty list, and the converter explained itself in a comment because it could do nothing else.

Two error codes were supposed to catch the empty case. Neither could. The full account is in Leniency Policy, Decision 1: one code was deliberately disabled because bare [*] is valid CHAT, its number was later reused for a different rule, and that rule was unreachable because an empty bracket is a parse error and the one genuinely empty construct was never validated.

Both bare variants exist now, the two content enums are symmetric, and the empty state is unconstructible. The rule is no longer something a validator looks for; it is something the compiler refuses.

What an annotation attaches to when constructs nest

Scoping follows the innermost construct. In <the big dog> [//] [* m] both annotations attach to the group. In <the [//] dog> the marker attaches to the word inside it, because that is what precedes it.

One consequence matters for anything reading language: a <...> [@s:spa] group opens a code-switch scope for the words inside it, and a retrace does not open one at all. Tools should ask the model for the governing scope rather than re-deriving it from the annotation list, because the two rules differ and the difference is invisible if you get it wrong.


This page last changed: 2026-08-27 (commit 8b445304). The whole book last changed: 2026-09-15 (commit bb4bef82).