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

Batchalign

Welcome to Batchalign. Batchalign is a audio and ML pipeline for producing and enriching CHAT transcripts. It supports transcription, forced alignment, morphotagging, utterance segmentation, translation, diarization, comparison, and the Batchalign desktop shell.

User Guides

User guides for getting started are available via the following links:

Installation

Batchalign is available in two variants: as an application or as an a command-line tool. Downloads for both versions follows:

GUI Application

Batchalign Application

Press the blue rectangle corresponding to the operating system which you use to download the Batchalign application:

Finding the latest installers…

All releases and other builds

Terminal Interface (Advanced)

Batchalign CLI

For the advanced terminal interface, the following scripts allows you to install Batchalign via uv.

On macOS or Linux:

curl -LsSf https://raw.githubusercontent.com/TalkBank/batchalign/main/bootstrap/bootstrap.sh | sh

On Windows, from PowerShell:

powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/TalkBank/batchalign/main/bootstrap/bootstrap.ps1').Content)"

Batchalign documentation

Use Batchalign to transcribe recordings, align checked transcripts with audio, and add language analysis to CHAT files.

If you are new to Batchalign, begin with the Quick Start. It walks you through opening a command window and processing your first files.

What you needStart here
A guided first experienceTutorials
Steps to complete a particular taskHow-to guides
Exact commands, options, or data contractsReference
An understanding of how processing worksExplanation

For common clinical and research workflows, the Batchalign guide helps you choose the next task. The sidebar also includes supporting CHAT-format, Chatter, CLAN, and editor material, grouped by the same kinds of help.

Batchalign documentation

Batchalign helps you turn conversation recordings into CHAT transcripts and add language analysis to transcripts you have checked.

Start here

If this is your first time using Batchalign, follow the Quick Start. It shows you how to open a command window, check your installation, and process a transcript. You do not need programming experience.

Choose what you need

Your goalWhere to go
Learn by completing your first taskTutorial: Quick Start
Install or update BatchalignHow to install Batchalign
Transcribe a recordingHow to transcribe
Add grammatical analysis to a transcriptHow to add morphology and grammar
Match a checked transcript to its recordingHow to align
Solve a problemTroubleshooting
Look up a command or optionCLI reference
Use Batchalign from PythonPython API reference
Understand processing speed and memory usePerformance explained
Understand what is reused on a repeat runCaching explained
Understand the implementationPipeline architecture

The documentation separates tutorials, how-to guides, reference, and explanation. Start with the kind of help you need; you do not need to read these pages in order.

Batchalign: tutorials

Learn by completing a guided task.

Quick Start

Status: Current Last updated: 2026-09-13

Install Batchalign with the command in the TalkBank/batchalign README.

1. Open a command window

2. Check the installation

Paste this command and press Enter:

batchalign --help

If Batchalign prints a list of commands, it is ready.

3. Process a file or folder

This example adds %mor and %gra tiers to every CHAT file in a folder:

batchalign morphotag "/Users/me/Documents/My Corpus"

In PowerShell, a Windows path looks like this:

batchalign morphotag "C:\Users\me\Documents\My Corpus"

Batchalign accepts either one CHAT file or a folder. Use quotation marks around paths containing spaces. Unless you specify an output folder, morphotag updates the files in place.

Before you start

Model downloads: The first use of a backend may download its models. Download size and startup time depend on the backend and language. Later runs reuse model files from disk.

Caching: Batchalign saves text and audio backend results in a local LMDB cache. Repeat runs can reuse matching results, although startup and file processing still take time. See Caching for details.

Basic command shape

batchalign [GLOBAL OPTIONS] COMMAND [COMMAND OPTIONS] [PATHS...]
  • Global options go before the command.
  • Most processing commands use -o/--out for a destination directory.
  • Omitting -o/--out means in-place processing when the command supports it.

Transcribe audio to CHAT

batchalign transcribe ~/recordings/ -o ~/transcripts/ --lang eng

To use the local OpenAI Whisper package instead of the default Rev.AI engine:

batchalign transcribe ~/recordings/ -o ~/transcripts/ \
  --engine openai --lang eng

To use a local Whisper model:

batchalign transcribe ~/recordings/ -o ~/transcripts/ \
  --engine whisper --lang eng

Align transcripts against audio

batchalign align ~/corpus/ -o ~/aligned/

Common useful flags:

batchalign align ~/corpus/ -o ~/aligned/ --engine whisper
batchalign align ~/corpus/ -o ~/aligned/ --utr-engine whisper

Add morphosyntactic analysis

batchalign morphotag ~/corpus/ -o ~/tagged/

Useful variants:

batchalign morphotag ~/corpus/ -o ~/tagged/ --retokenize
batchalign morphotag ~/corpus/ -o ~/tagged/ --keep-existing

Verbosity

batchalign align ~/corpus/ -o ~/aligned/
batchalign -v align ~/corpus/ -o ~/aligned/
batchalign -vv align ~/corpus/ -o ~/aligned/
batchalign -vvv align ~/corpus/ -o ~/aligned/

Next steps

Chatter: tutorials

Learn by completing a guided task.

Quick Start

Status: Current Last updated: 2026-05-11 17:25 EDT

This page gets you from zero to productive with chatter in five minutes. Install chatter first if you haven’t already.

Validate a CHAT file

Check a single transcript for errors:

chatter validate transcript.cha

If the file is valid:

✓ transcript.cha is valid

If there are problems, you’ll see rich diagnostics with the exact location and a stable error code. For example, an utterance missing its terminator:

  × error[E304]: Expected terminator not found (line 6, column 1)

 6 │ *CHI:	hello world

  help: Add a terminator at the end: Standard (. ? !), Interruption (+... +/.)

Every error code (E304, E705, etc.) links to documentation with fix guidance.

Validate an entire corpus

Point chatter at a directory — it walks recursively, validates in parallel, and caches results:

chatter validate corpus/

The interactive TUI shows progress and lets you browse errors per file. Use --format json for machine-readable output, or --quiet for CI (exit code 1 on errors).

Run an analysis

chatter reimplements the CLAN command surface across validation, analysis, transform, and format-converter categories. A handful of legacy NLP commands (the trie-and-rule MOR/POST/MEGRASP family) are deliberately not implemented — they emit a clear error pointing at Batchalign for the neural replacement. See the CLAN command status matrix for the authoritative per-command counts and per-command status.

Try frequency analysis:

chatter clan freq transcript.cha

Output shows word frequencies by speaker. Add filters:

chatter clan freq transcript.cha --speaker CHI    # one speaker
chatter clan mlu transcript.cha --speaker CHI     # mean length of utterance
chatter clan combo transcript.cha --include-word "want"  # boolean keyword search

All CLAN commands support --format json and --format csv for downstream processing.

Convert to JSON

Get a structured representation of any CHAT file:

chatter to-json transcript.cha

The output conforms to the TalkBank CHAT JSON Schema. Convert back with chatter from-json.

Watch for changes

Edit a file and get live validation feedback:

chatter watch transcript.cha

Every time you save, chatter re-validates and shows updated diagnostics.

What next?

VS Code: tutorials

Learn by completing a guided task.

Your First CHAT File

Status: Current Last updated: 2026-03-30 13:40 EDT

This chapter walks you through opening a CHAT transcript and exploring the extension’s core features one at a time. Each section introduces a single capability so you can see what the extension does before diving into the detailed reference chapters.

If you have not yet installed the extension, start with Installation.


How the Extension Works

Before we begin, here is a brief look at the architecture. The VS Code extension is a thin TypeScript layer that delegates all CHAT intelligence to a Rust language server. Every feature – validation, alignment, analysis, formatting – runs inside this server.

flowchart LR
    Editor["VS Code Editor\n(TypeScript)"] -->|"stdio LSP"| Server["chatter-lsp\n(Rust binary)"]
    Server --> Parser["Tree-Sitter\nParser"]
    Server --> Model["CHAT Data\nModel"]
    Server --> Validator["Validation\nPipeline"]
    Server --> Analysis["CLAN Analysis\nCommands"]

The editor sends your keystrokes and commands to the server over standard I/O. The server parses the file with a tree-sitter grammar, builds a typed data model, runs validation, and sends diagnostics and responses back. You do not need to know any of this to use the extension – it all happens behind the scenes.


Open a CHAT File

Open any .cha file in VS Code. If you have the talkbank-tools repository cloned, good files to start with are in the resources/corpus/reference/ directory. These files cover a range of CHAT features across 20 languages.

As soon as the file opens, you should see:

  • Colored text – headers (@UTF8, @Begin, @Participants) are bold, speaker codes (*CHI:, *MOT:) have their own color, dependent tiers (%mor:, %gra:) have another, and timing bullets are dimmed
  • Utterance counts above the @Participants header showing how many utterances each speaker contributes (e.g., “CHI: 42 utterances”)
  • A structured outline in the breadcrumb bar and Outline view (Cmd+Shift+O) showing every utterance by speaker

(SCREENSHOT: CHAT file with syntax highlighting) Capture this: a resources/corpus/reference/ file open in the editor. The screenshot should show colored headers, speaker codes, dependent tiers, dimmed timing bullets, and the code lens utterance counts above @Participants.


See Validation Errors

Open the Problems panel with Cmd+Shift+M. If the file has any issues – missing headers, undeclared speakers, alignment mismatches, malformed timing bullets – they appear here as errors and warnings.

Errors also show as red squiggly underlines in the editor text. Warnings show as yellow underlines. Hover over an underlined region to see the diagnostic message and error code.

Validation runs continuously as you type. Make an intentional edit – for example, delete the terminator (. or ?) at the end of an utterance line – and watch a new diagnostic appear within a fraction of a second.

(SCREENSHOT: Problems panel with diagnostics) Capture this: the Problems panel showing 2-3 diagnostics (mix of errors and warnings), with one error also visible as a red underline in the editor.


Play Audio from a Bullet

If the file has an @Media: header pointing to an audio or video file, you can play segments directly from the editor.

Click on a timing bullet – the bullet_number_number markers that look like 15230_18450 (these are millisecond timestamps). Or place your cursor anywhere on a main tier line and press Cmd+Shift+Enter (Play Media at Cursor).

The extension opens a media panel and plays the audio segment corresponding to that utterance.

To play continuously from the cursor to the end of the file, press Cmd+Shift+/ (Play Media Continuously). The editor cursor follows along, highlighting each utterance as it plays.

(SCREENSHOT: Media playback panel) Capture this: the editor with a CHAT file on the left and the media panel open on the right, showing the audio player with playback controls and a speed slider. The current utterance should be highlighted in the editor.


Hover for Cross-Tier Alignment

Place your mouse over any word on a *CHI: or *MOT: main tier line. A hover popup appears showing how that word aligns across all annotation tiers:

  • %mor – morphological breakdown (part of speech, lemma, affixes)
  • %gra – grammatical relation (SUBJ, OBJ, DET, etc.)
  • %pho – phonetic transcription
  • %sin – sign/gesture annotation

This alignment works bidirectionally: hover a token on %mor to see the aligned main tier word, or hover a %gra relation to see which word it annotates.

Click any word on any tier and all its aligned counterparts across every other tier highlight simultaneously.

(SCREENSHOT: Cross-tier hover popup) Capture this: the cursor hovering over a word on a *CHI: line, with the hover popup showing aligned %mor and %gra information. Optionally also show the document highlight (colored background) on the corresponding %mor token.


Show the Dependency Graph

Place your cursor on an utterance that has a %gra: tier, then press Cmd+Shift+G (Show Dependency Graph).

A panel opens beside the editor showing the grammatical dependency structure of that utterance as a color-coded graph. Arcs are labeled with grammatical relations (SUBJ, OBJ, DET, MOD, ROOT, etc.) and colored by type.

The graph renders locally using bundled Graphviz WASM – no internet connection required. You can export the graph as SVG or PNG using the toolbar buttons.

(SCREENSHOT: Dependency graph panel) Capture this: the editor with a CHAT file on the left and the dependency graph panel on the right, showing a tree with labeled arcs for a simple utterance like “the dog ate the cookie”.


Run a CLAN Analysis Command

Right-click anywhere in the editor to open the context menu. Under TalkBank: Analysis, select Run CLAN Analysis…

A QuickPick menu appears listing all 33 CLAN analysis commands with one-line descriptions. Select freq (word frequency counts).

The results appear in a styled panel with stat cards, tables, and bar charts. You can export the results to CSV using the “Export CSV” button for use in spreadsheets or statistical software.

Other commonly used commands:

CommandWhat It Does
freqWord frequency counts
mluMean length of utterance
mltMean length of turn
dssDevelopmental Sentence Score
kidevalCompare against normative databases
vocdVocabulary diversity

Commands that need extra input (keywords, file paths) prompt you before running. You can also run analysis on an entire directory by right-clicking a folder in the Explorer sidebar and selecting Run CLAN Analysis on Directory…

(SCREENSHOT: CLAN analysis results panel) Capture this: the analysis panel showing freq results – a stat card with total word count, a frequency table, and a bar chart of the top 10 words.


Apply a Quick Fix

If the Problems panel shows an error with a lightbulb icon, the extension can fix it automatically.

Place your cursor on the underlined error and press Cmd+. (Quick Fix). A menu appears with available fixes. For example:

  • xx on a main tier line? The fix replaces it with xxx
  • Missing terminator? The fix appends the appropriate punctuation
  • Undeclared speaker? The fix adds them to @Participants

The extension offers automatic fixes for 21 error codes covering the most common CHAT formatting issues.

(SCREENSHOT: Quick fix menu) Capture this: the cursor on a diagnostic underline, with the quick fix lightbulb menu showing one or two available corrections.


Open the Waveform View

Press Cmd+Shift+W (Show Waveform View) to see a visual representation of the linked audio file.

The waveform panel shows the audio amplitude with colored overlays marking each timed utterance. Click anywhere on the waveform to seek both the audio and the editor cursor to that point. Zoom in and out with the toolbar buttons or mouse wheel.

During playback, the waveform auto-scrolls to keep the current segment visible.

(SCREENSHOT: Waveform panel) Capture this: the waveform panel showing an audio waveform with colored utterance overlays. At least two or three utterance segments should be visible with different colors.


What to Explore Next

You have now seen the core features. Here are some paths to explore further:

  • Quick Reference – complete tables of all keyboard shortcuts, commands, and settings
  • Media Playback – continuous play, rewind, looping, and playback speed control
  • Walker Mode – step through utterances one at a time with Alt+Down / Alt+Up
  • Transcription Mode – transcribe from audio with F4 to stamp timing bullets
  • Coder Mode – load a codes file and step through uncoded utterances
  • Running CLAN Commands – the full set of 33 analysis commands
  • Editing Features – validation, quick fixes, completion, snippets, and formatting

Tutorial: Reviewing Aligned Files

Status: Current Last updated: 2026-03-30 13:40 EDT

This tutorial walks through reviewing a CHAT file that was aligned by batchalign align --bullet-repair. It takes about 5 minutes to review a typical file.

Prerequisites

  • VS Code Insiders with the TalkBank CHAT extension installed
  • A CHAT file aligned with --bullet-repair --review-level=low-confidence
  • The corresponding audio file accessible to the extension

Step 1: Open the aligned file

Open the .cha file in VS Code. You should see:

  • Yellow left borders on lines with %xrev: [?] — these need review
  • The %xalign tiers above them explaining what happened

If you don’t see any yellow lines, the file had no low-confidence decisions. Nothing to review!

Step 2: Start Review Mode

Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P elsewhere) and type “Start Review Mode”.

The extension will:

  1. Parse all %xrev tiers in the file
  2. Show a status bar item: “Review: 0/7” (0 of 7 rated)
  3. Jump to the first unreviewed utterance
  4. If the waveform panel is open, highlight the bullet segment

Step 3: Listen and rate

For each flagged utterance:

  1. Read the %xalign tier — it tells you what happened. For example, boundary_averaged overlap=155ms means two adjacent utterances overlapped and align split the difference.

  2. Play the audio — click the bullet (•12095_12335•) or use Cmd+Shift+Enter to play from the current line.

  3. Listen — does the audio match the text? Does speech start at the bullet’s timestamp?

  4. Rate with a single key:

    • 1 = Good (timing is correct)
    • 2 = Early (audio starts before the speech)
    • 3 = Late (audio starts after the speech)
    • 4 = Wrong (completely wrong location)
    • 5 = Skip (can’t tell)
  5. The extension auto-advances to the next unreviewed utterance.

Step 4: Add notes (optional)

If you want to explain your rating, edit the %xrev line directly. Add text after the rating marker:

%xrev:	[early] starts about 200ms before the actual speech

Or:

%xrev:	[wrong] this is a different speaker's audio entirely

Notes are especially valuable when they describe patterns rather than individual utterances:

%xrev:	[early] all backchannels from INV seem to be early by ~200ms

Step 5: Save and done

When the status bar shows all items rated (e.g., “Review: 7/7”), save the file with Cmd+S. The ratings are stored in the %xrev tiers — they’re part of the CHAT file now.

If you use tb-deploy to push changes, the ratings travel with the file.

Step 6: Stop Review Mode

Open the Command Palette and type “Stop Review Mode”, or just close the file. The status bar indicator disappears.

Keyboard shortcut reference

These shortcuts are only active during Review Mode:

ShortcutAction
Alt+]Next unreviewed utterance
Alt+[Previous unreviewed utterance
1Rate Good
2Rate Early
3Rate Late
4Rate Wrong
5Skip

The number keys only work when the editor is not focused for text input (i.e., when you’re in the command area or the cursor is on a read-only line). If you need to type in the editor, the number keys behave normally.

Tips

  • You don’t need to review every file. Even 5-10 files gives us useful data for tuning the algorithm.

  • Focus on the [?] markers. Utterances without %xrev were high-confidence — the algorithm is pretty sure they’re right.

  • Pattern notes are gold. “All short backchannels are early” is more useful than rating 50 individual backchannels.

  • If unsure, skip. Press 5 to move on. A skipped utterance is better than a wrong rating.

  • Review Mode + Walker Mode work well together. Start Review Mode, then use Alt+Down/Alt+Up (Walker) to step through utterances with automatic playback. When you land on a flagged utterance, rate it with 1-5.

What happens with your reviews

The experiment maintainer collects the ratings using a harvesting script and uses them to:

  • Evaluate --bullet-repair — is it better than CLAN FIXBULLETS?
  • Tune thresholds — how much overlap should be averaged? When should timing be stripped?
  • Identify systematic problems — if all backchannels are wrong, the algorithm needs a backchannel-specific fix
  • Build ground truth — corrected bullets become training data for improving the alignment algorithm

Your free-form notes are especially valuable — they capture observations that ratings alone can’t express.

CLAN: tutorials

Learn by completing a guided task.

Quick Start

Status: Current All CLAN commands follow the pattern:

chatter clan <command> [options] <file.cha>

Your first analysis

Given a CHAT transcript sample.cha:

# Word frequency count
chatter clan freq sample.cha

# Mean length of utterance for the child speaker
chatter clan mlu --speaker CHI sample.cha

# Mean length of turn
chatter clan mlt sample.cha

# Vocabulary diversity (D statistic)
chatter clan vocd sample.cha

Working with directories

Process all .cha files in a directory:

chatter clan freq corpus/

Output formats

Every analysis command supports text (default), JSON, and CSV output:

chatter clan freq --format json sample.cha
chatter clan mlu --format csv sample.cha

Filtering

Restrict analysis to specific speakers, tiers, words, or utterance ranges:

# Only the CHI speaker
chatter clan freq --speaker CHI sample.cha

# Exclude the investigator
chatter clan freq --exclude-speaker INV sample.cha

# Only utterances 10-50
chatter clan freq --range 10-50 sample.cha

# Only within gem boundaries
chatter clan freq --gem "story" sample.cha

See the Filtering chapter for the full guide.

Transforms

Transform commands modify CHAT files and write the result:

# Add fluent output tier
chatter clan flo sample.cha

# String replacement from a rules file
chatter clan chstring rules.cex sample.cha

# Reorder dependent tiers
chatter clan tierorder sample.cha

Format conversion

Convert between CHAT and other formats:

chatter clan elan2chat recording.eaf
chatter clan praat2chat recording.TextGrid
chatter clan chat2srt sample.cha

Batchalign: how-to guides

Choose the task you want to complete.

Install or update Batchalign

These instructions install the command-line application and its backend extras. The installer also installs uv if needed and selects Python 3.11 for Batchalign.

macOS or Linux

Open Terminal, paste this command, and press Enter:

curl -LsSf https://raw.githubusercontent.com/TalkBank/batchalign/main/bootstrap/bootstrap.sh | sh

Windows

Open PowerShell, paste this command, and press Enter:

powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/TalkBank/batchalign/main/bootstrap/bootstrap.ps1').Content)"

Check the installation

Close and reopen your command window, then run:

batchalign --help
batchalign version

You should see a list of commands and version information. Continue to the Quick Start for your first processing task. If the command cannot be found, see Troubleshooting.

Update an existing installation

Run the same installer again. It upgrades the batchalign[all] tool installation with prereleases allowed, matching the repository’s bootstrap scripts.

For an existing uv installation, the equivalent command is:

uv tool install --upgrade --python=3.11 --prerelease=allow 'batchalign[all]>=0.10'

The package on PyPI is named batchalign. It installs the batchalign command. Model files may download separately when you first use a backend; see Model downloads.

Transcribe a recording

Use transcribe to produce a draft CHAT transcript from a recording. You will need to listen to the recording and correct the draft afterward.

  1. Place the recording in a folder you can find, such as recordings.

  2. Run the following for English speech:

    batchalign transcribe recordings/ -o transcripts/ --lang eng
    
  3. The default engine is Rev.AI. If you have not configured it, follow the credential prompt. Rev.AI processes uploaded audio through your account. See Configure Rev.AI.

  4. Wait for the final summary. Open the .cha file in transcripts and review the words, speaker labels, and utterance boundaries against the recording. Generated transcripts include an Unchecked output of ASR model comment.

Use the appropriate three-letter language code, such as spa for Spanish or cmn for Mandarin, instead of eng. --lang is required. Without -o, the transcript is written beside the source recording.

Use a local speech model

To use the Hugging Face Whisper backend:

batchalign transcribe recordings/ -o transcripts/ --lang eng --engine whisper

The first run downloads the selected model. --engine openai selects the local openai-whisper package; it does not mean the hosted OpenAI transcription API. See CLI reference for the engine choices.

Request separate speaker diarization

batchalign transcribe recordings/ -o transcripts/ --lang eng --diarize --num-speakers 2

This adds a separate speaker-assignment stage using the selected diarization backend. Provider speaker labels may already be present without this option.

Add word timings or refine alignment

Use --wor to retain word-level timing output from transcription. For forced alignment of a corrected transcript, run align afterward. Transcription does not automatically run forced alignment or morphosyntactic analysis.

Align a transcript to its recording

Use align after checking the transcript’s words. It adds word-level timing information, including a %wor tier.

  1. Put the CHAT transcript and its recording together. Check that @Media: names the recording and @Languages: identifies the transcript language.

  2. Run:

    batchalign align corpus/ -o aligned/
    
  3. Wait for the final summary. Open the output transcript and check playback against the recording, especially long turns and overlapping speech.

The default forced-alignment engine is wav2vec. When utterance timings are missing, the default timing-recovery engine is Rev.AI; configure its credentials if prompted. Already-timed utterances can avoid that recovery step.

Use local timing recovery

batchalign align corpus/ -o aligned/ --utr-engine whisper

To use Whisper for forced alignment as well:

batchalign align corpus/ -o aligned/ --engine whisper --utr-engine whisper

Use --utr-engine none only when the transcript already has the timings required for forced alignment. Keep different transcript languages in separate alignment invocations: the CLI chooses a backend language from the selected CHAT inputs.

Without -o, source transcripts are updated in place. For model and device options, see CLI reference.

Add morphology and grammar to a transcript

Use morphotag on checked CHAT transcripts to add %mor morphology and %gra dependency tiers. Audio is not required.

  1. Put the CHAT files you want to process in a folder. Check that each file’s @Languages: header identifies its language.

  2. Choose a separate destination folder, then run:

    batchalign morphotag "corpus" -o "tagged"
    
  3. Wait for the final summary. The first use of a language can download Stanza models before tagging begins.

  4. Open a transcript in tagged and inspect its %mor and %gra tiers under the speaker lines. Review the analysis before using it in your workflow.

Without -o, this command updates source files in place. Folder input is recursive. You can also supply individual files or an input list.

Retag existing analysis

By default, the CLI removes existing %mor and %gra tiers from staged inputs before processing. Matching backend results may still come from the result cache. To preserve existing analysis and let the engine skip already-tagged utterances:

batchalign morphotag corpus/ -o tagged/ --keep-existing

Allow retokenization

If you need the backend to retokenize text, request it explicitly:

batchalign morphotag corpus/ -o tagged/ --retokenize

Check the resulting main tiers as well as the analysis tiers.

For options, see CLI reference. For reuse and speed, see Caching and Performance.

Segment a transcript into utterances

Use utseg when a CHAT transcript needs utterance boundaries revised.

batchalign utseg corpus/ -o segmented/

Open the output and check the new boundaries against the conversation. Without -o, the command updates source files in place.

The CLI uses the CHAT utterance-segmentation backend. The --language option is passed to that backend and defaults to en. To enable its Stanza fallback:

batchalign utseg corpus/ -o segmented/ --stanza-fallback

See CLI reference for device options. Backend results use the result cache.

Add translation tiers

Use translate to add translation text to CHAT transcripts:

batchalign translate corpus/ -o translated/ --target eng

The default engine is Google. Open the output and review the %eng tiers. The current output tier name remains %eng even when another target language is selected. Without -o, the command updates source files in place.

To select the local NLLB backend:

batchalign translate corpus/ -o translated/ --target eng --engine nllb

The other CLI engine choices are tencent and aliyun. Provider setup and language coverage depend on the selected backend. See CLI reference for options.

Compare a transcript with a reference

Use compare when you have a transcript and a checked reference transcript.

  1. Place session.cha and session.gold.cha in the same folder. Alternatively, use template.gold.cha as the reference for files in that folder; a per-file .gold.cha takes precedence.

  2. Run:

    batchalign compare corpus/ -o comparisons/
    
  3. Inspect the generated comparison metrics and the command’s summary. Gold files are references, not separate inputs to score. Files without a matching reference are skipped by input pairing.

See CLI reference for input selection.

Configure Rev.AI transcription

Rev.AI is the default speech-recognition backend for transcribe and the default utterance-timing recovery backend for align.

Set up your key

Run an interactive transcription command:

batchalign transcribe recordings/ -o transcripts/ --lang eng --engine rev

When prompted, enter your Rev.AI API key. Interactive configuration stores the key in ~/.batchalign.ini, under [asr] as engine.rev.key.

For automation, supply BATCHALIGN_REV_KEY through your environment or secret manager. Quiet mode suppresses interactive credential prompts. The current CLI has no setup subcommand.

Use the configured account

batchalign transcribe recordings/ -o transcripts/ --lang eng --engine rev
batchalign align corpus/ -o aligned/ --utr-engine rev

These operations can upload audio to Rev.AI. Use an account and recordings appropriate for your organization’s workflow. To select a local transcription backend, see Transcribe a recording.

The integration is implemented in the Python backends and configuration module, not a separate Rust server path.

Process Cantonese recordings and transcripts

For a local Cantonese ASR backend, select funaudio and the required language code yue:

batchalign transcribe recordings/ -o transcripts/ --lang yue --engine funaudio

Cloud choices include tencent and aliyun; their credentials and model options are owned by the corresponding Python backend. Use --engine, not the retired JSON engine-override flag. Review the draft words before subsequent analysis.

For morphology, check that the CHAT header contains @Languages: yue, then run:

batchalign morphotag transcripts/ -o tagged/ --retokenize

Retokenization allows word boundaries to change, so inspect the main tiers and analysis tiers in the output. For alignment, follow Align a transcript; model coverage must match the recording and transcript language.

Prepare model downloads

Use this guide before processing on a new computer or before taking a machine offline.

  1. Install the backend dependencies using the standard installer.
  2. While connected to the internet, run a small representative input with the same command, language, engine, and model you intend to use later.
  3. Let the command complete. A successful installation alone does not prove that all required models have downloaded.
  4. Repeat for other languages or models you plan to use. A Stanza model for one language does not cover every language.
  5. If you need offline operation, test the exact command and input type after disconnecting. Cloud engines still require their external services.

Downloads and disk locations are managed by each backend’s model libraries. Download sizes vary; there is no fixed total size for a Batchalign installation. The current CLI has no model-download or cache-prewarm subcommand.

The Stanza morphosyntax backend refreshes an existing resource catalog once per process, retaining the cached catalog if refresh fails. It loads language pipelines lazily unless a language is pinned at construction. Missing model files can still require downloads. See Performance for the distinction between model files on disk, loaded models in memory, and cached task results.

Manage the result cache

Use these commands when you need to inspect stored results or force a fresh run. They operate on the default result cache, not downloaded models.

Inspect the cache

batchalign cache path
batchalign cache stats

path prints the directory. stats prints its apparent file size and latest modification time, or reports that the cache is absent. It does not count entries.

Clear saved results

Finish or stop processing jobs that use the cache, then run:

batchalign cache clear

Confirm the displayed path when prompted. For an unattended script:

batchalign cache clear --yes

The next processing run recomputes the required backend results and can save them again. Your transcripts, recordings, and downloaded models remain in their own locations.

The current CLI has no per-run cache bypass flag. Python callers can use CacheSpec.bypass() or CacheSpec.refresh().

Tune file concurrency

Use --parallel when you want to change how many files a processing command can work on at once. Start with the default, 8. If memory use is too high, reduce it:

batchalign --parallel 2 morphotag corpus/ -o tagged/

The option goes before the command name. Its minimum is 1. Scripts using the old global --workers option should use --parallel instead.

Compare settings

  1. Choose a representative set of files, including the longer transcripts or recordings you normally process.
  2. Run with --parallel 1 and a separate output folder. Record elapsed time and peak resident memory with your operating system’s monitoring tools.
  3. Repeat with --parallel 4, then 8 if memory permits. Keep the inputs and backend options unchanged.
  4. Compare runs with the same cache state. A cached second run is not evidence that a higher concurrency setting improved inference. To compare uncached CLI runs, clear the result cache between trials after each job finishes.
  5. Keep the smallest setting that gives useful throughput on your workload.

For morphotag, active files share one Stanza batcher. Increasing --parallel does not create additional Stanza replicas. The default utterance dispatch window and backend batch maximum are both 128; there is no CLI batch-size flag. Python callers can set StanzaBackend(batch_size=..., batch_window_ms=...), but that does not change the Rust per-file dispatch window.

The Python Pipeline and recipe APIs still name file concurrency workers. For the scheduling and memory model, see Performance.

Review generated transcripts and timings

  1. Keep the original transcript or recording and write processing output to a separate folder with -o.
  2. Read the command’s final summary and inspect failed or skipped files before relying on the output set.
  3. For transcription, listen to the recording and correct words, speaker labels, and utterance boundaries. ASR output is marked as unchecked.
  4. For alignment, check playback at utterance and word boundaries, especially where speakers overlap or the recording is noisy.
  5. For morphology, inspect %mor and %gra against the intended words and language. If retokenization was enabled, review main-tier changes too.

Older files may contain %xalign decision notes or %xrev review markers from other processing versions. Their presence does not prove that the current run created them, and their absence does not prove an output is correct. The current CLI does not expose the old --review-level controls.

Use provenance comments to identify a producing build and task. A file modification timestamp alone cannot establish which utterances were edited or whether a previous review remains valid.

Troubleshoot a processing run

The command cannot be found

Close and reopen Terminal or PowerShell after installation, then try batchalign --help. If it is still unavailable, repeat the installation steps and check that the installer finished.

An option or command is unknown

Run batchalign --help and batchalign COMMAND --help using your installed version. The current global concurrency flag is --parallel; engine selection uses command-specific --engine options. The CLI does not expose the older serve, setup, logs, doctor, or bench interfaces.

A processing command appears to be waiting

The first use of a backend can download models. An interactive cloud backend may also be waiting for credentials. Keep the command window open and read its latest message. Use batchalign -v COMMAND ... for more detail or --plain before the command to avoid live terminal rendering. See Model downloads.

The transcript cannot be parsed

Read the error’s file location and CHAT diagnostic. Fix the indicated transcript and rerun a single file before processing the folder again. For morphology, check @Languages: and the CHAT header structure. Use the CHAT format reference to interpret markup.

Alignment cannot find the recording

Check the transcript’s @Media: header, keep the referenced recording beside the CHAT file, and confirm that the recording is readable. Use a small single-file run to check the pairing. The current local CLI does not use the old server’s remote media mappings.

The computer runs out of memory

Lower file concurrency, for example:

batchalign --parallel 1 morphotag corpus/ -o tagged/

For commands that expose it, --force-cpu selects CPU inference; it is not a morphotag option. Long transcripts and model batch sizes still affect memory at --parallel 1. See Performance.

A repeat run gives the same unexpected result

Check and correct the input first. If you need to rule out saved backend results, finish active processing and clear the result cache, then rerun. Removing model files is a separate operation and is not required to clear saved results.

Report a reproducible problem

Include the output of batchalign version, your operating system, the exact command with credentials removed, and the final error text. Attach a small input that reproduces the problem if you can share it. Say whether it also happens with a fresh result cache. Report issues in the Batchalign repository.

Run the desktop application from source

The Batchalign desktop application lives in apps/batchalign/batchalign-gui/. It uses React and Tauri, with a bundled Python HTTP-service sidecar.

From a development checkout with the repository build tools installed:

just batchalign gui dev

The Bazel dependency graph builds and stages the sidecar. To create a desktop bundle, use just batchalign gui build.

In the application, drop an input folder onto the window or choose open folder, add a command, review its language and engine settings, choose the output location, and start the batch. Folders are searched recursively. The app displays one folder at a time. Use choose… to replace it or close beside the folder chooser to return to the drop-folder screen. The speaker command and Speaker recognition option identify speaker turns. The elapsed timer continues updating while a model is busy.

For transcription, enter a valid three-letter ISO 639-3 language code before choosing an engine. Autocomplete searches codes and language names; Chinese uses zho. No ASR engine is selected automatically. Recommendations appear in a gray box, with the other engines listed below: CHAT-Whisper for English; Aliyun for adult Cantonese and FunASR/SenseVoice for child Cantonese; Paraformer-zh for Mandarin. Google GenAI, OpenAI Whisper, Qwen3, and Rev.AI are the general choices. Rev.AI is always offered after a valid language is entered; other engines follow service availability. Recommendations include cloud and local choices, with cloud engines first. Rev.AI supplies the cloud recommendation when there is no language-specific one; OpenAI Whisper or Qwen3 supplies a local fallback when available. The Recognition engine defaults to pyannoteAI (cloud). The GUI omits Malayalam Wav2Vec, Qwen, Tencent Cloud ASR, and Hugging Face Whisper; their CLI/API availability is unchanged.

For ordinary command-line use, start with the Quick Start. The CLI does not require the desktop service to be running.

Run the HTTP service

Use this guide for API development or a GUI integration. You do not need an HTTP service to process files with the CLI.

  1. Install the api extra in your Batchalign environment. The standard bootstrap installation includes the all extra, which includes API dependencies.

  2. Start the service in your command window:

    batchalign daemon --host 127.0.0.1 --port 8765
    
  3. Open http://127.0.0.1:8765/docs for the API schema, or request http://127.0.0.1:8765/capabilities to discover recipes and backends.

  4. Stop the foreground service with Ctrl-C when finished.

Keep the default one Uvicorn worker: the job registry is process-local. The daemon --workers option concerns HTTP processes; it is unrelated to the CLI’s global --parallel option. For supervised operation, run the foreground command under your service manager.

For local path-based API inputs, the service requires BATCHALIGN_API_ALLOW_PATHS=1. Enable it only for a trusted local integration: those paths are interpreted on the machine running the service. The desktop sidecar sets up its own local connection.

The service has no serve start/serve stop command group or server.yaml configuration. See CLI reference for supported options.

Update commands and integrations

  1. Update the batchalign package using the installer. Check batchalign version before comparing behavior.
  2. Replace the former global --workers N flag with --parallel N. It controls active files, not model-process replicas.
  3. Use command-specific --engine options for ASR and FA. Check the actual choices with batchalign COMMAND --help; old engine names are not all interchangeable.
  4. Use -o/--out for a destination. Processing commands accept multiple paths and -i/--input-list/--file-list.
  5. Remove old automatic-server, fleet, benchmark, and cache-override flags. Ordinary CLI processing now runs in process. The separate daemon is an HTTP service for GUI/API integrations.
  6. Update cache assumptions: text and audio backend results use LMDB. See Caching before comparing repeat-run performance.
  7. For Python integrations, use the current Pipeline, recipe, input, and backend interfaces. The old BA2 document/pipeline classes are not drop-in equivalents.

Run a small copied input through the revised workflow before processing a corpus. The CLI reference lists the supported surface.

Build Batchalign from source

Use the repository’s just recipes from the checkout root. They invoke Bazel and materialize the Rust extension, Python dependencies, and generated protocol types required by each target.

just batchalign build debug
just batchalign test debug
just batchalign cli --help

just batchalign cli is the authoritative development CLI entrypoint. It restores the invocation directory before running the Bazel Python binary, so relative input paths behave as they do in an installed CLI.

Build release artifacts

just batchalign wheel
just batchalign sidecar
just batchalign gui build

wheel produces the host-platform Python wheel in python/target/wheels/. sidecar builds the PyApp-bundled HTTP service. The GUI bundle embeds that sidecar. Cross-platform wheel builds run in the GitHub Actions matrix.

Run checks

just batchalign test
just batchalign pytest
just batchalign lint
just batchalign gui test
just docs build

See Testing for scope. just batchalign versions shows the version sources; just --list batchalign lists recipes. Use the repository wrappers instead of a separate editable-install procedure for ordinary development.

Test a Batchalign change

Run commands from the repository root using just.

CheckCommandScope
Product testsjust batchalign testCore Rust targets, explicitly named engine tests, Python pytest target
Python testsjust batchalign pytestPython suite through Bazel; extra arguments are forwarded to pytest
Type/lint checksjust batchalign lintMypy, followed by Ruff when available
GUI integrationjust batchalign gui testPlaywright against the Bazel-built PyApp service, with Tauri APIs stubbed
Book and linksjust docs buildmdBook HTML and configured linkcheck renderer

For a focused Python case:

just batchalign pytest -k cli_input_list

For an integration check, use the real development entrypoint:

just batchalign cli --parallel 2 morphotag /path/to/fixtures -o /tmp/tagged-check

Choose inputs that exercise the behavior being changed. For inference changes, compare semantic outputs as well as elapsed time and RSS; record cache state and model settings. Tests with fake backends check orchestration, not model accuracy.

The product recipe explicitly includes engine unit, cache-smoke, and pipeline-smoke targets that a broad wildcard can omit because of manual tags. The wheel CI matrix also checks native platform packaging and Python 3.10 imports.

GUI checks need a browser installed for the pinned Playwright version. On a cold machine, the bundled runtime may need first-launch setup before it can serve requests. Distinguish setup failures from frontend assertions when diagnosing results.

Add a processing command

  1. Identify the input/output types and task sequence. Reuse a recipe in python/batchalign/recipes.py or add one that constructs Pipeline.
  2. Implement a command module under python/batchalign/cli/. Use resolve_inputs for multiple paths and file lists, and cli_options(ctx) for global settings.
  3. Pass opts.parallel as the recipe’s workers value. Use Interface for progress and the shared output helpers for outcomes where their contract fits.
  4. Register the module in _LAZY_SUBCOMMANDS in cli/__init__.py. A file under cli/hidden/ is not automatically a public command.
  5. Test help, argument validation, output destinations, and failures. Exercise a real invocation through just batchalign cli.
  6. Add a task-focused how-to page and update the CLI reference.

morphotag.py demonstrates staged CHAT preprocessing; convert.py demonstrates output collision checks. Use the example that matches the new command’s behavior. See Pipeline architecture for the runtime.

Add an inference backend

  1. Choose the task interface in python/batchalign/backends/base.py. Implement the corresponding backend marker, a stable name, its BatchPolicy, and call behavior. Start from an existing backend for the same task.
  2. Return the typed output expected by the core proto/ definition. Preserve input/output correspondence and return one output per input in a batch.
  3. Include output-affecting model/configuration changes in backend identity when they are not already in the typed input. The engine uses that name in cache keys.
  4. Export the backend through the package’s lazy import surface where appropriate, add its dependencies to the relevant python/pyproject.toml extra, and supply it to a recipe. For a CLI engine choice, update that command’s enum and builder.
  5. Test protocol shape and failure behavior with deterministic fixtures, then perform a small real-backend smoke run through just batchalign cli.
  6. Update the command reference and any backend-specific instructions.

Batching and result caching are engine responsibilities. Do not add a worker process or a second cache solely to integrate an ordinary Python backend. A genuinely new task also needs core input/output types, a runner, and routing registration; inspect the existing task family before extending those contracts.

See Source map, Python API, and Testing.

Add or change language support

  1. Identify the command and backend you are extending. Language-code recognition, model availability, and useful analysis quality are separate checks.
  2. Check python/batchalign/lang.py and the backend’s vendor-code conversion. For Stanza, inspect backends/morphosyntax/ud/lang.py and stanza.py.
  3. Test representative text or audio with the selected model. Include the language’s contractions, script, punctuation, and conversational forms.
  4. For ASR, check number expansion and cleanup in the Rust core asr/ modules. For morphology, check token reconciliation and typed tier alignment as well as the model’s raw output.
  5. Add regression fixtures at the responsible layer and run the appropriate tests, followed by a small CLI integration run.
  6. Update language selection and the language-specific reference. Record measured quality with model versions and corpus scope instead of inferring support from a language list.

Handle a model-library defect

  1. Reduce the failure to a small reproducible input and record library/model versions and configuration.
  2. Determine whether the error originates in the model, a vendor adapter, token reconciliation, or CHAT result application.
  3. Add a regression at that boundary. Test a nearby valid case so a workaround does not merely suppress a class of outputs.
  4. Implement a narrowly scoped correction where the required information is available. Stanza token processing/rendering lives in the Python backend; typed CHAT injection lives in the Rust morphology runner.
  5. Re-run the affected command on representative inputs and compare semantic output. Record known limits rather than claiming a universal linguistic fix.
  6. If behavior affects cached outputs, update backend identity as appropriate; compiled build identity also participates in result-cache keys.

Historical model probes are useful evidence, but rerun a relevant case against the current pinned environment before treating an old mitigation table as active.

Diagnose a pipeline failure

  1. Reproduce with one small input through just batchalign cli, using a separate output directory. Preserve the original failing input.
  2. Add global -v or -vv and capture the command’s error text. Use --plain when saving terminal output to a file.
  3. Determine whether failure occurs during input parsing, backend construction, backend inference, result application, or output writing. Use the source map to find the owning layer.
  4. Compare cached and fresh behavior when relevant using the Python cache policy or the CLI cache-management command after active processing stops.
  5. Add a focused regression case at the layer that owns the defect. A fake backend can isolate scheduler and result-injection behavior from model behavior.

The current CLI does not expose the retired --debug-dir, server job logs, or worker-protocol trace commands. Inspect Outcome.error, progress callbacks, and the Python/Rust logging surfaces used by the current pipeline. Include version, command, input shape, and cache state in a reproducible report.

Release the Python package

  1. Set the Python package version in python/pyproject.toml. The repository wrapper refreshes python/uv.lock; check the resulting diff. Rust workspace and desktop versions have separate owners.
  2. Run the relevant checks, inspect their results, and record any known baseline failures separately from regressions.
  3. Commit the intended release state and push it to the branch CI will build.
  4. Dispatch .github/workflows/publish-pypi.yml with tag exactly matching the Python version. The workflow builds native macOS ARM/Intel, Linux ARM/x86-64, and Windows x86-64 wheels.
  5. Verify that every artifact came from the intended commit, has the expected version/platform tag, and passes bazel/python/verify_wheel.py and twine check.
  6. Publish through the configured channel. The workflow’s publish=true route requires PyPI trusted publishing. If that is not configured, build with publish=false, download all five wheel artifacts, and upload them using authenticated Twine.
  7. Verify PyPI’s file list and hashes against the artifacts you uploaded.

Do not publish local development wheels in place of the matrix artifacts. The exact workflow and package metadata are the release contract; historical batchalign3 distribution and Rust-server release instructions do not apply.

Chatter: how-to guides

Choose the task you want to complete.

Installation

Status: Current Last updated: 2026-06-01 00:50 PDT

chatter runs on Windows, macOS, and Linux. Pre-built binaries are available from the GitHub Releases page; choose the plain vX.Y.Z TalkBank core release entry and its chatter-v... assets (VS Code preview releases use vscode-vX.Y.Z tags in the same repository). To build from source, follow the instructions below.

Prerequisites

  • Rust (stable) — install via rustup (supports Windows, macOS, Linux)

If you are only building the chatter CLI from source, Rust is sufficient. The extras below are only needed when you work on the grammar or generated artifacts:

  • Node.js — required for tree-sitter grammar generation
  • tree-sitter CLI — cargo install tree-sitter-cli

Installing chatter (CLI)

The chatter CLI is the primary tool for working with CHAT files. It provides validation, format conversion, and batch processing.

From Source

Clone the repository:

mkdir -p ~/talkbank && cd ~/talkbank
git clone https://github.com/TalkBank/batchalign.git

Build and install:

cd ~/talkbank/batchalign
cargo install --path crates/chatter/chatter-cli

This installs the chatter binary to ~/.cargo/bin/ (macOS/Linux) or %USERPROFILE%\.cargo\bin\ (Windows).

Verify Installation

chatter --version
chatter --help

Building the Libraries

If you’re developing with the Rust crates directly:

cd ~/talkbank/batchalign
cargo build           # Build all crates
cargo test            # Run all tests
cargo clippy          # Lint check

See the Makefile targets for additional commands.

Directory Layout

Everything lives in a single repository:

~/talkbank/
└── batchalign/             # This repository
    ├── grammar/            # Tree-sitter grammar
    ├── crates/             # All Rust crates (talkbank-* and batchalign-*)
    ├── resources/spec/     # CHAT specification (source of truth)
    ├── resources/corpus/   # Reference corpus
    ├── apps/vscode-extension/   # VS Code extension
    ├── apps/chatter/       # Chatter Desktop (Tauri, experimental)
    ├── apps/batchalign/    # Batchalign Desktop + dashboard (Tauri, experimental)
    ├── python/             # batchalign Python package
    └── book/               # TalkBank Toolchain mdBook

The CLI, grammar, crates, Batchalign pipeline, and all related tools are in this single repository.

Migrating from CLAN to talkbank-tools

Status: Current Last updated: 2026-05-11 16:38 EDT

This page is for two audiences:

  • users moving day-to-day workflows from legacy CLAN binaries to chatter
  • developers moving implementation work from older CLAN- or Java/Python-based tooling to the Rust rewrite

It is not a promise of character-for-character equivalence with every historical CLAN binary. The goal of the rewrite is a stable, documented Rust toolchain for CHAT parsing, validation, normalization, JSON interchange, and CLAN-style analyses.

User Workflow Crosswalk

Legacy workflowtalkbank-tools workflowNotes
Run CHECK on one filechatter validate file.chaValidation uses stable Rust error codes and richer diagnostics
Run CHECK on a corpuschatter validate corpus/Recursive directory validation with cache support
Normalize with fixit / indent / longtierchatter normalize file.chaNormalization writes to stdout unless -o/--output is used
Convert to an interchange formatchatter to-json file.chaJSON output is defined by the published schema
Convert back from interchange formatchatter from-json file.jsonUses the same AST/serializer as the Rust toolchain
Run CLAN-style analysis commandschatter clan ...One entry point, with text/json/csv/clan output modes where supported

Legacy Flag Translation

chatter accepts both legacy CLAN syntax (+t*CHI, -t*MOT, +s"word", +z25-125) and the modern explicit syntax (--speaker CHI, --exclude-speaker MOT, etc.). Both work identically — you do not have to retrain muscle memory. Under the hood, a pre-clap pass in clan_core::clan_args::rewrite_clan_args rewrites any +flag/-flag tokens that follow the clan subcommand into their --flag equivalents before argument parsing proper begins. Anything the rewriter does not recognise passes through unchanged, so mistyped legacy flags surface as ordinary clap errors rather than silent misinterpretation.

The table below is the complete list of legacy flags the rewriter currently handles. If a flag does not appear here, it is not translated; use the modern --flag spelling.

Legacy CLANModern chatterPurpose
+t*CHI--speaker CHIInclude speaker
-t*MOT--exclude-speaker MOTExclude speaker
+t%mor--tier morInclude dependent tier
-t%gra--exclude-tier graExclude dependent tier
+t@ID="eng|*|CHI|*"--id-filter eng|*|CHI|*Filter by @ID header pattern
+s"word" or +sword--include-word wordInclude word in search / frequency
-s"word" or -sword--exclude-word wordExclude word
+glabel--gem labelRestrict to a named gem
-glabel--exclude-gem labelSkip a named gem
+z25-125--range 25-125Utterance index range
+r6--include-retracingsInclude retraced material (MLU, FREQ)
+dN--display-mode NNumeric display mode — currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:101 produces --display-mode, but no clap field consumes it. Per-command N tables are CLAN-historical and need source-grounded specs before the flag can be honoured; tracked in docs/superpowers/plans/2026-05-11-clan-rewriter-honor-three-flags.md Phase 3.
+k--case-sensitiveCase-sensitive matching — currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:104 produces --case-sensitive, but no clap field consumes it, so the flag fails parsing. Word matching is case-insensitive today.
+fEXT--output-ext EXTOutput file extension — currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:107 produces --output-ext, but no clap field consumes it. Phase 2 of the rewriter-honor plan adds the clap field with conflicts_with -o.
+wN--context-after NKWAL trailing-context lines
-wN--context-before NKWAL leading-context lines
+u(no-op)Merge speakers — default behaviour, dropped silently

CHECK (chatter clan check …) has its own context-sensitive overloads. Inside the check subcommand the rewriter recognises these additional forms:

Legacy CHECK flagModern chatterPurpose
+cN--bullets NBullet-check level
+e--list-errorsList all known error codes
+eN--error NReport only a specific error
-eN--exclude-error NSuppress a specific error
+g1(no-op)Prosodic-delimiter checks are always on
+g2--check-targetRequire CHI to have Target_Child role
+g3(no-op)Word-level checks run via the parser
+g4--check-id trueWarn on missing @ID tiers
+g5--check-unusedReport unused speakers
+u (in check)--check-udValidate Universal Dependencies features

+g1–+g5 are only rewritten as CHECK generics when the check subcommand is present in the argument list; in every other command, +g… remains gem filtering.

Common CLAN Workflows in chatter

Side-by-side examples for the most frequent CLAN tasks. Either the legacy or the modern form works — pick whichever is easier to read.

Frequency analysis for one speaker:

# Legacy CLAN
freq +t*CHI file.cha

# chatter, modern syntax
chatter clan freq file.cha --speaker CHI

# chatter, legacy syntax (rewritten internally)
chatter clan freq file.cha +t*CHI

MLU for a speaker over a utterance range:

# Legacy
mlu +t*CHI +z25-125 file.cha

# chatter
chatter clan mlu file.cha --speaker CHI --range 25-125

KWAL search with context window:

# Legacy
kwal +t*CHI +s"cookie" -w2 +w3 file.cha

# chatter
chatter clan kwal file.cha \
    --speaker CHI --include-word cookie \
    --context-before 2 --context-after 3

FREQ with multiple filters:

# Legacy
freq +t*CHI +t%mor -s"the" file.cha

# chatter
chatter clan freq file.cha --speaker CHI --tier mor --exclude-word the

CHECK restricted to a single error code:

# Legacy
check +e6 file.cha

# chatter
chatter clan check file.cha --error 6

Gem-scoped analysis:

# Legacy
freq +gstory +t*CHI file.cha

# chatter
chatter clan freq file.cha --gem story --speaker CHI

What’s Different

Practical changes to expect when you swap a legacy CLAN tool for chatter:

  • Output formats. chatter commands accept --format text|json|csv|clan where supported; legacy CLAN produced only its own text layout. Build pipelines on --format json rather than parsing text.
  • Error codes. Diagnostics use the stable E### / W### system documented in book/src/operations/errors/, not CLAN’s varied error numbering.
  • Cache. chatter validate memoises clean results in the OS cache directory and reuses them on subsequent runs. Pass --force to bypass the cache.
  • Unicode. Full UTF-8 throughout the pipeline; CLAN had several encoding quirks that chatter does not reproduce.
  • Determinism. The same input always produces the same output. A handful of legacy CLAN commands had timing- or filesystem-order-dependent behaviour that chatter intentionally does not preserve.
  • JSON interchange. chatter to-json / chatter from-json roundtrip through a published JSON schema. CLAN had no stable interchange format.
  • Platform. One binary builds and runs on Windows, macOS, and Linux. No more platform-specific CLAN binaries or encoding quirks.
  • Unknown legacy flags fail loudly. If the rewriter does not recognise a +x or -y token, it is passed through to clap unchanged, which then reports a normal argument error. There is no silent acceptance of unsupported flags.

Important Behavioral Differences

  • chatter clan is one command family, not a directory of separate CLAN executables.
  • JSON conversion is first-class and schema-backed.
  • Validation diagnostics use Rust error codes like E301 or W602, not CLAN’s historical numbering.
  • Cache behavior is explicit. validate reuses cached clean results unless --force is passed.
  • Normalization does not overwrite files unless you give an output path yourself.
  1. Start by replacing corpus-wide CHECK runs with chatter validate.
  2. Switch any JSON/export glue to chatter to-json and chatter from-json.
  3. Migrate CLAN analysis invocations to chatter clan ... and verify any output-format assumptions.
  4. Rebaseline automation on Rust error codes and structured JSON output rather than parsing legacy text output.

Developer Crosswalk

Legacy implementation styleRust rewrite replacement
String-oriented parsing and repairTyped AST/model in talkbank-model
Tool-specific parser logicShared parser/transform crates
Ad-hoc validation checksStable validation rules and error codes
CLAN-only output assumptionsShared clan-core command implementations plus JSON/CSV support
Loosely coupled scriptsWorkspace crates plus integration tests and corpus gates

What to Change in Developer Workflows

  • Build on the Rust crates instead of editing CLAN-era text manipulation logic.
  • Treat the AST, serializer, and validator as the source of truth for current behavior.
  • Add or update integration tests when changing CLI-visible behavior or corpus-wide semantics.
  • Document behavior changes in this book when they affect users or downstream integrations.

Scope of Compatibility

talkbank-tools aims for practical migration, not blind historical reproduction:

  • legacy data should continue to parse where feasible
  • CLAN-style analyses should document any deliberate divergence
  • new documented public contracts are the Rust CLI, preview source-first Rust crates, JSON Schema, and documented diagnostics

VS Code Extension

Status: Current Last updated: 2026-05-11 18:58 EDT

Support tier: Public preview. VSIX bundles are published on GitHub Releases, not yet available from the VS Code Marketplace.

A full-featured CHAT editor for VS Code built on the Rust-powered chatter-lsp language server. Edit, validate, analyze, transcribe, and play back audio — all inside your editor. Leverages the same CHAT engine as the chatter CLI with live real-time feedback.

Platform support: Windows, macOS, and Linux

Installation

The extension is located in apps/vscode-extension/. Two install paths, depending on what you want to do.

End users — install a released VSIX. Pre-built, platform-specific VSIX bundles (each ships with the matching chatter-lsp binary) are published on GitHub Releases. Download the .vsix for your platform, then in VS Code: Extensions sidebar → “…” menu → Install from VSIX… and pick the downloaded file.

Developers — run from source. Open the apps/vscode-extension/ folder in VS Code, install dependencies, compile, and launch the Extension Development Host:

cd vscode
npm install
npm run compile
# Then press F5 in VS Code to launch the dev host

The dev host runs the locally-compiled extension and talks to a chatter-lsp binary discovered via the resolver described in the Configuration and Architecture sections below.


Validation & Diagnostics

Live Validation

Errors and warnings appear inline as you type — the same error codes as chatter validate (currently 191; verify with rg -c '#\[code\("[EW]' crates/core/talkbank-model/src/errors/codes/error_code.rs), with source-level precision:

  • Squiggly underlines on the exact error location
  • Problems panel integration with error codes and descriptions
  • Validates on every keystroke (250ms debounce), plus immediately on open and save
  • Filter by severity: all (default), errors only, or errors and warnings

Quick Fixes

Press Cmd+. (Mac) / Ctrl+. (Windows/Linux) to auto-fix common issues. Supports 21 error codes including:

  • Add missing terminators, fix xx → xxx, insert undeclared speakers
  • Swap reversed timestamps, close unclosed brackets, delete empty utterances
  • Fix consecutive stress markers, consecutive commas, trailing-off markers

Inlay Hints

Inline annotations show alignment mismatches (e.g., [alignment: 3 main ↔ 2 mor]) and timing durations directly in the editor. Toggle with talkbank.inlayHints.enabled.

Validation Explorer

A tree view in the Explorer sidebar for bulk validation of entire directories. Runs the chatter validate CLI under the hood, caches results, and displays errors as navigable tree nodes. Right-click any folder to validate.


Cross-Tier Alignment Visualization

Hover

Hover over any word on the main tier to see its aligned morphological breakdown (%mor), grammatical relations (%gra), phonological form (%pho), and aligned items from %sin — all in a rich Markdown popup.

Hover works bidirectionally: hover a %mor item to see the aligned main-tier word.

Highlight Linking

Click any word on the main tier and all aligned items on dependent tiers light up. Click a %mor or %gra item and the corresponding main-tier word highlights. Bidirectional across %mor, %gra, %pho, %sin, %mod.

Dependency Graph

Cmd+Shift+G (Mac) / Ctrl+Shift+G (Windows/Linux) renders the %gra dependency tree for the utterance at your cursor. Words appear as labeled nodes with colored edges by relation type (subject, object, root, etc.). Rendered via Graphviz WASM — no internet required.

Toolbar: zoom, fit to window, export as SVG or PNG.


Media Playback

Single Segment

Cmd+Shift+Enter (Mac) / Ctrl+Shift+Enter (Windows/Linux) plays the timing bullet (•beg_end•) nearest the cursor. Audio/video resolves from the @Media: header.

Continuous Playback

Cmd+Shift+/ plays all segments from cursor to end of file. The editor cursor tracks the currently-playing utterance, auto-advancing through the transcript.

Playback Controls

ActionShortcutDescription
Play segmentCmd+Shift+EnterPlay nearest bullet
Continuous playCmd+Shift+/Play from cursor to EOF
Stop—Stop playback
RewindF8Jump back (default 2s, configurable 0.5–30s)
Loop segmentShift+F5Toggle looping current segment
SpeedToolbar slider0.25x–2x playback speed

Waveform View

Cmd+Shift+W opens an interactive waveform panel. Each timing bullet appears as a colored rectangle over the audio waveform. Click anywhere on the waveform to seek. Zoom in/out with buttons, slider, or mouse wheel. Auto-scrolls during playback.


Transcription Mode

A dedicated workflow for transcribing audio into CHAT format.

  1. Start: talkbank.startTranscription — opens the media panel with the full audio file
  2. Play: Audio plays from your cursor position
  3. Stamp: Press F4 to insert a timing bullet at the current playback position and open a new *SPEAKER: line
  4. Rewind: Press F8 to jump back and re-listen
  5. Stop: talkbank.stopTranscription — exits transcription mode

Configure the default speaker code (talkbank.transcription.defaultSpeaker, default: “CHI”) and rewind duration.


Walker Mode

Step through utterances one at a time with synchronized audio playback.

ActionShortcut
Next utteranceAlt+Down
Previous utteranceAlt+Up

Each step moves the editor cursor and optionally plays the corresponding audio segment. Configure auto-play, loop count (0 = infinite), pause between segments, and walk length.


CLAN Analysis Commands

Run any of the 34 CLAN analysis commands directly from VS Code — no terminal needed (see status-matrix.md for the authoritative list).

Right-click a .cha file or folder → Run Analysis → pick a command from the list:

  • Frequency/Count: freq, mlu, mlt, wdlen, wdsize, maxwd, freqpos, timedur, vocd, …
  • Syntax: complexity, dss, ipsyn, sugar, …
  • Search: kwal, combo, …
  • Assessment: kideval, eval, eval-d

Results appear in a dedicated panel with styled tables, stat cards, bar charts, and a CSV export button.

KidEval / Eval / Eval-D

Dedicated assessment panels for child language evaluation, general evaluation, and dementia assessment. Select language and normative database, filter by age and gender, and compare z-scores against population means.


FeatureShortcutDescription
OutlineCmd+Shift+OTwo-level tree: speakers → utterances
Go to DefinitionF12 / Cmd+ClickSpeaker code → @Participants; dependent tier → aligned main-tier word
Find All ReferencesShift+F12All occurrences of a speaker code
Rename SpeakerF2Renames across @Participants, @ID, and all main-tier lines
Workspace SymbolsCmd+TCross-file search by speaker and utterance
Scoped FindContext menuSearch within specific tiers (%mor, %gra, etc.) with optional speaker filter
FoldingStandardUtterance blocks (main + dependent tiers) and header blocks fold as units
Smart SelectionShift+Ctrl+Right/LeftExpand/shrink by syntactic unit: word → content → tier → transcript

Editing

Code Completion

  • Speaker codes (from @Participants)
  • Dependent tier prefixes (%mor, %gra, %pho, etc.)
  • Header names (triggers on @)
  • Bracket annotations (triggers on [)
  • Postcode punctuation

Snippets

8 built-in CHAT snippets: file header, @Participants, @ID, main tier, %mor, %gra, @Comment, Gem (@Bg/@Eg).

Participant Editor

talkbank.editParticipants opens a table editor for all @ID headers. Edit the 10 pipe-delimited fields (language, corpus, speaker, age, sex, group, SES, role, education, custom) in a form view. Changes are written back as canonical @ID lines.

Linked Editing

Edit a speaker code and all matching codes in the file update simultaneously.

Auto-Formatting

Format document rewrites to canonical CHAT format. On-type formatting auto-indents after tier prefixes.


Special Characters

Compose Mode

Type CA (Conversation Analysis) and CHAT special symbols without memorizing Unicode:

ShortcutModeSymbols
Cmd+Shift+1CA symbols30+ conversation analysis markers
Cmd+Shift+2CHAT symbols20+ CHAT annotation marks

Press a key to insert the symbol. Press Escape to cancel. Status bar shows the active mode.


Coder Mode

Annotate transcripts with coding schemes from .cut files.

  1. Start coder mode (context menu → “Start Coder Mode”)
  2. Load a .cut codes file with hierarchical codes
  3. Step through uncoded utterances (Cmd+Enter / Ctrl+Enter)
  4. Insert codes from a hierarchical picker (Cmd+Shift+C / Ctrl+Shift+C)
  5. Codes appear as %cod: tiers

Progress tracking shows how many utterances remain uncoded.


Speaker Filtering

Context menu → “Filter by Speaker…” opens a multi-select picker. Choose one or more speakers to create a filtered read-only view showing only those speakers’ utterance blocks, with all file headers preserved.

Code Lens

Utterance counts per speaker appear above the @Participants header (e.g., CHI: 42 utterances).


Additional Features

FeatureDescription
Syntax highlightingSemantic tokens via tree-sitter: headers, speaker codes, tiers, timing, markers, errors
Timing bullet displayConfigurable: dim (35% opacity, default), hidden, or normal visibility
Show elicitation pictureDisplay images referenced by %pic: tiers or same-name files
Open in CLANSend the current file to the CLAN application (macOS/Windows, requires CLAN installed)
Cache managementStatus bar shows cache statistics; commands to clear per-file or all cached validation results
Document links@Media: headers are clickable — opens the referenced media file

Configuration

SettingDefaultDescription
talkbank.lsp.binaryPath""Path to the chatter-lsp binary (empty = auto-detect: bundled extension binary, then PATH, then extension target dir)
talkbank.inlayHints.enabledtrueShow alignment and timing inlay hints
talkbank.validation.severity"all"Filter: "all", "errorsOnly", "errorsAndWarnings"
talkbank.bullets.display"dim"Bullet visibility: "dim", "hidden", "normal"
talkbank.media.defaultSpeed100Playback speed % (25–200)
talkbank.transcription.defaultSpeaker"CHI"Default speaker for new utterances
talkbank.transcription.rewindSeconds2Rewind duration in seconds (0.5–30)
talkbank.walker.autoPlaytrueAuto-play when stepping through utterances
talkbank.walker.loopCount1Loops per segment (0 = infinite)
talkbank.walker.pauseSeconds0Pause between segments (0–10s)
talkbank.walker.walkLength0Utterances to walk (0 = all remaining)

Architecture

The extension launches the dedicated chatter-lsp binary over stdio using the Language Server Protocol. (chatter-lsp is a separate Rust crate at crates/chatter/chatter-lsp/; the VS Code extension’s executable resolver at apps/vscode-extension/src/executableService.ts looks for it in the bundled extension binary, then on PATH, then under the extension’s target directory.) The LSP server provides:

  • Incremental parsing via tree-sitter
  • Full validation (currently 191 error codes; see live count above)
  • Cross-tier alignment computation
  • CLAN analysis command execution
  • Document formatting and symbol extraction

Interactive features (media playback, waveform, graphs, analysis panels, assessment tools) run as VS Code Webview panels with HTML/CSS/JS. The Graphviz renderer uses WASM — no external dependencies.

Development

cd vscode
npm install          # Install dependencies
npm run compile      # Build the extension
npm run watch        # Watch mode for development
npm test             # Run tests
npm run lint         # ESLint check

To debug: open the apps/vscode-extension/ folder in VS Code and press F5 to launch the Extension Development Host.

Chatter Desktop (Experimental)

Status: Current Last updated: 2026-06-01 00:50 PDT

⚠️ IMPORTANT: Chatter Desktop is an experimental in-repo evaluation app, NOT a supported end-user surface. Use the chatter CLI for production CHAT validation, or use the VS Code extension for interactive editing with live validation.

When to use Chatter Desktop

Chatter Desktop (apps/chatter/chatter-gui/) is intended only for:

  • Contributors and maintainers evaluating the native GUI implementation
  • Local development and testing of the desktop surface
  • Experimental internal workflows

For supported end-user workflows:

  • Validate CHAT from the command line: use chatter validate
  • Edit CHAT with live diagnostics: use the VS Code extension

This is also not the Batchalign desktop app. The two experimental desktop surfaces in this repository are:

  • Chatter Desktop (apps/chatter/chatter-gui/) — CHAT validation GUI only (experimental)
  • Batchalign Desktop (apps/batchalign/batchalign-gui/) — Batchalign processing UI (experimental)

Current status

  • Release contract: Experimental (see repo-root book/src/operations/release-contract.md)
  • Distribution: No supported DMG/MSI/AppImage release channel
  • CI: Not included in main PR CI gates
  • Recommendation: Contributors and maintainers only; do not use for production work

Getting Started (Development Only)

From source

cd apps/chatter/chatter-gui
npm install
cargo tauri dev       # launches the app with hot reload
cargo tauri build     # produces a distributable app bundle

Requires: Rust (stable, edition 2024), Node.js, and npm.

Using the App

Opening files

Chatter validates one target at a time: a single .cha file or one folder.

Three ways to start validating:

  1. Choose File — opens a file picker filtered to .cha files
  2. Choose Folder — opens a folder picker; validates all .cha files recursively
  3. Drag and drop — drag one .cha file or one folder onto the app window

When idle, if you’ve previously validated a target, the drop zone shows “Last: resources/corpus/reference/ — Re-validate?” as a clickable shortcut.

Reading results

The main window has three areas:

┌──────────────────────────────────────────────────────────────┐
│  [Choose File] [Choose Folder] or drag here  [System|Light|Dark] │
├──────────────────┬───────────────────────────────────────────┤
│ 3 FILES WITH     │  Filter by code… [All|Errors|Warnings]    │
│ ERRORS / 120     │                                           │
│                  │  ▾ [E302] Missing @End header              │
│  📁 corpus/      │  ┌───────────────────────┐                │
│    ✗ file1 (3)   │  │ 41 │ *CHI: hello .    │                │
│    ✗ file3 (1)   │  │ 42 │                   │                │
│                  │  │    │ ^                 │                │
│                  │  └───────────────────────┘                │
│                  │  💡 Add @End on the last line             │
│                  │  [Copy] [Open in CLAN]                    │
├──────────────────┴───────────────────────────────────────────┤
│  Progress: 45/120 │ 4 errors │ ~2m 30s remaining │ [Cancel]  │
└──────────────────────────────────────────────────────────────┘
  • File tree (left) — collapsible directory tree showing only files with errors (valid files are hidden to reduce clutter). A header shows “N files with errors / M total”. Files are sorted alphabetically.

  • Error panel (right) — for the selected file, shows each error with its code in [E001] format, severity color, message, source snippet with caret underlines, and multi-span labels for complex errors (e.g., alignment mismatches across tiers). CHAT-specific formatting is handled: tabs expanded to 8-column boundaries, \x15 bullets rendered as •, underline markers shown as styled underlined text. Suggestions prefixed with 💡.

  • Status bar (bottom) — streaming progress during validation, ETA after 5+ files, total error count, and action buttons.

Filtering errors

A compact filter bar appears above the error cards when a file has diagnostics:

  • Code filter — type “E7” to show only alignment errors, “W” for warnings, etc.
  • Severity toggle — switch between All / Errors / Warnings

The file header updates to show filtered vs. total count (e.g., “3 errors (7 total)”).

Collapsible error cards

Each error card has a clickable header that toggles between expanded and collapsed view. Collapsed cards show only the error code and first line of the message. When a file has 5 or more errors, an Expand All / Collapse All button appears.

Dark mode

Chatter follows your system appearance by default. A System / Light / Dark toggle in the drop zone area lets you override. Your preference is remembered across sessions.

The dark palette uses muted Apple-style colors — readable miette error highlighting on dark backgrounds.

Clickable file paths

Click the file name in the error panel heading to reveal the file in Finder (macOS), Explorer (Windows), or the default file manager (Linux).

Copy errors

Each error card has a Copy button that copies the full miette-rendered error text (plain text, not HTML) to your clipboard for pasting into issue reports or messages.

Actions

ActionWhereWhat it does
Re-validateStatus bar / last-target hintRe-run validation on the same target (picks up edits)
CancelStatus bar (during validation)Stop the current run
ExportStatus barSave results as JSON or plain text via a save dialog
Open in CLANPer-error buttonOpens the file at the error location in the CLAN editor
CopyPer-error buttonCopies the plain-text error to clipboard
Reveal in file managerFile name headingOpens the file’s parent directory

“Open in CLAN” only appears when the CLAN application is detected on your system (macOS and Windows only). It adjusts line numbers to account for headers that CLAN hides (@UTF8, @PID, @Font, @ColorWords, @Window).

Keyboard shortcuts

ShortcutAction
Ctrl+R / Cmd+RRe-validate
EscapeCancel running validation

All other navigation is mouse-driven (click files, scroll errors).

Window title

The window title updates to reflect the current state:

  • Idle: “Chatter”
  • Discovering: “Chatter — Discovering files…”
  • Running: “Chatter — Validating (45/120)”
  • Finished: “Chatter — 14 errors in 3 files” or “Chatter — All 74 files valid”

ETA

After 5 or more files have been processed, the status bar shows an estimated time remaining (e.g., “~2m 30s remaining”). The estimate updates every second.

Notifications

When validation finishes while the app is not focused, a system notification shows the summary (“Validation complete — 14 errors in 3 files”).

First launch

On first launch, an onboarding overlay explains the four main interactions: drag files, error panel, keyboard shortcuts, and export. Dismiss with “Got it” — it won’t appear again.

CLI Bundling

The desktop app can bundle the chatter CLI binary so power users who download the GUI can also run the CLI from their terminal (like VS Code ships the code command).

An Install CLI Command menu item (when available) symlinks the bundled binary to /usr/local/bin/chatter (macOS/Linux) or copies it to a PATH directory (Windows).

To build with the bundled CLI:

cargo build --release -p chatter-cli
mkdir -p apps/chatter/chatter-gui/src-tauri/resources
cp target/release/chatter apps/chatter/chatter-gui/src-tauri/resources/
cargo tauri build

Architecture

The desktop app lives in apps/chatter/chatter-gui/ as a sibling to apps/vscode-extension/:

apps/chatter/chatter-gui/
  src-tauri/          Rust backend (Tauri v2)
    src/
      main.rs         Bin entry — calls chatter_gui_lib::run()
      lib.rs          Tauri app setup (Builder + module wiring)
      protocol.rs     Shared command/event names + request types
      commands.rs     validate, cancel, open_in_clan, export, reveal, install_cli
      events.rs       ValidationEvent → frontend event bridge
      validation.rs   Desktop validation orchestration for one target
  src/                React + TypeScript frontend
    components/       DropZone, FileTree, ErrorPanel, ProgressBar, OnboardingOverlay
    hooks/            useValidation, validationState, useTheme
    protocol/         Command/event names + TypeScript transport mirrors
    runtime/          Tauri transport + capability-focused runtime seam

The Rust backend calls validate_directory_streaming() from talkbank-transform directly — the same streaming validation pipeline used by the TUI. Events flow over crossbeam channels to the Rust side, then are serialized to JSON and emitted to the frontend via Tauri’s event bridge.

Cancellation uses ArcSwapOption for lock-free atomic swap of the cancel sender — no mutex.

The frontend keeps Tauri-specific code confined to src/runtime/tauriTransport.ts. React components and hooks consume narrower capabilities (validationRunner, validationTarget, clan, exports) instead of reaching for one broad desktop service object.

Comparison with TUI

FeatureTUI (chatter validate)Desktop app
File selectionCLI argumentsDrag-and-drop, file picker
NavigationKeyboard (Tab, arrows)Mouse click
Error displayTwo-pane terminal UIScrollable panels with source snippets
Error filtering—Code filter + severity toggle
Copy error—Copy button per error
Open in CLANc keyButton per error
Export--format json --auditSave dialog (JSON or text)
Streaming progressProgress barProgress bar + ETA
Dark modeTerminal themeSystem/Light/Dark toggle
CachingSame engineSame engine
Who it’s forPower users, CIResearchers, linguists

Both use the identical validation engine and produce the same error codes.

When to Use Which Tool

The TalkBank toolchain offers validation through four interfaces. Each serves a different workflow:

ToolAudienceUse when
Chatter Desktop (experimental)Evaluators, contributorsYou are specifically trying the experimental apps/chatter/chatter-gui/ validation GUI from source.
VS Code extensionEditors, annotatorsYou’re editing CHAT files and want live diagnostics, quick fixes, CLAN analysis, and media playback.
chatter validate (TUI)Power usersYou’re comfortable in a terminal and want keyboard-driven navigation.
chatter validate (CLI)CI, scriptsYou need machine-readable output (--format json) or batch audits (--audit).

Chatter Desktop focuses on validation only — it does not run CLAN analysis commands or play media. If you need those features, use the VS Code extension.

Batch Workflows

Status: Current Last updated: 2026-05-02 02:30 EDT

The chatter CLI is designed for processing large CHAT corpora efficiently. This page covers common batch workflows.

Validating a Corpus

Validate all .cha files in a directory tree:

chatter validate /path/to/corpus/

The validator recursively discovers .cha files and processes them in parallel. Results are cached — subsequent runs skip unchanged files.

Forcing Revalidation

To bypass the cache and revalidate everything:

chatter validate /path/to/corpus/ --force

Filtering Output

Show only errors (hide warnings):

chatter validate /path/to/corpus/ --quiet

Stop after the first reported error:

chatter validate /path/to/corpus/ --max-errors 1

Write a JSONL audit file while validating:

chatter validate /path/to/corpus/ --audit validation.jsonl

CHAT-JSON Roundtrip

Convert an entire corpus to JSON and back:

# CHAT → JSON
for f in corpus/**/*.cha; do
  chatter to-json "$f" > "${f%.cha}.json"
done

# JSON → CHAT
for f in corpus/**/*.json; do
  chatter from-json "$f" > "${f%.json}.roundtrip.cha"
done

The roundtrip is designed to preserve the ChatFile model. In regression tests, compare normalized output rather than assuming byte-for-byte identity after parser or serializer changes.

Cache Management

The validation cache stores results for previously validated files (keyed by content hash). The cache database file is named talkbank-cache.db and lives in the OS cache directory:

  • macOS: ~/Library/Caches/talkbank-chat/talkbank-cache.db
  • Linux: ~/.cache/talkbank-chat/talkbank-cache.db
  • Windows: %LocalAppData%\talkbank-chat\talkbank-cache.db

It can hold results for large file collections.

chatter cache stats    # Show hit rates and entry count
chatter cache clear --all

Do not delete the cache file manually while chatter is running.

Reference Corpus Validation

The talkbank-tools repository includes a reference corpus at resources/corpus/reference/ (currently ~100 .cha files; verify by find resources/corpus/reference -name '*.cha' | wc -l). The parser must handle every file in this corpus at 100%:

cargo nextest run -p talkbank-parser-tests -E 'test(parser_equivalence)'

This runs the parser equivalence test — each .cha file is its own test, so nextest runs them in parallel and reports individual failures.

Integration with batchalign

The Batchalign pipeline uses the same Rust core (via PyO3) for CHAT parsing and serialization. Since the 2026-04-28 monorepo merge, Batchalign source lives inside this repository under crates/batchalign-* (the standalone batchalign3 GitHub repo was archived). Files processed by Batchalign produce valid CHAT that passes chatter validate.

CI Integration

Status: Current Last updated: 2026-04-13 19:23 EDT

How to use chatter in continuous integration pipelines.

Exit Codes

CodeMeaning
0All files valid / command succeeded
1Validation errors found or command failed
2Invalid arguments or missing required options

All examples below rely on exit code 1 to signal validation failure.

Basic Usage

chatter validate corpus/ --quiet --tui-mode disable
  • --quiet suppresses per-file success output
  • --tui-mode disable prevents interactive TUI (required in non-TTY environments)
  • Exit code 0 means all files valid; 1 means errors found

GitHub Actions Example

- name: Validate CHAT corpus
  run: |
    chatter validate corpus/ --quiet --tui-mode disable --format json --audit results.jsonl

- name: Upload validation report
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: validation-report
    path: results.jsonl

The --audit results.jsonl flag streams per-error JSON lines to a file, which is useful for archiving or downstream analysis even when the step fails.

JSON Output for Automation

chatter validate corpus/ --format json --tui-mode disable 2>/dev/null

Each file produces a JSON object on stdout with status, error_count, and errors array. The exit code still reflects overall pass/fail.

Pre-commit Hook

#!/bin/sh
# .git/hooks/pre-commit
chatter validate . --quiet --tui-mode disable

This blocks commits that introduce invalid CHAT files. The hook runs quickly on cached files; only modified files are re-validated.

Suppressing Specific Errors

Some corpora have known issues that should not block CI. Use --suppress to ignore specific error codes or named groups:

chatter validate corpus/ --suppress E726,E727,E728 --tui-mode disable

Or use the named group shorthand:

chatter validate corpus/ --suppress xphon --tui-mode disable

Suppressed errors do not appear in output and do not affect the exit code.

Audit Mode for Large Corpora

For bulk corpus validation where you want a full error database without caching overhead:

chatter validate corpus/ --audit errors.jsonl --tui-mode disable

The --audit flag streams one JSON object per error to the specified file. A summary is printed to stderr at the end.

CHAT Processing Playbook for Editors and Analysts

Status: Current Last updated: 2026-03-24 00:01 EDT

Objective

Provide practical guidance for non-compiler users who create, edit, and validate CHAT files, with emphasis on error interpretation and correction workflow.

Who This Is For

  • Transcript editors,
  • corpus curators,
  • QA reviewers,
  • linguists using tooling outputs but not parser internals.

Core Editing Workflow

  1. Open file in editor with CHAT diagnostics enabled.
  2. Run validation (single file first, then batch).
  3. Fix highest-severity structural issues first (headers, tier markers, unmatched delimiters).
  4. Re-run validation and inspect warnings.
  5. Only then address style and normalization suggestions.

Error Triage Heuristic

  • Errors at file start: likely header formatting or encoding issues.
  • Errors at tier prefix: likely malformed */% tier syntax.
  • Errors inside words: likely symbol, marker, or annotation boundary issues.
  • Repeated same error class: likely one systemic rule violation pattern.

Fast Interpretation Guide

  • Error: parser/validator could not accept structure; must fix.
  • Warning: valid but suspicious or non-canonical; review strongly recommended.
  • Info: advisory normalization or convention hints.

Common Fix Recipes

  • Header spacing problems:
    • Ensure expected separators and avoid accidental tabs/spaces drift.
  • Unclear language/form markers:
    • Confirm @s usage and suffix ordering with house style guide.
  • Duration/annotation confusion:
    • Verify bracketed annotation form and avoid malformed punctuation.
  • Dependent tier attachment issues:
    • Ensure % tiers follow intended main tier and keep indentation consistent.

Batch Validation Workflow

  1. Validate a small sample first.
  2. Group failures by error code.
  3. Fix by pattern, not file-by-file random order.
  4. Re-run and confirm error count decreases monotonically.
  5. Save run report for audit trail.

Collaboration Workflow with Developers

When reporting parsing issues, include:

  • exact file path,
  • minimal excerpt around failing span,
  • observed diagnostic code/message,
  • expected behavior (if known).

This reduces back-and-forth and speeds defect triage.

Quality Checklist Before Publishing Corpus Updates

  • No unresolved error-level diagnostics.
  • Warning classes reviewed and accepted or fixed.
  • Participant headers and IDs internally consistent.
  • Roundtrip serialization check passes for representative samples.
  • Changelog note recorded for major normalization edits.

Training Recommendations

  • Maintain short examples for each common error class.
  • Provide editor cheat sheet for tier prefixes and marker syntax.
  • Run periodic QA calibration sessions across editors.

Sanitize (chatter debug sanitize)

Status: Current Last updated: 2026-04-28 22:18 EDT

chatter debug sanitize strips contributor lexical content from a CHAT file while preserving structure (timing bullets, %wor per-word offsets, speaker codes, dependent-tier scaffolding, structural counts, POS tags, language markers). Output is structurally identical to the input but contains no participant words, names, or free-text annotations.

The command exists so engineering tooling — including LLM-assisted debugging — can operate on protected-corpus files (aphasia/, dementia/, rhd/, fluency/Password/, clinical-children corpora, etc.) without exposing contributor speech to commercial LLM services.

When to use it

Run chatter debug sanitize on the source file before loading it into any tool (LLM-backed debugger, scratch directory, screen-shareable session) where you don’t want participant content visible.

When you need to ask a contributor for help debugging a specific file, frame the request as “run the sanitizer locally and send me the output” rather than asking for the raw file.

Usage

# Write sanitized output to stdout
chatter debug sanitize input.cha

# Write sanitized output to a file
chatter debug sanitize input.cha --output sanitized.cha

Working location for sanitized files: prefer a stable, non-/tmp scratch directory (e.g. set TB_SCRATCH_DIR to a per-project dir under your workstation’s persistent storage) for any state that should outlive a single command. macOS clears /tmp on reboot.

What is preserved (byte-exact)

  • Timing bullets •start_end• on the main tier.
  • %wor per-word offsets (word START_END triples).
  • Speaker codes (*PAR, *INV, *CHI, …).
  • Utterance count, word count per utterance, dependent-tier count.
  • Structural markers: compound +, clitic ~, CA elements, overlap points, lengthening, stress markers, syllable pause, underline begin/end, proper-noun @n markers.
  • Language markers (@s:LANG), form types (@a, @b), POS tags ($adj, $n).
  • Headers: @Languages, @Birth, @Date, @Media, @PID, @L1Of, @Begin/@End/@UTF8.
  • %mor POS categories and morphological features (e.g., n|, -Past).
  • %gra (numeric grammatical relations) and %tim (timing).
  • Untranscribed tokens xxx / yyy / www — preserving them changes semantic meaning, so they pass through unchanged.

What is replaced or redacted

SourceReplacement
WordContent::TextwN placeholder, indexed by document position
Shortening text(x)
%mor lemmas (MorWord.lemma)lemmaN; POS + features preserved
%pho / %mod / %modsyl / %phosyl / %phoaln / %sintier dropped
Free-text dependent tiers (%com %add %exp %sit %spa %int %gpx %eng %gls %ort %flo %def %coh %fac %par %alt %err)[redacted]
@Comment, @Transcriber, @Birthplace, @Activities, @Situation, @RoomLayout, @Location, @TapeLocation, @Warning, @Bck[redacted] (when content was free text)
@Participants participant-name fielddropped (Participant_<SPEAKER_CODE> is implied by speaker code + role)
@ID custom_field and educationcleared
Event event_type (&=imitates:Mary → &=[redacted])[redacted]
Freecode text ([^ aside])[redacted]
OtherSpokenEvent text[redacted]

Determinism + Idempotence

Placeholder generation is keyed off (utterance_index, word_index) tree position, not a global counter. Two consequences:

  • Deterministic: sanitizing the same input twice produces byte-identical output.
  • Idempotent: sanitizing a sanitized file produces the same file again — no double-replacement, no shifting placeholder numbers.

Pipeline

flowchart LR
    Input["Source .cha\n(protected corpus)"] --> Parser["TreeSitterParser\n(talkbank-parser)"]
    Parser --> Model["ChatFile model\n(talkbank-model)"]
    Model --> Sanitize["sanitize()\n(talkbank-transform::redact)"]
    Sanitize --> Walker["walk_words_mut\n+ header walker\n+ dep-tier walker\n+ scoped-annot walker"]
    Walker --> Mutated["Mutated ChatFile\n(placeholders + redactions)"]
    Mutated --> Writer["WriteChat\n(byte-exact bullets)"]
    Writer --> Output["Sanitized .cha\n(scratch path)"]

The walker step replaces WordContent::Text segments inside Word.content, mutates MorWord.lemma fields, redacts free-text header / dep-tier / scoped-annotation strings, and drops phonological tiers. WriteChat then re-serializes — and because it serializes from Word.content (not from Word.raw_text), every CA element, compound marker, clitic boundary, and timing bullet round-trips byte-exact.

Out of v1 scope

Documented for transparency; v2 work:

  • Speaker-code anonymization (graph rewrite across @Participants, @ID, *SPK:, @Birth, @L1Of).
  • @Birth / @Date fuzzing (exact birth dates can be identifying).
  • @Media filename redaction.
  • Audio-side sanitization. (Audio bytes are never touched by the sanitizer; the audio stays at its original path.)
  • “Unsanitize” or round-trip mapping. Explicitly not built — the sanitizer is one-way, the mapping table that would reverse it is the exact artifact we don’t want to exist.

Implementation

Library module: talkbank_transform::redact. CLI surface: chatter debug sanitize. The strict policy is the only public preset in v1; future variants can grow on SanitizationPolicy.

VS Code: how-to guides

Choose the task you want to complete.

Installation

Status: Current Last updated: 2026-06-01 00:52 PDT

This chapter walks you through installing VS Code and the TalkBank CHAT extension. The language server (chatter-lsp) ships inside each platform-specific VSIX, so there is no separate binary to install. By the end, you will have a working environment where opening any .cha file gives you syntax highlighting, real-time validation, and all the features described in this book.


Prerequisites

You need:

  1. Visual Studio Code Insiders version 1.110 or later
  2. A platform-specific .vsix package — each release publishes five VSIXes (darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64) with a prebuilt chatter-lsp bundled inside

The extension is a .vsix package installed into VS Code. Each platform-specific VSIX bundles the matching chatter-lsp binary at <extension>/server/chatter-lsp[.exe], so end users do not need a Rust toolchain or any PATH setup.


Step 1: Install VS Code Insiders

macOS

Install via Homebrew:

brew install --cask visual-studio-code@insiders

This gives you the code-insiders command in your terminal.

Windows

Download from https://code.visualstudio.com/insiders/. Run the installer and ensure “Add to PATH” is checked so the code-insiders command is available from the terminal.

Linux

Download the .deb or .rpm package from https://code.visualstudio.com/insiders/, or use snap:

sudo snap install code-insiders --classic

Step 2: Install the Extension

  1. Open https://github.com/TalkBank/batchalign/releases

  2. Choose the vscode-vX.Y.Z release entry and download the .vsix that matches your host:

    • talkbank-chat-darwin-arm64.vsix (Apple Silicon Macs)
    • talkbank-chat-darwin-x64.vsix (Intel Macs)
    • talkbank-chat-linux-x64.vsix
    • talkbank-chat-linux-arm64.vsix
    • talkbank-chat-win32-x64.vsix
  3. Install it:

    code-insiders --install-extension talkbank-chat-<platform>.vsix
    

    Or from inside VS Code: open the Command Palette (Cmd+Shift+P), type “Extensions: Install from VSIX…”, and select the .vsix file.

Each platform VSIX bundles a matching chatter-lsp binary. No Rust toolchain or PATH setup is required.

Option B: Development mode (building from source)

Contributors running from a cloned workspace can build chatter-lsp and launch the extension directly:

# From the talkbank-tools/ repository root:
cargo build --release -p chatter-lsp

cd apps/vscode-extension
npm install
npm run compile     # or: just vscode build (from repo root)

# Launch VS Code with the extension loaded in development mode:
code-insiders --extensionDevelopmentPath=.

In this mode the extension picks up the dev-tree build at ../target/release/chatter-lsp (or target/debug/ after cargo build -p chatter-lsp). Producing a full set of platform VSIXes is covered in the Releasing chapter.


Step 3: Verify the Installation

  1. Open VS Code Insiders
  2. Open any .cha file (if you have the repository, try one from resources/corpus/reference/)
  3. You should see:
    • Syntax highlighting – headers, speaker codes, dependent tiers, and annotations are colored by category
    • Validation diagnostics – any errors or warnings appear as squiggly underlines and in the Problems panel (Cmd+Shift+M)
    • “CHAT Validation” tree view in the Explorer sidebar

(SCREENSHOT: VS Code Insiders with a CHAT file open) Capture this: a .cha file from resources/corpus/reference/ open in the editor, showing syntax highlighting, the Problems panel with diagnostics, and the CHAT Validation tree view in the Explorer sidebar.

If you see plain, uncolored text, the extension did not activate. Check:

  • The file has a .cha extension
  • The language mode in the bottom status bar says “CHAT” (not “Plain Text”)
  • The Output panel (Cmd+Shift+U) shows a “TalkBank Language Server” channel with server startup messages

How the LSP Binary Is Found

When the extension starts, it searches for the chatter-lsp binary in this order:

  1. talkbank.lsp.binaryPath setting – if set and the file exists, that path is used directly (explicit override)
  2. Bundled binary – <extension>/server/chatter-lsp[.exe], shipped with each platform-specific VSIX
  3. System PATH – which chatter-lsp, for developers who install via cargo install
  4. Dev-tree builds – <extension>/../target/debug/chatter-lsp or .../target/release/chatter-lsp, for contributors running from a cloned workspace
  5. If none of these find a binary, an error notification appears with a link to the talkbank.lsp.binaryPath setting

The binary is spawned with no arguments. It speaks the Language Server Protocol over stdio as soon as it starts.

See Settings Reference for the talkbank.lsp.binaryPath setting, and LSP Connection for troubleshooting a missing binary.


Next Steps

With the extension installed, open a CHAT file and follow along with Your First CHAT File to explore the core features.

Syntax Highlighting

Status: Current Last updated: 2026-03-30 13:40 EDT

The extension provides two layers of syntax highlighting for CHAT files: a TextMate grammar for immediate fallback coloring, and LSP semantic tokens for context-aware precision. Together they make CHAT transcripts readable at a glance, with distinct colors for headers, speaker codes, annotations, morphology, and other structural elements.

Two Highlighting Layers

TextMate Grammar (Fallback)

The TextMate grammar (syntaxes/chat.tmLanguage.json) provides basic token coloring that activates instantly when you open a .cha file, before the language server has finished starting. It uses pattern-matching rules to identify CHAT constructs by their textual form.

LSP Semantic Tokens (Context-Aware)

Once the language server is running, it provides semantic tokens that override the TextMate grammar with more precise coloring based on the parsed AST. For example, the semantic layer can distinguish specific POS tags in morphology, identify error-coded tokens, and differentiate between header types that look similar as raw text.

The language server advertises 11 semantic token types:

IndexToken TypeUsed For
0keywordHeaders (@Begin, @End, @UTF8), tier prefixes
1variableSpeaker codes (*CHI, *MOT, *INV)
2stringQuoted strings, word content
3commentComment lines (%com:)
4typeType annotations, complex structures
5operatorPostcodes, morphological separators (|, -, +, &)
6numberTiming values, indices
7functionDependent tier prefixes, special markers
8tagTier labels, annotation markers
9punctuationTerminators (. ? !), special punctuation
10errorSyntax errors, malformed tokens

Range-based semantic tokens are supported, computing tokens only for the visible range. This reduces work for large files.

Color Categories

The following table summarizes how CHAT elements are colored in the editor. Exact colors depend on your VS Code color theme, but the semantic categories are consistent:

ElementExamplesColor Category
Required headers@UTF8, @Begin, @EndBlue / bold keyword
Metadata headers@Participants, @ID, @LanguagesBlue / keyword
General headers@Date, @Location, custom headersBlue / keyword
Speaker codes*CHI:, *MOT:, *INV:Bold / variable color (distinct from headers)
Dependent tier prefixes%mor:, %gra:, %pho:Italic / function color
Utterance terminators. ? ! +/. +...Red / punctuation
Annotations[= text], [: alt], [*], [+ post]Green / string-operator
Scoped groups<word word>Bracket highlighting
Retrace markers[/], [//], [//?]Operator color
Actions and events&=laughs, &ClapsCyan / tag
Pauses(0.5), (.)Gray / number
Omissions0wordSpecial styling
Morphologyn|cookie, v|go-PROGPOS / stem / affix coloring
Grammar relations1|2|DET, 3|0|ROOTIndex / relation coloring
Comments%com: linesComment color (typically gray-green)

On-Type Formatting

When you type : on a *SPEAKER: or %tier: line, a tab character is automatically inserted after the colon. This matches the CHAT format convention where a tab always follows the colon on speaker and tier lines. No manual tab insertion is needed.

This behavior is provided by the language server’s textDocument/onTypeFormatting handler and works in real time as you type.

Document Formatting

Format the entire document to canonical CHAT style with Shift+Alt+F (or your configured format shortcut), or via the Command Palette (Format Document).

The formatter normalizes:

  • Whitespace and indentation
  • Header ordering
  • Speaker code formatting
  • Tier indentation (tabs after colons)
  • Line endings

If the document is already in canonical form, no changes are applied. You can also enable Format on Save via VS Code’s editor.formatOnSave setting to automatically normalize the file every time you save.

The formatting operation is backed by the language server’s textDocument/formatting handler, which serializes the document through the canonical CHAT serializer. This ensures the output matches the authoritative CHAT format exactly.

Real-Time Validation

Status: Current Last updated: 2026-04-16 22:27 EDT

The extension validates your CHAT file continuously as you type. Errors appear as underlined squiggles with diagnostic messages in the Problems panel — no need to save or run a separate command.

What gets validated

CategoryExamples
SyntaxMalformed headers, invalid tier prefixes, missing terminators
Required structure@UTF8, @Begin, @End, @Languages, @Participants
Participant consistencyUndeclared speakers, @ID field counts, role/language mismatches
Tier alignment%mor word count vs main tier, %gra index consistency
Morphology formatPOS tag syntax, feature syntax, compound structure
Grammar relationsValid relation types, index ranges, ROOT presence
Timing bulletsMonotonicity, same-speaker overlap, start < end
TerminatorsMissing or invalid utterance terminators

Diagnostic structure

Each diagnostic includes:

  • Error code (e.g., E301, E705) — links to the error reference
  • Severity — Error (red), Warning (yellow), Information (blue), Hint (gray)
  • Related information — points to the conflicting location (e.g., the @Participants header when a speaker is undeclared)

(SCREENSHOT: Editor showing validation errors with squiggles and Problems panel) Capture this: Open a CHAT file with deliberate errors (missing @End, undeclared speaker). Show the red squiggles and the Problems panel at the bottom.

Inlay hints

When the main tier and %mor tier have different word counts, inlay hints appear as muted inline annotations showing the alignment status.

When counts match, a checkmark appears. When they mismatch, the hint shows both counts with a cross mark, making it easy to spot alignment issues without running a separate command.

Toggle with talkbank.inlayHints.enabled setting.

Diagnostic tags

Some diagnostics use VS Code’s diagnostic tag system for visual styling:

  • Unnecessary — fade-out (dimmed text) for deprecated constructs
  • Deprecated — strikethrough for obsolete syntax

Severity filtering

The talkbank.validation.severity setting controls which diagnostics appear:

ValueShows
all (default)Errors, warnings, and information
errorsAndWarningsErrors and warnings only
errorsOnlyErrors only

Validation pipeline

flowchart TD
    Change["Document Change"] --> Debounce["250ms Debounce"]
    Debounce --> Parse["Parse\n(tree-sitter)"]
    Parse --> Validate["Model Validation\n(~190 error codes)"]
    Validate --> Convert["Convert to\nLSP Diagnostics"]
    Convert --> Publish["Publish to\nProblems Panel"]

Changes are debounced at 250ms to avoid overwhelming the parser during rapid typing. After parsing, the full model validation pipeline runs (the same pipeline as chatter validate), producing diagnostics with error codes, spans, and related information.

See also

Quick Fixes

Status: Current Last updated: 2026-03-30 13:40 EDT

When the language server detects certain errors, it can offer automatic corrections. These appear as a lightbulb icon in the left gutter, or you can invoke them directly with Cmd+. (macOS) / Ctrl+. (Windows/Linux). Quick fixes are the fastest way to resolve common CHAT formatting and structural problems without manual editing.

How Quick Fixes Work

  1. The language server continuously validates the document as you type (see Real-Time Validation).
  2. For errors that have a well-defined automatic correction, a code action is registered.
  3. When your cursor is on a diagnostic that has a quick fix, a lightbulb icon appears in the gutter.
  4. Click the lightbulb or press Cmd+. to see the available fixes.
  5. Select a fix to apply it instantly.

Some errors offer multiple fix options (for example, E301 offers three different terminators). Others apply a single unambiguous correction.

(SCREENSHOT: Quick fix lightbulb showing options for E308 (undeclared speaker)) Capture this: open a .cha file with a speaker code on a main tier that is not listed in @Participants. Place cursor on the error squiggle, then press Cmd+. to show the lightbulb menu with the “Add ‘INV’ to @Participants” option.

Available Quick Fixes

The extension currently provides automatic fixes for 21 error codes:

Error CodeDescriptionQuick Fix Action
E241Illegal untranscribed marker (xx)Replace xx with xxx (the correct CHAT untranscribed speech marker)
E242Incomplete wordAppend the trailing-off marker (+...) to the word
E244Consecutive stress markersRemove the duplicate separator
E258Consecutive commasClean up consecutive comma punctuation
E259Form errorCorrect the malformed expression
E301Missing terminatorInsert a terminator: offers . (declarative), ? (question), or ! (exclamation)
E305Missing periodInsert the missing period
E306Empty utteranceDelete the empty utterance line (shown with fade-out styling)
E308Undeclared speakerAdd the speaker code to the @Participants header
E312Bare group (unclosed bracket)Insert the missing closing bracket
E313Bare annotation (unclosed paren)Insert the missing closing parenthesis
E322Empty colonDelete the empty colon line (shown with fade-out styling)
E323Missing colon after speakerInsert the missing colon and tab after the speaker code
E362Timestamp swapSwap the reversed start and end timestamps in the timing bullet
E501Missing @BeginInsert @Begin after the @UTF8 header line
E502Missing @EndInsert @End at the end of the file
E503Missing @UTF8Insert @UTF8 at the very start of the file
E504Missing @LanguagesInsert an empty @Languages: header template
E506Missing @ParticipantsInsert an empty @Participants: header template
E507Participant format errorCorrect the participant entry format
E604Unknown/orphaned tierRemove or correct the orphaned dependent tier

Diagnostic Tags

Some quick-fixable errors use VS Code’s fade-out styling to visually indicate that content can be safely removed. Empty utterances (E306) and empty colons (E322) appear with dimmed text in the editor, making them easy to spot as candidates for deletion.

Tips

  • Batch fixing: When a file has many instances of the same error (for example, multiple undeclared speakers), you can fix them one at a time with Cmd+. or use the Problems panel (Cmd+Shift+M) to navigate between them.
  • Undo: Every quick fix is a normal text edit, so Cmd+Z reverts it if the fix was not what you intended.
  • Format after fixing: Some fixes (like inserting a missing @Begin) may leave whitespace that is not perfectly canonical. Use Document Formatting (Shift+Alt+F) to normalize the file after applying fixes.

Code Completion & Snippets

Status: Current Last updated: 2026-03-30 13:40 EDT

The extension provides context-aware autocompletion and snippet templates to speed up CHAT editing. Completions are triggered by specific characters at the start of a line or within an utterance, and snippets expand common CHAT structures with a single Tab press.

Code Completion

Completion suggestions appear automatically as you type certain trigger characters. You can also invoke the completion menu manually with Ctrl+Space.

Speaker Codes (trigger: *)

When you type * at the start of a line, the extension offers all speakers declared in the file’s @Participants header. Each suggestion includes the speaker’s role and auto-inserts the colon and tab separator:

*|  <-- cursor here

Suggestions:
  CHI   target child     --> inserts "*CHI:\t"
  MOT   mother           --> inserts "*MOT:\t"
  FAT   father           --> inserts "*FAT:\t"

This ensures you never mistype a speaker code or use a code that is not declared in the file.

Tier Types (trigger: %)

When you type % at the start of a line after a main tier, the extension offers all standard dependent tier types:

%|  <-- cursor here

Suggestions:
  mor   Morphological analysis       --> inserts "%mor:\t"
  gra   Grammatical relations        --> inserts "%gra:\t"
  pho   Phonological transcription   --> inserts "%pho:\t"
  mod   Model phonology              --> inserts "%mod:\t"
  sin   Gesture/sign annotations     --> inserts "%sin:\t"
  act   Action coding                --> inserts "%act:\t"
  cod   General coding               --> inserts "%cod:\t"
  com   Comments                     --> inserts "%com:\t"
  ...and more

Headers (trigger: @)

When you type @ at the start of a line, the extension offers all 28 standard CHAT headers with descriptions. Each suggestion auto-inserts the header name, colon, and tab separator:

(SCREENSHOT: Completion menu showing @ header options) Capture this: in a .cha file, place cursor at the beginning of a blank line in the header section and type @. The completion menu should appear showing headers like @Languages, @Participants, @ID, @Media, @Date, @Location, etc.

Standard headers include @Begin, @End, @UTF8, @Languages, @Participants, @ID, @Media, @Date, @Location, @Situation, @Activities, @Comment, @Bg, @Eg, @New Episode, @Birth of, @Birthplace of, @Education of, @Group of, @SES of, @Sex of, @Warning, @Transcriber, @Coder, @Options, @Font, @ColorWords, and @Window.

Brackets (trigger: [)

When you type [ inside an utterance, the extension offers 17 standard bracket annotations with descriptions:

  • [//] – retracing with correction
  • [/] – retracing without correction
  • [//?] – uncertain retracing
  • [*] – error marking
  • [= text] – explanation
  • [: alt] – replacement
  • [=! text] – paralinguistic material
  • [+ post] – postcode
  • [- pre] – precode
  • [%act: text] – inline action
  • [%com: text] – inline comment
  • And others

This eliminates the need to memorize all bracket codes and ensures correct formatting.

Postcodes (trigger: +)

When you type + in an utterance context, the extension offers valid CHAT postcodes:

+|  <-- cursor here

Suggestions:
  +"     Quotation follows
  +,.    Self-completion
  +/.    Interruption
  +/?    Interruption question
  +//.   Self-interruption
  +...   Trailing off

Code Lens

Above the @Participants header, a code lens annotation displays the utterance count for each speaker in the file. For example:

CHI: 42 utterances | MOT: 38 utterances
@Participants:	CHI Target_Child, MOT Mother

This provides an at-a-glance summary of speaker activity without running an analysis command. The counts update automatically as you edit the document.

Code Folding

The extension provides CHAT-aware code folding via the language server:

  • Utterance blocks: Each main tier line (*SPEAKER:) together with its dependent tiers (%mor:, %gra:, etc.) folds as a single unit. Collapsing an utterance hides all its annotation tiers while keeping the main tier visible.
  • Header block: The header section (@Begin through the first utterance) folds as one unit, letting you hide metadata to focus on the transcript body.

Use the fold/unfold controls in the gutter, or the keyboard shortcuts:

  • Fold: Cmd+Shift+[ (macOS) / Ctrl+Shift+[ (Windows/Linux)
  • Unfold: Cmd+Shift+] (macOS) / Ctrl+Shift+] (Windows/Linux)
  • Fold All: Cmd+K Cmd+0
  • Unfold All: Cmd+K Cmd+J

Snippets

The extension includes 8 CHAT snippets for common file structures. Type the prefix and press Tab to expand. Tab stops (shown as $1, $2, etc. in the template) let you fill in values sequentially by pressing Tab.

PrefixNameDescription
@UTF8 / header / newfileHeader blockComplete file skeleton: @UTF8, @Begin, @Languages, @Participants, @ID lines, first utterance, @End
@Participants / participantParticipants@Participants header with two speaker placeholders
@ID / idID headerSingle @ID line with all 10 pipe-delimited fields as tab stops
* / utteranceMain tier*CHI: utterance line with terminator
%morMor tier%mor: dependent tier line
%graGra tier%gra: dependent tier line
@Comment / commentComment@Comment: header with content placeholder
@Bg / @Eg / gemGem block@Bg/@Eg pair wrapping a cursor position for gem segments

Example: Using the Header Block Snippet

  1. Open a new .cha file
  2. Type newfile and press Tab
  3. The full file skeleton expands with cursor positioned at the language code
  4. Press Tab to move through each placeholder: language, participant names, ID fields, first utterance content

Snippets are defined in snippets/chat.json and are available in any .cha file.

Special Characters

Status: Current Last updated: 2026-03-30 13:40 EDT

CHAT transcription uses many Unicode symbols that are not available on a standard keyboard – overlap markers, prosody arrows, IPA-adjacent characters, and specialized punctuation. The extension provides a compose-key system for inserting these characters without memorizing Unicode codepoints or relying on system input methods.

How Compose Mode Works

  1. Press the compose shortcut to enter a mode:
    • Cmd+Shift+1 (macOS) / Ctrl+Shift+1 (Windows/Linux) for CA mode (Conversation Analysis symbols)
    • Cmd+Shift+2 (macOS) / Ctrl+Shift+2 (Windows/Linux) for CHAT mode (CHAT-specific symbols)
  2. A status bar indicator appears showing the active compose mode (e.g., “CA Char…” or “CHAT Char…”).
  3. Press the trigger key corresponding to the symbol you want to insert.
  4. The Unicode symbol is inserted at the cursor position, and compose mode deactivates.
  5. Press Escape at any time to cancel without inserting anything.

(SCREENSHOT: Status bar showing compose mode active) Capture this: press Cmd+Shift+1 in a .cha file. The status bar at the bottom of the VS Code window should show a “CA Char…” indicator. Capture the status bar region.

This compose-key system is ported from the CLAN macOS application’s CharToSpChar() function, so researchers familiar with CLAN’s character input will find the same key mappings.

CA Mode Characters (Cmd+Shift+1)

Conversation Analysis notation uses specialized Unicode symbols for prosody, overlap, voice quality, and other phonetic features. After pressing Cmd+Shift+1, press one of the following keys:

KeySymbolNameMeaning
1⇗North-east arrowRise to high
2↗Up-right arrowRise to mid
3→Rightward arrowLevel pitch
4↘Down-right arrowFall to mid
5⇘South-east arrowFall to low
[⌈Left ceilingOverlap start (first speaker)
]⌉Right ceilingOverlap end (first speaker)
{⌊Left floorOverlap start (second speaker)
}⌋Right floorOverlap end (second speaker)
.∙Bullet operatorInhalation
=≈Almost equalLatching (no gap between turns)
0°Degree signSofter / quieter speech
)◉FisheyeLouder speech
w∬Double integralWhisper
s∮Contour integralSinging
b♋Cancer signBreathy voice
*⁎Low asteriskCreaky voice
/⁇Double question markUnsure / uncertain transcription

Overlap Markers

The four overlap bracket symbols (⌈ ⌉ ⌊ ⌋) mark the boundaries of overlapping speech between two speakers. In CA transcription:

  • Speaker A’s overlap is marked with ceiling brackets: ⌈overlapping words⌉
  • Speaker B’s overlap is marked with floor brackets: ⌊overlapping words⌋

Prosody Contours

The five numbered keys (1-5) map to pitch contour arrows arranged from highest to lowest, matching standard CA prosody notation conventions.

CHAT Mode Characters (Cmd+Shift+2)

CHAT format uses additional special characters for phonological notation, grouping, and morphological analysis. After pressing Cmd+Shift+2, press one of the following keys:

KeySymbolNameMeaning
qʔGlottal stopIPA glottal stop
QʕPharyngeal fricativeHebrew glottal (pharyngeal)
:ːTriangular colonLong vowel (IPA length mark)
HʰSuperscript hAspiration marker
<‹Single left angle quoteGroup start
>›Single right angle quoteGroup end
{〔Left tortoise shell bracketSign group start
}〕Right tortoise shell bracketSign group end
1ˈModifier letter vertical linePrimary stress
2ˌModifier letter low vertical lineSecondary stress
m…Horizontal ellipsisMissing word (used on %pho tier)
/↫Leftward arrow with loopLeft arrow with circle
=≠Not equalCrossed equal sign

Tips

  • One character at a time: Compose mode deactivates after each character insertion. To insert multiple special characters in sequence, press the compose shortcut again before each one.
  • Mode confusion: If you are unsure which mode you are in, check the status bar indicator. Press Escape to cancel and start over.
  • System input methods: On macOS, you can also use the built-in Character Viewer (Ctrl+Cmd+Space) as an alternative for occasional use. The compose-key system is faster for frequent transcription work.

Participant Editor

Status: Current Last updated: 2026-03-30 13:40 EDT

CHAT files encode participant metadata in @ID headers as pipe-delimited strings with 10 fields. Editing these by hand is tedious and error-prone – it is easy to miscount pipes, leave a field blank, or use an invalid format. The Participant Editor provides a visual table interface for viewing and editing all participant information at once.

Opening the Editor

There are two ways to open the Participant Editor:

  • Right-click in a .cha file and select Edit Participants… from the context menu
  • Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and search for TalkBank: Edit Participants

A webview panel opens beside the editor showing all @ID lines in a tabular form.

(SCREENSHOT: Participant editor panel with filled fields) Capture this: open a .cha file that has at least two @ID headers (e.g., one for CHI and one for MOT). Open the participant editor via right-click. The panel should show a table with rows for each participant and columns for all 10 fields. Fill in some values (age, sex, role) so the table is not empty.

Fields

The table displays one row per participant with 10 editable columns, corresponding to the 10 pipe-delimited fields of the @ID header:

ColumnDescriptionExample Values
LanguageThree-letter language codeeng, fra, zho
CorpusCorpus nameMacWhinney, Brown, Sachs
Speaker CodeThree-letter speaker code (matches @Participants)CHI, MOT, FAT, INV
AgeAge in years;months.days format2;6., 3;0.15, 4;
SexBiological sexmale, female
GroupExperimental or demographic grouptypical, SLI, control
SESSocioeconomic statusMC, WC, UC
RoleSpeaker’s roleTarget_Child, Mother, Father, Investigator
EducationEducation levelhigh_school, college, graduate
CustomFree-form field for project-specific dataAny text

Editing and Saving

  1. Click any cell in the table to edit its value.
  2. Modify as many fields as needed across any number of participants.
  3. Click Save Changes to write the updated @ID lines back to the document.

When you save, the editor generates canonical @ID header lines and replaces the existing ones in the .cha file. The formatting is handled entirely by the language server – the TypeScript UI layer sends the edited data to the LSP via talkbank/formatIdLine, and the server produces correctly formatted pipe-delimited output.

Architecture

The Participant Editor is a thin UI layer over two LSP commands:

  • talkbank/getParticipants – parses all @ID headers in the current document and returns the structured field data.
  • talkbank/formatIdLine – takes edited field data and serializes it back to a canonical @ID header string.

No CHAT parsing happens in TypeScript. This ensures that the editor always produces correctly formatted output that matches the language server’s understanding of the document.

Tips

  • Adding a new participant: Add the speaker to @Participants first (either manually or via the E308 quick fix), then open the Participant Editor to fill in the @ID fields.
  • Speaker code renaming: If you need to change a speaker code, use the Rename feature (F2) instead, which updates the code across @Participants, @ID, and all main tier lines in one operation.
  • Age format: CHAT ages follow the format years;months.days – for example, 2;6. means 2 years and 6 months (days omitted). The editor does not enforce this format, so take care to use it correctly.

Document Symbols

Status: Current Last updated: 2026-03-30 13:40 EDT

The extension provides standard VS Code symbol navigation features – document outline, workspace search, rename, find references, linked editing, and code lens – all aware of CHAT structure. These make it easy to navigate large transcript files and perform safe, file-wide speaker operations.

Document Symbols (Outline)

Press Cmd+Shift+O (macOS) / Ctrl+Shift+O (Windows/Linux) to open the document symbol picker. The outline shows:

  • @ headers – @Begin, @Participants, @ID, @Languages, @Media, and all other headers
  • Speaker lines – every *SPK: main tier line, grouped under the speaker code

This is the same data that populates the Outline view in the Explorer sidebar. Use it to jump directly to any header or speaker utterance.

(SCREENSHOT: Document outline showing headers and speaker lines in the Outline sidebar view) Capture this: open a .cha file with multiple speakers and several headers. Show the Outline view in the Explorer sidebar with the hierarchical symbol tree.

Workspace Symbols

Use Cmd+T (macOS) / Ctrl+T (Windows/Linux) to search across all open CHAT files for headers and speaker lines. Type a speaker code like CHI or a header name like @Media to jump to any matching location across the entire workspace, without switching files manually.

This is especially useful when working with a corpus of related transcripts and you need to find a specific speaker or header across multiple files.

Rename Speaker (F2)

Place your cursor on a speaker code – either on a *CHI: main tier line or in the @Participants header – and press F2. Type the new code and all occurrences are updated atomically:

  • The speaker entry in @Participants
  • The corresponding @ID header line
  • All main tier lines using that speaker code (*CHI: becomes *NEW:)

This is a standard LSP rename operation. It also works via right-click Rename Symbol or the Command Palette (TalkBank: Rename Symbol).

(SCREENSHOT: Rename dialog on a speaker code showing the preview of all affected locations) Capture this: place cursor on *CHI and press F2. Type a new code like TAR and show the rename preview with all locations that will change.

Find All References (Shift+F12)

Place your cursor on a speaker code and press Shift+F12 (or right-click Find All References). The References panel shows every location where that speaker appears:

  • @Participants declaration
  • @ID header line
  • All main tier lines for that speaker

This helps you quickly see the full extent of a speaker’s contributions and verify that all references are consistent.

Linked Editing

When you edit a speaker code, all other occurrences of that speaker in the document are highlighted for simultaneous editing. As you type, every matching instance updates in real time.

This is different from F2 Rename: linked editing is live, in-place, character-by-character editing of all matching speaker codes. F2 Rename is a one-shot atomic operation with a preview dialog.

Enable linked editing via VS Code’s editor.linkedEditing setting:

{
  "editor.linkedEditing": true
}

Code Lens

Above the @Participants header, a code lens annotation shows the utterance count for each speaker (e.g., CHI: 42 utterances | MOT: 38 utterances). This provides an at-a-glance summary of speaker activity without running an analysis command.

The counts update automatically as you edit the file.

Go to Definition

Status: Current Last updated: 2026-03-30 13:40 EDT

Press F12 or Cmd+Click (macOS) / Ctrl+Click (Windows/Linux) on a speaker code or dependent tier item to jump to its definition or aligned source. The extension knows the alignment between main tier words, %mor items, and %gra relations, so it can navigate across tiers in a single keystroke.

Speaker Definition

Clicking on a speaker code (e.g., *CHI on a main tier line, or CHI inside a %gra or %mor tier) jumps to its declaration in the @Participants header. This is the canonical location where the speaker’s full name and role are defined.

%mor to Main Tier

Clicking on a %mor item jumps to the aligned word on the main tier. For example, clicking n|cookie on the %mor tier jumps the cursor to the word cookie on the *CHI: line above.

The alignment is computed from the parsed model’s positional correspondence between main tier words and morphological items, not from string position guessing.

(SCREENSHOT: Go-to-definition from %mor tier to main tier) Capture this: Cmd+Click on a %mor item like n|cookie. The cursor should jump to the corresponding word cookie on the main tier line above.

%gra to Main Tier

Clicking on a %gra relation jumps through the %mor tier to the aligned main tier word. The %gra tier indexes into %mor positions (e.g., 3|2|OBJ means “word 3 depends on word 2”), so the extension follows the chain: %gra item -> %mor position -> main tier word.

This two-hop navigation is resolved transparently – you click once and land on the main tier word.

How It Works

The extension’s go-to-definition handler inspects the cursor position to determine what kind of element you clicked:

  1. Speaker code – resolves to the @Participants header line via the speaker registry.
  2. %mor item – resolves to the main tier word at the same alignment index.
  3. %gra item – parses the source index from the source|head|relation triple, maps it through %mor alignment, and resolves to the main tier word.

All resolution uses the typed ChatFile AST produced by talkbank-parser, never raw text offsets.

Cross-Tier Alignment

Status: Current Last updated: 2026-04-16 22:24 EDT

CHAT transcripts are layered: a main tier line carries the spoken words, while dependent tiers (%mor, %gra, %pho, %sin) carry aligned annotations. The extension surfaces these alignment relationships through hover tooltips, document highlighting, and smart selection, so you never have to count positions manually.

Hover: Cross-Tier Alignment

Hover over any word or tier item to see how it aligns with elements on other tiers. The tooltip is computed from the parsed ChatFile model via talkbank-model’s positional alignment, not from string-position heuristics.

Hovering on a Main Tier Word

Place your cursor over a word on a main tier line (e.g., *CHI: I want cookie .). A tooltip appears showing every tier item aligned to that word:

Main Tier Word: "cookie"

- %mor tier
  POS: n (noun)
  Stem: cookie

- %gra tier
  3|2|OBJ -- OBJ -> want (word 2)

- %pho tier
  kuki

---
Alignment computed by talkbank-model

(SCREENSHOT: Hover popup on main tier word showing %mor, %gra, and %pho alignment) Capture this: hover the cursor over the word “cookie” on a *CHI: main tier line that has %mor, %gra, and %pho dependent tiers. The tooltip should show all three aligned items.

Hovering on a %mor Item

Hover over a morphological analysis item to see its full breakdown, including POS subcategory, stem, and any prefixes, suffixes, clitics, or translations:

Morphology Element: "pro:sub|I"

POS: pro:sub (subject pronoun)
Stem: I

<- Main tier: "I" (word 1)

- %gra tier
  1|2|SUBJ -- SUBJ -> want (word 2)

Compound morphology (n|+n|pine+n|apple), fusional items, and clitic chains are all displayed when present.

Hovering on a %gra Item

Hover over a grammatical relation to see the dependency it encodes, with back-links to both the %mor tier and the main tier:

Grammar Relation: "3|2|OBJ"

Source: cookie (word 3)
Head: want (word 2)
Relation: OBJ

<- %mor tier: n|cookie
<- Main tier: "cookie"

Other Dependent Tiers

Hovering also works on %pho (phonological transcription), %mod (model phonology), and %sin (gesture/sign language) tier items, showing their alignment back to the main tier word and, where applicable, the corresponding %mor item.

Header Hover

Hovering over @ headers (@Languages, @Participants, @ID, @Media, etc.) shows inline documentation describing the header’s purpose, syntax, and valid values. The @ID hover is especially detailed, displaying a field-by-field table explaining each of the 10 pipe-delimited fields (language, corpus, code, age, sex, group, SES, role, education, custom).

Bullet Hover

Hovering over a timing bullet (*NNN_NNN*) shows the formatted start time, end time, and duration of the segment in human-readable form (e.g., 01:23.456 - 01:25.789 (2.333s)). This makes it easy to inspect timing without mentally converting millisecond values.

Stale-baseline indicator

When you edit a document so heavily that the parser cannot produce a fresh ChatFile (typically because of an in-progress syntax error), alignment-consuming hover cards continue working from the last successful parse rather than going blank. In that state the hover appends a footer:

⚠ Stale baseline — alignment reflects the last successful parse.

The vocabulary — “stale baseline” — mirrors the internal ParseState::StaleBaseline identifier so the same term appears in server logs and source code. Once your edit parses cleanly the footer disappears on the next hover.

Document Highlighting: Visual Alignment

Click on any word or tier item and all aligned elements across tiers are highlighted simultaneously. This provides an instant visual map of how a single word flows through the annotation layers.

For example, clicking on the word “cookie” on the main tier highlights:

  • “cookie” on the main tier (primary highlight, write access)
  • “n|cookie” on the %mor tier (secondary highlight, read access)
  • “3|2|OBJ” on the %gra tier (secondary highlight, read access)
  • The corresponding item on %pho, %mod, or %sin if present

This works bidirectionally: clicking a %gra item highlights both its source word and head word on %mor, plus the corresponding main tier words.

(SCREENSHOT: Cross-tier highlighting with a word selected on the main tier) Capture this: click on a word like “cookie” on a main tier line that has %mor, %gra, and %pho tiers. All aligned items across tiers should show colored highlights.

Clitics and the %mor Chunk Sequence

English contractions (it's, I'll, can't), French elisions (l'homme), and similar multi-word tokens appear as a single orthographic word on the main tier but expand into multiple morphemes on the %mor tier. CHAT writes the expansion with a post-clitic marker ~. Each side of the ~ gets its own %gra relation. Understanding how the extension resolves alignments through clitic groups explains why hover, highlights, and the dependency graph all “just work” on real transcripts.

Worked example

Consider this line:

*CHI:   it's cookies .
%mor:   pron|it~aux|be noun|cookie .
%gra:   1|2|SUBJ 2|0|ROOT 3|2|OBJ 4|2|PUNCT

The %mor tier has two items — pron|it~aux|be and noun|cookie — but it has three non-terminator chunks: the pron|it main word, the aux|be post-clitic, and noun|cookie. Adding the terminator gives a total of four chunks. Every %gra relation aligns with exactly one chunk, not one item.

ChunkPositionKindHost %mor itemMain-tier word
01 (1-indexed in %gra)Main0: `pronit~aux
12Post-clitic0: `pronit~aux
23Main1: `nouncookie`
34Terminator—.

Key property: the post-clitic chunk and its host’s main chunk share the same main-tier word. Clicking either the SUBJ relation (1|2|SUBJ → chunk 0) or the ROOT relation (2|0|ROOT → chunk 1) highlights the same word it's on the main tier. The extension collapses chunks to their host %mor item before projecting through the main↔%mor alignment, which is keyed by item position.

Why this matters

%gra relation indices are 1-indexed positions in the %mor chunk sequence (main word, then each post-clitic, then the terminator), while the main↔%mor alignment is keyed by %mor item position. Every handler that projects a %gra relation onto the main tier collapses chunks to their host item via MorTier::item_index_of_chunk; without that step, any %gra click on a post-clitic silently lands on the next item after the clitic (usually the wrong word). The single primitive is used by:

  • The %gra relation hover (word stem, head stem, dependents list).
  • The %gra click → main-tier highlight handler.
  • The dependency-graph DOT builder (each chunk gets its own node; edges connect chunk node IDs directly).
  • The %gra → main-tier Go to Definition jump.

See the Developer: Architecture chapter for the model-side primitive (MorTier::chunks(), MorChunk<'a>) and the three distinct index spaces that the Rust types now enforce at the compiler level.

Smart Selection

Use VS Code’s Expand Selection command to expand through CHAT structural levels:

KeystrokemacOSWindows/Linux
Expand selectionCmd+Shift+Right ArrowCtrl+Shift+Right Arrow
Shrink selectionCmd+Shift+Left ArrowCtrl+Shift+Left Arrow

The expansion follows CHAT document structure:

  1. Word – a single word or tier item
  2. Tier content – all content on the current tier line
  3. Full line – the entire tier line including the tier label
  4. Utterance block – the main tier line plus all its dependent tiers
  5. Entire file – the whole document

This mirrors VS Code’s standard selection expansion but is tuned for the CHAT document structure, making it easy to select exactly the scope you need for copying, deleting, or searching.

Dependency Graphs

Status: Current Last updated: 2026-04-16 22:25 EDT

The extension can render the grammatical dependency structure of any utterance as an interactive, color-coded graph. This turns the compact %gra tier notation (1|2|SUBJ 2|0|ROOT 3|2|OBJ) into a visual diagram where you can see at a glance how words relate syntactically.

Opening a Graph

  1. Place your cursor on an utterance that has both %mor and %gra tiers.
  2. Invoke the command using any of these methods:
    • Keyboard: Cmd+Shift+G (macOS) / Ctrl+Shift+G (Windows/Linux)
    • Context menu: right-click, then select Show Dependency Graph
    • Command Palette: Cmd+Shift+P and type TalkBank: Show Dependency Graph
  3. A side panel opens with the rendered graph.

If the utterance lacks a %gra tier, the command reports a message and does nothing.

Graph Layout

Words are displayed as labeled boxes, arranged left-to-right in utterance order (matching the main tier). Colored arcs connect each word to its syntactic head, with the grammatical relation label on each arc. The ROOT relation connects to an invisible root node above the word row.

Relation Color Coding

ColorRelations
BlueSUBJ
RedOBJ, OBJ2
GreenROOT
OrangeJCT (adjunct)
PurpleMOD (modifier)
Light blueDET (determiner)
TealQUANT (quantifier)
GrayAll other relations

(SCREENSHOT: Dependency graph for a complex utterance with colored arcs) Capture this: open a dependency graph for an utterance like *CHI: I want the big cookie . that has SUBJ, OBJ, DET, and MOD relations. The graph should show multiple colored arcs.

Toolbar Controls

The toolbar at the top of the graph panel provides zoom and export controls:

ButtonAction
Zoom InIncrease zoom level by 10%
Zoom OutDecrease zoom level by 10%
SliderDrag to set zoom (10%–300%)
FitAuto-fit the graph to the panel size
SVGDownload the graph as an SVG vector file
PNGDownload the graph as a high-resolution PNG (2x pixel density)

Singleton Panel

The graph panel reuses a single tab. Invoking the command on a different utterance updates the existing panel rather than opening a new one. This keeps the editor layout clean when you are stepping through multiple utterances.

Stale-baseline indicator

If you request a graph while the current document has parse errors that block a fresh reparse, the graph renders from the last successful parse rather than failing. In that state the DOT output gains a muted top-left label:

stale baseline

rendered in Courier 10pt, color #888888. The label is subordinate to the graph content (placement + font choice keep it out of the visual center) and disappears the next time you request a graph after a successful parse.

This is one of two surfaces that expose ParseState::StaleBaseline to users — the other is alignment-consuming hover cards (see Cross-Tier Alignment → Stale-baseline indicator). Go to Definition, highlights, and inlay hints stay silent in the same state because their single-position outputs are typically still correct, and a warning there would be noise.

How It Works

The rendering pipeline is entirely offline – no internet connection required.

  1. Server side (chatter-lsp, in graph/): The LSP server receives the request via the talkbank/showDependencyGraph custom command. It extracts word labels from the %mor tier (graph/labels.rs), styles each %gra relation with a color (graph/edges.rs), and assembles a Graphviz DOT digraph (graph/builder.rs) with invisible ordering edges to preserve left-to-right word order.

  2. Client side (graphPanel.ts): The extension receives the DOT string and passes it to the bundled @hpcc-js/wasm Graphviz renderer, which runs Graphviz entirely in WebAssembly. The rendered SVG is injected into a webview panel. The toolbar provides zoom, fit, and export controls over the SVG DOM.

Because the Graphviz engine is bundled as WASM (@hpcc-js/wasm), the entire pipeline works offline on any platform – no external Graphviz installation is needed.

Speaker Filtering

Status: Current Last updated: 2026-03-30 13:40 EDT

In multi-party conversations, it is often useful to focus on a single speaker’s contributions. The speaker filtering feature creates a read-only filtered view of the transcript showing only the utterances from selected speakers.

How to Use

  1. Open a .cha file.
  2. Invoke the command using any of these methods:
    • Context menu: right-click in the editor, then select Filter by Speaker…
    • Command Palette: Cmd+Shift+P (macOS) / Ctrl+Shift+P (Windows/Linux), then type TalkBank: Filter by Speaker
  3. A multi-select picker appears listing all speakers defined in the file’s @Participants header.
  4. Select one or more speakers and confirm.
  5. A filtered read-only document opens in a side-by-side panel.

What the Filtered View Shows

The filtered document includes:

  • All file headers – @UTF8, @Begin, @Participants, @ID, @Languages, @Media, and all other @ headers are preserved so the document remains structurally valid.
  • Selected speakers’ utterance blocks – each main tier line for the selected speakers, along with all their dependent tiers (%mor, %gra, %pho, etc.).
  • No other speakers – utterance blocks from non-selected speakers are omitted entirely.

The filtered view is a virtual document – it does not modify the original file on disk. The language server still processes it, so you get full validation, highlighting, and hover information even in the filtered view.

Use Cases

  • Focus on child speech: filter to CHI only to review a child’s productions without the surrounding adult turns.
  • Examine interviewer questions: filter to INV to see just the interviewer’s prompts.
  • Compare two speakers: select both CHI and MOT to see the mother-child dyad without other participants.
  • Quick utterance review: combine with the Document Symbols outline to jump between the filtered speaker’s utterances.

Scoped Find

Status: Current Last updated: 2026-03-30 13:40 EDT

VS Code’s built-in Find (Cmd+F) searches the entire file as raw text. Scoped Find adds CHAT-aware filtering: search within specific tiers, specific speakers, or both. This replaces the tedious workflow of visually scanning tier lines or using fragile regex patterns that try to match CHAT structure.

How to Use

  1. Open a .cha file.
  2. Invoke the command using any of these methods:
    • Context menu: right-click in the editor, then select Find in Tier…
    • Command Palette: Cmd+Shift+P (macOS) / Ctrl+Shift+P (Windows/Linux), then type TalkBank: Find in Tier
  3. A picker appears with two filter dimensions:
    • Tier type: main tier (*SPK:), %mor, %gra, %pho, %sin, %act, %cod, %com, %exp, or all tiers.
    • Speaker: optionally restrict the search to utterance blocks belonging to one or more speakers.
  4. Enter your search query. Supports both plain text and regex (prefix with / for regex mode).
  5. Results appear in a QuickPick list with clickable matches that navigate directly to the matching location in the file.

Search Modes

Plain Text

Type a word or phrase to find exact matches within the selected tier scope. The search is case-insensitive by default.

Example: searching for cookie in the %mor tier finds all morphological items containing “cookie” (e.g., n|cookie, n|cookie-PL).

Regex

Prefix your query with / to use regular expression matching.

Example: searching for /v\|want in the %mor tier finds all verb entries for “want” (matching v|want, v|want-PAST, etc.).

Use Cases

  • Find a word in one speaker’s speech: select the main tier, filter to CHI, and search for cookie. This finds only the child’s productions of that word, ignoring adult uses.
  • Search %mor for POS tags: select the %mor tier and search for adj| to find all adjective uses across the file.
  • Find specific dependency relations: select the %gra tier and search for SUBJ to find all subject relations.
  • Search annotations: select %cod or %act to search within coding or speech act tiers only.

How It Works

The scoped find uses the parsed ChatFile model to determine tier boundaries. It identifies which spans of the document belong to each tier type and each speaker, then searches raw text only within those spans. This means it never produces false matches from other tiers or headers – unlike a raw regex search that might accidentally match a %mor pattern appearing in a comment or header.

Results appear in a VS Code QuickPick list. Selecting a result navigates the cursor to the exact location of the match.

Comparison with Legacy CLAN

Legacy CLAN has no equivalent to scoped find. Users must use the generic find-and-replace dialog without any tier or speaker awareness, relying on manual inspection to filter irrelevant matches. The scoped find feature brings the semantic awareness of CLAN’s KWAL and COMBO search commands into the interactive editor experience.

Media Playback

Status: Current Last updated: 2026-03-30 13:40 EDT

Play audio or video files linked via @Media: headers directly inside VS Code. The extension sources timing segments from the language server’s alignment sidecar when available, falling back to direct bullet parsing (bulletParser.ts) when the LSP is unavailable.

Play at Cursor

Shortcut: Cmd+Shift+Enter (macOS) / Ctrl+Shift+Enter (Windows/Linux)

Plays the single bullet segment nearest to the cursor position. A media panel opens in the editor showing playback controls. The segment is determined by the timing bullet on or nearest to the current editor line — for example, if the cursor is on a *CHI: line containing go home ., playback covers the time range in that line’s bullet.

(SCREENSHOT: Media panel with speed slider and playback controls) Capture this: open a .cha file with media, press Cmd+Shift+Enter, and screenshot the media panel showing the speed slider, play/pause button, and time display.

Continuous Play

Shortcut: Cmd+Shift+/ (macOS) / Ctrl+Shift+/ (Windows/Linux)

Plays all segments from the cursor position through to the end of the file. The editor selection tracks the currently-playing utterance line, scrolling the document to keep the active line visible.

Segments play sequentially in document order (top to bottom), not sorted by media time. When one segment finishes, playback seeks to the next segment’s start time.

[VIDEO: 30s demo of continuous playback) Capture this: start continuous playback from the middle of a multi-speaker file. Show the editor cursor advancing through utterance lines as each segment plays.

Playback Speed

The media panel toolbar includes a speed slider ranging from 0.25x to 2x. Drag the slider to adjust playback speed — the current rate is shown next to the slider. This is particularly useful for slowing down fast speech during transcription or alignment review.

Rewind

Shortcut: F8

Rewinds playback by a configurable number of seconds (default: 2). The rewind amount is controlled by the talkbank.transcription.rewindSeconds setting:

{
  "talkbank.transcription.rewindSeconds": 3
}

The minimum value is 0.5 seconds. Rewind works during both single-segment and continuous playback.

Loop

Shortcut: F5

Toggles looping of the current segment. When enabled, the current bullet’s time range repeats automatically. The loop count is controlled by talkbank.walker.loopCount:

ValueBehavior
1Play once (no looping)
3Play three times, then advance
0Loop indefinitely until manually stopped

Playback State Machine

The following diagram shows the states a playback session transitions through:

stateDiagram-v2
    [*] --> Idle
    Idle --> Loading : Cmd+Shift+Enter\nor Cmd+Shift+/
    Loading --> Playing : media loaded,\nsegment resolved
    Loading --> Error : media not found\nor decode failure
    Playing --> Paused : pause button
    Paused --> Playing : resume button
    Playing --> SegmentEnd : currentTime >= seg.end
    SegmentEnd --> Playing : loop count > 0\n(replay same segment)
    SegmentEnd --> Seeking : next segment exists\n(continuous mode)
    SegmentEnd --> Idle : no more segments\nor single-segment mode
    Seeking --> Playing : seek complete
    Playing --> Rewinding : F8
    Rewinding --> Playing : seek complete\n(currentTime - rewindSeconds)
    Error --> [*]
    Idle --> [*]

The playback loop in mediaPanel.js polls currentTime every 100 ms (matching CLAN’s polling interval). When currentTime >= seg.end / 1000, the loop either advances to the next segment (continuous mode), replays (loop mode), or stops (single-segment mode).

Overlapping Bullets (Cross-Speaker Overlap)

CHAT allows cross-speaker overlapping bullets — two different speakers can have utterances whose time ranges overlap, as long as utterances are ordered by start time in the file (validation rule E701). Same-speaker self-overlap beyond 500 ms is prohibited by E704.

When the file contains overlapping bullets, playback works as follows:

  • Play at Cursor plays exactly the clicked utterance’s time range. No other utterances are affected.

  • Continuous Play plays each utterance in full, in document order. If speaker A’s utterance covers 0–3.5s and speaker B’s covers 2–2.5s, you hear A’s full range, then B’s full range. The overlapping audio region (2–2.5s) is heard twice — once as part of each speaker’s turn. This matches CLAN behavior and lets you hear each speaker’s complete utterance without truncation.

  • Waveform overlays render each segment independently. Overlapping segments appear as stacked colored bars on the waveform (see Waveform Visualization).

This behavior is intentional. For transcripts with frequent backchannels and short overlapping speech (common in aphasia protocols, conversation analysis, and multi-party recordings), hearing each speaker’s complete turn in sequence is more useful than trying to play simultaneous audio streams.

Requirements

  1. The .cha file must contain an @Media: header specifying the filename and type (audio or video). For example: @Media: interview, audio
  2. The referenced media file must be findable by the extension’s media resolver — see Media Resolution for the full search order.
  3. The media format must be playable by the browser engine embedded in VS Code (typically MP3, WAV, MP4, WebM).

Stop Playback

Use the Command Palette (Cmd+Shift+P) and search for TalkBank: Stop Playback to halt any active playback session.

See Also

Waveform Visualization

Status: Current Last updated: 2026-03-30 13:40 EDT

Visualize the audio waveform of the linked media file with timed utterance overlays. The waveform panel uses the Web Audio API to decode and render audio amplitude data, with colored segments marking each speaker’s bullet ranges.

Opening the Waveform

Shortcut: Cmd+Shift+W (macOS) / Ctrl+Shift+W (Windows/Linux)

  1. Open a .cha file with an @Media: header
  2. Press Cmd+Shift+W
  3. A waveform panel opens showing the audio amplitude as a continuous waveform trace

The waveform panel is a singleton — pressing the shortcut again when the panel is already open brings it to focus rather than creating a duplicate.

(SCREENSHOT: Waveform panel with colored bullet segments) Capture this: open a multi-speaker .cha file with media, press Cmd+Shift+W, and screenshot the waveform panel showing colored segment overlays for different speakers. Ensure at least two speaker colors are visible.

Colored Segment Overlays

Each bullet segment is drawn as a colored overlay on the waveform. Speakers are assigned distinct colors, so you can visually distinguish which parts of the audio belong to which participant. Overlapping bullets from different speakers appear as stacked colored bars (see Playback — Overlapping Bullets).

Interaction

Click to Seek

Click anywhere on the waveform to seek to that time position. When you click:

  • The media panel (if open) seeks to the clicked time
  • The editor cursor moves to the utterance line nearest to that time
  • If playback is active, it continues from the new position

This makes it easy to jump to a specific point in the audio by visual inspection of the waveform shape.

Coordination with Media Panel

The waveform and media panels are fully coordinated:

  • Playing a segment highlights it in the waveform
  • Seeking in the waveform updates the media panel position
  • The currently-playing segment is visually distinguished (brighter overlay or outline) so you can track playback progress

Zoom

The waveform panel provides several zoom controls:

ControlAction
Zoom in/out buttonsToolbar buttons for incremental zoom steps
Zoom sliderContinuous zoom from 100% to 2000%
Fit buttonReset zoom to 100% (fit entire waveform in view)
Mouse wheelScroll wheel zooms in/out, centered on the pointer position

Pointer-Centered Zoom

Mouse wheel zooming is centered on the pointer position. This means the point under your cursor stays fixed as you zoom in, allowing precise navigation to a specific region of the audio. This is particularly useful when examining a short segment within a long recording.

Scroll

When zoomed in beyond the panel width, the waveform scrolls horizontally:

  • Manual scroll: Use the horizontal scrollbar or shift+scroll wheel
  • Auto-scroll during playback: When playback is active, the view automatically scrolls to keep the current segment visible. The scroll follows the playback position so you never lose sight of what is currently playing.

Typical Workflow

  1. Open a .cha file and press Cmd+Shift+W to open the waveform
  2. Press Cmd+Shift+Enter to open the media panel
  3. Click on the waveform to jump to a region of interest
  4. Zoom in with the scroll wheel for fine-grained inspection
  5. Use Walker Mode (Alt+Down / Alt+Up) to step through utterances while watching the waveform highlight each segment

See Also

Walker Mode

Status: Current Last updated: 2026-03-30 13:40 EDT

Step through utterances one segment at a time, playing each segment’s audio automatically. Walker mode is the primary tool for reviewing transcript alignment — verifying that each utterance’s timing bullet matches the actual speech in the audio.

Keyboard Shortcuts

ShortcutAction
Alt+DownStep to the next utterance
Alt+UpStep to the previous utterance

Each step moves the editor cursor to the corresponding utterance line. If media is available and auto-play is enabled, the segment’s audio plays immediately.

[VIDEO: 30s demo of walker stepping through utterances) Capture this: open a .cha file with media and the waveform panel. Press Alt+Down repeatedly, showing the cursor advancing through utterance lines while each segment’s audio plays and the waveform highlights the active segment.

How It Works

  1. The walker maintains a pointer to the current utterance index in the document
  2. Pressing Alt+Down advances the pointer and moves the editor cursor to the next main tier line (a line starting with *SPK:)
  3. If talkbank.walker.autoPlay is enabled, the utterance’s bullet segment plays automatically
  4. The waveform panel (if open) highlights the active segment
  5. After playback completes, the walker waits for the next keypress (or pauses for pauseSeconds in continuous mode)

Configuration

All walker settings live under the talkbank.walker namespace:

SettingTypeDefaultDescription
talkbank.walker.autoPlaybooleantrueAutomatically play the media segment when stepping to a new utterance
talkbank.walker.loopCountinteger1Number of times to loop each segment before allowing advance. 0 = loop indefinitely until manually advanced.
talkbank.walker.pauseSecondsnumber0Seconds to pause between segments during continuous playback
talkbank.walker.walkLengthinteger0Number of utterances to play continuously. 0 = all remaining utterances from the current position.

Example Configuration

To set up the walker for careful alignment review — playing each segment twice with a 1-second pause between utterances:

{
  "talkbank.walker.autoPlay": true,
  "talkbank.walker.loopCount": 2,
  "talkbank.walker.pauseSeconds": 1
}

To step through utterances without audio (text-only review):

{
  "talkbank.walker.autoPlay": false
}

Document Change Reset

The walker resets its position when the active document changes. Switching to a different .cha file or a non-CHAT document clears the walker state. When you return to the original file, stepping resumes from wherever the cursor is positioned.

Tips

  • Combine with the waveform panel (Cmd+Shift+W) for visual feedback — you can see the highlighted segment on the waveform as you step through utterances.

  • Use with Review Mode for systematic post-alignment review. The Post-Alignment Review workflow describes the full process of verifying that timing bullets match the actual speech.

  • Adjust loop count for difficult segments. When reviewing a transcript with fast or unclear speech, set loopCount to 2 or 3 so each segment replays automatically before advancing.

  • Use walkLength for focused review. If you only need to review the next 10 utterances, set walkLength to 10. The walker stops after that many steps rather than continuing to the end of the file.

See Also

Transcription Mode

Status: Current Last updated: 2026-03-30 13:40 EDT

Create new CHAT transcripts from audio by typing utterance text while the audio plays, then stamping timing bullets at utterance boundaries. Transcription mode turns VS Code into a transcription workstation — the CLAN equivalent of Transcribe mode.

Prerequisites

The .cha file must contain an @Media: header pointing to an audio or video file. The media file must be findable by the extension’s resolver (see Media Resolution). A minimal starting file looks like:

@UTF8
@Begin
@Languages: eng
@Participants: CHI Target_Child
@Media: interview, audio
*CHI:
@End

Starting Transcription Mode

  1. Open the .cha file
  2. Open the Command Palette (Cmd+Shift+P)
  3. Search for TalkBank: Start Transcription Mode
  4. Audio begins playing from the start of the file

The waveform panel opens automatically to provide visual reference.

Workflow

The core transcription loop is:

  1. Listen to the audio as it plays
  2. Type the speaker’s utterance text on the current *SPK: line
  3. Press F4 to stamp a timing bullet and advance

When you press F4:

  • A timing bullet is inserted at the end of the current utterance line, marking the time span from the previous stamp (or file start) to the current playback position: go home .
  • The cursor automatically advances to a new *SPK: line, ready for the next utterance
  • The speaker code on the new line uses the configured default speaker
  1. Repeat until the recording is complete
  2. Open the Command Palette and search for TalkBank: Stop Transcription Mode

[VIDEO: 60s demo of transcription workflow with F4 stamping) Capture this: start transcription mode on a short audio file. Type 3-4 utterances, pressing F4 after each one. Show the timing bullets being inserted and new speaker lines being created. Include the waveform panel in the recording.

Key Commands During Transcription

ShortcutCommandDescription
F4Stamp timing bulletInsert bullet with current time range, advance to next line
F8RewindJump back by rewindSeconds to re-listen
F5LoopToggle looping of the current playback region

Configuration

SettingTypeDefaultDescription
talkbank.transcription.defaultSpeakerstringCHISpeaker code inserted on new utterance lines (e.g., CHI, MOT, INV)
talkbank.transcription.rewindSecondsnumber2Seconds to rewind when pressing F8. Minimum: 0.5

Example Configuration

For an investigator transcribing an adult interview:

{
  "talkbank.transcription.defaultSpeaker": "INV",
  "talkbank.transcription.rewindSeconds": 3
}

Foot-Pedal and Custom Keybindings

USB foot pedals are commonly used in transcription to control playback without taking hands off the keyboard. To remap transcription keys for a foot pedal:

  1. Open the Command Palette (Cmd+Shift+P)
  2. Search for TalkBank: Configure Transcription Keybindings
  3. The VS Code keybindings editor opens, pre-filtered to TalkBank transcription commands
  4. Click the pencil icon next to any command to assign a new keybinding

This is the recommended way to map foot-pedal buttons to F4 (stamp), F8 (rewind), and F5 (loop). Most USB foot pedals present as standard keyboard devices and can be mapped to any VS Code keybinding.

See Keyboard Shortcuts for more on customizing keybindings.

Tips

  • Use the waveform panel for visual reference. The waveform shows where you are in the recording and helps you identify utterance boundaries by looking at silence gaps between speech regions.

  • Rewind generously. The default 2-second rewind (F8) lets you re-hear the end of an utterance. For fast or overlapping speech, increase rewindSeconds to 3 or 4.

  • Slow down playback using the speed slider on the media panel. Dropping to 0.5x or 0.75x gives you more time to type without pausing.

  • Edit timing after transcription. Transcription mode creates an initial pass. Use Walker Mode afterward to review each segment and adjust timing bullets as needed.

See Also

Media Resolution

Status: Current Last updated: 2026-03-30 13:40 EDT

This chapter explains how the extension locates media files referenced by CHAT transcripts. Understanding the resolution process helps when media playback fails or when organizing corpus files on disk.

The @Media Header

Every .cha file that uses media features must include an @Media: header specifying the base filename and the media type:

@Media: interview, audio
@Media: session03, video

The header provides two pieces of information:

  1. Base filename — the name without extension (e.g., interview)
  2. Media type — either audio or video, which determines the file extensions the resolver tries

For audio, the resolver looks for: .mp3, .wav, .m4a, .ogg, .flac

For video, the resolver looks for: .mp4, .mov, .avi, .webm, .mkv

Resolution Search Order

When you trigger playback or open the waveform, the extension’s media resolver (mediaResolver.ts) searches for the media file in the following order:

  1. Same directory as the .cha file
  2. media/ subdirectory — a sibling media/ folder next to the .cha file
  3. Configured media roots — additional search paths from extension settings

At each location, the resolver tries every applicable file extension for the declared media type. The first match wins.

Example

Given a file at /corpus/English/Brown/Adam/adam01.cha with header @Media: adam01, audio, the resolver searches:

/corpus/English/Brown/Adam/adam01.mp3
/corpus/English/Brown/Adam/adam01.wav
/corpus/English/Brown/Adam/adam01.m4a
  ...
/corpus/English/Brown/Adam/media/adam01.mp3
/corpus/English/Brown/Adam/media/adam01.wav
/corpus/English/Brown/Adam/media/adam01.m4a
  ...

@Media file references are rendered as clickable links in the editor. If the resolver finds the media file, clicking the link opens it directly in the system’s default media player (or in VS Code’s built-in preview for supported formats).

This provides a quick way to verify that the media file exists and is accessible without starting playback mode.

Picture Display

The extension can display elicitation pictures (Cookie Theft, picture descriptions, etc.) alongside the transcript — the CLAN equivalent of PictController.

Opening a Picture

  1. Open a .cha file
  2. Right-click in the editor and select TalkBank: Media then Show Elicitation Picture, or use the Command Palette

How Pictures Are Found

The extension searches for associated images using three strategies, in order:

  1. %pic: tier references — if the document contains dependent tier lines like %pic: "image002.jpg", those filenames are used directly

  2. Same-name image files — image files matching the .cha file’s base name (e.g., adam01.cha matches adam01.jpg, adam01.png)

  3. Directory contents — any image files in the same directory as the .cha file

Multi-Image Picker

If multiple candidate images are found, a picker dialog appears listing all matches. Select the desired image to display it.

Display

The selected picture opens in a side panel and scales to fit the available space. You can arrange the picture panel alongside the transcript and waveform for a complete annotation workspace.

Troubleshooting

When media playback or the waveform panel fails to open, check the following:

1. Verify the @Media Header

The .cha file must contain a valid @Media: line. Check for:

  • Missing header entirely — add @Media: filename, audio (or video)
  • Typos in the filename — the base name must match the actual media file
  • Wrong media type — audio vs video must match the actual file format

2. Check That the File Exists

Verify that the media file is present in one of the resolver’s search locations (same directory or media/ subdirectory). The filename must match the @Media base name exactly (case-sensitive on macOS and Linux).

3. Check the File Extension

The media file must have a recognized extension for its type. An .mp3 file declared as video in the @Media header will not be found — the resolver only tries video extensions (.mp4, .mov, etc.) for video-typed headers.

4. Check the Output Panel

Open the Output panel in VS Code (Cmd+Shift+U) and select the TalkBank channel. The extension logs media resolution attempts and failures here, showing exactly which paths were searched and why resolution failed.

5. Large or Unsupported Formats

VS Code’s embedded browser engine (Chromium) has limits on media format support. If the file exists but playback fails, the format may not be supported. Common working formats are MP3, WAV, and MP4 (H.264). Uncommon codecs (e.g., FLAC in some configurations, AVI with legacy codecs) may not play.

See Also

Running CLAN Commands

Status: Current Last updated: 2026-04-16 16:55 EDT

The TalkBank extension includes 33 CLAN analysis commands that run directly inside VS Code. No external CLAN binary is needed – all analysis runs inside the Rust language server.

Pipeline

Every analysis command — single-file or directory — follows the same pipeline:

flowchart LR
    u["User<br/>(context menu / Command Palette)"]
    qp{{"QuickPick<br/>(33 commands)"}}
    needsIn{"Command<br/>needs extra<br/>input?"}
    inbox["Input dialog<br/>(text / file picker)"]
    cancel[["Cancel → exit"]]
    scope{"Single file<br/>or directory?"}
    walk["Recursive walk<br/>resolve_files()"]
    dispatch["LSP talkbank/analyze<br/>(typed request)"]
    runner["AnalysisRunner<br/>(clan-core crate)"]
    result["JSON result<br/>(typed Response)"]
    panel["AnalysisPanel<br/>webview"]

    u --> qp
    qp --> needsIn
    needsIn -->|"yes (6 commands)"| inbox
    needsIn -->|"no"| scope
    inbox -->|"user provides"| scope
    inbox -->|"user cancels"| cancel
    scope -->|"single file"| dispatch
    scope -->|"directory"| walk
    walk --> dispatch
    dispatch --> runner
    runner --> result
    result --> panel
    panel -->|"Export CSV"| csv(["CSV file<br/>on disk"])

The six commands that prompt for extra input are listed below under Commands that prompt for additional input. Directory analysis reuses the single-file dispatch per file and aggregates results in the panel.

Running an analysis on a single file

  1. Open a .cha file in the editor.
  2. Right-click anywhere in the editor and choose Run CLAN Analysis…, or open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and select TalkBank: Run CLAN Analysis.
  3. A QuickPick list appears with all 33 commands. Each entry shows a short description of what the command computes.
  4. Select a command. If the command requires additional input (see below), a follow-up dialog appears.
  5. Results open in a styled side panel with stat cards, tables, and proportional bar charts.

(SCREENSHOT: Context menu showing “Run CLAN Analysis…” and “Run KidEval…” entries) Capture this: right-click in an open .cha file to show the full context menu with CLAN analysis options

Running an analysis on a directory

To analyze all .cha files in a directory at once:

  1. In the Explorer sidebar, right-click a folder.
  2. Choose Run CLAN Analysis on Directory….
  3. Select an analysis command from the picker.
  4. The language server walks the directory tree recursively and runs the command on every .cha file it finds.
  5. Results from all files are aggregated in a single analysis panel.

This is equivalent to CLAN’s batch-mode directory analysis and is useful for computing corpus-wide statistics such as total vocabulary or mean MLU across participants.

The analysis panel

Results display in a WebviewPanel with several presentation elements:

  • Stat cards at the top showing key numeric results (e.g., MLU value, type-token ratio, total word count) with color-coded indicators.
  • Tables with sortable columns for detailed breakdowns (e.g., word frequencies, speaker-level statistics).
  • Proportional bar charts for visual comparison of values across speakers or categories.

(SCREENSHOT: Analysis panel showing FREQ results with stat cards at top and frequency table below) Capture this: run FREQ on a multi-speaker .cha file, showing the stat cards and word frequency table

Exporting results as CSV

Click the Export CSV button in the analysis panel toolbar. A save dialog appears where you choose the output location and filename. The CSV file includes:

  • All stat card values as key-value rows.
  • All visible tables with their column headers.

The CSV format is compatible with Excel, Google Sheets, R (read.csv()), and Python (pandas.read_csv()). For longitudinal studies, export results from multiple files and combine them in a spreadsheet.

Commands that prompt for additional input

Six commands require information beyond the .cha file itself. After selecting one of these commands, a follow-up dialog appears:

CommandInput typeWhat to provide
kwalText input boxOne or more keywords, space-separated. The command finds all utterances containing any of the keywords.
comboText input boxA Boolean search expression. Use + for AND (want+cookie finds utterances with both words) and , for OR (want,milk finds utterances with either word).
keymapText input boxKeyword codes, space-separated. Used for contingency mapping of coded behaviors.
mortableFile pickerA morpheme script file (.cut) defining the morpheme categories to tabulate.
scriptFile pickerA template CHAT file (.cha) to compare the current transcript against.
relyFile pickerA second CHAT file (.cha) for computing inter-rater reliability.

For mortable, script, and rely, the file picker dialog opens to the current workspace folder by default. You can navigate to any location on disk.

Tips

  • Run the same command on multiple files by using the directory analysis option. This is faster than running the command file by file.
  • Compare speakers within a single file: most commands (FREQ, MLU, MLT, VOCD) break results out per speaker automatically.
  • Combine with speaker filtering: use Speaker Filtering first to focus on specific participants, then run an analysis.
  • Save CSV exports with consistent naming conventions for longitudinal data collection (e.g., CHI_freq_session01.csv).

Next steps

Profiling Commands

Status: Current Last updated: 2026-03-30 13:40 EDT

Profiling commands measure structural properties of a speaker’s language: how long their utterances are, how diverse their vocabulary is, and how grammatically complex their sentences are. These are the most commonly used measures in child language research and clinical assessment.

All profiling commands are available through the standard analysis workflow described in Running CLAN Commands.

MLU – Mean Length of Utterance

What it measures: The average number of morphemes per utterance. MLU is the single most widely used index of grammatical development in young children (Brown, 1973).

How it works: The command counts morphemes on the %mor tier for each utterance, then computes the mean across all utterances for each speaker. If the file has no %mor tier, it falls back to counting words on the main tier.

When to use it:

  • Tracking grammatical development in children aged 1;6 to 5;0
  • Screening for language delay (compare against age norms)
  • As a baseline measure in treatment studies

Results include:

  • MLU value per speaker
  • Total morphemes and total utterances
  • Standard deviation

Tip: MLU is most meaningful for children in Brown’s Stages I–V (roughly MLU 1.0 to 4.5). Beyond MLU 4.5, it becomes less sensitive to development. For older children, consider DSS or IPSyn instead.

MLT – Mean Length of Turn

What it measures: The average number of utterances per conversational turn, along with words per turn. A “turn” is a sequence of consecutive utterances by the same speaker, bounded by utterances from other speakers.

How it works: The command identifies turn boundaries by detecting speaker changes, counts the utterances and words in each turn, and computes the mean.

When to use it:

  • Measuring conversational participation and verbosity
  • Comparing turn-taking patterns between child and adult speakers
  • Assessing pragmatic language skills

Results include:

  • Mean utterances per turn
  • Mean words per turn
  • Total turns per speaker

VOCD – Vocabulary Diversity

What it measures: The D statistic, a measure of vocabulary diversity that is more robust than a simple type-token ratio (TTR). TTR is heavily influenced by sample size – longer samples always produce lower TTR. VOCD corrects for this by computing D from random subsamples of varying sizes.

How it works: The command draws random subsamples of 35 to 50 tokens from the transcript, computes TTR for each subsample size, and fits a mathematical curve to the TTR-by-sample-size function. The D parameter of that curve is the vocabulary diversity score. Higher D indicates greater diversity.

When to use it:

  • Comparing lexical diversity across speakers or sessions with different sample lengths
  • Research on vocabulary development
  • Any context where simple TTR would be misleading due to unequal sample sizes

Results include:

  • D statistic per speaker
  • Type count and token count
  • Type-token ratio (for reference, but prefer D)

Tip: VOCD requires a minimum of about 50 tokens to produce a reliable estimate. Very short transcripts will produce unstable D values.

DSS – Developmental Sentence Scoring

What it measures: Grammatical complexity of children’s sentences, scored against a weighted system of eight grammatical categories: indefinite pronouns, personal pronouns, main verbs, secondary verbs, negatives, conjunctions, interrogative reversals, and wh-questions. Each sentence receives a total score; the mean across sentences is the DSS score.

How it works: The command applies scoring rules from the DSS rule files in the clan-info library. Only complete sentences with a subject and predicate are scored. Incomplete utterances, single-word responses, and imitations are excluded.

Language support: DSS rules are available for:

FileLanguage
eng.cutAmerican English
engu.cutBritish English
bss.cutBilingual Syntax Score
jpn.cutJapanese

When to use it:

  • Assessing grammatical maturity in children aged 2;0 to 6;11
  • Identifying children with specific language impairment
  • Measuring treatment outcomes for grammar intervention

Results include:

  • DSS score per speaker
  • Per-sentence scores
  • Category-level breakdown (pronouns, verbs, negatives, etc.)
  • Number of sentences scored vs. excluded

Tip: DSS is designed for English-speaking children. For non-English transcripts, consider language-specific profiling tools or use MLU as a general measure.

IPSyn – Index of Productive Syntax

What it measures: The range of syntactic and morphological structures a child produces, based on a checklist of 56 (or 100) language forms across four categories: noun phrases, verb phrases, questions/negation, and sentence structure.

How it works: The command searches the first 100 utterances for evidence of each structure on the checklist. Each structure earns 0 points (not found), 1 point (found once), or 2 points (found in two different utterances). The maximum score is 112 (56-item version) or 200 (100-item version).

Rule files:

FileDescription
eng.cutStandard 56-item English checklist
eng-100.cutExtended 100-item English checklist

When to use it:

  • Assessing syntactic development in children aged 1;6 to 4;6
  • Complementing MLU with a more detailed structural profile
  • Research on syntactic bootstrapping and acquisition order

Results include:

  • Total IPSyn score per speaker
  • Subscale scores (noun phrase, verb phrase, question/negation, sentence)
  • Per-item breakdown showing which structures were found

Tip: IPSyn samples only the first 100 utterances. If your transcript is longer, only the first 100 contribute to the score. This is by design – the measure assesses productive range, not frequency.

Choosing the right profiling command

QuestionUse
How long are the child’s utterances?MLU
How much does the child talk per turn?MLT
How varied is the child’s vocabulary?VOCD
How grammatically complex are the sentences?DSS
What syntactic structures does the child produce?IPSyn
Quick overall snapshot?Run MLU + VOCD together
Detailed clinical profile?Run all five, then use KidEval for normative comparison

Next steps

Frequency & Distribution

Status: Current Last updated: 2026-03-30 13:40 EDT

Frequency and distribution commands count words, morphemes, and phonological segments in various ways. They answer questions like “what words appear most often?”, “how long are the words?”, and “which parts of speech dominate the transcript?”.

All commands in this chapter are available through the standard analysis workflow described in Running CLAN Commands.

FREQ – Word Frequency

What it does: Counts every word in the transcript and reports the frequency of each unique word form, organized by speaker.

Results include:

  • Total word tokens (running words)
  • Total word types (unique forms)
  • Type-token ratio (TTR)
  • Frequency table: each word with its count, sorted by frequency

FREQ is the most general-purpose counting command. It is the starting point for any lexical analysis.

Tip: Use the CSV export to load frequency lists into a spreadsheet for further analysis. Sort by frequency to find the most common words, or alphabetically to check for spelling variants.

FREQPOS – Frequency by Part of Speech

What it does: Counts word frequency broken down by part-of-speech categories from the %mor (morphology) tier.

Requires: A %mor tier. Files without morphological coding will produce empty results.

Results include:

  • Frequency table with columns for word, POS tag, and count
  • Summary counts per POS category (nouns, verbs, adjectives, etc.)

When to use it: When you need to know not just which words appear, but what grammatical roles they fill. For example, a child might have a large vocabulary but use very few verbs – FREQPOS reveals this.

WDLEN – Word Length Distribution

What it does: Reports the distribution of word lengths (measured in characters) across the transcript.

Results include:

  • Histogram of word lengths (how many 1-character words, 2-character words, etc.)
  • Mean word length per speaker
  • Longest and shortest words

When to use it: For phonological or literacy research where word length is a variable of interest. Comparing word length distributions across sessions can reveal changes in lexical sophistication.

WDSIZE – Vocabulary Size

What it does: Reports the cumulative vocabulary size – the number of unique word types – as the transcript progresses. This produces a vocabulary growth curve.

Results include:

  • Total vocabulary size per speaker
  • Character length statistics from %mor stems

When to use it: For studying vocabulary growth over the course of a session or across sessions. The growth curve shows whether a speaker is still introducing new words or has plateaued.

MAXWD – Longest Words

What it does: Finds and lists the longest words produced by each speaker, measured in characters.

Results include:

  • Longest words per speaker, with their lengths
  • Context (the utterance in which each word appeared)

When to use it: As a quick check on lexical sophistication, or to spot transcription errors (unusually long “words” may be coding mistakes or missing spaces).

PHONFREQ – Phonological Frequency

What it does: Counts the frequency of phonological segments on the %pho (phonology) tier.

Requires: A %pho tier with IPA transcription.

Results include:

  • Frequency of each phoneme
  • Consonant vs. vowel breakdown
  • Position-in-word analysis (initial, medial, final)

When to use it: For phonological research, speech sound disorder assessment, or studying sound acquisition patterns.

MODREP – Model and Replica

What it does: Analyzes imitation patterns by comparing a child’s utterance (the replica) to the preceding adult utterance (the model). Reports what was repeated exactly, what was modified, and what was added or omitted.

When to use it: For studying imitation and language learning strategies. High exact-imitation rates in older children may indicate echolalia; selective imitation patterns reveal what structures the child is actively acquiring.

COOCCUR – Co-occurrence

What it does: Counts how often pairs of words appear together in the same utterance.

Results include:

  • Co-occurrence matrix or list of word pairs with counts
  • Statistical association measures

When to use it: For studying lexical associations, collocations, and formulaic language. Useful in discourse analysis and vocabulary research.

DIST – Distribution

What it does: Analyzes the distribution and dispersion of specific words or codes across the transcript, showing where they cluster and where they are absent.

Results include:

  • Distribution profile across utterance ranges
  • Clustering and dispersion statistics

When to use it: For studying whether certain words or behaviors are evenly distributed or concentrated in particular portions of the interaction.

UNIQ – Unique Forms

What it does: Identifies repeated and unique utterances in the transcript. Finds exact duplicates and near-duplicates.

Results include:

  • List of repeated utterances with their frequencies
  • Unique utterances (appearing only once)

When to use it: For identifying stereotyped or formulaic language, echolalia, or transcription artifacts (accidental duplicate lines).

Working with frequency results

CSV export for spreadsheet analysis

All frequency commands support CSV export via the Export CSV button in the analysis panel. This is the recommended workflow for quantitative analysis:

  1. Run FREQ (or another frequency command) on a file or directory.
  2. Click Export CSV in the panel toolbar.
  3. Open the CSV in your analysis tool of choice.

Combining with other commands

Frequency results are most informative when paired with profiling measures:

  • Run FREQ to get raw counts, then VOCD to get a sample-size-corrected diversity measure. See Profiling Commands.
  • Run FREQPOS to see POS distributions, then MLU to relate vocabulary composition to utterance length.
  • Use FREQ on a directory to get corpus-wide word lists for normative comparison or stimuli selection.

Next steps

Assessment Tools

Status: Current Last updated: 2026-03-30 16:35 EDT

The assessment tools — KidEval, Eval, and Eval-D — compare a speaker’s language measures against normative databases. Instead of reporting raw numbers (MLU = 3.2), they tell you how that number compares to age-matched peers (z-score = -1.5, below average).

These tools have dedicated commands and panels, separate from the general CLAN analysis picker. They are the primary workflow for clinical assessment and research benchmarking.

KidEval — Child Language Assessment

KidEval computes a battery of language measures (MLU, MLT, VOCD, DSS, IPSyn, and others) and compares each against normative data from age-matched, typically developing children.

How to run KidEval

  1. Open a .cha file containing child language data.
  2. Right-click in the editor and choose Run KidEval, or use the Command Palette: TalkBank: Run KidEval.
  3. The KidEval panel opens with characteristic selection controls.

Step 1: Select language and activity

The panel presents two cascading dropdowns:

  • Language — choose the child’s language (English NA, English UK, French, Japanese, Spanish, Chinese, Dutch, Cantonese)
  • Activity Type — choose the data collection context (Narrative or Toyplay). The activity dropdown filters automatically to show only the activities available for the selected language.

If the CHAT file has @Languages and @Types headers, the panel auto-detects the correct language and activity — no manual selection needed.

Below the dropdowns, an availability grid shows all language × activity combinations at a glance. Click any cell to select it:

              Narrative    Toyplay
English (NA)   ● 375       ● 3503
English (UK)   ● 540       ● 3459
French         ● 399       ● 627
Japanese       —            ● 167
Spanish        ● 106       ● 936
Chinese        ● 392       ● 1196
Dutch          —            —       *
Cantonese      —            —       *

● = available with sample count
— = not available
* = general database (not activity-specific)

When a language + activity is selected, a matching database card appears showing the database filename, sample count, and metadata.

The activity type matters: narrative tasks (story retelling, picture description) and toy play sessions elicit different language patterns. Use the database that matches your data collection protocol.

Step 2: Filter by age and gender

After selecting language and activity, you can optionally narrow the comparison group:

  • Gender — filter to Male, Female, or All
  • Age range — enter minimum and maximum age in months to compare against children within a specific developmental window

Leave these fields blank to compare against the full normative sample.

(SCREENSHOT: KidEval panel showing language/activity dropdowns, availability grid with a cell selected, and the matching database card) Capture this: open the KidEval panel on an English narrative .cha file. The language should be auto-selected to “English (NA)”, activity to “Narrative”, and the grid should highlight the eng/narrative cell. Show the matching database card below.

Available KidEval databases

LanguageActivityDatabaseSamples
English (NA)Narrativeeng_narrative_db.cut375
English (NA)Toyplayeng_toyplay_db.cut3,503
English (UK)Narrativeengu_narrative_db.cut540
English (UK)Toyplayengu_toyplay_db.cut3,459
FrenchNarrativefra_narrative_db.cut399
FrenchToyplayfra_toyplay_db.cut627
JapaneseToyplayjpn_toyplay_db.cut167
SpanishNarrativespa_narrative_db.cut106
SpanishToyplayspa_toyplay_db.cut936
ChineseNarrativezho_narrative_db.cut392
ChineseToyplayzho_toyplay_db.cut1,196
DutchGeneralnld.cut121
CantoneseGeneralyue.cut49

Step 3: View results

Click Run to compute the analysis. Results appear in three sections:

Stat cards at the top of the panel show the key measures with color-coded z-score indicators:

  • Green: within 1 standard deviation of the mean (typical range)
  • Yellow: between 1 and 2 standard deviations (borderline)
  • Red: more than 2 standard deviations from the mean (atypical)

(SCREENSHOT: KidEval results showing stat cards with z-scores and comparison table) Capture this: KidEval results after clicking “Compare with Database” for an English narrative file, showing colored stat cards (green/red z-scores) and the detailed comparison table with Measure, Child, Norm Mean, Norm SD, z-score, N columns.

Comparison table showing side-by-side values:

MeasureChildNorm MeanNorm SDz-score
MLU3.213.850.62-1.03
MLT1.451.800.35-1.00
VOCD42.355.112.4-1.03
……………

Per-measure breakdown with additional detail for each metric.

What each measure means

MeasureWhat it captures
MLUUtterance length in morphemes (grammatical complexity)
MLTUtterances per turn (conversational participation)
VOCDVocabulary diversity (D statistic)
DSSDevelopmental Sentence Score (grammatical maturity)
IPSynIndex of Productive Syntax (structural range)
Total wordsOverall productivity
Total utterancesSample size
TTRType-token ratio (raw vocabulary breadth)

For detailed explanations of each measure, see Profiling Commands.

Eval – General Language Assessment

Eval uses the same panel interface as KidEval but with databases designed for adult or general language assessment rather than child development norms.

How to run Eval

  1. Open a .cha file in the editor.
  2. Right-click and choose Run Eval, or use the Command Palette: TalkBank: Run Eval.
  3. Select a database, optionally filter, and click Run.

Available Eval databases

Database fileLanguageDescription
eng_eval_db.cutAmerican EnglishGeneral adult language norms
fra_eval_db.cutFrenchGeneral adult language norms

Eval databases contain normative data from healthy adult speakers, useful as a baseline for comparing clinical populations.

Eval-D – Dementia Assessment

Eval-D is a specialized assessment mode for evaluating language in dementia contexts. It uses normative databases from DementiaBank.

How to run Eval-D

  1. Open a .cha file in the editor.
  2. Right-click and choose Run Eval-D (Dementia), or use the Command Palette: TalkBank: Run Eval-D (Dementia).
  3. Select a database and click Run.

Available Eval-D databases

Database fileLanguageDescription
eng_eval-d_db.cutAmerican EnglishDementiaBank norms (MCI, Possible AD, Probable AD, Vascular, Control)
engu_eval-d_db.cutBritish EnglishDementiaBank norms (UK)

Eval-D results use the same panel layout as KidEval and Eval: stat cards with z-scores, comparison table, and per-measure breakdown. The normative groups include control participants and multiple dementia subtypes, allowing clinicians to see where a patient falls relative to each group.

Exporting assessment results

All three assessment tools support CSV export:

  1. After results are displayed, click Export CSV in the panel toolbar.
  2. Choose a save location.
  3. The CSV includes all stat card values, z-scores, and the full comparison table.

This is useful for:

  • Importing results into a clinical report template
  • Longitudinal tracking (export at each visit, combine in a spreadsheet)
  • Research data collection across multiple participants

Choosing the right assessment tool

PopulationToolDatabase to use
Children (English, narrative task)KidEvaleng_narrative_db.cut
Children (English, toy play)KidEvaleng_toyplay_db.cut
Children (other languages)KidEvalLanguage-specific database
Healthy adults (baseline)Evaleng_eval_db.cut or fra_eval_db.cut
Adults with suspected dementiaEval-Deng_eval-d_db.cut
Adults with aphasiaEvaleng_eval_db.cut (compare against healthy norms)

Next steps

Command Reference

Status: Current Last updated: 2026-03-30 13:40 EDT

Complete reference for all 33 CLAN analysis commands available in the TalkBank extension. Run any command via right-click Run CLAN Analysis… or the Command Palette. See Running CLAN Commands for the general workflow.

All commands

CommandCategoryDescriptionRequiresExample use case
chainsCodingTraces code chains and sequences on the %cod tier%cod tierAnalyzing sequential patterns in coded behaviors
chipProfilingChild/parent interaction profile – measures responsiveness and initiationMulti-speaker fileStudying parent-child conversational dynamics
codesCodingFrequency counts of coding tier codes%cod tierSummarizing annotation categories across a session
comboSearchBoolean keyword search with AND/OR operatorsInput: search expressionFinding utterances with specific word combinations (e.g., want+cookie)
complexityProfilingSyntactic complexity ratio from the %gra dependency tier%gra tierMeasuring subordination and embedding depth
cooccurFrequencyWord co-occurrence counting within utterances–Studying lexical associations and collocations
corelexFrequencyCore vocabulary analysis – identifies frequent, high-utility words–Checking whether a child uses age-appropriate core words
distFrequencyWord distribution and dispersion analysis across the transcript–Detecting whether target words cluster in one part of the session
dssProfilingDevelopmental Sentence Scoring – weighted grammatical complexity%mor tier; English onlyClinical assessment of grammatical maturity (ages 2;0–6;11)
evalAssessmentCombined language evaluation with normative comparison–Adult language assessment against healthy norms
evaldAssessmentDementia-specific evaluation using DementiaBank norms–Screening language decline in dementia populations
flucalcOtherFluency calculation – measures disfluency types and rates–Assessing stuttering severity and disfluency patterns
freqFrequencyWord and morpheme frequency counts with type-token ratio–Basic vocabulary analysis, word lists for any transcript
freqposFrequencyFrequency broken down by part-of-speech from %mor tier%mor tierAnalyzing grammatical category distribution (e.g., noun-to-verb ratio)
ipsynProfilingIndex of Productive Syntax – 56 or 100-item structural checklist%mor tier; English onlyAssessing range of syntactic structures in child language
keymapCodingKeyword-based contingency mapping between coded eventsInput: keyword codesStudying temporal relationships between coded behaviors
kidevalAssessmentChild language evaluation with age-normed comparison. Panel auto-detects language and activity from file headers.–Clinical assessment comparing a child to typically developing peers
kwalSearchKeyword-in-context search – finds utterances containing target wordsInput: keywordsFinding all instances of a target word with surrounding context
maxwdFrequencyLongest words per speaker–Checking lexical sophistication or spotting transcription errors
mltProfilingMean Length of Turn – utterances and words per conversational turnMulti-speaker fileMeasuring conversational participation and verbosity
mluProfilingMean Length of Utterance – morphemes per utterance–The most common measure of grammatical development in children
modrepFrequencyModel and replica analysis of imitation patternsMulti-speaker fileStudying what children imitate from adult speech
mortableMorphologyMorpheme frequency table based on a user-defined scriptInput: .cut script fileCustom morphological analysis (e.g., counting specific inflections)
phonfreqFrequencyPhonological segment frequency from the %pho tier%pho tierPhonological inventory analysis, sound acquisition studies
relyOtherInter-rater reliability between two transcriptionsInput: second .cha fileVerifying transcription agreement between two coders
scriptOtherCompares a transcript against a template for structural conformanceInput: template .cha fileChecking that a transcript follows a required protocol structure
sugarProfilingSampling Utterances and Grammatical Analysis Elicited in Narrative–Narrative language assessment using the SUGAR protocol
timedurOtherTime duration computed from bullet timing marksBullet timestampsCalculating speaking time, pause duration, session length
trnfixOtherCompares two dependent tiers for mismatchesMultiple dependent tiersQuality control: finding alignment errors between %mor and %gra
uniqFrequencyFinds repeated and unique utterances–Detecting echolalia, formulaic language, or duplicate transcription lines
vocdProfilingVocabulary Diversity – D statistic (sample-size-corrected TTR)Minimum ~50 tokensComparing lexical diversity across speakers or sessions
wdlenFrequencyWord length distribution (character counts)–Studying word length patterns for phonological or literacy research
wdsizeFrequencyVocabulary size and character length statistics from %mor stems%mor tierTracking cumulative vocabulary growth

Commands by category

Frequency (11 commands)

Word counting, vocabulary, and distribution analysis. See Frequency & Distribution for detailed descriptions.

  • freq – word frequency counts
  • freqpos – frequency by part of speech
  • wdlen – word length distribution
  • wdsize – vocabulary size
  • maxwd – longest words
  • phonfreq – phonological segment frequency
  • modrep – model and replica (imitation)
  • cooccur – word co-occurrence
  • corelex – core vocabulary
  • dist – word distribution
  • uniq – unique/repeated utterances

Profiling (8 commands)

Structural and developmental measures of language. See Profiling Commands for detailed descriptions.

  • mlu – Mean Length of Utterance
  • mlt – Mean Length of Turn
  • vocd – Vocabulary Diversity (D)
  • dss – Developmental Sentence Scoring
  • ipsyn – Index of Productive Syntax
  • chip – child/parent interaction profile
  • complexity – syntactic complexity ratio
  • sugar – SUGAR narrative sampling

Assessment (3 commands)

Normative comparison against reference databases. See Assessment Tools for detailed descriptions.

  • kideval – child language assessment
  • eval – general language assessment
  • evald – dementia assessment

Search (2 commands)

Finding utterances by keyword or pattern.

  • kwal – keyword in context
  • combo – Boolean keyword search (AND/OR)

Coding (3 commands)

Working with coding tiers (%cod).

  • codes – code frequency counts
  • chains – code chains and sequences
  • keymap – keyword contingency mapping

Morphology (1 command)

Custom morphological tabulation.

  • mortable – morpheme frequency table from a script

Other (5 commands)

Specialized tools for specific tasks.

  • flucalc – fluency/disfluency calculation
  • rely – inter-rater reliability
  • script – template comparison
  • timedur – timing duration
  • trnfix – tier mismatch detection

Library files used by commands

Several commands use rule files from the clan-info/lib/ directory. These files ship with the extension and are discovered automatically by the language server.

CommandLibrary pathFiles
dssclan-info/lib/dss/eng.cut, engu.cut, bss.cut, jpn.cut
ipsynclan-info/lib/ipsyn/eng.cut, eng-100.cut
kidevalclan-info/lib/kideval/15 database files (see Assessment Tools)
eval / evaldclan-info/lib/eval/4 database files (see Assessment Tools)
corelexclan-info/lib/corelex/cat.cut, cinderella.cut, cookie.cut, cookie-short.cut, cookie_modern.cut, sandwich.cut, umbrella.cut, window.cut
flucalcclan-info/lib/fluency/ALLflucodes.cut

Next steps

Rating Utterances

Status: Current Last updated: 2026-03-30 13:40 EDT

Rating values

RatingKeyMarkerMeaning
Good1[ok]Bullet timing is correct — audio matches text
Early2[early]Bullet starts before speech actually begins
Late3[late]Bullet starts after speech actually begins
Wrong4[wrong]Completely wrong — hearing different content
Skip5(unchanged)Can’t tell or don’t want to rate — stays [?]

What “Good” means

The bullet points to the audio location where the words in the utterance are actually spoken. A few milliseconds of imprecision is fine — “Good” means “I can hear the right words when I play from this bullet.”

What “Early” means

The bullet plays audio from before the utterance starts. You hear silence, the previous speaker’s words, or background noise before the current utterance’s words begin.

This is the most common issue with boundary-averaged bullets — the algorithm split the difference between two overlapping utterances, but split it in the wrong place.

What “Late” means

The bullet plays audio that starts partway through the utterance — you miss the first word or two. The speaker is already talking when playback begins.

What “Wrong” means

The bullet is wildly off — you hear a completely different part of the conversation, a different speaker, or content that doesn’t match the text at all. This typically happens when the alignment algorithm lost sync.

Adding notes

After pressing a rating key, the %xrev tier updates automatically. If you want to add a note, edit the line directly in the editor:

%xrev:	[early] starts about 200ms before speech, consistent with other INV lines

Notes don’t affect the rating — they’re free text after the marker. The harvesting script collects them for manual analysis.

Useful note patterns

  • Timing offset: “early by ~300ms” or “late by about half a second”
  • Speaker pattern: “all INV backchannels seem early”
  • Overlap pattern: “this is where two speakers talk over each other”
  • Algorithm feedback: “LIS shouldn’t have stripped this — timing was correct”
  • Correction hint: “should start at roughly 12300ms, not 12095ms”

Interactive Bullet Correction

Status: Current Last updated: 2026-03-30 13:40 EDT

Status: This feature is planned but not yet implemented. This chapter documents the design for when it ships.

Beyond rating utterances as good/early/late/wrong, the extension will support interactive correction — clicking on the waveform to set the correct bullet boundary.

Why correction is more valuable than rating

  • Rating “early” tells us the direction but not the magnitude
  • A corrected bullet tells us “was 12095ms, should be 12295ms” — a precise measurement of how far off the algorithm was
  • Corrected bullets become ground truth for tuning algorithm thresholds
  • The %xalign tier records both values: machine=12095_12335 corrected=12295_12335

Planned workflow

  1. During Review Mode, navigate to a flagged utterance
  2. Click on the waveform where speech actually begins — the bullet start time updates
  3. Shift-click to set the end time
  4. Preview the adjusted region to verify
  5. Accept (Enter) — the correction is saved and %xrev set to [corrected]
  6. Revert (Escape) — restore the original bullet

What gets recorded

When you correct a bullet, the %xalign tier is updated with both the machine’s original timing and your correction:

%xalign:	boundary_averaged overlap=155ms machine=12095_12335 corrected=12295_12335 delta_start=+200ms
%xrev:	[corrected]

The delta_start=+200ms field tells us exactly how much the algorithm was off, in which direction. Aggregated across many corrections, this data reveals systematic biases that we can fix in the algorithm.

Stamping untimed utterances

For utterances where align stripped timing entirely (LIS removal), you can stamp a new bullet from scratch using the same F4 mechanism as Transcription Mode:

  1. Play the audio and listen for the utterance
  2. Press F4 at the start to stamp the begin time
  3. Press F4 again at the end to stamp the end time
  4. The %xrev tier is set to [stamped]
%xalign:	lis_removal same_speaker_non_monotonic machine=none stamped=11940_27820
%xrev:	[stamped] timing was actually correct

Harvesting Review Results

Status: Current Last updated: 2026-03-30 13:40 EDT

After reviewers have rated utterances in aligned CHAT files, the ratings need to be collected for analysis. The %xrev and %xalign tiers are part of the CHAT files, so harvesting is just parsing.

Using the harvesting script

python3 scripts/harvest_reviews.py path/to/reviewed/files/ --output reviews.csv

This scans all .cha files in the directory, extracts %xrev and %xalign tiers, and produces a CSV with one row per reviewed utterance:

ColumnDescription
fileFilename
utterance_idxUtterance number in the file
speakerSpeaker code (e.g., “CHI”, “PIL”)
strategyWhat %xalign reports (e.g., “boundary_averaged”)
ratingHuman rating: ok, early, late, wrong, or ?
machine_startOriginal bullet start (ms)
machine_endOriginal bullet end (ms)
corrected_startCorrected start if [corrected] (ms), else empty
corrected_endCorrected end if [corrected] (ms), else empty
delta_msDisplacement from machine timing (ms), else empty
noteFree-form reviewer note

What to do with the results

Aggregate quality metrics

# How many rated good vs early vs late vs wrong?
cut -d, -f5 reviews.csv | sort | uniq -c | sort -rn

Per-strategy breakdown

# Are boundary-averaged bullets rated differently than gap-filled?
awk -F, '{print $4, $5}' reviews.csv | sort | uniq -c | sort -rn

Displacement analysis (for corrected bullets)

# Average displacement for corrected bullets
awk -F, '$5=="corrected" && $11!="" {sum+=$11; n++} END {print sum/n "ms avg"}' reviews.csv

Pattern detection

Look for systematic issues in the notes column:

grep -i 'backchannel\|overlap\|speaker' reviews.csv

Comparing conditions

To compare raw align vs --bullet-repair vs FIXBULLETS, review the same files under each condition and harvest separately:

python3 scripts/harvest_reviews.py raw-output/ --output raw.csv
python3 scripts/harvest_reviews.py repaired-output/ --output repaired.csv
python3 scripts/harvest_reviews.py fixbullets-output/ --output fixbullets.csv

Then compare the %good rate across conditions.

Codes Files (.cut)

Status: Current Last updated: 2026-03-30 13:40 EDT

Codes files (.cut) define the hierarchical coding scheme used by Coder Mode. Each file describes the valid codes for a particular research task – speech act annotation, error coding, conversational strategy labeling, and so on.

File Format

A .cut file is a plain text file with the following structure:

  1. Header lines – metadata and CLAN configuration options (lines starting with #, \, @, or the magic first line)
  2. Target tier declaration – a %xxx: line specifying which dependent tier receives the codes (e.g., %spa:, %err:, %cod:)
  3. Code hierarchy – indented lines defining the valid codes and their tree structure

Header Section

The file begins with CLAN-specific metadata that the VS Code extension ignores:

lXs Special Text file saves all fonts LxS
@Font:	CAfont:13:0
@UTF8
# coder mode commands:
# ESC-e  = toggle between coders and text editing modes
# ...
\ +b50 +d +l1 +s1 +fCAfont

Lines starting with # are comments. The \ line contains CLAN coder options (auto-save interval, font selection, reorder behavior). These are specific to CLAN’s modal editor and have no effect in the VS Code implementation.

Target Tier

A line like %spa: or %err: declares the dependent tier that will receive the codes. When you select a code in the QuickPick, it is inserted as this tier type on the current utterance.

Code Hierarchy

Codes are organized in an indented tree. Each level of indentation represents a level in the hierarchy. Codes typically follow the pattern $CATEGORY:subcategory:

%spa:
 " $POS
  :QUE
   :NV
   :VE
  :ANS
   :NV
   :VE
  :COM
   :NV
   :VE
  :ACK
   :VE

In this example:

  • $POS is a top-level category (positive speech acts)
  • :QUE, :ANS, :COM, :ACK are subcategories under $POS
  • :NV and :VE are leaf codes under each subcategory (non-verbal and verbal)

The " prefix marks speaker scope boundaries (e.g., " $MOT means the following codes apply when coding the mother’s utterances).

Example: codes-basic.cut

The simplest standard code file, codes-basic.cut, defines a basic speech act scheme for mother-child interaction:

%spa:
 " $MOT
  :POS
   :Que
   :Res
  :NEG
 " $CHI

This creates a hierarchy where:

  • Mother ($MOT) utterances can be coded as positive (:POS) with subtypes question (:Que) or response (:Res), or as negative (:NEG)
  • Child ($CHI) utterances use the same top-level scheme

Example: codeserr.cut

The error coding file defines categories for lexical errors on the %err tier:

%err:
 " $LEX
  :INC     incomplete
  :UNC     uncertain
  :ACC     accented
  :ADD     added
  :LOS     lost/omitted
  :HAP     haplology
  :BLE     blend
  :EX1     example type 1
  :EX2     example type 2
  :SH1     shift type 1
  :SH2     shift type 2

Standard Code Files

The following .cut files are distributed with TalkBank in clan-info/lib/coder/:

FileLinesTarget TierDescription
codes-basic.cut24%spaMinimal speech act scheme (POS/NEG, Que/Res)
codes1.cut35%spaExtended speech acts (QUE, ANS, COM, ACK with NV/VE subtypes)
codeserr.cut114%errLexical error classification (INC, UNC, ACC, ADD, LOS, etc.)
codeshar.cut109%spaConversational strategies (NIA: acknowledgment, adaptation, clarification, giving, greeting, etc.)

Creating Custom .cut Files

To create a custom coding scheme:

  1. Start from a template. Copy one of the standard .cut files and modify it, or start from scratch with the minimal structure below.

  2. Minimal structure:

    @UTF8
    %cod:
     " $TOP
      :sub1
      :sub2
       :leaf1
       :leaf2
    
  3. Choose your target tier. Use %cod: for general-purpose coding, %spa: for speech acts, %err: for error coding, or any valid CHAT dependent tier prefix.

  4. Define the hierarchy. Use single-space indentation for each level. Category codes start with $, subcodes start with :.

  5. Keep it flat enough to navigate. The VS Code QuickPick displays the hierarchy with indentation. Deep hierarchies (more than 4 levels) become difficult to scan in the picker.

Tips for Organizing Code Hierarchies

  • Use meaningful category prefixes. $PRA for pragmatic functions, $ERR for errors, $NIA for conversational acts. The prefix makes codes identifiable when reading coded tiers in the transcript.

  • Balance breadth and depth. A flat list of 50 codes is hard to navigate; a deep tree with 6 levels is also hard. Aim for 3-4 levels with 5-10 items per level.

  • Document your codes. Add # comment lines in the .cut file explaining what each category means. Both CLAN and the VS Code extension ignore comment lines.

  • Use speaker scoping sparingly. The " prefix scopes codes to specific speakers. This is useful when different participants have different valid codes (e.g., adult vs. child speech acts), but adds complexity.

  • Test with a small file first. Open the .cut file in Coder Mode with a short transcript to verify the hierarchy displays correctly in the QuickPick before coding a large corpus.

Coding Workflow

Status: Current Last updated: 2026-04-16 16:45 EDT

This chapter walks through the complete Coder Mode workflow in VS Code, from starting a coding session to finishing and reviewing the results.

Prerequisites

Before starting:

  • A .cha transcript file open in the editor
  • A .cut codes file defining your coding scheme (see Codes Files)

Starting Coder Mode

  1. Open a .cha file in the editor
  2. Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Windows/Linux)
  3. Type “Start Coder Mode” and select TalkBank: Start Coder Mode
  4. A file picker appears – navigate to and select your .cut codes file
  5. The extension parses the .cut file and loads the code hierarchy

Alternatively, right-click in the editor and select TalkBank: Navigation then Start Coder Mode.

Once started, the cursor jumps to the first utterance that does not already have a coded tier. A status indicator confirms that Coder Mode is active.

(SCREENSHOT: Command Palette showing “Start Coder Mode” with the file picker for .cut files) Capture this: Open Command Palette, type “Start Coder”, show the QuickPick file selector with .cut files listed

Inserting Codes

With Coder Mode active, press Cmd+Shift+C (macOS) or Ctrl+Shift+C (Windows/Linux) to open the code picker.

The QuickPick displays all codes from the .cut file as a flat list with indentation to show the hierarchy:

  $POS
    :QUE
      :NV
      :VE
    :ANS
      :NV
      :VE
    :COM

Type to filter the list using VS Code’s fuzzy matching. Select a code and press Enter.

The selected code is inserted as a dependent tier immediately after the current utterance:

*CHI:	I want the red one .
%cod:	$PRA:request

The picker placeholder shows progress information (e.g., “12 of 45 utterances coded”) so you can track how much work remains.

(SCREENSHOT: QuickPick code picker showing hierarchical codes with progress indicator) Capture this: During active Coder Mode, press Cmd+Shift+C to show the picker with indented codes and the “X of Y coded” placeholder text

Stepping Through Utterances

After inserting a code, press Cmd+Enter (macOS) or Ctrl+Enter (Windows/Linux) to advance to the next uncoded utterance. The editor scrolls to center the utterance, and the cursor is positioned on the main tier line.

The stepping logic is backed by the LSP via the talkbank/getUtterances command. It identifies utterances that already have a coded tier and skips them, so you only visit utterances that need coding.

If all utterances are coded, the stepping command displays a notification that coding is complete.

Keyboard Shortcuts

These shortcuts are active only when Coder Mode is running (the talkbank.coderActive context is set):

ShortcutmacOSWindows/LinuxAction
Next uncoded utteranceCmd+EnterCtrl+EnterSkip to the next utterance without a coded tier
Insert codeCmd+Shift+CCtrl+Shift+COpen the code picker and insert the selected code

Stopping Coder Mode

When you are finished coding (or want to pause):

  1. Open the Command Palette
  2. Type “Stop Coder Mode” and select TalkBank: Stop Coder Mode

This deactivates the Coder Mode keyboard shortcuts and clears the session state. Your work is already saved – codes were inserted into the document as you went, and normal VS Code auto-save or manual save (Cmd+S) persists them to disk.

State Machine

Coder Mode is a stateful editing mode with five observable states. The talkbank.coderActive VS Code context is bound to whichever of these is non-idle; keyboard shortcuts Cmd+Enter and Cmd+Shift+C are gated on that flag.

stateDiagram-v2
    [*] --> Idle

    Idle --> LoadingCutFile: Start Coder Mode<br/>(file picker opens)
    LoadingCutFile --> Idle: user cancels file picker
    LoadingCutFile --> ScanningUtterances: .cut parses successfully
    LoadingCutFile --> Idle: .cut parse error<br/>(error notification)

    ScanningUtterances --> AtUncoded: LSP talkbank/getUtterances<br/>returns ≥ 1 uncoded
    ScanningUtterances --> Complete: every utterance already coded

    AtUncoded --> PickingCode: Cmd+Shift+C
    PickingCode --> AtUncoded: user cancels QuickPick
    PickingCode --> Inserting: user selects a code
    Inserting --> AtUncoded: code inserted,<br/>cursor stays on same utterance
    AtUncoded --> AtUncoded: Cmd+Enter advances<br/>to next uncoded (if any)
    AtUncoded --> Complete: no more uncoded utterances

    AtUncoded --> Idle: Stop Coder Mode
    Complete --> Idle: Stop Coder Mode<br/>(or auto after notification)
    Complete --> [*]

    note right of ScanningUtterances
        "Uncoded" means the utterance has
        no sibling %cod: tier. Detection
        is delegated to the LSP.
    end note
    note right of Inserting
        Insert edits the %cod: tier
        directly via WorkspaceEdit.
        No webview, no separate buffer.
    end note

Key properties the diagram documents:

  • The transitions out of LoadingCutFile form a three-way branch (cancel → Idle, parse error → Idle with notification, parse success → ScanningUtterances). A successor reading only the prose would miss the cancel branch.
  • AtUncoded ↔ PickingCode ↔ Inserting is a loop that keeps you on the same utterance — the code insertion does not auto-advance; advancement requires an explicit Cmd+Enter. This decouples “coded?” from “moved on?” so mis-codes can be corrected in place.
  • Stop Coder Mode is reachable from AtUncoded and Complete but not from LoadingCutFile / PickingCode / Inserting (those are modal dialogs VS Code owns).

Typical Session

A typical coding session looks like this:

  1. Open the .cha file
  2. Start Coder Mode, select your .cut file
  3. The cursor jumps to the first uncoded utterance
  4. Read the utterance, press Cmd+Shift+C, select the appropriate code
  5. Press Cmd+Enter to advance to the next uncoded utterance
  6. Repeat steps 4-5 until done
  7. Stop Coder Mode

For transcripts with many utterances, you can code in multiple sessions. The extension tracks which utterances already have codes and skips them on the next session.

[VIDEO: 60-second Coder Mode demo) Capture this: Screen recording showing the full workflow – start coder mode, select .cut file, code 3-4 utterances using Cmd+Shift+C and Cmd+Enter, then stop coder mode. Show the QuickPick hierarchy and the coded tiers being inserted.

Tips

  • Use fuzzy search in the picker. If you know the code you want, start typing its name instead of scrolling through the hierarchy. VS Code’s fuzzy matching is fast.

  • Code in batches. For large transcripts, code 50-100 utterances at a time to avoid fatigue-related errors.

  • Review before committing. After coding, use the Scoped Find feature to search within coded tiers specifically, reviewing all codes in context.

  • Use consistent .cut files across coders. When multiple people code the same corpus, ensure everyone uses the identical .cut file to maintain inter-rater reliability.

Corpus Validation

Status: Current Last updated: 2026-03-30 13:40 EDT

The Validation Explorer provides corpus-scale validation of .cha files directly within VS Code. Unlike CLAN, which validates one file at a time, the extension can check entire directory trees and display all errors in a navigable tree view.

Validation Explorer

The CHAT Validation panel appears automatically in the Explorer sidebar when the extension is active. It mirrors your workspace folder structure, filtered to show only .cha files and the directories that contain them.

(SCREENSHOT: Validation Explorer panel in the Explorer sidebar showing a directory tree with mixed pass/fail results) Capture this: Open a workspace with multiple .cha files, validate all, and show the tree with green checkmarks on passing files, red X badges with error counts on failing files, and expanded error items under a failing file

Validating Files

Single File

Click the checkmark icon next to any .cha file in the Validation Explorer tree. The file is validated and its status updates:

  • Green checkmark – the file is valid CHAT
  • Red X with error count (e.g., “3 errors”) – the file has validation errors

Directory

Click the double-checkmark icon next to a folder to validate all .cha files in that directory and its subdirectories. This runs the chatter validate CLI with --format json under the hood, using parallel workers for speed.

Entire Workspace

Click the double-checkmark button in the Validation Explorer toolbar to validate every .cha file in the workspace.

When a file fails validation, expand it in the tree to see individual error items. Each error shows:

  • The error code (e.g., E301, E501)
  • A description of the problem
  • The line number

Click on any error item to jump directly to the exact line and column in the editor. The editor opens the file if it is not already open.

Toolbar

The Validation Explorer toolbar provides two actions:

ButtonIconAction
Validate AllDouble-checkmarkValidate all .cha files in the workspace
RefreshRefreshUpdate the tree view to reflect filesystem changes

Context Menu

Right-click on files or directories in the Validation Explorer for additional options:

ActionDescription
Validate FileValidate a single .cha file
Validate DirectoryValidate all .cha files in a folder
Clear CacheRemove cached validation results, forcing revalidation

How It Works

The Validation Explorer does not use the LSP for bulk validation. Instead, it shells out to the chatter validate CLI with --format json, parses the JSON output, and builds the tree view from the results. This design allows it to validate entire directories without loading every file into the LSP, and it benefits from the CLI’s parallel validation workers (via crossbeam).

Results are cached in a SQLite database so that repeated views of the same files are instant. See Cache Management for details on the cache location and how to clear it.

Real-Time vs. Bulk Validation

The extension provides two complementary validation paths:

PathTriggerScopeSpeed
Real-time (LSP)Typing in the editorSingle open fileInstant (250ms debounce)
Bulk (CLI)Validation ExplorerEntire directoriesParallel, cached

Real-time validation shows errors as inline squiggles and in the Problems panel as you edit. Bulk validation via the Validation Explorer gives you a corpus-wide overview. Both use the same underlying validation logic.

Transcription from Audio

Status: Current Last updated: 2026-04-16 16:55 EDT

This chapter describes the end-to-end workflow for creating a new CHAT transcript from an audio recording using the extension’s Transcription Mode.

Overview

Transcription Mode turns VS Code into a transcription workstation. Audio plays from a media panel while you type the transcript. At each utterance boundary, press F4 to stamp a timing bullet that records the start and end time. The result is a properly timestamped CHAT file ready for annotation.

Pipeline

Each F4 keystroke triggers a fixed sequence across the three layers (editor, media panel webview, LSP):

flowchart LR
    start(["Start Transcription Mode"])
    play["Media panel plays audio<br/>(webview, Web Audio API)"]
    type["User types an utterance<br/>on the current *SPK: line"]
    f4{"F4 pressed?"}
    req["Request playback position<br/>(postMessage 'requestTimestamp')"]
    ts{{"Media panel replies with<br/>current currentTime"}}
    fmt["LSP: talkbank/formatBulletLine<br/>→ 'mmmmm_nnnnn' bullet"]
    insert["Insert bullet at end of line<br/>+ new '*SPK:\\t' line below"]
    continue{"More audio?"}
    stop(["Stop Transcription Mode"])

    start --> play --> type --> f4
    f4 -->|"no (keep typing)"| type
    f4 -->|"yes"| req --> ts --> fmt --> insert --> continue
    continue -->|"yes"| type
    continue -->|"no"| stop

    rewind["F8 rewind<br/>(configurable seconds)"]
    loop["Shift+F5 toggle loop<br/>(replay current segment)"]
    type -.->|"missed something"| rewind -.-> play
    play -.->|"re-listen needed"| loop -.-> play

Dashed edges show the two sideways playback controls (F8, Shift+F5) that don’t advance the editor cursor. The F4 path is the only one that mutates the document.

Prerequisites

  • An audio or video file (MP3, WAV, M4A, MP4, OGG)
  • A .cha file with at least a minimal CHAT header including an @Media: reference to the audio file

If you are starting from scratch, create a new .cha file with this template (available via the chatheader snippet):

@UTF8
@Begin
@Languages:	eng
@Participants:	CHI Target_Child
@ID:	eng|corpus|CHI|||||Target_Child|||
@Media:	recording, audio
*CHI:
@End

Place the audio file (e.g., recording.mp3) in the same directory as the .cha file.

Step-by-Step Workflow

1. Open the CHAT file

Open the .cha file in VS Code. The extension activates automatically.

2. Start Transcription Mode

Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and select TalkBank: Start Transcription Mode.

The extension opens a media panel and begins playing the audio from the start. The cursor is positioned on the first utterance line.

3. Type the utterance

Listen to the audio and type what the speaker says on the current *SPEAKER: line. Follow standard CHAT conventions for the transcription.

4. Stamp the timing bullet

When you reach an utterance boundary, press F4 to stamp a timing bullet. The extension:

  1. Requests the current playback position from the media panel
  2. Delegates bullet formatting to the LSP via talkbank/formatBulletLine
  3. Inserts a timing bullet at the end of the current line (e.g., 15320_18450)
  4. Creates a new *SPEAKER: line below with the configured default speaker code
  5. Positions the cursor on the new line, ready for the next utterance

5. Use playback controls as needed

KeyAction
F4Stamp timestamp bullet and advance to new line
F8Rewind by the configured number of seconds (default: 2)
Shift+F5Toggle segment loop (replay the current segment)

If you missed something, press F8 to rewind and re-listen. The rewind amount is configurable via talkbank.transcription.rewindSeconds.

6. Stop Transcription Mode

When you are finished, open the Command Palette and select TalkBank: Stop Transcription Mode. This stops playback and deactivates the transcription keybindings.

Configuration

SettingDefaultDescription
talkbank.transcription.defaultSpeakerCHISpeaker code inserted on new utterance lines
talkbank.transcription.rewindSeconds2Seconds to rewind when pressing F8

Foot-Pedal and Custom Keybindings

Many transcribers use a USB foot pedal to control playback hands-free. To remap the transcription keys:

  1. Open the Command Palette and select TalkBank: Configure Transcription Keybindings
  2. The VS Code keybindings editor opens, pre-filtered to TalkBank transcription commands
  3. Click the pencil icon next to any command to assign a new keybinding (e.g., map your foot pedal’s key codes to stamp, rewind, and loop)

This is the recommended way to set up foot-pedal controls.

Tips for Efficient Transcription

  • Listen before typing. Play each segment fully before starting to type. This produces more accurate transcriptions than trying to type in real time.

  • Use rewind liberally. F8 rewinds by 2 seconds (configurable). Use it whenever you are unsure about a word.

  • Transcribe in passes. On the first pass, capture the main content. On subsequent passes, add dependent tiers, fix unclear segments, and add annotations.

  • Save frequently. Use Cmd+S regularly. Transcription is time-consuming work and you do not want to lose progress.

  • Check validation as you go. The extension validates in real-time. Glance at the Problems panel periodically to catch formatting errors early rather than fixing them all at the end.

After Transcription

Once you have a complete transcript with timing bullets, the next steps typically include:

  1. Add dependent tiers – run batchalign morphotag to add %mor and %gra tiers automatically
  2. Validate – use the Validation Explorer to check for formatting errors
  3. Review – if you used automatic alignment, enter Review Mode to verify timing accuracy

Post-Alignment Review

Status: Current Last updated: 2026-03-30 13:40 EDT

After running automatic alignment with batchalign, timing bullets may not be perfectly placed. Review Mode provides a structured workflow for a human reviewer to step through flagged utterances, rate alignment quality, correct errors, and produce a final validated transcript.

Workflow Overview

The following diagram shows the end-to-end post-alignment review workflow:

flowchart TD
    A["batchalign align\n--bullet-repair"] --> B["Aligned .cha file\nwith timing bullets"]
    B --> C["Open in VS Code"]
    C --> D["Start Review Mode\n(Command Palette)"]
    D --> E["Step through\nflagged utterances"]
    E --> F{"Rate each\nutterance"}
    F -->|"1 = Good"| G["Accept alignment"]
    F -->|"2 = Early"| H["Mark as early"]
    F -->|"3 = Late"| I["Mark as late"]
    F -->|"4 = Wrong"| J["Mark as wrong"]
    F -->|"5 = Skip"| K["Skip for now"]
    G --> L["Next flagged\nutterance"]
    H --> L
    I --> L
    J --> L
    K --> L
    L --> E
    L -->|"All reviewed"| M["Save reviewed .cha"]
    M --> N["Harvest ratings\nfor quality metrics"]

Step-by-Step

1. Align the transcript

Run batchalign to add or repair timing bullets:

batchalign align --bullet-repair input.cha -o output/

This produces an aligned .cha file where each utterance has timing bullets derived from the audio.

2. Open in VS Code

Open the aligned .cha file. The extension loads and begins real-time validation.

3. Start Review Mode

Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and select TalkBank: Start Review Mode.

The extension enters Review Mode:

  • The cursor jumps to the first flagged utterance
  • Review-specific keybindings activate
  • The media panel opens for playback

4. Review each utterance

For each flagged utterance:

  1. Listen – the segment plays automatically so you can hear the audio
  2. Read – compare the transcript text against what you hear
  3. Rate – press a number key to rate the alignment quality:
KeyRatingMeaning
1GoodTiming is correct, no changes needed
2EarlyBullet starts too early
3LateBullet starts too late
4WrongAlignment is completely off
5SkipCome back to this one later

After rating, the cursor advances to the next flagged utterance.

5. Navigate between flagged utterances

ShortcutAction
Alt+]Jump to next flagged utterance
Alt+[Jump to previous flagged utterance

6. Correct errors

When you rate an utterance as Early, Late, or Wrong, you can manually adjust the timing bullets in the transcript. Edit the bullet values directly, then use Alt+] to move on.

7. Stop Review Mode

When all flagged utterances have been reviewed, open the Command Palette and select TalkBank: Stop Review Mode. Save the file.

8. Harvest ratings

The ratings embedded by Review Mode can be harvested for quality metrics – how many utterances were correctly aligned, what percentage needed correction, and so on. This data feeds back into alignment quality assessment.

Rating Key Behavior

The number keys (1-5) for rating are only active when:

  • Review Mode is active (talkbank.reviewActive context is set)
  • The editor does not have text focus (!editorTextFocus)

This means you can freely type in the editor to correct transcript text without accidentally triggering ratings. Click outside the text area (e.g., on the editor margin or the media panel) to re-enable the rating keys.

Tips

  • Use headphones. Alignment review requires careful listening. External speakers make it harder to catch small timing errors.

  • Rate before correcting. Rate every utterance first to get a quality overview, then go back and fix the ones rated Early, Late, or Wrong.

  • Use the waveform. Open the Waveform View alongside the editor. The visual waveform helps you see where speech starts and ends relative to the bullet boundaries.

  • Batch your reviews. Review 50-100 utterances at a time. Alignment review is cognitively demanding and accuracy drops with fatigue.

Batch Processing

Status: Current Last updated: 2026-03-30 13:40 EDT

This chapter covers workflows that operate on multiple files at once – directory-level analysis, corpus-scale validation, and integration with the chatter CLI and batchalign for bulk operations.

Directory-Level CLAN Analysis

The extension supports running any of the 33 CLAN analysis commands on an entire directory of .cha files.

From the Explorer

  1. Right-click on a folder in the VS Code Explorer sidebar
  2. Select Run CLAN Analysis on Directory
  3. A QuickPick lists all 33 available commands (freq, mlu, mlt, etc.)
  4. Select a command
  5. The extension runs the analysis across all .cha files in the directory
  6. Results are aggregated and displayed in a single Analysis Panel

The LSP’s resolve_files() function walks the directory tree to find all .cha files. Results display as styled tables with section headings, key-value statistics, and bar charts – the same rendering used for single-file analysis.

(SCREENSHOT: Analysis Panel showing aggregated results from a directory-level FREQ analysis) Capture this: Right-click a folder containing several .cha files, run FREQ analysis, show the results panel with aggregated frequency tables

CSV Export

Both single-file and directory-level analysis results can be exported to CSV:

  1. Click the Export CSV button in the Analysis Panel toolbar
  2. Choose a save location in the file dialog
  3. The CSV file contains all visible tables and statistics

This is useful for importing results into spreadsheets or statistical software (R, SPSS, etc.).

Corpus-Scale Validation

The Validation Explorer provides a tree view for validating entire directory trees. For even larger-scale validation, the chatter CLI can be used directly from the terminal:

# Validate an entire corpus directory
chatter validate path/to/corpus/ --force

# Validate with roundtrip checking (parse then re-serialize, compare)
chatter validate path/to/corpus/ --roundtrip --force

# Faster validation (skip tier alignment checks)
chatter validate path/to/corpus/ --skip-alignment

# JSON output for programmatic consumption
chatter validate path/to/corpus/ --format json

# Limit error output
chatter validate path/to/corpus/ --max-errors 100

# Control parallelism
chatter validate path/to/corpus/ --jobs 4

The CLI runs validation in parallel using crossbeam workers. Results are cached in SQLite for instant re-display on subsequent runs.

Integration with batchalign

A common workflow for processing a corpus involves multiple stages:

Align, Morphotag, Validate Cycle

# 1. Automatic transcription and alignment
batchalign transcribe audio_corpus/ -o transcribed/ --lang eng

# 2. Add morphological annotation (no --lang — language read per-file from @Languages)
batchalign morphotag transcribed/ -o annotated/

# 3. Validate the results
chatter validate annotated/ --force

# 4. Open in VS Code for review and correction
code annotated/

Bulk Alignment Repair

# Repair timing bullets on an existing corpus
batchalign align --bullet-repair corpus/ -o repaired/

# Validate the repaired files
chatter validate repaired/ --force

# Open for post-alignment review
code repaired/

After opening the repaired corpus in VS Code, use Review Mode to step through and verify the alignment.

Using chatter CLI for Bulk Operations

The chatter CLI supports several bulk operations that complement the VS Code extension:

CommandPurpose
chatter validateValidate files or directories
chatter normalizeReformat CHAT files to canonical form
chatter to-jsonConvert CHAT to JSON for programmatic processing
chatter clan freqRun frequency analysis
chatter clan mluCompute mean length of utterance

All CLI commands accept directories and process files in parallel. Use --help on any command for full usage.

Tips for Batch Workflows

  • Use --force for fresh results. The cache speeds up repeated runs, but use --force when you have modified files and want guaranteed fresh validation.

  • Start with validation. Before running analysis commands, validate the corpus first. Analysis on malformed CHAT files can produce misleading results.

  • Monitor memory on large corpora. The CLI processes files in parallel. For very large corpora (tens of thousands of files), use --jobs N to limit parallelism if memory is a concern.

  • Use JSON output for automation. chatter validate --format json produces machine-readable output that can be piped to other tools for automated quality checks in CI pipelines.

Common Issues

Status: Current Last updated: 2026-03-30 13:40 EDT

This chapter covers the most frequently encountered problems and their solutions.

No Diagnostics Appearing

Symptom: You open a .cha file but see no error squiggles, no entries in the Problems panel, and no inlay hints.

Possible causes and fixes:

  1. The language server is not running. Check the Output panel: click the dropdown and select “TalkBank Language Server”. If there is no such entry, the server failed to start. See LSP Connection for troubleshooting.

  2. The file is not recognized as CHAT. Check the language mode indicator in the bottom-right of the status bar. It should show “CHAT”. If it shows “Plain Text” or another language, click on it and select “CHAT” from the list, or ensure the file has a .cha extension.

  3. Validation severity is set to filter out all diagnostics. Check talkbank.validation.severity in your settings. If set to "errorsOnly", warnings and information-level diagnostics are hidden. Try setting it to "all".

  4. The file is actually valid. If the file has no errors, no diagnostics will appear. This is correct behavior.

Extension Not Activating

Symptom: Opening a .cha file does not trigger syntax highlighting or any extension features.

Fixes:

  1. Check the file extension. The extension activates only on .cha files. Files with other extensions (.txt, .chat) will not trigger activation.

  2. Check that the extension is installed and enabled. Open the Extensions sidebar (Cmd+Shift+X) and search for “TalkBank CHAT”. The extension should show as installed and enabled.

  3. Check the development host. If running via code --extensionDevelopmentPath=., ensure you are running the command from the apps/vscode-extension/ directory and that npm run compile has been run.

Formatting Does Nothing

Symptom: Running Format Document (Shift+Alt+F) produces no changes.

This is usually correct. The formatter re-serializes the document through the canonical CHAT serializer. If the document is already in canonical form, no changes are needed and the formatter correctly reports no edits.

If you expected changes (e.g., fixing indentation):

  1. Ensure the file has @UTF8 and @Begin headers – the formatter requires a structurally valid CHAT document.
  2. Check that the language server is running (see above).

Dependency Graph Not Rendering

Symptom: The dependency graph panel shows “Failed to load Graphviz renderer” or a blank panel.

Fixes:

  1. Check that node_modules is present. The Graphviz WASM renderer (@hpcc-js/wasm) is bundled with the extension. If node_modules/@hpcc-js/wasm/ is missing, run npm install in the apps/vscode-extension/ directory.

  2. Ensure the utterance has a %gra tier. The dependency graph requires a %gra (grammatical relations) tier on the current utterance. Position your cursor on an utterance that has %gra data and try again.

  3. Offline usage. The Graphviz renderer works fully offline – no internet connection is required. If it worked before and stopped, try reinstalling the extension.

CLAN Integration Not Working

Symptom: “Open in CLAN” does nothing or shows an error.

Platform support:

PlatformStatus
macOSSupported via Apple Events
WindowsSupported via Windows messaging
LinuxNot supported (CLAN is macOS/Windows only)

macOS fixes:

  • Ensure CLAN is installed and has been launched at least once
  • The extension uses Apple Events IPC – check that CLAN is not blocked by macOS security settings

Windows fixes:

  • Ensure CLAN is installed and running
  • The extension uses Windows messaging (WM_APP) – CLAN must be running to receive the message

This feature is entirely optional. All editing, validation, and analysis features work without CLAN installed.

Hover Shows No Alignment Data

Symptom: Hovering over a word on the main tier shows no cross-tier alignment information.

Requirements for alignment:

  1. The utterance must have at least one dependent tier (%mor, %gra, %pho, or %sin)
  2. Both the main tier and the dependent tier must be syntactically valid
  3. The alignment counts must match (or the extension shows a mismatch hint instead)

If the main tier has words but no %mor tier is present, there is nothing to align.

Scoped Find Returns No Results

Symptom: “Find in Tier” finds nothing even though you know the term exists.

Check:

  • Are you searching the right tier? The scoped find restricts to the selected tier type only.
  • Regex mode is activated by prefixing the query with /. If your search term contains /, it will be interpreted as a regex pattern.
  • Speaker filtering is applied. If you selected specific speakers, results from other speakers are excluded.

LSP Connection

Status: Current Last updated: 2026-04-16 13:37 EDT

The TalkBank extension is powered by a Rust language server (chatter-lsp) that communicates with VS Code over stdio. If the language server fails to start or crashes, most extension features will not work. This chapter covers how to diagnose and fix LSP connection issues.

Checking the Output Panel

The first step for any LSP issue is the Output panel:

  1. Open the Output panel: View > Output or Cmd+Shift+U (macOS) / Ctrl+Shift+U (Windows/Linux)
  2. Click the dropdown in the top-right of the Output panel
  3. Select “TalkBank Language Server”

This shows the LSP communication logs and any server stderr output. Look for:

  • Startup messages – the server logs its version and capabilities on startup
  • Error messages – parse failures, missing files, or crashes
  • Connection lost – if the server crashed, you will see a disconnection message

Binary Not Found

Symptom: The Output panel shows an error about not finding the chatter-lsp binary, or a notification appears with a link to the talkbank.lsp.binaryPath setting.

The extension searches for the language-server binary in this order:

  1. talkbank.lsp.binaryPath setting – if set and the file exists, used as an explicit override
  2. Bundled binary – <extension>/server/chatter-lsp[.exe], shipped inside platform-specific VSIXes
  3. System PATH – runs which chatter-lsp (or where chatter-lsp on Windows); used by developers who install via cargo install
  4. Dev-tree builds – <extension>/../target/debug/chatter-lsp or .../target/release/chatter-lsp, for contributors running from a cloned workspace
  5. If none are found, an actionable error notification appears with a link to the setting

Fixes:

  • Users: install the VSIX that matches your platform (e.g. talkbank-chat-darwin-arm64.vsix). Each platform-specific VSIX bundles the correct prebuilt chatter-lsp. See Installation.
  • Developers who installed via Cargo: cargo install --path crates/chatter-lsp and confirm which chatter-lsp resolves
  • Contributors running from source: cargo build --release -p chatter-lsp so the dev-tree path exists
  • Manual override: set talkbank.lsp.binaryPath in your settings to the absolute path of an existing chatter-lsp binary

Enabling Trace Logging

For detailed server diagnostics, enable trace-level logging:

Server-side logging (RUST_LOG)

Set the RUST_LOG environment variable before launching VS Code:

RUST_LOG=debug code .

Or for more targeted tracing:

RUST_LOG=chatter_lsp=debug code .
RUST_LOG=chatter_lsp::alignment=trace code .

The server uses the tracing crate for structured logging. Trace output appears in the Output panel under “TalkBank Language Server”.

LSP message inspection

To see the raw JSON-RPC messages between VS Code and the server, check whether the extension supports the chatter-lsp.trace.server setting. Set it to "verbose" to log all request/response payloads. Alternatively, use VS Code’s built-in LSP inspector if available.

Server Crash Recovery

The extension automatically restarts the language server if it crashes. You should see a brief interruption in diagnostics and features, followed by automatic recovery.

If the server crashes repeatedly:

  1. Check the Output panel for the crash message
  2. Note the file you were editing when the crash occurred
  3. Try opening a different .cha file to determine if the crash is file-specific
  4. If the crash is reproducible, file a bug report with:
    • The .cha file that triggers the crash (or a minimal reproduction)
    • The Output panel log
    • The RUST_LOG=debug output

Server Hangs (No Response)

Symptom: The language server appears to be running but features stop responding (no hover, no diagnostics, no completion).

Possible causes:

  • Large file parsing. Very large CHAT files (thousands of utterances) can take time to parse. Wait a few seconds after opening or editing.
  • Debounce delay. The server debounces validation by 250ms after each edit. During fast typing, diagnostics are intentionally delayed.
  • Deadlock. Rare, but possible. Restart VS Code to recover. If reproducible, file a bug report with RUST_LOG=debug output.

Verifying Server Version

To confirm which version of the language server is running, check the Output panel at startup. The server logs its version string. Compare this against the output of:

chatter --version

If the versions do not match, the extension may be using a stale binary. Rebuild or update talkbank.lsp.binaryPath.

Media Not Found

Status: Current Last updated: 2026-03-30 13:40 EDT

This chapter covers issues with media playback – audio or video files not found, unsupported formats, and remote connection limitations.

@Media Header Missing

Symptom: Play at Cursor, Continuous Play, and Waveform View commands do nothing or show an error about missing media.

Fix: The .cha file must have an @Media: header that references the audio or video file:

@Media:	interview, audio

The value before the comma is the base filename (without extension). The value after the comma is the media type (audio or video). Add this header manually or use the Participant Editor to edit file metadata.

Media File Not Found

Symptom: The @Media: header is present, but the extension reports that the media file cannot be found.

The extension resolves media files by searching for a file whose base name matches the @Media value, in these locations:

  1. Same directory as the .cha file
  2. media/ subdirectory relative to the .cha file

For example, if the .cha file is at /data/resources/corpus/interview.cha and the header says @Media: interview, audio, the extension looks for:

  • /data/resources/corpus/interview.mp3
  • /data/resources/corpus/interview.wav
  • /data/resources/corpus/interview.m4a
  • /data/resources/corpus/interview.mp4
  • /data/resources/corpus/media/interview.mp3
  • (and so on for other supported extensions)

Fixes:

  1. Ensure the media file exists in one of these locations
  2. Ensure the base filename in @Media: matches the actual file (case-sensitive on macOS/Linux)
  3. Check that the file extension is one the extension supports (see below)

Unsupported Format

The extension uses the browser’s built-in audio/video API (Web Audio API and HTML5 <audio>/<video> elements) for playback. Supported formats depend on the Electron/Chromium runtime bundled with VS Code:

FormatExtensionTypically Supported
MP3.mp3Yes
WAV.wavYes
AAC / M4A.m4a, .aacYes
MP4 (video).mp4Yes
OGG Vorbis.oggYes
WebM.webmYes
FLAC.flacUsually yes
WMA.wmaNo
AVI.aviNo (use MP4)

If your media file is in an unsupported format, convert it to MP3 or WAV using a tool like ffmpeg:

ffmpeg -i recording.wma recording.mp3

No Audio on Remote Connection

Symptom: Media playback does not work when connected via VS Code Remote SSH or Remote Containers.

This is a known limitation. VS Code’s webview panels (which the media player uses) have limited media support over remote connections. The audio file must be accessible to the VS Code client, not just the remote server.

Workarounds:

  • Copy the media file locally and open the .cha file locally
  • Use VS Code’s port forwarding to make the media accessible (may not work for all formats)
  • Use the desktop chatter app instead of the VS Code extension for remote media review

Waveform Not Rendering

Symptom: The waveform panel opens but shows a blank or error state.

The waveform is rendered using the Web Audio API, which must decode the entire audio file into memory. Issues can occur with:

  • Very large files – audio files over 1 GB may exceed memory limits
  • Unsupported codecs – the Web Audio API decoder may reject certain codec variants even if the <audio> element can play them
  • Corrupted files – truncated or corrupted audio files will fail to decode

Fixes:

  1. Try a smaller audio file to confirm the waveform feature works
  2. Convert the file to WAV or MP3 format
  3. Check the developer console (Help > Toggle Developer Tools) for specific decode errors

Playback and Timing Mismatch

Symptom: The highlighted utterance does not match what you hear, or segments seem offset.

This can happen when:

  • Timing bullets are incorrect – the bullet values in the transcript do not match the actual audio timing. Use Review Mode to verify and correct.
  • Variable bitrate encoding – some VBR-encoded files have seeking inaccuracies. Convert to constant bitrate (CBR) if precision is important.

Architecture

Status: Current Last updated: 2026-04-16 16:19 EDT

This chapter describes the internal architecture of the TalkBank CHAT VS Code extension and its backing language server. It is written for developers who want to understand or contribute to the codebase.

Three-Layer Architecture

The system is split into three layers, each with a clear responsibility:

flowchart TD
    subgraph "VS Code Extension (TypeScript)"
        Ext["extension.ts\nActivation, commands, LSP client"]
        Panels["Webview Panels\nGraph, Media, Waveform,\nAnalysis, KidEval, ID Editor"]
        Commands["Effect Commands\nWalker, Coder, Review,\nTranscription, Special Chars"]
        TreeView["Validation Explorer\nTreeDataProvider"]
        Cache["Cache Manager\nStatus bar, CLI polling"]
    end

    subgraph "Language Server (Rust)"
        LSP["chatter-lsp\nmain.rs, lib.rs"]
        Backend["Backend (state.rs)\nDashMap caches, debounce"]
        Features["Feature Handlers\nhover, completion, rename,\ncode actions, highlights..."]
        Align["Alignment Module\nCross-tier mapping"]
        Graph["Graph Module\nDOT generation"]
        Diag["Diagnostics\nValidation orchestrator"]
    end

    subgraph "Core Crates (Rust)"
        Model["talkbank-model\nData types, validation,\nalignment"]
        Parser["talkbank-parser\nTree-sitter CST to AST"]
        Clan["clan-core\n33 analysis commands"]
    end

    Ext -->|"stdio JSON-RPC"| LSP
    LSP --> Backend
    Backend --> Features
    Backend --> Align
    Backend --> Graph
    Backend --> Diag
    Diag --> Model
    Diag --> Parser
    Features --> Model
    Align --> Model
    Graph --> Model
    LSP --> Clan

Key principle: the LSP server is a presentation layer. All parsing, validation, and alignment computation lives in talkbank-model and talkbank-parser. The LSP crate formats and routes that information to the VS Code client. The TypeScript extension is a thin UI layer that registers commands, manages webviews, and communicates with the LSP over stdio.

Document Lifecycle

Every .cha file goes through a well-defined lifecycle as it is opened, edited, and closed:

stateDiagram-v2
    [*] --> Opened: did_open(uri, text)
    Opened --> Parsed: Parse immediately
    Parsed --> Validated: Validate + publish diagnostics
    Validated --> Cached: Populate all caches

    Cached --> Editing: did_change(uri, text)
    Editing --> Debouncing: Increment counter, sleep 250ms
    Debouncing --> Editing: Superseded by newer edit
    Debouncing --> Parsed: ID still current

    Cached --> Saved: did_save(uri)
    Saved --> Parsed: Re-validate immediately

    Cached --> [*]: did_close(uri)\nRemove from all caches\nPublish empty diagnostics

The 250ms debounce on did_change prevents thrashing during fast typing. When the user saves, validation runs immediately without debounce.

Backend State

All LSP server state lives in the Backend struct (backend/state.rs). It is Clone-safe (everything behind Arc), shared across all async request handlers.

Caches

CacheTypeContentsLifetime
documentsDashMap&lt;Url, String&gt;Raw file textdid_open to did_close
parse_treesDashMap&lt;Url, Tree&gt;tree-sitter parse treesRebuilt on every edit
chat_filesDashMap&lt;Url, ChatFile&gt;Parsed model objectsRebuilt on every edit
parse_cleanDashMap&lt;Url, bool&gt;Parse health flagRebuilt on every edit
validation_cacheDashMap&lt;Url, ValidationCache&gt;Errors grouped by scopeRebuilt on validation
pending_validationsDashMap&lt;Url, u64&gt;Debounce IDsTransient

All caches use DashMap for lock-free concurrent reads. Parser and semantic-token generation use lazily initialized thread-local instances instead of shared mutexes.

Cache Miss Strategy

Feature handlers (hover, highlights, etc.) first check the chat_files cache. On miss, they re-parse the document text from documents. This means features always work even if validation has not completed yet.

Key Source Files

If you are new to the codebase, read these files in order:

Rust (Language Server)

  1. backend/state.rs – the shared state struct and all caches
  2. backend/mod.rs – how LSP requests are dispatched to handlers
  3. backend/documents.rs – the document lifecycle (open/change/save/close)
  4. diagnostics/validation_orchestrator.rs – the validation pipeline
  5. alignment/mod.rs – how hover and highlight find alignment data
  6. alignment/tier_hover/main_tier.rs – concrete example of a tier handler
  7. graph/mod.rs – dependency graph DOT generation
  8. backend/capabilities.rs – all 23 advertised LSP capabilities

TypeScript (Extension)

  1. extension.ts – entry point: LSP client setup, command registration
  2. validationExplorer.ts – TreeDataProvider for bulk validation
  3. analysisPanel.ts – webview rendering for CLAN analysis results
  4. mediaPanel.ts – audio/video playback with segment tracking
  5. coderPanel.ts – Coder Mode: .cut file parsing and code insertion

Alignment and the %mor Chunk Sequence

Cross-tier alignment — main ↔ %mor, %mor ↔ %gra, main ↔ %pho / %sin / %wor — is implemented once in talkbank-model. The LSP crate is a consumer; it never recomputes alignment. This boundary is strict and is codified in crates/chatter-lsp/CLAUDE.md.

The %mor chunk primitive

%gra relations address a sequence that is distinct from MorTier::items: each item expands to one chunk for its main word plus one chunk per post-clitic, and the optional terminator is one final chunk. MorTier::chunks() is the canonical iterator over this sequence; MorTier::chunk_at(idx) indexes directly into it; MorTier::item_index_of_chunk(idx) projects a chunk back to its host item for main↔%mor lookups.

// talkbank-model
pub enum MorChunk<'a> {
    Main(&'a Mor),
    PostClitic(&'a Mor, &'a MorWord),
    Terminator(&'a str),
}

impl MorTier {
    pub fn chunks(&self) -> impl Iterator<Item = MorChunk<'_>>;
    pub fn chunk_at(&self, chunk_index: usize) -> Option<MorChunk<'_>>;
    pub fn item_index_of_chunk(&self, chunk_index: usize) -> Option<usize>;
}

Five places in the workspace used to walk the chunk expansion by hand (count_chunks, align_mor_to_gra, extract_mor_chunk_items, the LSP hover helper, the graph DOT label builder). All now delegate to MorTier::chunks(). Any future consumer — CLI, CLAN analyses, batchalign — must do the same.

Three distinct index spaces

%mor / %gra address three integer spaces that all look identical as usize; the compiler now tells them apart via newtypes in talkbank-model::alignment::indices:

Type0- or 1-indexedOver whatWhere it comes from
MorItemIndex0-indexedMorTier::itemsAlignmentPair.target on the main↔mor alignment
MorChunkIndex0-indexedMorTier::chunks() sequenceGraAlignmentPair.mor_chunk_index
SemanticWordIndex11-indexedAuthor-written position in a %gra relationGrammaticalRelation::index, ::head

The bug class the newtypes prevent: taking a MorChunkIndex from a %gra alignment pair and using it as a MorItemIndex in the main↔mor alignment. On any post-clitic, that lands on the wrong item and mis- renders highlights, hovers, or navigation jumps. GraAlignmentPair carries Option<MorChunkIndex> / Option<GraIndex> rather than bare Option<usize>, so the two spaces cannot swap at the type level. GraHeadRef { Root | Word(SemanticWordIndex1) } forces callers to handle the head == 0 ROOT sentinel explicitly instead of inlining the magic number.

Flow for a %gra click

sequenceDiagram
    participant U as User
    participant TS as VS Code (TS)
    participant LSP as chatter-lsp
    participant M as talkbank-model

    U->>TS: Click "2|0|ROOT" on %gra line
    TS->>LSP: textDocument/documentHighlight
    LSP->>LSP: find_gra_item_index_at_offset → GraIndex(1)
    LSP->>LSP: gra_alignment.pairs find GraIndex(1) → MorChunkIndex(1)
    LSP->>M: MorTier::item_index_of_chunk(1)
    M-->>LSP: Some(MorItemIndex(0))   // same host as chunk 0
    LSP->>LSP: mor_alignment.pairs find target=MorItemIndex(0) → alignment_idx 0
    LSP->>LSP: get_alignable_content_by_index(main, 0) → "it's"
    LSP-->>TS: DocumentHighlight[TEXT] on "it's"
    TS-->>U: Highlight renders on main tier

The user-facing behavior is described in Navigation → Cross-Tier Alignment.

Validation Pipeline

The validation pipeline is the most critical path in the server:

validate_and_publish(resources, uri, text, old_text?)
  1. Parse text with TreeSitterParser -> ChatFile + Vec<ParseError> + Tree
  2. Run model validation (ChatFile::validate()) -> Vec<ParseError>
  3. Convert ParseError -> lsp_types::Diagnostic
     - Map Span to LSP Range
     - Map severity to DiagnosticSeverity
     - Set error code, source ("talkbank")
     - Attach related_information (context pointers)
  4. Build ValidationCache (grouped by scope)
  5. Update all backend caches
  6. Publish diagnostics via client.publish_diagnostics()

Performance Notes

Edit debounce

A 250 ms debounce on did_change trades responsiveness against CPU usage. For large files, parsing + validation can take 10–50 ms, so 250 ms prevents thrashing on fast typing while keeping diagnostics near-live. Saves bypass the debounce and validate immediately.

DashMap vs thread-local services

DashMap is used for every per-document cache because multiple LSP requests can arrive concurrently (e.g. hover while validation is running). The tree-sitter parser and semantic-token provider are thread-confined resources; the backend reaches them through thread-local language services instead of holding a shared Mutex, eliminating one contention point under load.

Semantic tokens

Full semantic tokens are recomputed on every request. Range-based semantic tokens compute tokens only for the visible range — cheaper on large files. Delta (incremental) semantic tokens are not yet implemented and are not on the critical path.

Validation Explorer vs per-file validation

The Validation Explorer shells out to the chatter CLI, which uses crossbeam workers for fully parallel corpus-scale validation. This is intentionally separate from the LSP: the LSP handles per-file validation on every edit; the CLI handles bulk validation across thousands of files. Keeping these two paths separate avoids burning the LSP loop on multi-second corpus scans.

Webview Panels

All webview panels follow the singleton pattern: createOrShow() reuses an existing panel or creates a new one. Communication between the extension and webviews uses the PostMessage JSON protocol.

PanelFilePurpose
AnalysisPanelanalysisPanel.tsCLAN analysis results as styled tables
GraphPanelgraphPanel.tsDependency graph (DOT to SVG via Graphviz WASM)
MediaPanelmediaPanel.tsAudio/video playback with segment tracking
WaveformPanelwaveformPanel.tsWeb Audio API waveform visualization
KidevalPanelkidevalPanel.tsKidEval/Eval/Eval-D normative comparison
IdEditorPanelidEditorPanel.ts@ID header table editor
PicturePanelpicturePanel.tsElicitation picture display
CoderPanelcoderPanel.tsCoder Mode code insertion

Analysis Pipeline

The analysis pipeline illustrates the full flow from user action to rendered output:

flowchart LR
    user["User\nright-click > Run Analysis"]
    pick["QuickPick\n(33 commands)"]
    lsp["LSP Request\ntalkbank/analyze"]
    runner["AnalysisRunner\n(clan-core crate)"]
    json["JSON result"]
    panel["AnalysisPanel\nWebview"]
    table["HTML tables\n+ stat cards\n+ bar charts"]

    user --> pick --> lsp --> runner --> json --> panel --> table

No per-command renderer is needed – the AnalysisPanel renders JSON results generically as section headings, key-value fields, and tables.

Adding Features

Status: Current Last updated: 2026-04-16 22:07 EDT

Step-by-step for adding a new LSP capability. The pattern is consistent across all features; follow it and the compiler will refuse to let you leave a capability half-wired.

Standard LSP capability

1. Advertise the capability

Edit crates/chatter-lsp/src/backend/capabilities.rs and add the provider to build_initialize_result():

// Example: adding rename support
rename_provider: Some(OneOf::Left(true)),

Without this, VS Code never sends requests for the feature.

2. Create the feature handler

Add a new file in crates/chatter-lsp/src/backend/features/ (e.g. rename.rs). Handlers are pure functions over backend state:

use tower_lsp::lsp_types::*;
use crate::backend::LspBackendError;
use crate::backend::chat_file_cache::load_chat_file;
use crate::backend::state::Backend;

pub(crate) fn handle_rename(
    backend: &Backend,
    params: RenameParams,
) -> Result<Option<WorkspaceEdit>, LspBackendError> {
    let uri = &params.text_document_position.text_document.uri;
    let text = backend
        .documents()
        .get_text(uri)
        .ok_or(LspBackendError::DocumentNotFound)?;
    let chat_file = load_chat_file(backend, uri, &text)?;
    // ... build the WorkspaceEdit from the typed model
    Ok(Some(edit))
}

Two invariants:

  • Return Result<_, LspBackendError>, never a stringly Result<_, String>. Add a new variant to LspBackendError (or a subsystem sub-enum like GraphEdgeError) when the failure doesn’t fit an existing one.
  • Route through chat_file_cache::load_chat_file (or its pair variant load_document_and_chat_file) for parsed-document access. Do not re-implement the cache-then-reparse shape. The single loader emits the tracing::debug! used for stale-baseline observability.

3. Wire it into the LanguageServer trait

Edit the appropriate request-handler module under crates/chatter-lsp/src/backend/requests/:

async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
    Ok(features::rename::handle_rename(&self.backend, params)
        .map_err(stringify_for_lsp)
        .ok()
        .flatten())
}

The tower-lsp trait’s return type is jsonrpc::Result<_> so the typed error collapses to a stringified LSP error at the protocol boundary. Everything before that point stays typed.

4. Register the feature module

Add the new module to backend/features/mod.rs:

pub(crate) mod rename;

5. Write tests

Add a #[cfg(test)] mod tests inside the feature file. Use the shared fixtures:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_fixtures::{parse_chat, parse_tree};

    #[test]
    fn rename_emits_workspace_edit() {
        let content = "@UTF8\n@Begin\n...\n@End\n";
        let chat_file = parse_chat(content);
        let tree = parse_tree(content);
        // ... call handle_rename and assert
    }
}
}

Never redefine parse_chat / parse_tree locally — crate::test_fixtures owns the shared helpers. Duplicating these helpers in a new test module leads to drift between how production and test code construct CHAT inputs and has caused real bugs to slip through in the past.

Standard handler pattern

Every feature handler follows the same shape:

  1. Pull the document text via backend.documents().get_text(uri).
  2. Get (or cache-miss-then-parse) the ChatFile via chat_file_cache::load_chat_file (or _document_and_chat_file when text is also needed).
  3. Get the tree-sitter Tree via backend.language_services when the handler walks the CST rather than the model.
  4. Compute the result using the typed model (ChatFile, AlignmentSet, MorTier, …). Never re-parse CHAT text.
  5. Convert to LSP types and return.

All CHAT semantics live in talkbank-model; the LSP layer is format conversion and routing. If a feature needs a new primitive on the model (e.g. a chunk-walking iterator), add it in talkbank-model and delegate — do not grow the primitive in the LSP crate. See crates/chatter-lsp/CLAUDE.md.

Adding a VS Code command

In package.json

Add the command declaration:

{
    "command": "talkbank.myFeature",
    "title": "My Feature",
    "category": "TalkBank"
}

Add keybindings, menu entries, and when clauses as needed.

In src/activation/commands/<family>.ts

Register the handler through the effect runtime:

import { Effect } from 'effect';
import { registerEffectCommand } from '../../effectCommandRuntime';

export function registerMyFeatureCommand(
    context: vscode.ExtensionContext,
    services: ExtensionRuntimeServices,
): void {
    registerEffectCommand(context, 'talkbank.myFeature', () =>
        Effect.gen(function* () {
            // ... typed-effect work, including LSP RPC calls
        }),
    );
}

Direct vscode.commands.registerCommand is reserved for the Validation Explorer (see ADR-002). New commands default to the effect runtime — its typed error algebra composes across RPC + webview + per-feature boundaries.

Where command handlers live

Registration pathWhereWhat goes here
registerEffectCommand(...) via src/activation/commands/{analysis,editor,media,utility}.tsEffect runtime with typed errorsDefault choice for new commands.
vscode.commands.registerCommand(...) via src/activation/validation.tsDirect VS Code APIValidation Explorer only — the tree view’s context-menu state does not fit the effect runtime.
crates/chatter-lsp/src/backend/execute_commands.rs + per-family handlerServer-side LSP RPC dispatchThe 12 talkbank/* commands invoked from TS via workspace/executeCommand. See RPC Contracts.

A registration sanity test asserts every contributes.commands entry has a handler in one of the three paths — adding a command to package.json without wiring it is a test failure, not a silent runtime miss.

Adding a custom LSP RPC command

For features that don’t map to standard LSP capabilities, use workspace/executeCommand.

  1. Add a new variant to ExecuteCommandName and ExecuteCommandRequest in crates/chatter-lsp/src/backend/execute_commands.rs. Assign it to a feature family (Documents, Analysis, Participants, ChatOps).
  2. Add the command-name string to ExecuteCommandName::as_str / ::parse / ::ALL. The unit test in the same file will pin the change against advertised_commands().
  3. Add the handler in the appropriate family module (analysis.rs, participants.rs, chat_ops/, etc.) returning Result<serde_json::Value, LspBackendError>.
  4. Add dispatch to the family’s CommandService in backend/requests/execute_command.rs.
  5. On the TS side, add a typed method to TalkbankExecuteCommandClient in src/lsp/executeCommandClient.ts and the request/response schemas in src/lsp/executeCommandPayloads.ts.
  6. Document the new endpoint in reference/rpc-contracts.md — per the workspace policy, RPC contracts are book-first.

Custom Commands

Status: Current Last updated: 2026-04-16 22:09 EDT

The TalkBank language server defines twelve custom LSP commands invoked via workspace/executeCommand. They provide functionality that doesn’t fit standard LSP capabilities (hover, completion, rename) — things like “run a CLAN analysis,” “produce an alignment sidecar,” “filter a document by speaker.”

The per-command reference lives at reference/rpc-contracts.md. That page is the single source of truth for request/response shapes, TS callers, Rust handlers, and example payloads. This chapter covers the shared architecture behind all twelve endpoints.

Why custom commands?

Standard LSP capabilities have fixed request/response shapes defined by the LSP specification. Custom commands allow arbitrary JSON parameters and typed return values — the escape hatch protocols use for extension-specific functionality. Alternatives considered and rejected: ad-hoc HTTP sidecars, embedded WASM parsers, shared-memory bridges. See ADR-001.

Round-trip sequence

%% Verified against: apps/vscode-extension/src/lsp/executeCommandClient.ts,
%% crates/chatter-lsp/src/backend/requests/execute_command.rs,
%% crates/chatter-lsp/src/backend/execute_commands.rs,
%% crates/chatter-lsp/src/backend/analysis.rs.
sequenceDiagram
    participant C as Caller<br/>(command / panel)
    participant E as TalkbankExecuteCommandClient<br/>(src/lsp/executeCommandClient.ts)
    participant LC as LanguageClient<br/>(vscode-languageclient)
    participant S as chatter-lsp dispatcher<br/>(backend/requests/execute_command.rs)
    participant D as Request decoder<br/>(backend/execute_commands.rs)
    participant H as Handler<br/>(e.g. backend/analysis.rs)
    participant M as talkbank-model<br/>(ChatFile, alignment, CLAN runner)

    C->>E: invoke(talkbank/analyze, typed args)
    E->>E: Effect.Schema encode args
    E->>LC: workspace/executeCommand(name, [json])
    LC->>S: JSON-RPC over stdio
    S->>D: ExecuteCommandRequest::parse(name, args)
    D-->>S: typed ExecuteCommandRequest variant
    S->>H: dispatch to family handler
    H->>M: ChatFile lookup, alignment use, CLAN runner
    M-->>H: typed result
    H-->>S: Result<serde_json::Value, LspBackendError>
    S-->>LC: JSON-RPC response (or string-valued error)
    LC-->>E: Promise<unknown>
    E->>E: Effect.Schema decode result
    E-->>C: typed TS value

Typed boundary rules

  • Every command name is a variant of ExecuteCommandName in execute_commands.rs. Adding a command means adding a variant — the compiler forces you to handle it in dispatch and the unit test pins the advertised-list against the enum.
  • Every request payload decodes through ExecuteCommandRequest::parse. On malformed input the dispatcher returns a typed LspBackendError, never panics.
  • Every response is serde_json::Value at the LSP wire boundary; handlers build typed structs and serde_json::to_value at the edge. The ? operator lifts serialization failures via LspBackendError::JsonSerializeFailed.
  • On the TypeScript side, TalkbankExecuteCommandClient wraps client.sendRequest('workspace/executeCommand', …) and runs the JSON through an Effect Schema decoder. Callers receive typed values, never unknown. Three tagged error classes classify failure modes — see executeCommandErrors.ts.

Usage from TypeScript

// Example: getting speakers for the filter picker
import type { ExecuteCommandStructuredError } from './lsp/executeCommandErrors';

const speakersEffect: Effect.Effect<SpeakerInfo[], ExecuteCommandStructuredError> =
    client.getSpeakers({ uri: document.uri.toString() });

Never call client.sendRequest('workspace/executeCommand', ...) directly — every custom command flows through the TalkbankExecuteCommandClient facade so the payload decode stays typed. If a new command is needed, add a method to the facade (see Adding Features for the full checklist).

Adding a new custom command

See the detailed checklist in Adding Features → Adding a custom LSP RPC command. The short version: variant in ExecuteCommandName, handler in the appropriate family module, dispatch in backend/requests/execute_command.rs, typed method on TalkbankExecuteCommandClient, and a new row in reference/rpc-contracts.md.

Testing

Status: Current Last updated: 2026-06-01 00:52 PDT

This chapter covers how to test both the Rust language server and the TypeScript VS Code extension.

Rust Language Server Tests

Running Tests

cargo test -p chatter-lsp

Or with the preferred parallel test runner:

cargo nextest run -p chatter-lsp

Test Locations

Tests live next to the code they cover, in #[cfg(test)] mod tests blocks. Notable places:

LocationWhat It Tests
alignment/tests.rsCross-tier alignment computation and hover info
graph/tests.rsDOT graph generation from %gra data, including stale-baseline marker
backend/features/*/{mod,tests}.rsPer-feature handlers (hover, highlights, completion, rename, references, …)
test_fixtures.rsShared parse_chat / parse_chat_with_alignments / parse_tree / parse_tree_incremental helpers

Shared fixtures

Tests build their CHAT + tree-sitter inputs through crate::test_fixtures. The module exposes four helpers and nothing else; new test modules must not redefine their own parse_chat or parse_tree — duplicating these helpers leads to drift between how production and test code construct CHAT inputs, which has caused real bugs to slip through in the past.

use crate::test_fixtures::{parse_chat, parse_chat_with_alignments, parse_tree};
HelperReturnsUse when
parse_chat(content)ChatFileTest needs a parsed model but no per-utterance alignments
parse_chat_with_alignments(content)ChatFile with utterance.alignments = Some(_)Test exercises main↔%mor/%gra/%pho/%sin alignment or the %wor timing sidecar
parse_tree(content)tree_sitter::TreeTest walks the CST (most feature tests)
parse_tree_incremental(content)tree_sitter::Tree via TreeSitterParser::parse_tree_incrementalTest exercises the incremental-parse code path (references / rename)

Test pattern

Feature handlers are pure functions (&Backend or &Utterance + params → result), making them straightforward to test:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_fixtures::{parse_chat_with_alignments, parse_tree};

    #[test]
    fn hover_on_main_tier_word_includes_mor_lemma() {
        let content = "@UTF8\n@Begin\n...\n*CHI:\thello .\n%mor:\tn|hello .\n@End\n";
        let chat_file = parse_chat_with_alignments(content);
        let tree = parse_tree(content);
        let pos = Position { line: 3, character: 7 };

        let hover = hover(&chat_file, &tree, pos, content, ParseState::Clean)
            .expect("hover must resolve on the main-tier word");
        let text = match &hover.contents {
            HoverContents::Markup(m) => &m.value,
            _ => panic!("expected markup"),
        };

        assert!(text.contains("%mor") || text.contains("Lemma"));
    }
}
}

Mandatory regression gates

Any change touching parser, model, validation, alignment, or serialization must pass the following before commit:

cargo nextest run -p talkbank-parser-tests -E 'test(parser_equivalence)'
cargo nextest run -p talkbank-parser-tests --test roundtrip_reference_corpus

Both must show 93 passed and 98 passed respectively (current numbers). See the workspace CLAUDE.md for the full pre-merge gate list.

TypeScript Extension Tests

Running Tests

cd apps/vscode-extension
npm test              # Run all tests (vitest)
npm run lint          # ESLint
npm run test:coverage # Tests with coverage report

Alternatively, run via the just wrapper from the repo root:

just vscode test

Test Framework

The extension uses Vitest for unit tests. Test files live in src/test/.

What to Test

  • Utility functions (bulletParser.ts, alignmentSidecar.ts, mediaResolver.ts) – these are pure functions with no VS Code API dependencies
  • Panel message handling – test that PostMessage payloads are correctly formatted
  • Command argument construction – test that LSP command arguments are correctly assembled

VS Code API interactions (commands, webviews, tree views) are harder to unit test. For those, use manual testing.

Manual Testing

Launch Development Extension

cd apps/vscode-extension
code --extensionDevelopmentPath=.

This opens a new VS Code window with the extension loaded from source. Changes require recompiling (npm run compile) and reloading the window (Cmd+Shift+P > “Developer: Reload Window”).

Test Files

Use files from resources/corpus/reference/ for manual testing. This directory contains 87 CHAT files covering 20 languages and diverse CHAT constructs.

Debugging the Language Server

Enable detailed logging by setting RUST_LOG before launching VS Code:

RUST_LOG=debug code --extensionDevelopmentPath=.

For targeted tracing of a specific module:

RUST_LOG=chatter_lsp::alignment=trace code --extensionDevelopmentPath=.

Server output appears in the Output panel under “TalkBank Language Server”.

LSP Message Tracing

To inspect the raw JSON-RPC messages between VS Code and the server:

  1. Check if the extension supports the chatter-lsp.trace.server setting
  2. Set it to "verbose" to log all request/response payloads
  3. Alternatively, use VS Code’s built-in LSP inspector (if available in your version)

Coverage

cd apps/vscode-extension
npm run test:coverage

This generates a coverage report using Vitest’s built-in coverage via @vitest/coverage-v8. Check the report to identify untested utility functions or edge cases.

What Must Pass Before Committing

For LSP changes:

cargo nextest run -p chatter-lsp        # LSP tests
cargo nextest run -p talkbank-parser-tests  # Parser equivalence (if touching parsing)

For extension changes:

cd apps/vscode-extension && npm test && npm run lint

For any change:

bazel build //... && bazel test //...    # Full pre-merge verification gates (G0-G13)

Releasing

Status: Current Last modified: 2026-06-01 01:05 PDT

The VS Code extension is currently published manually. There is no release workflow in .github/workflows/; the only CI for the extension is bazel-typescript.yml, which on every PR builds, tests, and packages a single-platform .vsix and uploads it as a workflow artifact (vscode-extension-vsix).

An automated multi-platform release workflow (with a bundled chatter-lsp per target, GitHub Release upload, and optional marketplace push) is a planned follow-up. Until then, the process below is the source of truth.

CI coverage today

WorkflowTriggerWhat it does
bazel-typescript.yml (vscode-extension job)push/PR on apps/vscode-extension/** or schemas/**bazel run //apps/vscode-extension:build, :test, :package; uploads .vsix as workflow artifact

There is no vscode-release.yml. Any reference to a 5-platform matrix or vscode-vX.Y.Z tagging workflow is aspirational.

Cutting a release (manual)

  1. Bump apps/vscode-extension/package.json version (semver).
  2. Open a PR with the version bump; merge once green.
  3. On a maintainer workstation, from the repo root:
    just vscode build
    just vscode package      # produces apps/vscode-extension/*.vsix
    
    Or grab the vscode-extension-vsix artifact from the bazel-typescript.yml run on the merge commit.
  4. Publish to the Visual Studio Marketplace using the operator’s PAT:
    cd apps/vscode-extension
    npx vsce publish --packagePath talkbank-chat-<version>.vsix
    
    The PAT is held by the operator, not in repo secrets. Create at https://dev.azure.com/_usersSettings/tokens with scope “Marketplace > Manage”.
  5. Optionally attach the .vsix to a manually-created GitHub Release for users who prefer side-loading.

Local packaging for testing

Same flow as a release build, just skip the marketplace step:

just vscode build
just vscode package
code --install-extension apps/vscode-extension/talkbank-chat-<version>.vsix

If you want the extension to talk to a locally-built chatter-lsp, make sure that binary is on your PATH (e.g. bazel build //crates/chatter/chatter-lsp:chatter-lsp then point PATH at bazel-bin/...). Today’s .vsix does not bundle a server binary; the extension finds chatter-lsp via discovery order documented in LSP Binary Discovery.

Side-loading a .vsix (users)

code --install-extension talkbank-chat-<version>.vsix

Users still need chatter-lsp on PATH. Bundling a per-platform chatter-lsp into the .vsix is part of the planned release-workflow follow-up.

Version-number discipline

apps/vscode-extension/package.json is the single source of truth. Pre-release identifiers (0.X.Y-rc.1) are allowed per semver. Do not hand-roll release tags; once an automated workflow exists, it will own the tag format.

Planned follow-ups

  • A bazel-vscode-release.yml (or similar) that:
    • builds chatter-lsp per target platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64),
    • bundles the matching binary into a per-platform .vsix,
    • uploads .vsix files to a vscode-vX.Y.Z GitHub Release,
    • optionally runs vsce publish using a repo-held PAT secret.
  • Signed chatter-lsp binaries inside each .vsix (see Code Signing and Distribution).

CLAN: how-to guides

Choose the task you want to complete.

Installation

Status: Current CLAN commands are part of the chatter CLI tool.

From source

# Clone the repository
git clone https://github.com/TalkBank/batchalign.git

# Build
cd batchalign
cargo install --path crates/chatter/chatter-cli

Verify installation

chatter clan --help
chatter clan freq --help

Requirements

  • Rust 2024 edition (rustc 1.85+)
  • macOS, Linux, or Windows
  • No runtime dependencies beyond the binary

Migrating from CLAN

Status: Current Last updated: 2026-05-11 18:58 EDT

This guide helps users of the original CLAN C binaries transition to chatter clan.

Command mapping

Legacy CLAN commands map directly:

LegacyNewNotes
freq file.chachatter clan freq file.cha
mlu +t*CHI file.chachatter clan mlu --speaker CHI file.cha
combo +s"the" file.chachatter clan combo --include-word "the" file.cha
flo file.chachatter clan flo file.cha

Flag translation

Legacy CLAN uses +flag/-flag syntax. Both styles work — the CLI automatically translates:

CLAN flagModern equivalentMeaning
+t*CHI--speaker CHIInclude speaker
-t*CHI--exclude-speaker CHIExclude speaker
+t%mor--tier morInclude dependent tier
-t%gra--exclude-tier graExclude dependent tier
+t@ID="..."--id-filter "..."Filter by @ID fields
+s<word>--include-word <word>Include word
-s<word>--exclude-word <word>Exclude word
+g<label>--gem <label>Include gem
-g<label>--exclude-gem <label>Exclude gem
+z25-125--range 25-125Utterance range
+r6--include-retracingsCount retraced material
+u(default)Merge speakers (always on)
+dN--display-mode NDisplay mode — currently non-functional, see flag-translation.md for the rewriter-honor plan
+k--case-sensitiveCase-sensitive matching — currently non-functional, see flag-translation.md
+fEXT--output-ext EXTOutput file extension — currently non-functional, see flag-translation.md

You can continue using +t*CHI syntax for the functional flags above if you prefer — it works identically.

New capabilities

Features not available in legacy CLAN:

  • JSON and CSV output — --format json or --format csv for programmatic processing
  • Recursive directories — pass a directory path to process all .cha files
  • Unified CLI — all commands under one chatter clan namespace
  • Structured errors — rich diagnostics with file location and context

Known differences

See Why We Diverge and Per-Command Divergences for a complete list. Key points:

  • Output formatting may differ slightly (spacing, alignment)
  • Some legacy CLAN C bugs are fixed rather than replicated
  • Commands not ported are documented in Commands Not Ported

Flag Translation Guide

Status: Current Last updated: 2026-05-11 18:58 EDT

The original CLAN uses a +flag/-flag syntax that differs from standard CLI conventions. The Rust reimplementation accepts both styles — legacy CLAN flags are automatically rewritten to modern --flag equivalents before parsing.

Complete Flag Mapping

CLAN FlagModern EquivalentMeaning
+t*CHI--speaker CHIInclude speaker
-t*CHI--exclude-speaker CHIExclude speaker
+t%mor--tier morInclude dependent tier
-t%gra--exclude-tier graExclude dependent tier
+t@ID="..."--id-filter "..."Filter by @ID header pattern
+s<word>--include-word <word>Include word
-s<word>--exclude-word <word>Exclude word
+g<label>--gem <label>Include gem segment
-g<label>--exclude-gem <label>Exclude gem segment
+z25-125--range 25-125Utterance range
+r6--include-retracingsCount retraced material
+u(default behavior)Merge speakers (already default)
+dN--display-mode NNumeric display mode — currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:101 produces --display-mode, but no clap field consumes it. Tracked in docs/superpowers/plans/2026-05-11-clan-rewriter-honor-three-flags.md Phase 3 (pending PI review).
+k--case-sensitiveCase-sensitive matching — currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:104 produces --case-sensitive, but no clap field consumes it. Word matching is case-insensitive today.
+fEXT--output-ext EXTOutput file extension — currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:107 produces --output-ext, but no clap field consumes it. Tracked as Phase 2 of the rewriter-honor plan (blocked on a batch-output prerequisite).
+wN--context-after NContext lines after match
-wN--context-before NContext lines before match

Examples

Speaker Filtering

# Original CLAN: include CHI, exclude MOT
freq +t*CHI -t*MOT file.cha

# Modern equivalent
chatter clan freq --speaker CHI --exclude-speaker MOT file.cha
# Original CLAN: search for "want" and "need"
combo +s"want" +s"need" file.cha

# Modern equivalent
chatter clan combo --include-word want --include-word need file.cha

Combined Filters

# Original CLAN: CHI speaker, utterances 10-50, include retraced material
freq +t*CHI +z10-50 +r6 file.cha

# Modern equivalent
chatter clan freq --speaker CHI --range 10-50 --include-retracings file.cha

Notes

  • The +u flag (merge speakers into a single analysis) is the default behavior and is accepted but ignored.
  • Flags are position-independent — they can appear before or after file arguments.
  • Unknown flags that don’t match CLAN patterns pass through unchanged to clap, which will report an error with suggestions.
  • The strikethrough entries above (+dN, +k, +fEXT) are rewritten by the legacy-flag layer but currently rejected by clap because no consuming field exists yet. See migrating-from-clan.md for the same caveats and the rewriter-honor plan that tracks them.

Running Commands

Status: Current

Basic invocation

chatter clan <command> [options] <input>

The <input> can be a single .cha file or a directory (all .cha files are processed recursively).

Getting help

chatter clan --help              # List all commands
chatter clan freq --help         # Help for a specific command

Command categories

Commands are grouped into three categories:

  • Analysis commands — read CHAT files and produce statistics (FREQ, MLU, etc.)
  • Transform commands — read CHAT files and write modified CHAT files (FLO, CHSTRING, etc.)
  • Format converters — convert between CHAT and other formats (ELAN2CHAT, CHAT2SRT, etc.)

Exit codes

CodeMeaning
0Success
1Error (parse failure, missing file, invalid options)

Environment variables

VariableEffect
RUST_LOGControl log verbosity (debug, trace, etc.)
NO_COLORDisable colored output

Filtering

Status: Current The framework provides a unified filtering system shared by all commands. Filters restrict which utterances, speakers, tiers, and words are processed.

Multiple filters can be combined — they are applied as an AND (all must match for an utterance to be included).

flowchart TD
    utt["Each utterance"]
    speaker{"Speaker\nfilter?"}
    tier{"Tier\nfilter?"}
    word{"Word\nfilter?"}
    gem{"Gem\nfilter?"}
    range{"Range\nfilter?"}
    process["process_utterance()"]
    skip["Skip"]

    utt --> speaker
    speaker -->|pass| tier
    speaker -->|fail| skip
    tier -->|pass| word
    tier -->|fail| skip
    word -->|pass| gem
    word -->|fail| skip
    gem -->|pass| range
    gem -->|fail| skip
    range -->|pass| process
    range -->|fail| skip

Filter types

FilterFlagCLAN equivalentEffect
Speaker--speaker / --exclude-speaker+t* / -t*Include/exclude by speaker code
Tier--tier / --exclude-tier+t% / -t%Include/exclude dependent tiers
Word--include-word / --exclude-word+s / -sFilter by word content
Gem--gem / --exclude-gem+g / -gRestrict to gem segments
Range--range+zUtterance number range
ID--id-filter+t@ID=Filter by @ID header fields

Examples

# Child speaker only, within a gem
chatter clan freq --speaker CHI --gem "story" file.cha

# All speakers except investigator, utterances 10-50
chatter clan mlu --exclude-speaker INV --range 10-50 file.cha

# Only utterances containing "the"
chatter clan kwal --include-word "the" file.cha

Speaker Filtering

Status: Current Last updated: 2026-05-11 18:58 EDT

Speaker filters restrict which speakers’ utterances are analyzed. This is one of the most frequently used filters — most CLAN analyses target a specific participant (e.g., the child in a child language study).

Include speakers

Analyze only specific speakers:

chatter clan freq --speaker CHI file.cha
chatter clan mlu --speaker CHI --speaker MOT file.cha

CLAN equivalent: +t*CHI, +t*CHI +t*MOT

Multiple --speaker flags use OR logic: utterances from any listed speaker are included.

Exclude speakers

Remove specific speakers from analysis:

chatter clan freq --exclude-speaker INV file.cha

CLAN equivalent: -t*INV

@ID filtering

Filter speakers by metadata fields in the @ID header. The filter is pipe-delimited, in CHAT @ID column order:

lang|corpus|speaker|age|sex|group|ses|role|education|custom

Each field is either * (wildcard), empty (also wildcard), or a literal exact match. Trailing fields may be omitted entirely.

# All children (role = Target_Child, at position 8)
chatter clan freq --id-filter "*|*|*|*|*|*|*|Target_Child" file.cha

# English-language speakers only
chatter clan freq --id-filter "eng|" file.cha

# A specific speaker code (position 3)
chatter clan freq --id-filter "*|*|CHI" file.cha

CLAN equivalent: +t@ID="*|*|*|*|*|*|*|Target_Child".

Authoritative parser: crates/clan/clan-core/src/framework/id_filter.rs. The language field is a comma-separated list in the underlying @ID; the filter matches if any language in the @ID matches the pattern.

Interaction with other filters

  • Speaker filtering is applied first, before range or word filters
  • Include and exclude can be combined; excludes are applied after includes
  • Speaker codes are case-sensitive and must match the @Participants header exactly (e.g., CHI, not chi)

Tier Filtering

Status: Current Tier filters control which dependent tiers are included in command output. This primarily affects commands like KWAL and COMBO that echo matching utterances with their dependent tiers.

Include tiers

Show only specific dependent tiers:

chatter clan kwal --tier mor --include-word "want" file.cha

CLAN equivalent: +t%mor

This outputs matching utterances with only the %mor tier, omitting %gra, %pho, and other tiers.

Exclude tiers

Hide specific dependent tiers from output:

chatter clan kwal --exclude-tier gra file.cha

CLAN equivalent: -t%gra

Common dependent tiers

TierFull nameContent
morMorphologyPOS tags and lemmas: noun|dog-PL
graGrammarDependency relations: 1|3|NSUBJ
phoPhonologyPhonological transcription
floFluent outputSimplified main-tier text
codCodesResearcher-assigned codes
modModelTarget pronunciation
retRetraceCopy of main tier

Notes

  • Tier names are specified without the % prefix
  • Tier filtering does not affect the analysis itself (e.g., MLU still counts from %mor even if %mor is excluded from display)
  • Main speaker tiers (*CHI:, *MOT:) are always included

Word Filtering

Status: Current Last updated: 2026-05-11 18:58 EDT

Word filters restrict analysis to utterances containing (or not containing) specific words. Primarily useful with KWAL (keyword search) and COMBO (boolean search), but available on all commands.

Include words

Only process utterances containing a specific word:

chatter clan kwal --include-word "the" file.cha
chatter clan combo --include-word "dog" --include-word "cat" file.cha

CLAN equivalent: +s"the", +s"dog" +s"cat"

Multiple --include-word flags use OR logic by default: utterances containing any listed word are included.

Exclude words

Skip utterances containing specific words:

chatter clan freq --exclude-word "um" --exclude-word "uh" file.cha

CLAN equivalent: -s"um" -s"uh"

Case sensitivity

By default, word matching is case-insensitive (the matches The, THE, the).

The CLAN +k flag (and its --case-sensitive rewrite) is currently non-functional: the rewriter at crates/clan/clan-core/src/clan_args.rs:104 produces --case-sensitive, but no clap field consumes it. Word matching today is always case-insensitive. Tracked alongside +dN and +fEXT in docs/superpowers/plans/2026-05-11-clan-rewriter-honor-three-flags.md. See also flag-translation.md.

What counts as a “word”

Word matching uses the same countable-word logic as other commands:

  • Regular words and proper nouns match
  • Untranscribed markers (xxx, yyy, www) do not match
  • Zero words (0word) do not match
  • Fillers and fragments (&-um, &~frag) do not match
  • Events (&=laughs) do not match

Gem Filtering

Status: Current Gem filters restrict analysis to utterances within @Bg / @Eg (Begin Gem / End Gem) boundaries. Gems are used to mark segments of a transcript — for example, a story retelling, a conversation about a specific topic, or a test activity.

Include gems

Analyze only utterances within a labeled gem segment:

chatter clan freq --gem "story" file.cha
chatter clan mlu --gem "frog story" file.cha

CLAN equivalent: +g"story"

Exclude gems

Skip utterances within specific gem segments:

chatter clan freq --exclude-gem "warmup" file.cha

CLAN equivalent: -g"warmup"

How gems work in CHAT files

Gem boundaries are marked with header lines:

@Bg:	story
*CHI:	once upon a time .
*CHI:	there was a frog .
@Eg:	story

Only utterances between matching @Bg/@Eg pairs with the specified label are included. Utterances outside all gem boundaries are excluded.

  • GEMLIST — list all gem segment labels in a file
  • freq --gem — frequency analysis restricted to gem segments (equivalent to legacy GEMFREQ)

Utterance Range Filtering

Status: Current Range filters restrict analysis to a numbered subset of utterances, useful for analyzing specific portions of a transcript without splitting the file.

Usage

chatter clan freq --range 10-50 file.cha
chatter clan mlu --range 1-100 file.cha

CLAN equivalent: +z10-50

Behavior

  • Utterance numbering starts at 1
  • Both endpoints are inclusive (e.g., --range 10-50 includes utterances 10, 11, …, 50)
  • Only main-tier utterances (*SPK: lines) are counted; headers, dependent tiers, and comments are not numbered
  • Range filtering is applied after speaker filtering — if you filter to --speaker CHI, utterance 1 is the first CHI utterance, not the first utterance in the file

Open-ended ranges

To analyze from a point to the end of the file, use a large upper bound:

chatter clan freq --range 50-9999 file.cha

Combining with other filters

Range composites with all other filters:

# First 50 CHI utterances within a gem segment
chatter clan mlu --speaker CHI --range 1-50 --gem "story" file.cha

Output Formats

Status: Current Every analysis command produces a typed result that can be rendered in multiple formats.

Text (default)

Human-readable output, designed for terminal display:

chatter clan freq file.cha

CLAN-compatible text

Legacy CLAN output format, used for parity testing. Activated internally by render_clan().

JSON

Structured output for programmatic consumption:

chatter clan freq --format json file.cha

JSON output preserves all fields from the typed result struct, including metadata that text format may omit.

CSV

Tabular output for spreadsheet tools:

chatter clan mlu --format csv file.cha

Notes

  • Transform commands write modified CHAT files, not analysis output — format flags don’t apply
  • Converters produce the target format (SRT, ELAN XML, etc.)

Working with Directories

Status: Current Pass a directory path to process all .cha files recursively:

chatter clan freq corpus/
chatter clan mlu corpus/English/

Behavior

  • All .cha files under the directory are discovered and processed
  • Subdirectories are traversed recursively
  • Non-.cha files are silently ignored
  • Results are aggregated across all files (same as CLAN’s default behavior)

Per-file output

Some commands support --per-file mode to get separate results for each file:

chatter clan mlu --per-file corpus/

Combining with filters

Directory processing composes with all filters:

# CHI utterances across an entire corpus
chatter clan freq --speaker CHI corpus/

# First 100 utterances in each file
chatter clan mlu --range 1-100 corpus/English/

JSON output for scripting

Combine directory processing with JSON output for batch analysis:

chatter clan mlu --format json corpus/ > results.json

See Pipelines and Scripting for more advanced workflows.

Pipelines and Scripting

Status: Current Last updated: 2026-05-11 18:58 EDT

JSON for Scripting

For programmatic consumption, use --format json and pipe to jq. The schemas are the typed result structs in crates/clan/clan-core/src/commands/<cmd>.rs (each implements Serialize).

# Total token count for the first speaker in a FREQ run
chatter clan freq --format json file.cha | jq '.speakers[0].total_tokens'

# Get MLU value for CHI (assuming CHI is the only speaker after filtering)
chatter clan mlu --speaker CHI --format json file.cha | jq '.speakers[0].mlu'

Batch Processing

Process many files and collect results:

# Frequency analysis across an entire corpus
chatter clan freq --format csv corpus/ > results.csv

# Per-file MLU in JSON
for f in corpus/*.cha; do
    echo "=== $f ==="
    chatter clan mlu --speaker CHI --format json "$f"
done

Combining with Other Tools

# Find files with low MLU
chatter clan mlu --format json corpus/ | jq 'select(.speakers[].mlu < 2.0)'

# Top 10 most frequent words per speaker (FreqEntry.count is sorted desc by default)
chatter clan freq --format json file.cha | jq '.speakers[].entries[0:10]'

Transform Chains

Transforms write the rewritten CHAT to stdout by default, or to a path you pass with --output. To chain them, either redirect each stage to a file and feed it to the next, or pass --output explicitly:

# Clean up a file: lowercase, then compute ages — explicit intermediate files
chatter clan lowcase file.cha --output file.low.cha
chatter clan dates   file.low.cha --output file.final.cha

# Or with shell redirection
chatter clan lowcase file.cha > file.low.cha
chatter clan dates   file.low.cha > file.final.cha

Transforms do not modify the input file in place; see crates/clan/clan-core/src/framework/transform.rs::run_transform.

Adding a Command

Status: Current Last updated: 2026-05-12 17:30 EDT

Steps

  1. Create the module — crates/clan/clan-core/src/commands/<name>.rs with four types:

    • <Name>Config — command configuration (from CLI flags)
    • <Name>State — mutable accumulator (implements Default)
    • <Name>Result — typed output (implements Serialize, Debug, CommandOutput); this is what the trait’s associated Output type resolves to
    • <Name>Command — unit struct implementing AnalysisCommand
  2. Register — add pub mod <name>; to crates/clan/clan-core/src/commands/mod.rs

  3. Wire CLI — add a subcommand variant to ClanCommands in crates/chatter/chatter-cli/src/cli/args/clan_commands.rs

  4. Wire dispatch — add the match arm in the appropriate family module (analysis.rs, transforms.rs, converters.rs, compatibility.rs, or helpers.rs) under crates/chatter/chatter-cli/src/commands/clan/; keep run_clan() in crates/chatter/chatter-cli/src/commands/clan/mod.rs as the thin top-level dispatcher

  5. Add golden test — add a test case in the relevant file under crates/clan/clan-core/tests/clan_golden/

Skeleton

The trait is defined in crates/clan/clan-core/src/framework/command.rs. It has three associated types (Config, State, Output) and three methods (process_utterance, end_file with a default no-op impl, and finalize).

//! # NAME — Brief description
//!
//! What the command does and when to use it.

use serde::Serialize;
use talkbank_model::Utterance;

use crate::framework::{AnalysisCommand, CommandOutput, FileContext};

/// Command configuration.
pub struct NameConfig {
    // fields from CLI flags
}

/// Per-run accumulator (carried across all files).
#[derive(Default)]
pub struct NameState {
    // mutable state built up during processing
}

/// Typed analysis result.
#[derive(Debug, Serialize)]
pub struct NameResult {
    // typed output fields
}

impl CommandOutput for NameResult {
    fn render_text(&self) -> String { todo!() }
    fn render_clan(&self) -> String { todo!() }
}

/// The command.
pub struct NameCommand {
    pub config: NameConfig,
}

impl AnalysisCommand for NameCommand {
    type Config = NameConfig;
    type State = NameState;
    type Output = NameResult;

    fn process_utterance(
        &self,
        utterance: &Utterance,
        file_context: &FileContext<'_>,
        state: &mut Self::State,
    ) {
        // accumulate per-utterance data into `state`
    }

    // end_file has a default no-op impl; override only if you need
    // per-file finalization.

    fn finalize(&self, state: Self::State) -> Self::Output {
        // compute final result from the accumulated state
        todo!()
    }
}

Conventions

  • Use countable_words() for word iteration — don’t roll your own filter
  • Use NormalizedWord for frequency maps
  • Handle missing %mor gracefully (skip morpheme counting, don’t panic)
  • Keep the module under 400 lines; split into submodules well before it becomes another 800+ line file

Current Architecture Seams

Status: Current Last updated: 2026-05-12 17:35 EDT

This page documents the current internal seams that contributors should preserve when adding or restructuring CLAN-related functionality.

CLI command registration

Top-level CLI argument wiring is no longer in one file.

  • shared CLI args live in crates/chatter/chatter-cli/src/cli/args/core.rs
  • shared CLAN filters and common options live in crates/chatter/chatter-cli/src/cli/args/clan_common.rs
  • CLAN command variants live in crates/chatter/chatter-cli/src/cli/args/clan_commands.rs

If you add a new CLAN command, register it in the appropriate split argument module instead of rebuilding a monolithic args.rs.

CLAN dispatch

run_clan now lives in crates/chatter/chatter-cli/src/commands/clan/mod.rs and dispatches into category files:

  • analysis.rs
  • transforms.rs
  • converters.rs
  • compatibility.rs
  • helpers.rs (shared utilities consumed by the others)

Keep family-specific logic in those modules. Shared file resolution, filtering, and output helpers belong in helpers.rs, not copied into each family.

Validation output

Parallel validation output now has a renderer seam:

  • orchestration and stats live in crates/chatter/chatter-cli/src/commands/validate_parallel/runtime.rs
  • output shaping lives in crates/chatter/chatter-cli/src/commands/validate_parallel/renderer.rs
  • audit-specific behavior lives in crates/chatter/chatter-cli/src/commands/validate_parallel/audit.rs

If you need a new output mode, add a renderer implementation instead of extending a large runtime match.

Audit-mode JSONL writing is also intentionally isolated. crates/chatter/chatter-cli/src/commands/validate/audit_reporter.rs owns a dedicated writer thread and a cloneable reporting handle for workers, so future audit changes should preserve that explicit ownership boundary instead of reintroducing shared writer locks.

Dashboard state ownership

test-dashboard (under src/test_dashboard/ in the root talkbank-tools crate) now uses message passing rather than shared UI state. The worker sends DashboardEvent values and the UI reduces them into AppState.

That architecture is easier to test and reason about than Arc<Mutex<AppState>>. New dashboard features should generally be introduced as:

  1. a new variant of DashboardEvent in src/test_dashboard/app.rs
  2. reducer logic in AppState::apply_event in the same file
  3. worker-side emission in src/test_dashboard/runner.rs

Editor integration note

The VS Code extension and chatter-lsp use a typed execute-command boundary. The contract surface is documented in:

  • book/src/apps/vscode-extension/reference/rpc-contracts.md — the RPC contracts the extension and LSP speak
  • book/src/apps/vscode-extension/developer/custom-commands.md — how to add a new custom command
  • crates/chatter/chatter-lsp/CLAUDE.md — invariants for the server side

Testing Strategy

Status: Current Last updated: 2026-05-12 21:14 EDT

Unit tests

Each command module includes #[cfg(test)] unit tests that verify counting logic, edge cases, and output formatting against known inputs.

cargo nextest run -p clan-core          # All tests (preferred)
cargo test -p clan-core                 # Alternative
cargo test --doc -p clan-core           # Doctests only (nextest can't run these)

Snapshot tests

Snapshot tests use insta to capture command output and detect regressions. They live alongside the test that produces them:

  • crates/clan/clan-core/tests/snapshots/ — snapshots from transform_golden.rs, converter_golden.rs, and other top-level tests in the tests/ directory
  • crates/clan/clan-core/tests/clan_golden/snapshots/ — snapshots produced by the clan_golden test module (paired @clan/@rust pairs)
cargo insta review -p clan-core         # Review pending snapshot changes
cargo insta accept --all                    # Accept all pending changes

When you change a command’s output format, the snapshot test will fail. Review the diff to confirm the change is intentional, then accept.

Golden tests

Golden tests compare our output against legacy CLAN C binaries. See Golden Tests for details.

Running specific tests

# By name pattern
cargo nextest run -p clan-core -E 'test(freq)'

# Only golden tests
cargo nextest run -p clan-core -E 'test(golden)'

# Only indent tests
cargo nextest run -p clan-core -E 'test(indent)'

Test fixtures

Test fixtures are real CHAT files from the reference corpus at resources/corpus/reference/ (at the repo root). Never create ad hoc .cha test files — use existing corpus files or ask for new ones to be added to the reference corpus.

Current test counts

The clan-core suite spans unit, snapshot, golden, and integration tests. The absolute count moves as commands are added or extended; get the live number with:

cargo nextest list -p clan-core

The last line of the summary is the total count.

Golden Tests

Status: Current Last updated: 2026-05-12 17:42 EDT

Golden tests under crates/clan/clan-core/tests/clan_golden/ (driven by the top-level clan_golden.rs entry point) and crates/clan/clan-core/tests/transform_golden.rs compare chatter clan output against the legacy CLAN C binaries character-by-character.

How they work

  1. Run the legacy CLAN binary on a reference corpus fixture file
  2. Run the equivalent chatter clan command on the same file
  3. Snapshot both outputs with insta (suffixed @clan and @rust)
  4. Any difference is a parity divergence that must be explained or fixed

The @clan snapshots capture CLAN’s exact output and should never change (they represent the ground truth). The @rust snapshots capture our output and should match @clan as closely as possible.

Requirements

Golden tests require CLAN binaries. The lookup is a single env var:

  • CLAN_BIN_DIR — directory containing the CLAN command binaries (check, freq, mlu, …)

See clan_bin_dir() and clan_command_available() in crates/clan/clan-core/tests/common/mod.rs. If CLAN_BIN_DIR is unset or the specific command binary is missing from that directory, the test prints a skip notice via require_clan_command() and returns early, making it CI-safe.

(Note: the legacy CLAN library paths used by database_integration.rs follow a different resolver — CLAN_SOURCE_DIR env var → meta-repo sibling OSX-CLAN/ → ~/OSX-CLAN/. That is unrelated to the golden-test bin lookup.)

Fixture files

Test fixtures come from the reference corpus at resources/corpus/reference/ (at the repo root). Common fixtures include:

  • tiers/mor-gra.cha — morphology and grammar tiers
  • ca/overlaps.cha — CA overlap markers
  • Various language-specific files for encoding edge cases

Current parity

The current parity tally and the per-command divergence list live in Per-Command Divergences — that page is the single source of truth, so the numbers stay in one place instead of drifting between docs.

Adding a golden test

The harness in crates/clan/clan-core/tests/clan_golden/harness.rs provides two patterns:

  • Paired CLAN + Rust comparison — declare a ParityCase and let the parity_case_tests! macro generate the test. CLAN side is auto-skipped when clan_command_available() reports the binary missing. Example: clan_golden/check.rs:3 declares two paired CHECK cases.
  • Rust-only snapshot (when no CLAN binary corresponds, or the comparison is a one-off) — call the command’s typed Command/run_xxx API directly and snapshot the output. Example: clan_golden/rust_only.rs shows the MORTABLE, RELY, and SCRIPT patterns.

Helpers used by both patterns:

  • corpus_file("tiers/mor-gra.cha") — resolve a path under resources/corpus/reference/
  • clan_command_available("freq") / require_clan_command("freq", "skip context") — gate CLAN-side execution
  • run_clan("freq", &file, &["-t", "*CHI"]) — run a legacy CLAN binary, returning Option<String>
  • run_rust(...) / run_rust_filtered(...) — run the chatter side with optional filter args

The snapshot naming convention is <case>@clan for the CLAN output and <case>@rust for the chatter output; both land under tests/clan_golden/snapshots/.

For a transform command, see tests/transform_golden.rs for the pattern — transforms write to temp files rather than stdout, and run_rust_transform() returns the contents of the rewritten file.

Diagnosing parity breaks

When a golden test fails:

  1. Run cargo insta review to see the diff
  2. Check if the @clan snapshot changed (it shouldn’t — that means the CLAN binary or fixture changed)
  3. If only @rust changed, determine whether it’s a bug fix or regression
  4. If the divergence is intentional and justified, accept it and document in Per-Command Divergences

Coding Standards

Status: Current Last updated: 2026-05-12 17:36 EDT

Universal Rust Standards

These apply to all Rust code across the TalkBank workspace.

Edition and Tooling

  • Rust 2024 edition
  • cargo fmt before committing
  • Prefer cargo nextest run for faster test execution
  • cargo clippy --all-targets -- -D warnings periodically

Error Handling

  • No panics for recoverable conditions — use typed errors (thiserror)
  • No silent swallowing — no .ok(), .unwrap_or_default(), or silent fallbacks

Output and Logging

  • Library code: tracing macros only — never println!/eprintln!
  • CLI binaries: println!/eprintln! for user-facing output
  • Test code: println! is acceptable

Type Design

  • No boolean blindness. Enums over bools for anything beyond simple on/off. Banned: 2+ bool params, 2+ related bool fields, opposite bool pairs, ambiguous bool returns
  • BTreeMap for deterministic JSON in tests
  • Prefer explicit enums over ambiguous Option

File Size Limits

  • Recommended: 400 lines or fewer
  • Hard limit: 800 lines (must be split)

CLAN-Specific Standards

Typed Data, Not String Matching

Use the AST (word.category, word.untranscribed()) instead of string-prefix checks (starts_with('&'), == "xxx"). This is the fundamental reason for the Rust port.

No Panics

Handle missing data gracefully. No %mor tier? Skip morpheme counting. Use tracing::warn! for recoverable file-level issues.

Typed Results

Every command defines its own result struct implementing CommandOutput. Avoid the generic AnalysisResult container.

Stateless Commands

All mutable state goes in the State type. Commands hold only config.

Use Framework Utilities

  • countable_words() for word iteration
  • NormalizedWord for frequency maps
  • Never check filters in command code — the runner handles them

Library Crate

No println!/eprintln! — use tracing.

Documentation Standards

Use CLAN.html for legacy intent

When a command is documented in the CLAN manual, command docs should use that manual to explain the command’s intended semantics, examples, and tier assumptions. Do not infer intent from accidental legacy string behavior if the manual states the intended behavior more clearly.

Say when the manual is absent or incomplete

Some implemented commands do not have a standalone section in the CLAN manual, or are only mentioned indirectly. In those cases, say so explicitly in the command chapter and document what other evidence is being used instead.

Separate CLI docs from GUI docs

Carry over non-GUI command semantics into the clan-core book. GUI workflows from the legacy manual should be documented in the TalkBank VS Code extension docs instead of being mixed into the CLI command reference.

Contributors and operations: how-to guides

Choose the task you want to complete.

Setup

Status: Current Last updated: 2026-03-24 00:01 EDT

Development is supported on Windows, macOS, and Linux. The instructions below use Unix shell syntax; on Windows, use PowerShell or Git Bash equivalently.

Prerequisites

  • Rust (stable) via rustup (all platforms)
  • Node.js for tree-sitter grammar generation and symbol validation
  • tree-sitter CLI: cargo install tree-sitter-cli

Clone Repository

mkdir -p ~/talkbank && cd ~/talkbank
git clone https://github.com/TalkBank/batchalign.git

Build

cd ~/talkbank/batchalign
cargo build               # Build all crates
cargo build --all-targets # Including tests and benchmarks

Two Cargo Workspaces

The repository has two independent Cargo workspaces:

1. Root workspace (Cargo.toml)

Contains all Rust crates for parsing, model, validation, and transform:

cd ~/talkbank/batchalign
cargo build
cargo test

2. Spec workspace (crates/spec/talkbank-spec-testgen/Cargo.toml)

Contains two sibling crates for spec-driven artifacts:

cargo build --manifest-path ~/talkbank/batchalign/crates/spec/talkbank-spec-testgen/Cargo.toml
cargo build --manifest-path ~/talkbank/batchalign/crates/spec/talkbank-spec-testrun/Cargo.toml
cargo run --manifest-path ~/talkbank/batchalign/crates/spec/talkbank-spec-testgen/Cargo.toml --bin gen_tree_sitter_tests -- --help
cargo run --manifest-path ~/talkbank/batchalign/crates/spec/talkbank-spec-testrun/Cargo.toml --bin validate_error_specs -- --help

Makefile Targets

bazel build //...           # Build everything
bazel test //...            # Run all tests (nextest + parser-tests + doctests)
bazel build //... && bazel test //...          # Pre-merge verification gates
just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs        # Regenerate spec-driven artifacts when they actually changed
node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js     # Regenerate shared symbol sets
just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs && git diff --exit-code # Verify generated artifacts are committed
bazel build //...           # Fast compile check
bazel clean           # Clean build artifacts
just docs build            # Build documentation
just docs serve      # Serve documentation locally

Verification

Before submitting changes, run the full verification suite:

bazel build //... && bazel test //...

See Testing for the current gate breakdown. The important point is that bazel build //... && bazel test //... remains the pre-merge gate, while just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs is a targeted regeneration step rather than a universal parser-testing ritual.

Editor Setup

VS Code

Install the TalkBank extension from apps/vscode-extension/ for CHAT syntax highlighting and diagnostics.

rust-analyzer

The workspace should work out of the box with rust-analyzer. The root Cargo.toml workspace configuration is standard.

Grammar Workflow

Status: Current Last updated: 2026-03-23 23:49 EDT

The tree-sitter grammar at grammar/grammar.js is the formal definition of the CHAT format. Changes require careful validation.

The following diagram shows the complete regeneration pipeline. Every step must pass before committing a grammar change.

flowchart TD
    edit(["Edit grammar/grammar.js"])
    generate["tree-sitter generate\n→ src/parser.c\n→ src/node-types.json"]
    grammar_test["tree-sitter test\n(160 corpus tests)"]
    rust_test["cargo test -p talkbank-parser\n(CST-to-model conversion)"]
    equiv["parser equivalence\n(74 reference corpus files)"]
    spec_check{"Grammar change\naffects spec examples?"}
    test_gen["just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs\n→ grammar/test/corpus/\n→ parser-tests/tests/generated/\n→ book/src/operations/errors/"]
    commit(["Commit"])

    edit --> generate --> grammar_test --> rust_test --> equiv --> spec_check
    spec_check -->|Yes| test_gen --> commit
    spec_check -->|No| commit

Step-by-Step Procedure

1. Edit the Grammar

Modify grammar.js in the grammar/ directory. Key design principles:

  • Explicit whitespace (no extras)
  • Precedence annotations to resolve ambiguities
  • Named rules for all semantically meaningful nodes

2. Generate the Parser

cd grammar
tree-sitter generate

This produces src/parser.c and src/node-types.json. Never edit these files by hand.

3. Run Grammar Tests

tree-sitter test

All 160 tests must pass. Tests live in test/corpus/ and are partially auto-generated from specs.

4. Run Parser Tests

cargo test -p talkbank-parser

This verifies the Rust parser wrapper handles all CST nodes correctly.

5. Run Parser Equivalence

cargo nextest run -p talkbank-parser-tests -E 'test(parser_equivalence)'

Every file in the reference corpus must parse correctly. Each .cha file is its own test — nextest runs them in parallel and reports individual failures.

6. Regenerate Spec Tests

If the grammar change affects any spec examples:

just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs

This regenerates tree-sitter corpus tests and other generated outputs that still depend on the spec pipeline.

Do this when the grammar change actually affects generated artifacts.

7. Update node_types.rs

If new node types were added to the grammar, the generated node_types.rs in talkbank-parser needs updating. The spec tools handle this via node-types.json.

Critical Policy

The reference corpus at resources/corpus/reference/ (74 files) must pass parser equivalence at 100%. If a grammar change breaks even one file, revert immediately. The reference corpus is the ultimate arbiter of correctness.

Common Patterns

Adding a New Token

  1. Define the token in grammar.js
  2. Add handling in the Rust tier parser (match on the new node kind)
  3. Add a spec construct example
  4. Run the relevant generation and verification steps

For small, isolated syntax additions, the grammar workflow should stay local:

  • one grammar change
  • one grammar corpus example
  • one full-file fixture if needed

Changing a Rule

  1. Modify the rule in grammar.js
  2. tree-sitter generate && tree-sitter test
  3. Update Rust parser if CST node structure changed
  4. Update spec examples if the expected CST changed
  5. Run full verification

Spec Workflow

Status: Current Last updated: 2026-03-24 01:32 EDT

Specifications in spec/ are the source of truth for CHAT format intent, grammar examples, and validation/error contracts.

Adding a Construct Spec

Construct specs define valid CHAT patterns with expected parse trees.

1. Create the Spec File

Create a new markdown file in the appropriate resources/spec/constructs/ subdirectory:

resources/spec/constructs/
├── header/         # Header-related constructs
├── main_tier/      # Main tier patterns
├── tiers/          # Dependent tier patterns
├── utterance/      # Utterance-level patterns
└── word/           # Word syntax patterns

2. Write the Spec

# 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 code fence label (e.g., utterance, mor_dependent_tier) selects which template wraps the input into a full CHAT file.

3. Generate the CST

Parse your input with tree-sitter to get the actual CST, then copy it as the Expected CST (stripping positions and field names).

4. Regenerate The Affected Generated Artifacts

just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs

Use just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs when you intentionally changed generated grammar corpus tests, generated Rust tests, or generated error docs.

For isolated grammar additions, keep the change small:

  1. Add or adjust one grammar example.
  2. Add one full-file fixture if the change matters in context.
  3. Regenerate only the artifacts that truly changed.

Adding an Error Spec

Error specs define invalid CHAT patterns with expected error codes.

1. Create the Spec File

Error specs live in resources/spec/errors/, named by error code:

resources/spec/errors/E301_missing_participants.md

2. Write the Spec

# Error E301

## Metadata

- Code: E301
- Name: missing_participants
- Severity: Error
- Layer: parser

## Description

The @Participants header is required in every CHAT file.

## Examples

### missing_participants_basic

\```chat
@UTF8
@Begin
*CHI:	hello .
@End
\```

Key Metadata Fields

  • Layer: parser — the error is caught during parser.parse_chat_file() (file fails to parse)
  • Layer: validation — the error is caught by validate_with_alignment() after successful parse
  • Status: not_implemented — generates #[ignore] tests (validation logic not yet coded)

3. Regenerate The Affected Artifacts

just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs
bazel build //... && bazel test //...

Updating the Symbol Registry

The symbol registry at resources/spec/symbols/symbol_registry.json defines character sets used by the grammar and Rust crates.

flowchart TD
    registry["Edit resources/spec/symbols/\nsymbol_registry.json"]
    validate["validate_symbol_registry.js\n(structure check)"]
    gen_grammar["Generate grammar symbols\n(for tree-sitter)"]
    gen_rust["generate_rust_symbol_sets.js\n→ talkbank-model/src/generated/symbol_sets.rs\n→ crates/spec/talkbank-spec-testgen/src/generated/symbol_sets.rs"]
    fmt["rustfmt\n(format generated code)"]
    verify["node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js\nthen bazel build //... && bazel test //..."]

    registry --> validate --> gen_grammar & gen_rust
    gen_rust --> fmt --> verify
    gen_grammar --> verify

After editing:

node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js    # Regenerate Rust and JS constants
just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs       # If generated grammar/tests/docs depend on the symbols

Common Mistakes

  • Editing generated files — never edit grammar/test/corpus/ or crates/talkbank-parser-tests/tests/generated/ by hand
  • Running just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs reflexively — use it when generated artifacts changed, not as a substitute for thinking about what kind of test authority the change really needs
  • Wrong layer — parser-layer specs expect parse failure; validation-layer specs expect parse success + error report

Testing

Status: Current Last updated: 2026-04-29 10:39 EDT

Test Generation Pipeline

Specs are the source of truth. All grammar corpus tests, Rust parser tests, and error docs are generated from specs. just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs wipes the output directories and recreates them — never hand-edit generated files.

flowchart LR
    subgraph sources["Source of Truth"]
        constructs["resources/spec/constructs/\n(112 construct specs)"]
        errors["resources/spec/errors/\n(187 error specs)"]
        templates["crates/spec/talkbank-spec-testgen/templates/\n(Tera wrappers)"]
    end

    subgraph generators["just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs"]
        gen_ts["gen_tree_sitter_tests"]
        gen_rust["gen_rust_tests"]
        gen_docs["gen_error_docs"]
    end

    subgraph outputs["Generated Outputs (DO NOT EDIT)"]
        ts_tests["grammar/test/corpus/\n(166 tree-sitter tests)"]
        rust_tests["tests/generated/\n(167 Rust tests)"]
        error_docs["book/src/operations/errors/\n(182 error doc pages)"]
    end

    constructs & errors --> gen_ts
    templates --> gen_ts
    constructs & errors --> gen_rust
    errors --> gen_docs

    gen_ts --> ts_tests
    gen_rust --> rust_tests
    gen_docs --> error_docs

To add a grammar test or error test, add a spec file in resources/spec/constructs/ or resources/spec/errors/, then run just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs. See resources/spec/CLAUDE.md for the spec format.

Test Strategy

Testing is organized in layers, from fastest to most comprehensive.

flowchart TD
    unit["Unit + Integration Tests\n(cargo nextest run)\n~2300 tests, ~5s"]
    specgen["Spec-Generated Tests\n(bazel test //crates/core/talkbank-parser-tests/...)\nParser + validation layer"]
    grammar["Grammar Corpus\n(tree-sitter test)\n166 tree-sitter tests"]
    ref["Reference Corpus\n(97 files, 100% required)"]
    gates["Verification Gates\n(bazel build //... && bazel test //...)\nG0–G14 sequential pipeline"]

    unit --> specgen --> grammar --> ref --> gates

Unit Tests (nextest)

cargo nextest run

Runs all unit and integration tests across all crates (~2300+ tests). These test individual functions, serialization roundtrips, and model invariants.

cargo nextest does not run doctests. Keep cargo test --doc as a separate verification step when you change public API examples or doc comments.

Parser Equivalence

cargo nextest run -p talkbank-parser-tests -E 'test(parser_equivalence)'

Runs the parser on each file in the 78-file reference corpus and validates results. Each .cha file is its own test, enabling per-file parallelism and failure isolation via nextest.

Spec-Generated Tests

Part of talkbank-parser-tests. These are generated from specs via just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs and currently test:

  • Construct specs: input parses correctly
  • Parser-layer error specs: input fails to parse with expected error code
  • Validation-layer error specs: input parses but validation reports expected error code

Concrete entrypoint:

bazel test //crates/core/talkbank-parser-tests/...

Tree-Sitter Grammar Tests

bazel test //grammar/...

Runs the tree-sitter grammar corpus tests. This is the right gate for grammar structure changes.

Error Corpus Tests

Supplementary test fixtures in tests/error_corpus/. The expectations.json manifest maps .cha files to expected outcomes.

Tree-Sitter Tests

cd grammar
tree-sitter test

160+ tests that verify the grammar produces correct CSTs for known inputs.

Reference Corpus

The reference corpus at resources/corpus/reference/ contains 97 .cha files across 20 languages that represent the diversity of real-world CHAT data. The parser must handle every file at 100%.

This corpus is the ultimate arbiter of correctness for full-file parsing.

Verification Gates

bazel build //... && bazel test //... runs the pre-merge verification suite (G0–G14). All gates run sequentially — the first failure stops the pipeline.

flowchart TD
    start(["bazel build //... && bazel test //..."]) --> G0
    G0["G0: Parser signature guardrail\n(check-errorsink-option-signatures.sh)"]
    G0 -->|pass| G1["G1: Rust workspace compile\n(cargo check --workspace)"]
    G1 -->|pass| G2["G2: Spec tools compile\n(cd spec/tools && cargo check)"]
    G2 -->|pass| G3["G3: Spec runtime tools compile\n(spec/runtime-tools)"]
    G3 -->|pass| G4["G4: CHAT manual anchor links\n(check-chat-manual-anchors.sh)"]
    G4 -->|pass| G5["G5: Generated parser corpus\nequivalence suite"]
    G5 -->|pass| G6["G6: Golden fragment validity\n(words + tiers)"]
    G6 -->|pass| G7["G7: Bare-timestamp\nregression gate"]
    G7 -->|pass| G8["G8: Reference corpus\nsemantic equivalence\n(97 files)"]
    G8 -->|pass| G9["G9: %wor tier parsing\nand alignment"]
    G9 -->|pass| G10["G10: Golden tier roundtrip\n(%mor, %gra, %pho, %wor)"]
    G10 -->|pass| G11["G11: Reference corpus\nnode coverage audit"]
    G11 -->|pass| G12["G12: Generated artifacts\nmatch committed sources"]
    G12 -->|pass| G13["G13: Fuzz workspace\nisolation"]
    G13 -->|pass| G14["G14: Imported Batchalign\nRust/PyO3 gate"]
    G14 -->|pass| done(["All gates passed"])
    G0 & G1 & G2 & G3 & G4 & G5 & G6 & G7 & G8 & G9 & G10 & G11 & G12 & G13 & G14 -->|fail| abort(["Pipeline stops"])
GateCheck
G0Parser signature guardrail
G1Rust workspace compile check
G2Spec tools compile check
G3Spec runtime tools compile check
G4CHAT manual anchor links
G5Generated parser corpus equivalence suite
G6Golden fragment validity (words + tiers)
G7Bare-timestamp regression gate
G8Reference corpus semantic equivalence (97 files)
G9%wor tier parsing and alignment
G10Golden tier roundtrip (%mor, %gra, %pho, %wor)
G11Reference corpus node coverage
G12Generated artifacts match committed sources
G13Fuzz workspace isolation
G14Imported Batchalign Rust/PyO3 gate

Running Specific Tests

# Single test by name
cargo nextest run test_name

# Tests in a specific crate
cargo nextest run -p talkbank-model

# Tests matching a pattern
cargo nextest run -- mor

# With output
cargo nextest run --no-capture

What to Run When

What you changedRun
Grammar (grammar.js)cd grammar && tree-sitter generate && tree-sitter test then bazel test //crates/core/talkbank-parser-tests/...
Parser (CST-to-model)cargo nextest run -p talkbank-parser
Model (types, validation, alignment)cargo nextest run -p talkbank-model
CLAN commandcargo nextest run -p clan-core -E 'test(command_name)' + golden test
CLI (chatter args, dispatch)cargo nextest run -p chatter-cli
LSPcargo nextest run -p chatter-lsp
Spec filesjust spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs && bazel build //... && bazel test //...
Pre-merge (any change)bazel build //... && bazel test //...
Pre-push (quick)bazel build //... && bazel test //...

Mutation Testing

Use cargo-mutants to find code that can be changed without any test failing — the true coverage gaps.

# Install (once)
cargo install cargo-mutants

# Run against a specific crate (--jobs 1 to avoid OOM on 64 GB machines)
cargo mutants -p talkbank-parser --timeout 120 --jobs 1

# Run against CLAN commands
cargo mutants -p clan-core --timeout 120 --jobs 1

# Review results
cat mutants.out/missed.txt    # Mutations no test caught
cat mutants.out/caught.txt    # Mutations properly detected

Mutation testing is not part of CI but should be run periodically (after major changes) to find untested logic paths. Results guide where to add new tests.

Configuration: mutants.toml at the repo root excludes trivial functions.

Adding Tests

  • Model tests: add to the relevant crate’s tests/ directory or #[cfg(test)] module
  • Parser tests: if the change is about grammar shape or validation contracts, add or update specs and regenerate with just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs
  • Error corpus tests: add a .cha file to tests/error_corpus/ and update expectations.json
  • CLAN command tests: add golden test cases in tests/clan_golden/ using the manifest-driven ParityCase / RustSnapshotCase pattern

Coding Standards

Status: Current Last updated: 2026-03-24 00:01 EDT

Rust Conventions

  • Edition: 2024
  • Formatting: cargo fmt before every commit
  • Linting: cargo clippy --all-targets -- -D warnings must pass with zero warnings
  • No clippy silencing without explicit approval

Error Handling

  • No panics for recoverable conditions — use thiserror/miette for error types
  • Library code uses the ErrorSink trait for error reporting, not Result
  • Use ParseOutcome<T> in parser code (parsed or rejected)

Logging

  • Library crates use tracing (never println! or eprintln!)
  • CLI binaries write to stdout (results) and stderr (diagnostics)
  • Use appropriate log levels: error!, warn!, info!, debug!, trace!

Naming

  • Follow standard Rust conventions (snake_case for functions, CamelCase for types)
  • Conventional Commits for commit messages: <type>[scope]: <description>
    • Types: feat, fix, refactor, test, docs, chore

Dependencies

Preferred crates:

  • clap — CLI argument parsing
  • serde — serialization
  • miette — user-facing diagnostics
  • insta — snapshot testing
  • tracing — structured logging
  • rayon / crossbeam — concurrency
  • smallvec — small-buffer optimization

Code Organization

  • Keep crate boundaries clean — lower crates should not depend on higher ones
  • The model crate should not depend on any parser
  • Parsing code should not depend on serialization/transform code
  • All CHAT parsing and serialization goes through the AST — never ad-hoc string manipulation
  • Treat 10 or more named struct fields as an audit trigger. Wide boundary or report records can be acceptable, but wide runtime state bags need explicit review. See architecture/chat-model/wide-structs.md.

Testing

  • Prefer spec-driven tests over hand-written tests for parser behavior
  • Use cargo nextest run for unit tests (except doctests)
  • Snapshot tests with insta for complex output comparisons

Generated Files

Never hand-edit generated artifacts:

  • parser.c — generated from grammar.js
  • grammar/test/corpus/ — generated from specs
  • crates/talkbank-parser-tests/tests/generated/ — generated from specs
  • crates/talkbank-model/src/generated/symbol_sets.rs — generated from symbol registry

Always regenerate from source inputs.

Coding Standards and Engineering Practices

Status: Current Last updated: 2026-03-24 00:01 EDT

Objective

Set enforceable, language-specific standards that reduce ambiguity and improve long-term maintainability.

Global Standards

  1. Prefer explicit domain types over ad-hoc strings.
  2. Keep parsing, validation, and rendering logic separated.
  3. Eliminate magic numbers/strings/paths via named constants and config.
  4. Treat generated code as immutable artifacts.
  5. Require tests for every bugfix and behavior change.

Rust Standards

  • Enforce formatter and clippy in CI.
  • Minimize #[allow(clippy::...)]; each allowance needs rationale.
  • Prefer small focused modules with clear ownership.
  • Public APIs require doc comments with examples and error behavior.
  • In parser code, disallow ErrorSink + Option<T> signatures for fallible parse operations.
    • Use explicit outcome enums or Result with structured diagnostics.
    • Guardrail script: scripts/check-errorsink-option-signatures.sh.
  • For model enums that encode validation state, require ValidationTagged derive.
    • Explicit annotation: #[validation_tag(error|warning|clean)].
    • Naming convention fallback: variants ending in Error / Warning.

Grammar Standards

  • Grammar rules must map to documented token/category semantics.
  • No duplicated symbol sets in free-form literals.
  • Every non-obvious precedence/conflict decision must include rationale.

Spec and Generator Standards

  • Spec files must follow strict metadata template.
  • Generators must be deterministic and pure with respect to inputs.
  • No hardcoded user-specific paths in docs or generated outputs.

Magic Value Policy

Disallowed

  • Inline path literals tied to local machines.
  • Unnamed numeric constants encoding protocol behavior.
  • Repeated header/tier string literals across modules.

Required

  • Central constants/modules:
    • path defaults,
    • tier/header prefixes,
    • token categories,
    • formatting policies.

Review and PR Standards

  • PR template must include:
    • subsystem touched,
    • contract impact,
    • generated artifact impact,
    • tests added/updated,
    • docs updated.
  • Require at least one reviewer with subsystem ownership for core modules.

Internal Decision Records

Adopt short ADR format in the book’s architecture section:

  • context,
  • decision,
  • alternatives considered,
  • consequences,
  • rollback path.

Acceptance Criteria

  • Coding standards are documented once and enforced automatically.
  • Magic values are systematically reduced and tracked.
  • Every behavior change includes tests and doc impact assessment.
  • Architecture decisions are recorded and discoverable.

Parameter Design

Status: Current Last updated: 2026-05-01 18:02 EDT

Conventions for function-parameter shape in the batchalign server-side orchestrators (morphosyntax.rs, fa.rs, transcribe.rs, compare.rs). These orchestrators process CHAT files through multi-step pipelines that require many configuration values. Without deliberate parameter design, signatures accumulate 10–16 parameters, creating boolean blindness and making call sites unreadable.

For the companion data-shape rule, see Wide Struct Audit: large field bags, boolean-heavy structs, and where wide boundary types are acceptable.

The Problem

A pre-refactoring FA function signature looked like this:

pub async fn process_fa(
    chat_text: &str,
    audio_path: &str,
    audio_identity: &AudioIdentity,
    total_audio_ms: Option<u64>,
    pool: &WorkerPool,
    cache: &UtteranceCache,
    engine_version: &str,
    lang: &str,
    timing_mode: FaTimingMode,
    max_group_ms: u64,
    engine: FaEngineType,
    override_media_cache: bool,    // ← what does "true" mean?
    write_wor: bool,         // ← what does "false" mean?
    progress: Option<&ProgressSender>,
) -> Result<FaResult, ServerError>

14 parameters. Two booleans whose meaning requires reading the implementation. Three audio-related values always passed together. Three infrastructure values (pool, cache, engine_version) repeated across every orchestrator.

Boolean Blindness Elimination

CachePolicy

Every NLP orchestrator accepts a cache policy. The old override_media_cache: bool parameter inverted the natural reading — true meant skip the cache, not use it.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachePolicy {
    /// Use the cache normally (check for hits, store new results).
    UseCache,
    /// Skip cache lookups (always recompute; still stores results).
    SkipCache,
}

impl CachePolicy {
    pub fn should_skip(&self) -> bool {
        matches!(self, Self::SkipCache)
    }
}
}

Call sites read naturally:

// Before
if override_media_cache { /* skip */ } else { /* use */ }

// After
if cache_policy.should_skip() { /* skip */ } else { /* use */ }

WorTierPolicy

FA processing optionally generates %wor tiers (word-level timing). The old write_wor: bool is replaced by:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorTierPolicy {
    Include,
    Omit,
}

impl WorTierPolicy {
    pub fn should_write(&self) -> bool {
        matches!(self, Self::Include)
    }
}
}

From<bool> Bridge

Both enums implement From<bool> for the boundary where options are deserialized from CLI flags or JSON:

impl From<bool> for CachePolicy {
    fn from(override_media_cache: bool) -> Self {
        if override_media_cache { Self::SkipCache } else { Self::UseCache }
    }
}

The conversion happens once at the dispatch layer. Interior code never sees bare booleans.

Parameter Structs

PipelineServices

Infrastructure references needed by every orchestrator.

#[derive(Clone, Copy)]
pub(crate) struct PipelineServices<'a> {
    pub pool: &'a WorkerPool,
    pub cache: &'a UtteranceCache,
    pub engine_version: &'a EngineVersion,
}

Clone + Copy because all fields are references. Constructed once per dispatch and threaded through the orchestrator chain. Note that engine_version is &EngineVersion (not &str) — the newtype propagates through the cache layer.

MorphosyntaxParams

Groups the five parameters specific to morphosyntax processing:

pub struct MorphosyntaxParams<'a> {
    pub lang: &'a LanguageCode3,
    pub tokenization_mode: TokenizationMode,
    pub cache_policy: CachePolicy,
    pub multilingual_policy: MultilingualPolicy,
    pub mwt: &'a MwtDict,
}

Note lang is &LanguageCode3 (not &str), preventing confusion with other string parameters. The LanguageCode3 deref-coerces to &str at the few points where a raw string is needed (e.g., passing to LanguageCode::new()).

FaParams

Groups the five FA-specific processing parameters:

#[derive(Debug, Clone, Copy)]
pub struct FaParams {
    pub timing_mode: FaTimingMode,
    pub max_group_ms: u64,
    pub engine: FaEngineType,
    pub cache_policy: CachePolicy,
    pub wor_tier: WorTierPolicy,
}

Clone + Copy because all fields are small values. Constructed in the dispatch layer from CommandOptions::Align.

AudioContext

Groups the three audio-related values always passed together:

pub struct AudioContext<'a> {
    pub audio_path: &'a Path,
    pub audio_identity: &'a AudioIdentity,
    pub total_audio_ms: Option<u64>,
}

Note audio_path is &Path (not &str) — file paths use std::path types throughout the Rust domain code. Conversion to String happens only at the IPC boundary when serializing JSON for Python workers (path.to_string_lossy()).

Result: Reduced Signatures

FunctionBeforeAfter
process_morphosyntax9 params3 (chat_text, services, params)
process_morphosyntax_incremental10 params4 (before, after, services, params)
process_fa14 params5 (chat_text, audio, services, fa_params, progress)
process_fa_incremental15 params6
process_compare8 params6
process_transcribe6 params4 (audio_path, services, opts, progress)
process_one_transcribe_file16 params8

Where Grouping Doesn’t Help

Dispatch-level functions (dispatch_fa_infer, dispatch_transcribe_infer) still carry 8–11 parameters and retain #[allow(clippy::too_many_arguments)]. These are multi-concern routers that:

  1. Take Arc<WorkerPool> and Arc<UtteranceCache> (not references) because they clone into spawned JoinSet tasks.
  2. Carry job identity (job_id, correlation_id, store) alongside processing parameters.
  3. Read command-specific options from the job store, then construct the parameter structs described above for the inner orchestrators.

The parameter structs live at the orchestrator level. The dispatch level is intentionally a thin translation layer between job-store state and typed orchestrator parameters.

The same rule now applies inside the server store layer. SQLite write-through helpers use named records such as PersistedJobUpdate, PersistedFileUpdate, AttemptStartRecord, and AttemptFinishRecord instead of long ordered argument lists, so persistence boundaries stay explicit even inside internal control-plane code.

Layered Conversion

CLI flags / JSON body           ← bool, String, numbers
    │
    ▼
CommandOptions (options.rs)     ← deserialized, still bool for override_media_cache
    │
    ▼
Dispatch layer (infer.rs)       ← CachePolicy::from(opts.override_media_cache)
    │                              FaParams { ... }
    │                              MorphosyntaxParams { lang: &lang, ... }
    │                              PipelineServices { engine_version: &ev, ... }
    ▼
Orchestrator (fa.rs, etc.)      ← typed params: &LanguageCode3, &EngineVersion,
    │                              &Path, CachePolicy — no bare strings or bools
    ▼
Pipeline internals              ← params.cache_policy.should_skip()
    │                              params.lang.as_ref() (where &str needed)
    ▼
IPC boundary (worker JSON)      ← path.to_string_lossy(), &*lang (Deref)

Raw primitives enter at the CLI/JSON boundary. The dispatch layer converts booleans to enums, strings to newtypes, and string paths to PathBuf. From the dispatch layer inward, all code uses typed parameters. At the IPC boundary (JSON for Python workers), newtypes deref-coerce to &str and paths convert via to_string_lossy().

Files

FileContents
types/params.rsCachePolicy, WorTierPolicy, MorphosyntaxParams, FaParams, AudioContext
pipeline/mod.rsPipelineServices
transcribe.rsTranscribeOptions

Guidelines

  1. No bare booleans in orchestrator signatures. If a boolean controls behavior, wrap it in an enum.
  2. Group parameters that are always passed together. If three values always appear as consecutive function arguments, they belong in a struct.
  3. Convert at the boundary. From<bool> impls bridge CLI/JSON booleans to domain enums. The conversion happens once, in the dispatch layer.
  4. Don’t force grouping on dispatch functions. Dispatch routers read heterogeneous state from the job store and construct typed parameters for orchestrators. Their parameter lists are inherently wider.
  5. Prefer references in parameter structs. PipelineServices<'a> and MorphosyntaxParams<'a> borrow rather than own, avoiding unnecessary clones. Use Clone + Copy derives when all fields are references or small values.

CLI Option Wiring

Status: Current Last updated: 2026-03-17

This document maps every CLI option to its downstream consumer in the dispatch layer. It serves as the authoritative reference for whether a flag is actually wired to production code.

Architecture: Two Option Paths

CLI options flow through two separate paths to reach production code. Both must be audited when adding or reviewing flags.

Path 1: Per-Command Options (CommandOptions)

CLI args (clap) → CommandOptions (typed structs) → Dispatch extraction → Dispatch plan → Production code
      ↑                    ↑                              ↑                  ↑
  commands.rs         options.rs                    dispatch/options.rs  dispatch/plan.rs
  (parsing)        (build_typed_options)         (extract_*_dispatch_params)
  1. CLI parsing (crates/batchalign/src/args/commands.rs) — clap structs define flags with defaults.
  2. Options bridge (crates/batchalign/src/args/options.rs) — build_typed_options() converts parsed args into CommandOptions enum variants.
  3. Dispatch extraction (crates/batchalign/src/runner/dispatch/options.rs) — pure functions extract typed params from CommandOptions for each dispatch path.
  4. Dispatch plan build (crates/batchalign/src/runner/dispatch/plan.rs) — combines those extracted params with runner-only state (for example runtime_state toggles) once before the async dispatch modules run.

The extraction functions are covered by tests in options.rs::tests that verify every field is read. Adding a new field to a CommandOptions struct without adding a corresponding extraction test is a code review signal. The plan builders in plan.rs::tests then verify that the runner-facing command families consume those extracted parameters through a typed seam instead of re-reading CommandOptions directly inside async dispatch code.

Path 2: Job-Level Options (Job fields)

CLI args (clap) → dispatch/paths.rs resolution → JobSubmission → Job struct → Dispatch reads Job directly
      ↑                                                ↑                            ↑
  CommonOpts                                       api.rs                    fa_pipeline.rs (job.lang, etc.)

Some options bypass CommandOptions entirely and are stored directly on the Job struct. These are cross-cutting concerns shared by all commands:

CLI FlagCommonOpts FieldJob FieldDispatch ConsumerStatus
--lang(positional/global)job.langWorker pool key, worker CLIWired
-n / --num-speakers(global)job.num_speakersdispatch_transcribe_inferWired
--before PATHCommonOpts.beforejob.before_pathsdispatch_fa_infer, dispatch_batched_infer → incremental orchestratorsWired (morphotag, align)
--in-placeCommonOpts.in_placeResolved at CLI dispatchPath resolution in dispatch/paths.rsWired
--file-list FILECommonOpts.file_listResolved at CLI dispatchFile discovery in resolve.rsWired
--override-media-cacheCommonOpts.override_media_cacheCommonOptions.override_media_cacheVia CommandOptions (Path 1)Wired
--engine-overrides JSON(global)CommonOptions.engine_overridesTyped built-in/custom engine selection and worker command overridesWired
--lexicon FILE(global)CommonOptions.mwtMWT dictionary injectionWired

Why two paths? Per-command options (FA engine, pauses, retokenize) are command-specific and vary per CommandOptions variant. Job-level options (language, speaker count, before paths, file discovery) are shared infrastructure that applies across all commands.

Audit discipline: When adding a new CLI flag, determine which path it belongs to. If it’s per-command, add to the relevant CommandOptions variant AND add an extraction test. If it’s job-level, document it in the table above.

align

CLI FlagCommandOptions FieldDispatch ConsumerStatus
--fa-engine {wav2vec,whisper}AlignOptions.fa_engineextract_fa_dispatch_params → FaDispatchPlan → FaParams.engineWired
--fa-engine-custom NAMEAlignOptions.fa_engineSame as aboveWired
--utr / --no-utrAlignOptions.utr_engineextract_fa_dispatch_params → FaDispatchPlan.options.utr_engine → UTR pre-pass in process_one_fa_fileWired
--utr-engine {rev,whisper}AlignOptions.utr_engineSameWired
--utr-strategy {auto,global,two-pass}AlignOptions.utr_overlap_strategyextract_fa_dispatch_params → FaDispatchPlan.options.utr_overlap_strategy → run_utr_pass() strategy resolverWired
--pausesAlignOptions.pausesextract_fa_dispatch_params → FaDispatchPlan.options.fa_params.timing_modeWired
--wor / --noworAlignOptions.worextract_fa_dispatch_params → FaDispatchPlan.options.fa_params.wor_tierWired
--merge-abbrevAlignOptions.merge_abbrevextract_fa_dispatch_params → FaDispatchPlan.options.merge_abbrevWired
--before PATHCommonOpts.before (Path 2)job.before_paths → process_fa_incremental()Wired — copies stable %wor from the before file, refreshes preserved groups, and only re-aligns remaining groups

UTR behavior (implemented)

UTR is wired as a detect-and-skip pre-pass in process_one_fa_file (crates/batchalign/src/runner/dispatch/fa_pipeline.rs), with three optimizations: ASR result caching, fallback UTR after FA failure, and partial-window ASR for mostly-timed files.

The core logic is in run_utr_pass() (shared by both the pre-pass and fallback):

  1. Parse CHAT, call count_utterance_timing() → (timed, untimed).
  2. If untimed == 0: skip UTR → proceed to FA.
  3. If untimed > 0 AND utr_engine.is_some(): call run_utr_pass():
    • ASR caching: Check the analysis cache for a prior ASR result (key = BLAKE3("utr_asr|{audio_identity}|{lang}")). On hit, skip inference entirely. On miss, run the selected Rust-owned UTR backend and cache the result.
    • Partial-window mode: When the file is mostly timed (untimed_ratio < 0.5 and audio > 60s), find_untimed_windows() identifies time windows covering only the untimed regions. Worker-backed engines run on each extracted segment instead of the full file. Each segment’s result is cached independently.
    • Full-file mode: For mostly-untimed files, short audio, or Rust-owned Rev.AI UTR, timing recovery runs on the full audio.
    • Convert ASR tokens to AsrTimingToken, resolve the configured overlap strategy (auto, global, or two-pass), inject timing, and re-serialize.
  4. If untimed > 0 AND utr_engine.is_none(): log warning, proceed to FA with interpolation fallback.
  5. Fallback UTR: If FA fails with a retryable error and untimed utterances exist that were not recovered (UTR failed/skipped), the retry handler calls run_utr_pass() once before the next retry attempt. The utr_fallback_attempted flag ensures at most one extra ASR call across all retries.

This is smarter than ba2’s skip logic: ba2 skipped UTR if any utterance had timing (potentially missing untimed ones). ba3 skips only if all are timed.

The UTR module (fa/utr.rs) injects utterance-level bullets only — it does not set word-level timing. FA handles word-level alignment after UTR provides the utterance boundaries.

Without UTR, untimed utterances fall back to interpolation between neighboring timed utterances (see Proportional FA Estimation). This achieves ~96% coverage vs 100% with UTR.

transcribe

CLI FlagCommandOptions FieldDispatch ConsumerStatus
--asr-engine {rev,whisper,...}TranscribeOptions.asr_engineextract_transcribe_dispatch_params → TranscribeDispatchPlan.base_options.backendWired
--diarization / --diarizeTranscribeOptions.diarizeCLI routes to Transcribe vs TranscribeS command variant, then extract_transcribe_dispatch_params → TranscribeDispatchPlan.base_options.diarizeWired
--wor / --noworTranscribeOptions.worextract_transcribe_dispatch_params → TranscribeDispatchPlan.base_options.write_worWired — gates %wor tier generation in build_chat(). Default omit (BA2 parity).
--batch-size NTranscribeOptions.batch_sizeextract_transcribe_dispatch_params → TranscribeDispatchParams.batch_sizeExtracted but not consumed — not in BA2, hidden from --help
--merge-abbrevTranscribeOptions.merge_abbrevextract_transcribe_dispatch_params → TranscribeDispatchPlan.should_merge_abbrevWired
--langJob.langWorker pool key + worker CLIWired
-n / --num-speakersJob.num_speakersdispatch_transcribe_inferWired

Notes

--asr-engine was not a flag in batchalign2. BA2 used mutually exclusive --whisper/--rev/--whisperx/--whisper_oai flags, which BA3 preserves as compatibility aliases. The CLI now rejects invalid --engine-overrides JSON at parse time, and the resolved asr / fa selections flow through typed built-in-or-custom engine selectors before dispatch chooses the Rust-owned Rev.AI path versus the worker-backed ASR backends.

--batch-size was not a flag in batchalign2 (hardcoded to 8 for WhisperX).

morphotag

CLI FlagCommandOptions FieldDispatch ConsumerStatus
--retokenize / --keeptokensMorphotagOptions.retokenizeextract_morphotag_dispatch_params → BatchedInferDispatchPlan.tokenization_modeWired
--skipmultilang / --multilangMorphotagOptions.skipmultilangextract_morphotag_dispatch_params → BatchedInferDispatchPlan.multilingual_policyWired
--lexicon FILECommonOptions.mwtLoaded at CLI, injected into CommonOptions.mwtWired
--merge-abbrevMorphotagOptions.merge_abbrevextract_morphotag_dispatch_params → BatchedInferDispatchPlan.should_merge_abbrevWired
--before PATHCommonOpts.before (Path 2)job.before_paths → process_morphosyntax_incremental()Wired — skips NLP for unchanged utterances

benchmark

CLI FlagCommandOptions FieldDispatch ConsumerStatus
--asr-engineBenchmarkOptions.asr_engineextract_benchmark_dispatch_params → BenchmarkDispatchPlan.base_options.backendWired
--wor / --noworBenchmarkOptions.worextract_benchmark_dispatch_params → BenchmarkDispatchPlan.base_options.write_worWired — gates %wor tier generation on the benchmark’s transcription output. Default omit (BA2 parity).
--merge-abbrevBenchmarkOptions.merge_abbrevextract_benchmark_dispatch_params → BenchmarkDispatchPlan.should_merge_abbrevWired
CLI FlagCommandOptions FieldDispatch ConsumerStatus

translate, coref, utseg, compare

These commands have only --merge-abbrev as a command-specific option, which is wired through BatchedInferDispatchPlan.should_merge_abbrev.

No command-specific options (only global options).

Testing

Path 1: CommandOptions extraction tests

The extraction functions in crates/batchalign/src/runner/dispatch/options.rs have comprehensive tests that exercise every field of every CommandOptions variant with non-default values, and plan.rs verifies the runner-facing command-family plans consume those extracted values. Run with:

cargo nextest run -p batchalign-cli -E 'test(runner::dispatch::options)'
cargo nextest run -p batchalign-cli -E 'test(runner::dispatch::plan)'

Path 2: Job-level options

Job-level options (before_paths, lang, num_speakers) are tested via:

  • Diff engine tests (batchalign): 11 tests covering diff_chat() classification
  • Incremental orchestrator tests: process_fa_incremental and process_morphosyntax_incremental are integration-tested via worker tests
  • Path resolution tests: dispatch/paths.rs --before directory/file matching
cargo nextest run -p batchalign-cli -E 'test(diff)'
cargo nextest run -p batchalign-cli -E 'test(job_level_options)'

UTR testing

# Unit tests for UTR (timing injection, cache keys, window finder)
cargo nextest run -p batchalign-cli -E 'test(fa::utr)'

# Cache key stability tests
cargo nextest run -p batchalign-cli -E 'test(cache_key)'

# All FA tests (UTR + grouping + alignment + injection)
cargo nextest run -p batchalign-cli -E 'test(fa::)'

Last updated: 2026-03-16

CI and Release

Status: Current Last modified: 2026-06-01 01:05 PDT

This is the top-level map of the CI and release surface. For per-product detail, see Release Pipeline; for code-signing posture, see Code Signing and Distribution.

Pre-merge verification

Every change must pass bazel build //... && bazel test //... before merging:

bazel build //... && bazel test //...

See Quality Gates and Testing for the full local gate definitions and the mapping into CI jobs.

The pre-push hook (scripts/pre-push.sh, installed via ln -sf ../../scripts/pre-push.sh .git/hooks/pre-push) runs the fast subset on git push.

Workflow map

flowchart LR
    subgraph PR["Per-PR (path-scoped)"]
        W1["bazel-rust.yml<br/>(ubuntu + macos-14)"]
        W2["bazel-python.yml<br/>(ubuntu + macos-14)"]
        W3["bazel-grammar.yml"]
        W4["bazel-typescript.yml"]
        W5["bazel-docs.yml"]
        W6["bazel-tauri-batchalign.yml<br/>smoke (+ bundle on main)"]
        W7["bazel-wheels.yml<br/>(4-platform matrix)"]
    end
    subgraph SCHED["Scheduled"]
        W8["bazel-build-all.yml<br/>(cron 06:00 UTC)"]
    end
    subgraph DISPATCH["workflow_dispatch"]
        W9["publish-pypi.yml"]
    end

    W9 --> PYPI[(PyPI)]
    W6 -->|"artifact, unsigned"| ART[(workflow artifacts)]
    W7 -->|"artifact (cp310-abi3)"| ART
    W4 -->|"vscode-extension-vsix artifact"| ART
    W5 -->|"book-html artifact"| ART

Verified against .github/workflows/ on 2026-06-01.

Workflows in detail

Workflow fileWorkflow nameTriggersJobs
bazel-rust.ymlbazel · rustpush/PR on Rust pathsbuild-and-test (ubuntu + macos-14)
bazel-python.ymlbazel · pythonpush/PR on Python / engine pathsbuild-and-test (ubuntu + macos-14)
bazel-grammar.ymlbazel · grammarpush/PR on grammar / symbol pathsrust-binding, generated-artifacts-fresh
bazel-typescript.ymlbazel · typescriptpush/PR on apps/vscode-extension/**, schemas/**vscode-extension
bazel-docs.ymlbazel · docspush/PR on book/**, bazel/book/**build-and-linkcheck
bazel-tauri-batchalign.ymlbazel · tauri-batchalignpush/PR/dispatch on Batchalign GUI pathssmoke (every PR), bundle (main + dispatch only; macOS arm64, macOS x86_64, Linux x86_64)
bazel-wheels.ymlbazel · wheels (PR matrix)push/PR on wheel-affecting pathsbuild (macos-14, macos-13, ubuntu, ubuntu-arm)
bazel-build-all.ymlbazel · build-all (nightly)cron 0 6 * * *, dispatchbuild-all (ubuntu + macos-14, full //...)
publish-pypi.ymlpublish · pypi (batchalign)workflow_dispatch onlyverify-version, build (5-platform matrix), publish (gated on publish=true input)

Windows is not exercised in any workflow today. The bundle matrix and the bazel-wheels.yml matrix both have Windows rows commented out (tools/bazel bash wrapper / multitool uv.exe layout, respectively).

Generated artifact freshness

The grammar’s generated artifacts (grammar/src/parser.c, grammar/src/grammar.json, grammar/src/node-types.json) are checked by bazel-grammar.yml’s generated-artifacts-fresh job. If they drift, regenerate locally:

cd grammar && tree-sitter generate
git diff grammar/src/parser.c grammar/src/grammar.json grammar/src/node-types.json
git add grammar/src/ && git commit -m "grammar: regenerate"

Spec-driven artifacts (tree-sitter corpus tests, generated Rust parser tests, error docs) are regenerated by:

just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs
git diff --exit-code

Spec drift is not enforced by a dedicated CI job today; it’s caught by the per-package test jobs failing when generated tests don’t match generated source.

Release process, per surface

Only one surface has a fully automated release workflow today (batchalign → PyPI). The rest are either workflow-artifact-only or hand-published. Full per-surface narrative lives in Release Pipeline. Summary:

SurfaceBuild in CIRelease path todayGap
batchalign PyPI wheelbazel-python.yml + bazel-wheels.ymlpublish-pypi.yml (workflow_dispatch, OIDC trusted publisher)none for PyPI
chatter / chatter-lspbazel-rust.yml, nightly bazel-build-all.ymlNone — install from sourceNo release workflow
VS Code extension .vsixbazel-typescript.yml (artifact only)Manual vsce publish from operator workstationNo automated marketplace workflow
Batchalign desktop bundlebazel-tauri-batchalign.yml bundle job (main + dispatch)Workflow artifacts only; unsigned; macOS + LinuxNo GitHub Release upload; no Windows; no signing
Chatter desktop GUINightly bazel-build-all.yml onlyNoneNo CI bundle target; no release
mdBookbazel-docs.yml (artifact only)NoneNo GitHub Pages deploy

Promoting a surface

When automating a release for any surface above:

  1. Run the relevant per-PR workflow on a clean main and confirm green.
  2. Update the canonical version source for that surface (just versions to inspect — current values: batchalign wheel 0.3.0, Rust workspace 0.2.0, batchalign-engine 0.3.0, Chatter GUI bundle 0.1.0).
  3. Add the publish workflow alongside (e.g. a publish-chatter.yml modelled on publish-pypi.yml).
  4. Update Release Pipeline, Release Contract, and Code Signing and Distribution in the same patch.

First-release release-ops guardrails

  • VS Code stays manual-publish until a release workflow exists. Do not add marketplace-claiming language to install docs until automation lands.
  • Desktop bundles are unsigned. Any download instructions must mark them as “unsigned development build”. See Code Signing and Distribution.
  • publish-pypi.yml is the only publish workflow. Treat any reference to release.yml, vscode-release.yml, batchalign-release.yml, batchalign-desktop.yml, publish-chatter.yml, publish-vscode.yml, or publish-desktop.yml in older docs as obsolete — those workflows do not exist.

Cross-repo testing

After changes to core crates, verify the downstream consumer locally:

# batchalign (Python + Rust)
cd /path/to/batchalign
uv run maturin develop    # Rebuild Rust extension
uv run pytest

CLI, LSP, and CLAN coverage runs as part of bazel test //....

Testing and Quality Gates

Status: Current Last modified: 2026-06-01 01:05 PDT

This page describes the current relationship between local verification and CI. The local canonical gate is bazel build //... && bazel test //.... CI is a collection of scoped bazel-*.yml workflows that mirror parts of that gate on the paths each workflow watches, plus a nightly that runs the whole thing.

See Testing for the canonical local gate definitions.

Local pre-merge contract

bazel build //... && bazel test //... is the maintainer-facing local contract. Run it before pushing any change that touches Rust, Python, grammar, specs, or the book. The pre-push hook (scripts/pre-push.sh, installed via ln -sf ../../scripts/pre-push.sh .git/hooks/pre-push) runs the fast subset (fmt, affected compile, parser guardrail, generated-check, fuzz-check) on every git push.

CI workflow surface

Workflow fileJobsWhen
bazel-rust.ymlbuild-and-test (ubuntu + macos-14)push/PR on Rust paths
bazel-python.ymlbuild-and-test (ubuntu + macos-14)push/PR on Python / engine paths
bazel-grammar.ymlrust-binding, generated-artifacts-freshpush/PR on grammar / symbol paths
bazel-typescript.ymlvscode-extension (ubuntu)push/PR on apps/vscode-extension/**, schemas/**
bazel-docs.ymlbuild-and-linkcheck (ubuntu)push/PR on book/**, bazel/book/**
bazel-tauri-batchalign.ymlsmoke (every PR), bundle (main + dispatch only, macOS arm64 / macOS x86_64 / Linux x86_64)push/PR/dispatch on Batchalign GUI paths
bazel-wheels.ymlbuild (macos-14, macos-13, ubuntu, ubuntu-arm)push/PR on wheel-affecting paths
bazel-build-all.ymlbuild-all (ubuntu + macos-14, full //...)nightly cron 0 6 * * * + manual dispatch
publish-pypi.ymlverify-version, build, publishworkflow_dispatch only

CI is not a byte-for-byte mirror of the local gate sequence. Each workflow runs the Bazel build + test targets relevant to its path filter; the nightly bazel-build-all.yml covers everything that per-path workflows skip.

Gate-to-job mapping

ConcernLocal commandCI job(s) that cover it
Rust workspace compile + testbazel build //crates/... && bazel test //crates/...bazel-rust.yml build-and-test; nightly bazel-build-all.yml build-all
Grammar binding buildsbazel build //grammar:tree_sitter_talkbankbazel-grammar.yml rust-binding
Generated parser.c / grammar.json / node-types.json freshcd grammar && tree-sitter generate && git diff --exit-codebazel-grammar.yml generated-artifacts-fresh
Python wheel + engine cdylib + py_testbazel build //python/batchalign:wheel && bazel test //python/batchalign:pytestbazel-python.yml build-and-test
Wheel platform-matrix smoke (cp310-abi3)bazel run -c opt //python/batchalign:wheelbazel-wheels.yml build
VS Code extension build / test / packagejust vscode build, just vscode test, just vscode packagebazel-typescript.yml vscode-extension
mdBook builds, internal links resolvebazel run //book:html && bazel run //book:linkcheckbazel-docs.yml build-and-linkcheck
Batchalign desktop generated protocol artifacts buildbazel build //apps/batchalign/batchalign-gui:protocol_artifactsbazel-tauri-batchalign.yml smoke
Batchalign desktop bundle builds end-to-endbazel run //apps/batchalign/batchalign-gui/src-tauri:bundle -- --target <triple>bazel-tauri-batchalign.yml bundle (main + dispatch only)
Exhaustive //... build + testbazel build //... && bazel test //...bazel-build-all.yml build-all (nightly)

What’s intentionally not enforced in CI

  • Python lockfile drift (//python:requirements_test). The target exists but is tagged manual because uv 0.5.18 --universal is host-OS-dependent. Contributors regenerate the lockfile via bazel run //python:requirements; CI does not assert no drift. See the comment in bazel-python.yml.
  • Windows builds (any surface). tools/bazel is a bash wrapper incompatible with PowerShell; multitool.lock.json has a uv.exe layout issue. Windows rows are commented out in bazel-tauri-batchalign.yml and bazel-wheels.yml. See Code Signing and Distribution.
  • Code signing / notarization. Nothing is signed today.
  • Chatter desktop GUI bundle. No dedicated workflow; only the nightly bazel-build-all.yml exercises it.

Old job names that no longer exist

For anyone hunting through the git history: rust-check-and-test, spec-tools, chat-manual-anchor-check, generated-artifacts, fuzz-smoke, vscode-vsix-smoke, cross-platform-smoke, dependency-audit, semver-checks, ci-report are all from the old ci.yml. That workflow has been retired in favor of the scoped bazel-*.yml set described above. The current job names live inside those workflows (e.g. build-and-test, rust-binding, generated-artifacts-fresh, smoke, bundle).

Write and maintain documentation

Use Diátaxis throughout this book. Choose one reader need for each page before writing it:

FormReader needWhat belongs on the page
TutorialLearn through a successful first experienceA guided task, prerequisites, concrete steps, visible checkpoints
How-to guideComplete a task the reader already wants to doGoal-focused steps, relevant choices, verification
ReferenceLook up a fact or contractAccurate options, defaults, types, formats, limits, source ownership
ExplanationUnderstand a mechanism or decisionContext, relationships, reasoning, tradeoffs

Update a page

  1. State its reader and purpose. For a first-time clinician, explain where to enter a command and what success looks like. Keep architecture and tuning details in linked explanations.
  2. Check every command against its current entrypoint, every default against implementation, and every source path against the checkout. Distinguish model availability from measured quality. Do not infer current behavior from an old design proposal or a comment copied from a previous runtime.
  3. Keep one canonical treatment of a topic. Link to it from other forms instead of repeating tables or architectures. Split a mixed page when its tutorial, reference, and explanation serve different needs.
  4. Use SUMMARY.md to place it in the appropriate Diátaxis section. File paths may remain stable for existing links; the directory name does not determine the documentation form.
  5. When retiring obsolete instructions, keep a short pointer at an established URL if it is linked elsewhere. Link historical studies to a fixed repository revision and identify their date/version scope. Do not label them current.
  6. Run just docs build and inspect the rendered page. It includes the configured link checker. Verify changed command examples through just batchalign cli when they describe Batchalign integration behavior.

Keep tutorials focused on completing their exercise; keep how-to guides free of architecture tours; keep reference factual; keep explanations separate from copy-and-paste operational instructions. A landing page routes readers by need instead of repeating the repository README.

The Batchalign documentation audit records the architecture cleanup and replacement pages.

CHAT Processing Playbook for Developers

Status: Current Last updated: 2026-03-23 23:49 EDT

Objective

Provide an implementation playbook for developers building or extending CHAT parsing, validation, transformation, and serialization logic.

Mental Model

Treat CHAT processing as a layered pipeline:

  1. Ingest bytes and normalize line boundaries.
  2. Parse syntax into structured model with exact spans.
  3. Validate semantic rules with structured diagnostics.
  4. Transform or enrich model without breaking invariants.
  5. Serialize in canonical form.

Developer Workflow

  1. Start from a concrete fixture or corpus case.
  2. Add/adjust parser behavior with contract tests first.
  3. Add semantic validator rules separately from parser acceptance.
  4. Confirm roundtrip and equivalence gates.
  5. Update docs for any visible behavior or policy change.

Tier Dispatch Strategy

Use cheap byte-prefix dispatch before heavy parsing:

  • @ => header candidate,
  • * => main tier,
  • % => dependent tier,
  • continuation rules and whitespace handled deterministically.

This preserves performance and isolates error contexts earlier.

For downstream batchalign consumers, tier dispatch is only the front door. The important contract is what happens after dispatch: parse-health taint, recovery vs rejection, and whether a tier is safe to pass into alignment.

Word Parsing Rules of Thumb

  • Parse suffix markers in strict order (@..., @s..., $...) with explicit precedence.
  • Keep raw_text exact, cleaned_text policy-driven and test-locked.
  • Treat CA delimiters and special symbols via centralized symbol sets.
  • Never embed ad hoc symbol literals in multiple files.

Error Handling Contract

  • Every parser failure should produce structured diagnostics with:
    • code,
    • severity,
    • span,
    • context,
    • message.
  • Avoid silent fallback behavior unless policy explicitly allows it.
  • If fallback occurs, emit warning-grade diagnostics where relevant.
  • Never fabricate semantic placeholders (empty required text, arbitrary enum default, fake word/chunk) to satisfy type construction.
  • Prefer None/partial outcome + diagnostics over synthetic model values.

Span Discipline

  • Offsets are absolute across full file content.
  • Nested parser helpers must accept base offset and return shifted spans.
  • Add tests for boundary and continuation-line spans.

Performance Policy

  • Prefer byte-oriented prechecks for top-level dispatch and simple delimiters.
  • Use parser combinators for structural parsing, not for obvious constant-prefix routing.
  • Measure parser performance on representative corpus slices before/after major changes.

Common Failure Patterns and Fixes

  • Symptom: semantic mismatch only in snapshots.
    • Fix: compare parser outputs directly and isolate first structural delta.
  • Symptom: generated tests pass, corpus fails.
    • Fix: add missing fixture, decide parse-vs-validate placement, lock behavior.
  • Symptom: output drift after grammar edit.
    • Fix: run full regeneration and equivalent parser contract suite before merge.

Batchalign3 Surface Checks

When a change affects the surface used by batchalign, confirm:

  • full-file parse equivalence still holds for corpus coverage
  • alignment-sensitive downstream tiers still gate on parse-health appropriately

Review Checklist for Parser PRs

  • New or changed behavior has targeted tests.
  • Equivalence suite status is attached.
  • Snapshot updates are intentional and explained.
  • No hidden magic symbols or magic string literals introduced.
  • Docs updated where user-visible behavior changes.

Required Artifacts for Significant Changes

  • Design note (architecture decision record in the book).
  • Before/after examples.
  • Impacted fixtures list.
  • Migration implications for integrators.

GitHub Readiness and Open Source Governance

Status: Current Last modified: 2026-06-01 01:05 PDT

Objective

Prepare talkbank-tools to operate as a healthy public project with clear legal, security, contribution, and release processes.

Root artifacts

ArtifactStatusNotes
LICENSEDoneBSD-3-Clause, license.workspace = true in all crates
CONTRIBUTING.mdDoneSetup, standards, PR flow, pre-PR checklist
CODE_OF_CONDUCT.mdDoneContributor Covenant 2.1 with repo contact
SECURITY.mdDoneIssue-template contact link resolves to a real policy
CODEOWNERSTODONot yet added; no path-level review ownership configured
.github/workflows/Done (scoped)Eight bazel-*.yml workflows + publish-pypi.yml; see CI and Release
.github/ISSUE_TEMPLATE/*DoneBug report + feature request (YAML forms)
Pull request templateDone.github/PULL_REQUEST_TEMPLATE.md mirrors CONTRIBUTING + PR requirements

CI governance policy

The CI surface is a set of scoped bazel-*.yml workflows rather than a single monolithic ci.yml. Each workflow watches a path filter and runs the Bazel build / test targets relevant to that path. The full bazel build //... && bazel test //... runs in bazel-build-all.yml as a nightly cron (0 6 * * *) plus on-dispatch.

  • Required status checks: see Branch Protection for the mapping of recommended checks to workflow jobs.
  • There is no aggregate summary job (ci-report) today; required checks reference per-workflow jobs directly.
  • The local pre-merge gate (bazel build //... && bazel test //...) and the pre-push hook (scripts/pre-push.sh) are documented in Quality Gates.

Release governance

ConcernStatus
batchalign PyPI publish workflowDone (publish-pypi.yml, manual dispatch, OIDC trusted publisher)
Batchalign desktop bundle CIDone (bazel-tauri-batchalign.yml); artifacts only; macOS + Linux; no GitHub Release upload; unsigned
Chatter CLI / chatter-lsp release workflowTODO — built in CI by bazel-rust.yml, but no release workflow exists
Chatter desktop GUI workflowTODO — only covered by nightly bazel-build-all.yml
VS Code extension release workflowTODO — built and packaged by bazel-typescript.yml; marketplace publish is manual today (see vscode/developer/releasing.md)
Code signing / notarizationTODO on every surface; see Code Signing and Distribution
Pre-1.0 release cadence and taggingTODO
Changelog policyTODO

Community operations

  • Label taxonomy: bug and enhancement auto-applied by issue templates. Richer taxonomy (drift, spec, grammar, parser, docs, good first issue): TODO (GitHub settings).
  • Contributor pathway: CONTRIBUTING.md covers setup and PR flow. First-time / advanced contributor pathways: TODO.
  • Public project roadmap: TODO.

Supply chain and security

  • Dependency scanning: TODO — no rustsec/audit-check or cargo-deny job is wired into the current bazel-*.yml set. Automated update PRs (Dependabot / Renovate): TODO.
  • Signed release artifacts: TODO on every platform.
  • Security advisories process: documented in SECURITY.md.

Acceptance criteria

  • Repo has complete governance artifacts at root.
  • CI workflows and branch protections enforce stated policy.
  • Contributors can onboard and submit PRs without tribal knowledge.
  • Release process is repeatable and documented (where it exists; the TODOs above are the gaps).

See also

Rust Compilation Times: Findings and Optimizations

Status: Current Last updated: 2026-03-24 00:01 EDT

This document summarizes the compilation performance analysis for the talkbank-tools workspace (10 crates, Apple M4 Max 16-core) and the changes made to improve iterative development speed.

Background: How Rust Compilation Works

Rust compilation has two key mechanisms for speed:

  1. Incremental compilation — When you change one file and rebuild, the compiler remembers which “codegen units” within each crate were affected and only recompiles those. This is the primary speedup mechanism for local iterative development (edit-compile-test cycles).

  2. Crate-level caching — Cargo tracks which crates have changed inputs (source files, dependencies, feature flags). Unchanged crates are skipped entirely. This helps when you edit a leaf crate and don’t need to rebuild unrelated crates.

Additionally, there are external tools:

  1. sccache — A shared compilation cache that stores compiled artifacts by content hash. Designed for CI environments where builds start from a clean state. It works by wrapping rustc and checking a cache before invoking the real compiler.

  2. Linker choice — The linker runs after all crates are compiled to produce the final binary. Faster linkers (like lld) can shave seconds off link time for large binaries.

What We Found

Problem 1: sccache Was Disabling Incremental Compilation (Critical)

The global ~/.cargo/config.toml had:

[build]
rustc-wrapper = "/opt/homebrew/bin/sccache"

This caused two compounding problems:

  • sccache disables Rust incremental compilation entirely. When a rustc-wrapper is set, Cargo cannot use incremental mode because the wrapper interposes between Cargo and rustc, breaking the incremental artifact protocol.

  • sccache had near-zero cache benefit for this workspace. The sccache stats showed a 2.7% Rust cache hit rate. Out of 37 compilations, 36 were marked “non-cacheable” because rlib crates (library crates, which is what most workspace crates produce) cannot be cached by sccache.

The result: every cargo build after a one-line change was effectively a clean rebuild of the entire dependency chain. A change to talkbank-model (near the root of the crate graph) triggered a full recompile of 11+ downstream crates, taking 60-90 seconds even for a trivial edit.

The dev profile was generating full DWARF debug info (level 2), which includes:

  • Type definitions for every struct/enum
  • Variable location info for debugger inspection
  • Full scope and lifetime metadata

This produces large .dSYM bundles and .o files, increasing linker input size and slowing down the link phase.

Problem 3: Third-Party Dependencies at -O0

All third-party crates (serde, regex, tree-sitter, etc.) were compiled at opt-level = 0 in dev builds. Since these crates rarely change, this was a pure penalty: slow runtime (tests using serde deserialization, tree-sitter parsing, or regex matching ran ~10x slower than necessary) with no compile-time benefit after the first build.

Non-Problem: lld Linker

The linker = "lld" setting in the global cargo config was fine. On macOS this uses ld64.lld from Homebrew’s LLVM toolchain (LLD 21.1.8), which is slightly faster than Apple’s default linker for workspaces of this size. No change needed.

Changes Made

Change 1: Project-Local sccache Override

Created .cargo/config.toml in the project root:

[build]
rustc-wrapper = ""

This overrides the global sccache setting for this project only, re-enabling incremental compilation. Other Rust projects on the system are unaffected.

Why not modify the global config? Keeping the project-local override is safer — sccache may still be useful for other projects or CI workflows. The override is also version-controlled, so all contributors benefit.

Change 2: Reduced Debug Info

In the workspace Cargo.toml:

[profile.dev]
debug = "line-tables-only"

[profile.test]
debug = "line-tables-only"

This generates only file/line number information for backtraces, skipping the bulky type and variable metadata. You still get useful panic/backtrace output with source locations — you just can’t inspect local variables in a debugger (lldb/gdb). For most development workflows this is the right tradeoff.

Change 3: Optimized Third-Party Dependencies

[profile.dev.package."*"]
opt-level = 1

The "*" selector targets all non-workspace (third-party) crates. -O1 is a lightweight optimization level that enables basic optimizations (inlining, dead code elimination) without the compile-time cost of -O2/-O3. Since Cargo caches compiled dependencies, this is a one-time cost that pays off every time you run tests.

Results

ScenarioBeforeAfter
Clean build~3-5 min (est.)39s
Incremental rebuild (touch talkbank-model)~60-90s4s
Test runtime (serde/regex/tree-sitter hot paths)Slow (-O0)Faster (-O1)

The incremental rebuild improvement is the headline win: ~15-20x faster for the most common development operation (change one file, rebuild).

Optional: Cranelift Backend for Maximum Iteration Speed

For the fastest possible “does it compile?” checks during rapid iteration, Rust nightly supports the Cranelift codegen backend:

cargo +nightly -Z codegen-backend=cranelift build

Cranelift generates code ~2x faster than LLVM but produces unoptimized output and is nightly-only. It is useful for compile-check cycles but not for correctness testing or benchmarking.

General Principles for Rust Compile Time

  1. Incremental compilation is king for local dev. Anything that disables it (sccache, certain rustc-wrapper tools) is a net negative for iterative development.

  2. sccache is for CI, not local dev. It shines when doing clean builds from scratch (CI runners, cross-compilation). For edit-rebuild cycles, incremental compilation is far more valuable.

  3. Optimize dependencies, not your own crates. [profile.dev.package."*"] with opt-level = 1 gives you faster test execution with minimal compile cost (dependencies rarely change).

  4. Debug info has a real cost. Full DWARF debug info inflates binary sizes and link times. Use line-tables-only unless you actively need a debugger.

  5. Measure before optimizing. Use cargo build --timings to generate an HTML report showing per-crate compile times and parallelism. Use sccache --show-stats to verify cache effectiveness.

  6. Watch for crate graph bottlenecks. Crates that sit at the root of the dependency graph (like talkbank-model) are the critical path — changes to them trigger the longest rebuild chains. Keep these crates lean and consider splitting them if they grow too large.

Developer Verification Checks

Status: Current Last updated: 2026-04-29 10:39 EDT

This page defines the canonical local verification gates that must pass before opening or merging a PR.

Canonical Command

Run:

bazel build //... && bazel test //...

This runs 15 gates (G0–G14). See Testing > Verification Gates for the full table. Key gates include:

  • G0 Parser signature guardrail
  • G1 Rust workspace compile check
  • G2 Spec tools compile check
  • G5 Generated parser corpus equivalence suite
  • G6 Golden fragment validity (words + tiers)
  • G8 Reference corpus semantic equivalence
  • G10 Golden tier roundtrip (%mor, %gra, %pho, %wor)
  • G12 Generated artifacts match committed sources
  • G13 Fuzz workspace isolation
  • G14 Imported Batchalign Rust/PyO3 gate

When to Run

  • Always before creating a PR.
  • Always before merging parser, spec-tool, or generated artifact changes.
  • After rebasing if upstream changed parser/spec-tooling.

Additional Engineering Checks

Run these in addition to bazel build //... && bazel test //... when touching parser/model code:

  1. cargo fmt from repo root (use cargo fmt, not direct rustfmt).
  2. cargo test -p talkbank-parser --test test_parse_health_recovery.
  3. cargo nextest run -p talkbank-parser-tests --test parser_equivalence_files.

These protect against regressions in:

  • parser recovery without sentinel fabrication
  • parse-health taint propagation
  • parser semantic equivalence

Failure Policy

  • If any gate fails, do not merge.
  • Fix the failing gate or scope down the change.
  • If the failure is unrelated and pre-existing, document it in the PR and open a blocker issue.

Use narrower loops while iterating, then run bazel build //... && bazel test //... before final review:

cargo test -p talkbank-parser --lib

For grammar-only edits, prefer the smallest relevant loop first:

tree-sitter test
cargo test -p talkbank-parser

Only reach for just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs when the change truly affects generated artifacts.

For dependency-aware local sweeps, the canonical entrypoint is now the Rust xtask:

cargo run -q -p xtask -- affected-rust check
cargo run -q -p xtask -- affected-rust test

bazel build //... and bazel test //... both delegate to the same xtask implementation.

Branch Protection and Required CI Checks

Status: Current Last modified: 2026-06-01 01:05 PDT

This page documents the recommended required-status-check set for main. The current .github/workflows/ set is a collection of scoped bazel-*.yml workflows; required checks should reference real job names from those files.

Operators: the list below is what the docs prescribe. Whether each check is currently configured as required in GitHub branch protection is settings-side state and can only be confirmed by reading the repo’s branch-protection rules directly.

Branch protection policy

Enable branch protection for main with:

  • Require pull request before merge.
  • Require approvals (minimum 1; maintainers may set higher).
  • Require conversation resolution before merge.
  • Require status checks to pass before merge.
  • Restrict force pushes and branch deletions.

Required status checks

The following CI jobs should be required on main. Each entry names the workflow file and the job within it; GitHub’s branch-protection UI exposes these as "<workflow name> / <job name>".

Workflow fileWorkflow name (name: field)JobWhy required
bazel-rust.ymlbazel · rustbuild-and-testRust workspace compile + test
bazel-python.ymlbazel · pythonbuild-and-testPython wheel + engine + py_test
bazel-grammar.ymlbazel · grammarrust-bindingGrammar Rust binding
bazel-grammar.ymlbazel · grammargenerated-artifacts-freshNo drift in grammar/src/parser.c etc.
bazel-typescript.ymlbazel · typescriptvscode-extensionVS Code extension builds + tests
bazel-docs.ymlbazel · docsbuild-and-linkcheckmdBook + linkcheck pass
bazel-tauri-batchalign.ymlbazel · tauri-batchalignsmokeFrontend + openapi snapshot drift
bazel-wheels.ymlbazel · wheels (PR matrix)buildMulti-platform wheel build + abi3 smoke

The bundle job in bazel-tauri-batchalign.yml only runs on main and on dispatch (not on PRs), so it cannot be a PR-required check. bazel-build-all.yml is a nightly cron and also cannot be a PR-required check.

Optional hardening

  • Require branches to be up to date before merging.
  • Enable merge queue if PR volume increases.
  • Restrict who can dismiss stale reviews.

Operational rule

If required checks fail:

  • Do not bypass protection.
  • Fix the issue (or revert the breaking change).
  • Re-run checks until green.

Old check names that no longer exist

References to Rust Check and Test, Spec Tools Check and Test, Grammar Generate and Test, Generated Artifacts Up To Date, ci-report, or the ci.yml workflow point at a retired CI layout. The current workflows are the bazel-*.yml set described above.

Reference Corpus Overhaul

Status: Reference Last updated: 2026-05-01 09:47 EDT

Motivation

The reference corpus (resources/corpus/reference/) is the 100%-pass quality gate for all parser/grammar changes. The parser must handle every file at 100%. Before this overhaul, the corpus had three problems:

  1. Language monoculture: 345 files, all English. We have 100K+ real files across 42 languages in the corpus data directory but the gate only tested English.
  2. Construct gaps: 18 concrete grammar node types were never exercised (e.g., interrupted_question, scoped_best_guess, trailing_off_question). A grammar regression affecting these constructs would pass CI undetected.
  3. Error coverage gaps: 27 error specs were stubs (no CHAT example), 4 error codes had no spec file at all.

Strategy

Fresh build, not incremental patching. We kept the existing 345 English files as-is (they encode years of parser fixes) and added multilingual files + construct gap-fillers on top.

Phase 0: Coverage Tooling

Built corpus_node_coverage (crates/spec/talkbank-spec-testgen/src/bin/corpus_node_coverage.rs) to measure which of the 334 concrete grammar node types the corpus exercises. Running against the old 345-file corpus confirmed exactly 18 gaps.

Phase 1: Language Selection & File Extraction

Built extract_corpus_candidates (crates/spec/talkbank-spec-testrun/src/bin/extract_corpus_candidates.rs) to automatically select representative files from the corpus data directory for 20 target languages:

eng, zho, fra, deu, spa, jpn, nld, heb, por, ell,
tur, hrv, pol, ita, hun, rus, est, dan, ara, isl

Selection criteria:

  • Clean tree-sitter parsing (no ERROR nodes) — mandatory
  • Short files (under 200 lines, preferring 15–100)
  • Varied tiers (%mor/%gra/%pho/%com)
  • Multiple speakers preferred
  • Privacy: explicitly skip Password directories in the corpus data directory

For each language, the tool scored and ranked candidates. We selected 1–2 files per language (25 files total across 20 language subdirectories).

Phase 2: Construct Gap-Filling

Created 4 handcrafted files in resources/corpus/reference/constructs/ to exercise the 18 missing node types that don’t appear in real-world data:

FileNode types exercised
rare-terminators.chainterrupted_question, self_interrupted_question, self_interruption, trailing_off_question
uptake.chauptake_symbol
best-guess.chascoped_best_guess
unsupported.chathumbnail_header, unsupported_header, unsupported_dependent_tier, unsupported_line, unsupported_header_prefix, unsupported_tier_prefix

Other gaps (l1_of_header, utf8_header, etc.) were already covered by the language files or were confirmed as supertypes (not concrete).

Result: 334/334 concrete types exercised (100%).

Phase 3: Tier Regeneration

Ran batchalign morphotag on all 25 language files to generate fresh %mor/%gra tiers:

cd /path/to/batchalign
uv run batchalign morphotag /path/to/talkbank-tools/resources/corpus/reference/{lang}/ --in-place

All 20 languages are covered by Stanza’s UD models. Validation confirmed all 374 files pass parser equivalence and roundtrip.

Phase 4: Error Corpus Expansion

4.1: Created 3 missing error specs (E707, E711, E717) with CHAT examples and metadata. Fixed E376 (had wrong error code E208 in metadata).

4.2: Filled 17 triggerable stub specs with CHAT examples:

  • Cross-utterance validation (E341, E351–E355)
  • Parser recovery warnings (E319–E322, E325, E326)
  • Underline tier errors (E356–E357)
  • Overlap index errors (E373)
  • Direct parser tier errors (E381, E384)

4.3: Documented 12 untriggerable stubs (internal, deprecated, or not-yet-wired error codes) with explanations of why no example is possible: E001, E002, E211, E317, E318, E340, E374, E377, E378, E380, E385, E386.

4.4: Corrected 5 misclassified specs where examples triggered different error codes than intended (E319–E322, E376). Added Status: not_implemented and explanatory notes.

4.5: Built perturbation tool (crates/spec/talkbank-spec-testgen/src/bin/perturb_corpus.rs) with 11 mutation strategies that take a valid .cha file and produce controlled mutations targeting specific error codes:

PerturbationTarget Error
delete-participantsE501
delete-languagesE503
delete-idE504
undeclared-speakerE308
delete-terminatorE305
extra-mor-wordE706
fewer-mor-wordsE705
delete-beginE502
delete-endE510
duplicate-participantsE511
mor-terminator-mismatchE716

Also includes a mining mode (--mine DIR) that scans real data for tree-sitter ERROR nodes, with automatic Password directory exclusion.

4.6: Regenerated golden artifacts — all 8 golden generators + audit + bootstrap:

ArtifactLines
golden_words.txt769 (1949 unique words)
golden_mor_tiers.txt405
golden_gra_tiers.txt7
golden_main_tiers.txt607
golden_pho_tiers.txt25
golden_wor_tiers.txt7
golden_sin_tiers.txt5
golden_com_tiers.txt24
golden_words_featured.txt96
golden_words_minimal.txt62

Bootstrap regenerated reference_corpus.rs with 374 test cases.

Phase 5: CI Integration & Validation

All verification gates pass:

  • Parser equivalence: 377/377 (374 files + 3 extra)
  • Node coverage: 334/334 (100%)
  • Error coverage: 181/181 (100%), 169 with CHAT examples, 12 documented stubs
  • bazel build //... && bazel test //... passes all gates (G0–G10)

Phase 6: Cleanup & Documentation

  • Updated file count references (339→374) across CLAUDE.md files
  • Rewrote corpus/README.md with new structure
  • Updated memory files

Final State

resources/corpus/reference/           374 files total
  *.cha                     345 files (original English corpus)
  constructs/                 4 files (rare grammar constructs)
  {20 language dirs}/        25 files (multilingual, from corpus data)
MetricBeforeAfter
Total files345374
Languages1 (English)20
Concrete node coverage316/334 (94.6%)334/334 (100%)
Error specs177/181 (97.8%)181/181 (100%)
Error specs with examples~150169
Documented stubs012
Golden artifactsStaleFreshly regenerated

Tools Built

ToolPathPurpose
corpus_node_coveragecrates/spec/talkbank-spec-testgen/src/bin/Grammar node type coverage
extract_corpus_candidatescrates/spec/talkbank-spec-testrun/src/bin/Automated file selection from corpus data
perturb_corpuscrates/spec/talkbank-spec-testgen/src/bin/Error file generation by mutation

What Worked

  • extract_corpus_candidates: Automated scoring eliminated guesswork in file selection. Files were high-quality, short, and diverse.
  • construct gap-filling: 4 handcrafted files closed 18 gaps efficiently.
  • Keeping existing 345 files: No breakage, no regressions. The new files are purely additive.
  • batchalign morphotag: Generated correct %mor/%gra for all 20 languages without manual intervention.

What Didn’t Work / Lessons Learned

  • Mining real errors from corpus data: The MacWhinney subcorpus (407 files) had zero tree-sitter parse errors — the data is too clean. Mining is slow on large directories (>4 minutes for all of Eng-NA). The perturbation approach is more effective for systematic error coverage.
  • Parser recovery error specs (E319–E322): Writing examples that trigger specific tree-sitter error recovery codes is very difficult. Tree-sitter’s error recovery is robust and routes most malformed input through generic paths (E316) rather than the specific recovery codes. These remain as documented stubs.
  • Direct parser vs unsupported.cha (historical — direct parser has been removed): The former Chumsky direct parser could not handle unsupported_line nodes (failed on constructs/unsupported.cha). This is no longer relevant since tree-sitter is now the sole parser.

Known Remaining Gaps

  1. 12 untriggerable error stubs: Internal (E001, E002), deprecated (E211, E317, E318, E340, E374, E377, E378, E380, E385, E386). These are legitimate — the codes either have no emission path or are reserved.
  2. No audio files: Phase 3.3 (audio subset with %wor tiers) was deferred. Adding ~10 short audio clips would test the alignment pipeline end-to-end.
  3. Direct parser roundtrip (historical — direct parser has been removed): 373/374 passed under the former Chumsky direct parser (unsupported.cha failed). No longer relevant since tree-sitter is now the sole parser.
  4. 5 parser recovery specs not_implemented: E319–E322, E376. Examples don’t trigger the intended codes due to tree-sitter’s error recovery routing.

Desktop App Testing

Status: Current Last updated: 2026-06-01 00:52 PDT

This document covers the testing strategy for the Chatter desktop app (apps/chatter/chatter-gui/). Testing is split into three tiers by speed and scope.

Testing Tiers

┌─────────────────────────────────────────────────────────┐
│  Tier 3: E2E (WebdriverIO + tauri-driver)               │
│  Real app, real DOM, real IPC. Slow (~5-10s/test).       │
│  Catches: rendering bugs, IPC wiring, platform quirks.   │
│  Run: manually before releases, optionally in CI.        │
├─────────────────────────────────────────────────────────┤
│  Tier 2: Rust integration tests                          │
│  Real validation pipeline, real event bridge, no GUI.    │
│  Catches: serialization mismatches, event ordering,      │
│  stats consistency, single-file handling.                 │
│  Run: every commit, CI required.                         │
├─────────────────────────────────────────────────────────┤
│  Tier 1: Unit tests (Rust + TypeScript)                  │
│  Pure functions and thin runtime seams in isolation.     │
│  Catches: protocol drift, reducer bugs, CLAN math.       │
│  Run: every commit, CI required.                         │
└─────────────────────────────────────────────────────────┘

Most bugs will be caught by Tier 2. The Rust integration tests exercise the exact same code path as the Tauri commands — they call validate_target_streaming() and the frontend event bridge directly, then verify the JSON shape, field names, event ordering, and stats consistency.

Tier 1 & 2: Unit and integration tests

Running

# TypeScript capability/seam tests
cd apps/chatter/chatter-gui && npm run test:unit

# Rust contract/integration tests
cargo nextest run -p chatter-gui --test validation_bridge

What they cover

TestWhat it verifies
apps/chatter/chatter-gui/tests/unit/validationRunner.test.cjsValidation capability uses centralized command names, subscribes before invoke, and disposes listeners exactly once
apps/chatter/chatter-gui/tests/unit/validationState.test.cjsValidation reducer computes relative file names and merges diagnostics/status immutably
reference_corpus_no_hard_errors74 reference files produce zero Severity::Error (warnings allowed)
event_lifecycle_has_correct_sequenceDiscovering → Started → FileComplete×N → Finished ordering
frontend_events_serialize_to_expected_json_shapeEvery event has type field; camelCase field names match TypeScript types; diagnostics include renderedText
protocol_contracts_serialize_to_expected_json_shapeRust command/event constants and request payloads stay aligned with the TypeScript protocol module
single_file_validationSingle-file path validates exactly the selected file
finished_stats_match_file_eventsvalid + invalid + parseErrors == totalFiles; FileComplete count matches
rendered_html_present_for_errorsEvery diagnostic carries non-empty miette HTML with box-drawing characters and style= attributes (ANSI colors converted to HTML)

Adding new tests

Test file: apps/chatter/chatter-gui/src-tauri/tests/validation_bridge.rs

The tests use collect_events() which runs the real validation pipeline and collects all FrontendEvent values. To test a specific scenario:

#![allow(unused)]
fn main() {
#[test]
fn my_scenario() {
    let target = workspace_root().join("path/to/corpus");
    let events = collect_events(&target);
    let summary = summarize(&events);
    // assert on summary fields or individual events
}
}

Miette rendering pipeline

Error rendering is server-side. Each FrontendDiagnostic carries two renderings:

  • rendered_html — render_error_with_miette_with_source_colored() produces ANSI-colored text, ansi-to-html converts it to HTML <span style="...">. The frontend displays it in a <pre> block via dangerouslySetInnerHTML. This guarantees identical output to the CLI.
  • rendered_text — render_error_with_miette_with_source() produces plain text (no ANSI codes) for clean clipboard copy-paste.

The rendered_html_present_for_errors integration test verifies that every error diagnostic includes non-empty HTML containing miette box-drawing characters and style= attributes from ANSI color conversion.

TypeScript seam tests

The TypeScript unit tests compile a focused subset of apps/chatter/chatter-gui/src/ to a temporary CommonJS directory, then run Node’s built-in test runner against the compiled output. This keeps the test toolchain small while still exercising the runtime seam as real JavaScript.

  • Runner script: apps/chatter/chatter-gui/scripts/run-unit-tests.mjs
  • Compile config: apps/chatter/chatter-gui/tsconfig.unit.json
  • Test files: apps/chatter/chatter-gui/tests/unit/*.test.cjs

TypeScript ↔ Rust contract

The Rust integration tests verify that serialized JSON matches what the TypeScript frontend expects. If you change a field name or event structure in events.rs, the frontend_events_serialize_to_expected_json_shape test will catch the mismatch before you discover it at runtime.

The key serde attributes:

  • #[serde(tag = "type", rename_all = "camelCase")] on enums — variant names become camelCase tag values (fileComplete, not FileComplete)
  • #[serde(rename_all = "camelCase")] on individual variants — field names become camelCase (totalFiles, not total_files)
  • Both must be present: the enum-level rename_all only affects tag names, not field names within variants

Tier 3: E2E Tests (WebdriverIO)

Prerequisites

cargo install tauri-driver    # WebDriver backend for Tauri (Linux/Windows only)
cargo tauri build --debug     # Build the app binary

Note: tauri-driver only works on Linux and Windows. On macOS, WKWebView does not support WebDriver. Run E2E tests in CI (Linux) or on a Windows machine.

Running

# Terminal 1: start tauri-driver (WebDriver server on :4444)
tauri-driver

# Terminal 2: run the tests
cd apps/chatter/chatter-gui
npm run test:e2e

What they cover

The smoke tests in tests/e2e/smoke.spec.ts verify that the app launches and renders the expected UI elements:

  • Drop zone with Choose File / Choose Folder buttons
  • Empty file tree (“No files loaded”)
  • Empty error panel (“Select a file to view errors”)
  • Status bar showing “Ready”

Limitations

File dialogs cannot be driven via WebDriver. The native file picker (@tauri-apps/plugin-dialog) opens an OS-level dialog that WebDriver can’t interact with. Options for testing the validation flow:

  1. Test-only Tauri command — add validate_for_test(path) behind #[cfg(debug_assertions)] that bypasses the file dialog
  2. Programmatic invoke — use driver.executeScript() to call window.__TAURI__.core.invoke("validate", { path }) directly
  3. Drag-and-drop simulation — possible but platform-dependent and fragile

For now, the Rust integration tests cover the full validation pipeline. E2E tests focus on UI rendering and user-visible layout.

Adding E2E tests

Test file: apps/chatter/chatter-gui/tests/e2e/*.spec.ts

WebdriverIO provides $() and $$() for CSS selectors, plus Tauri-aware capabilities:

it("should show validation results", async () => {
  // Programmatically trigger validation (bypasses file dialog)
    await browser.executeAsync(async (path, done) => {
      await (window as any).__TAURI__.core.invoke("validate", {
        path,
      });
      // Wait for finished event
      setTimeout(done, 5000);
  }, "/path/to/corpus");

  const tree = await $(".file-tree-panel");
  const text = await tree.getText();
  expect(text).not.toContain("No files loaded");
});

When to run E2E tests

  • Before releases — manual run to verify the built app works end-to-end
  • Optionally in CI — requires tauri-driver and a display server (Xvfb on Linux). Slow, so consider running only on release branches.
  • Not on every commit — the Rust integration tests are fast and cover more ground

Platform-Specific Considerations

PlatformWebView engineE2E support
macOSWKWebViewNot supported — tauri-driver does not work on macOS (WKWebView has no WebDriver API)
WindowsWebView2 (Chromium)Full support via tauri-driver
LinuxWebKitGTKFull support via tauri-driver; requires Xvfb for headless

macOS limitation: Apple’s WKWebView does not expose a WebDriver endpoint, so tauri-driver cannot drive the app on macOS. E2E tests must run on Linux (CI) or Windows. For local macOS development, rely on the Rust integration tests (Tier 2) and manual smoke testing.

CSS rendering differs slightly between WebKit (Linux) and Chromium (Windows). Visual regressions are possible — consider screenshot comparison tests if this becomes a problem.

Test Data

All tests use the reference corpus at resources/corpus/reference/ (74 files). This corpus is checked into the repo and must always pass validation. Two files currently produce warnings (E534, E603) but zero hard errors.

Do not create ad-hoc .cha test files. Use existing reference corpus files or ask the user to provide test data.

CI Integration

Add to the existing CI workflow:

# Rust integration tests (fast, always run)
- name: Desktop integration tests
  run: cargo nextest run -p chatter-gui --test validation_bridge

# E2E tests (slow, release branches only)
- name: Build desktop app
  if: startsWith(github.ref, 'refs/heads/release')
  run: cargo tauri build --debug
- name: E2E smoke tests
  if: startsWith(github.ref, 'refs/heads/release')
  run: |
    tauri-driver &
    sleep 2
    cd apps/chatter/chatter-gui && npm run test:e2e

Operations

Status: Current Last updated: 2026-06-01 00:50 PDT

This section holds repo-level release-contract policy documents plus the auto-generated validation-error catalog.

Authoritative repo-level policy

PathRole
platform-support.mdSupported-platform matrix and per-surface support tiers
release-contract.mdStability tiers and public-surface contract
versioning.mdVersion authority and compatibility policy
release-pipeline.mdHow a code change becomes a published artifact
code-signing-and-distribution.mdSigning/notarization policy and allowed channels
validation-feature-flags.mdReference for validation behavior switches

Generated reference

PathRole
errors/Auto-generated validation-error catalog (one page per error code, regenerated by just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs from resources/spec/errors/). Pages are not in the rendered TOC — open individual E###.md files in source.

Release Pipeline

Last modified: 2026-06-01 01:05 PDT

How a code change becomes a published artifact, as the repo actually ships today. Several products emerge from this monorepo; only one (batchalign on PyPI) has automated publishing wired up. The rest are either workflow-artifact-only or hand-published. This page is a map of that reality, not a wishlist.

Workflows that exist

Every CI workflow under .github/workflows/ is Bazel-driven:

Workflow fileTriggersPurpose
bazel-rust.ymlpush/PR on Rust pathsBuild + test all Rust tiers (ubuntu + macos-14)
bazel-python.ymlpush/PR on Python / batchalign-engine pathsBuild cdylib + py_test + CLI smoke (ubuntu + macos-14)
bazel-grammar.ymlpush/PR on grammar/**, symbolsRust binding build + generated-artifact drift check
bazel-typescript.ymlpush/PR on apps/vscode-extension/**, schemasBuild / test / package the VS Code extension (uploads .vsix artifact)
bazel-docs.ymlpush/PR on book/**bazel run //book:html + //book:linkcheck; uploads book-html
bazel-tauri-batchalign.ymlpush/PR/dispatch on Batchalign GUI pathsPR-only smoke (ubuntu); on main + dispatch, matrix bundle of the Batchalign desktop app (macOS arm64, macOS x86_64, Linux x86_64)
bazel-wheels.ymlpush/PR on wheel-affecting pathsMulti-platform wheel build + abi3 smoke (4-cell matrix); artifact-only
bazel-build-all.ymlnightly cron + dispatchExhaustive bazel build //... + bazel test //... on ubuntu-latest and macos-14
publish-pypi.ymlworkflow_dispatch onlyBuild wheels via Bazel + maturin and upload to PyPI via OIDC trusted publisher

Everything else (chatter binary release, VS Code Marketplace push, desktop bundle distribution, code signing) is not automated yet. Where contributors and operators need to ship those today, the process is manual; the gaps are listed at the bottom of this page.

Products and how each ships today

flowchart TD
    subgraph CI["CI / nightly"]
        A1["bazel-rust.yml<br/>(ubuntu + macos-14)"]
        A2["bazel-python.yml<br/>(ubuntu + macos-14)"]
        A3["bazel-grammar.yml"]
        A4["bazel-typescript.yml"]
        A5["bazel-tauri-batchalign.yml<br/>smoke + bundle matrix"]
        A6["bazel-wheels.yml<br/>(4-platform matrix)"]
        A7["bazel-docs.yml"]
        A8["bazel-build-all.yml<br/>(nightly cron 06:00 UTC)"]
    end

    subgraph PUB["Publish surfaces"]
        P1["publish-pypi.yml<br/>(manual dispatch)"]
        P2["GitHub workflow artifacts<br/>(no release publishing)"]
        P3["Manual: vsce publish<br/>(operator's workstation)"]
    end

    subgraph PROD["Products"]
        R1["batchalign wheel<br/>→ PyPI"]
        R2["Batchalign desktop bundle<br/>(unsigned .app/.dmg/AppImage)"]
        R3["VS Code extension .vsix"]
        R4["chatter CLI / chatter-lsp"]
        R5["chatter desktop GUI"]
    end

    A2 --> P1
    A6 --> P1
    P1 --> R1
    A5 --> P2
    P2 --> R2
    A4 --> P2
    P2 --> R3
    R3 -.->|"manual today"| P3
    A1 --> R4
    A8 --> R4
    A8 --> R5

Verified against .github/workflows/ on 2026-06-01.

1. batchalign Python wheel → PyPI

Build path in CI: bazel-python.yml runs on every PR touching Python / batchalign-engine. It builds the cdylib (_core_so), runs py_test, and does a py_binary --help smoke. bazel-wheels.yml additionally builds the wheel on a 4-cell matrix (macos-14, macos-13, ubuntu-latest, ubuntu-24.04-arm) and smoke-installs it under Python 3.10 and 3.12 to prove the cp310-abi3 tag works.

Publish path: publish-pypi.yml is the only publish workflow in the repo and is workflow_dispatch only.

  1. Operator dispatches with inputs tag (must match python/pyproject.toml [project].version) and publish (boolean).
  2. verify-version job asserts the version string matches.
  3. build matrix (macOS arm64, macOS x86_64, Linux x86_64, Linux aarch64, Windows x86_64) runs bazel run -c opt //python/batchalign:wheel on each runner and uploads .whl artifacts. The wheel is built via maturin (sole reader of pyproject.toml for the platform-tagging matrix); the day-to-day Bazel py_library path uses pyo3 directly, not maturin.
  4. If publish=true, the publish job downloads all artifacts and runs pypa/gh-action-pypi-publish with id-token: write (OIDC trusted publisher; no API token in the repo).

Local wheel build: bazel run -c opt //python/batchalign:wheel lands the wheel in python/target/wheels/.

2. Batchalign desktop GUI → workflow artifacts (unsigned)

Build path in CI: bazel-tauri-batchalign.yml. Two jobs:

  • smoke runs on every PR (ubuntu-latest): builds the frontend filegroup and generated protocol artifacts.

  • bundle runs on pushes to main and on workflow_dispatch only. Matrix:

    • macos-14 → aarch64-apple-darwin
    • macos-13 → x86_64-apple-darwin
    • ubuntu-latest → x86_64-unknown-linux-gnu

    Each cell builds the sidecar (bazel build //python/batchalign:sidecar) then runs bazel run //apps/batchalign/batchalign-gui/src-tauri:bundle -- --target <triple>. Bundles upload as workflow artifacts (batchalign-<target>), retention 14 days.

Windows is not built. The bundle matrix has a windows-latest row commented out: tools/bazel is a bash wrapper that bazelisk rejects on PowerShell hosts. Re-enabling requires a portable wrapper.

Publishing: there is no GitHub Release publishing step. To get a bundle to a tester, an operator downloads the workflow artifact and hands it over directly.

Signing: bundles are unsigned. See code-signing-and-distribution.md.

3. VS Code extension → manual marketplace publish

Build path in CI: bazel-typescript.yml runs on every PR touching apps/vscode-extension/** or schemas/**. It runs bazel run //apps/vscode-extension:build, :test, and :package on ubuntu-latest and uploads the resulting .vsix as a workflow artifact (vscode-extension-vsix).

Publishing is manual today. There is no publish-vscode.yml or equivalent in the repo. To publish a release:

  1. Bump apps/vscode-extension/package.json version on main.
  2. On a maintainer workstation, just vscode build && just vscode package (or download the vscode-extension-vsix artifact from the matching CI run).
  3. npx vsce publish --packagePath <vsix> using a marketplace PAT held by the operator.

An automated release workflow (cross-platform .vsix matrix with bundled chatter-lsp, GitHub Release upload, optional marketplace push) is a planned follow-up. See vscode/developer/releasing.md for the current step-by-step.

4. chatter CLI + chatter-lsp → no release workflow

bazel-rust.yml builds and tests these on every PR (//crates/chatter/...). bazel-build-all.yml re-builds them exhaustively each night. There is no workflow that publishes binaries to a GitHub Release. Users today install from source:

bazel build //crates/chatter/chatter-cli:chatter
# or
cargo install --path crates/chatter/chatter-cli

A chatter binary release workflow is a follow-up.

5. Chatter desktop GUI → nightly only

apps/chatter/chatter-gui has no dedicated CI workflow. The only coverage today is the nightly bazel-build-all.yml, which builds and tests everything but does not produce or upload a Tauri bundle.

A dedicated bazel-tauri-chatter.yml modelled on the Batchalign one is a follow-up.

6. mdBook documentation

bazel-docs.yml builds the book and runs the link checker on every PR touching book/** or bazel/book/**, and uploads the HTML as a book-html artifact. There is no GitHub Pages deploy step yet; adding one is a one-line change (e.g. peaceiris/actions-gh-pages consuming the artifact).

Triggers, in one table

WorkflowPush to mainPRCronDispatch
bazel-rust.ymlyes (Rust paths)yes (Rust paths)——
bazel-python.ymlyes (Py / engine paths)yes——
bazel-grammar.ymlyes (grammar paths)yes——
bazel-typescript.ymlyesyes——
bazel-tauri-batchalign.ymlyes (smoke + bundle)yes (smoke only)—yes (bundle)
bazel-wheels.ymlyesyes——
bazel-docs.ymlyesyes——
bazel-build-all.yml——0 6 * * *yes
publish-pypi.yml———yes

Publishing always requires a deliberate human gesture (workflow_dispatch). Everything else is automatic on push/PR.

Versioning

just versions is the source of truth. Current values:

SurfaceSource fieldToday’s value
Python wheel batchalignpython/pyproject.toml [project].version0.3.0
Rust workspace (chatter, talkbank-, clan-)[workspace.package].version in root Cargo.toml0.2.0
batchalign-engine Rust crateper-crate [package].version0.3.0
Chatter GUI bundleapps/chatter/chatter-gui/src-tauri/tauri.conf.json0.1.0
VS Code extensionapps/vscode-extension/package.json versionbump per Marketplace push

Pinned toolchain (from just versions): Python 3.12, uv 0.5.18, maturin 1.7.4, Rust 1.95.0, pyapp 0.27.0.

The Rust workspace and the Python preview line are on separate release trains by design.

Local pre-merge gate

bazel build //... && bazel test //... is the canonical pre-merge gate. The pre-push hook (scripts/pre-push.sh, installed via ln -sf ../../scripts/pre-push.sh .git/hooks/pre-push) runs the fast subset locally. See Quality Gates.

Known gaps (follow-up work)

  • No chatter / chatter-lsp GitHub Release workflow.
  • No automated VS Code extension release; manual vsce publish today.
  • No GitHub Release upload for Batchalign desktop bundles (artifacts only). No Windows bundle.
  • No Chatter desktop GUI CI workflow beyond the nightly.
  • No code signing or notarization on any platform; see code-signing-and-distribution.md.
  • No GitHub Pages deploy for the book.
  • //python:requirements_test (lockfile drift) is manual-tagged and not enforced in CI; see the comment in bazel-python.yml.

See also

Code Signing and Distribution

Status: Current Last modified: 2026-06-01 01:05 PDT

This page describes the current signing and trust posture of every artifact produced by this repository. Nothing the repo currently ships is code-signed, notarized, or marked as a trusted installer on any platform. Release documentation must reflect that — overclaiming trust is a documentation bug.

Current state, per surface

SurfaceBuilt bySigned?Notarized?Distribution today
batchalign Python wheelpublish-pypi.yml (manual dispatch)Wheel itself is not code-signed; PyPI upload uses OIDC trusted publishing for provenance, not artifact signingn/aPyPI
Batchalign desktop bundle (.app/.dmg/AppImage)bazel-tauri-batchalign.yml bundle matrixNoNoGitHub Actions workflow artifact only (no public release)
VS Code extension .vsixbazel-typescript.yml (CI) + manual vsce publishNon/aMarketplace push is currently manual from an operator workstation
chatter / chatter-lsp binariesbazel-rust.yml (build only)NoNoNo release workflow exists; users build from source
Chatter desktop GUI bundlebazel-build-all.yml (nightly build only)NoNoNo bundle is published anywhere

PyPI’s OIDC trusted-publisher flow proves the upload came from a specific GitHub workflow in this repo; it does not sign the wheel bytes. Treat OIDC as a deploy-key story, not as artifact signing.

What docs may claim today

  • Allowed: “GitHub Release archive”, “PyPI wheel”, “uv pip install batchalign”, “VSIX file”, “workflow artifact”, “terminal-first archive”, “unsigned development bundle”.
  • Not allowed unless we wire the automation in the same patch: “signed”, “notarized”, “Gatekeeper-trusted”, “SmartScreen-trusted”, “Apple-notarized”, “Authenticode-signed”, “Marketplace-published” (unless an automated marketplace workflow exists).

If a release note or install doc claims any of the blocked phrases, it is a documentation bug — fix the doc or fix the workflow, in the same patch.

Gaps and what would unblock them

macOS (Batchalign + Chatter desktop, chatter CLI)

To ship signed and notarized macOS bundles, the workflow needs:

  • Apple Developer ID Application certificate (org-level).
  • Secrets in the workflow environment: APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_API_KEY, APPLE_API_ISSUER, APPLE_TEAM_ID.
  • tauri-action (or an equivalent step) wired into the bazel-tauri-batchalign.yml bundle matrix so signing happens before artifact upload.
  • Notarization via notarytool after signing, before stapling.

bazel-tauri-batchalign.yml has a TODO: signing / notarization comment at the bottom that names the same secrets — this is the follow-up.

Windows (all surfaces)

Windows is not built at all in CI today:

  • No windows-latest row in bazel-tauri-batchalign.yml (commented out — tools/bazel bash wrapper incompatible with PowerShell).
  • No windows-x86_64 cell in bazel-wheels.yml (commented out — multitool’s uv.exe resolves to /uv.exe under bash-on-Windows; blocked on a multitool.lock.json workaround).

To ship signed Windows artifacts the repo first needs to fix those portability gaps, then add an EV code-signing certificate and an Authenticode step on the relevant workflows.

Linux

For .deb/.rpm/AppImage distribution, signing would mean GPG-signing the packages and publishing a public key. Not in scope until a Linux distribution channel exists; today Linux artifacts are workflow artifacts only.

VS Code extension

Marketplace publishing today is manual. The extension is not bundled with a signed chatter-lsp binary; users install from a .vsix and the extension expects chatter-lsp on PATH or from a known fallback location. An automated release would also need to settle the LSP-bundling story (one VSIX per platform with the signed chatter-lsp inside).

Operator checklist

Before any release goes out:

  1. Confirm the workflow output matches the channel claimed in the docs.
  2. Confirm docs do not overclaim signing / notarization status.
  3. If the release is a desktop bundle, mark it explicitly as “unsigned development build” in any download instructions.
  4. If the channel changes, update this doc, the relevant release docs, and the workflow in the same patch.

See also

Validation Feature Flags — Future Design

Status: Draft Last updated: 2026-05-01 09:47 EDT

Current State

chatter validate runs a fixed set of validation rules. The only customization is --suppress E### which suppresses specific error codes from the output (they still run, just hidden).

chatter clan check accepts CLAN-compatible flags:

  • --bullets N / +cN — bullet consistency mode (0=full, 1=missing only)
  • --check-target / +g2 — require CHI Target_Child
  • --check-unused / +g5 — flag unused speakers
  • --check-ud / +u — validate UD features on %mor
  • --check-id / +g4 — require @ID tiers (on by default)

Problem

CLAN CHECK allows users to enable/disable specific validation categories. chatter validate does not. This creates two issues:

  1. Users can’t opt in to stricter checks (bullet consistency, CHI requirement, unused speakers) without switching to chatter clan check.

  2. Users can’t opt out of noisy checks without knowing the specific error code to suppress.

Proposed: --enable and --disable Flags

# Enable stricter checks (off by default)
chatter validate --enable bullets        # CHECK +c0 equivalent
chatter validate --enable target-child   # CHECK +g2 equivalent
chatter validate --enable unused-speakers # CHECK +g5 equivalent
chatter validate --enable ud-features    # CHECK +u equivalent

# Disable always-on checks
chatter validate --disable cross-speaker-overlap  # suppress E729
chatter validate --disable bullet-timing          # suppress E701/E704/E729

# Combine
chatter validate --enable bullets --disable cross-speaker-overlap corpus/

Validation Categories

CategoryDefaultErrorsCHECK Flag
structureONE5xx (headers, @Begin/@End)always
contentONE2xx, E3xx (words, terminators)always
tiersONE6xx (dependent tier validation)always
alignmentONE7xx (mor/gra/pho alignment)always
bullet-timingONE701, E704, E729always
bulletsOFFE730, E732 (gap, missing)+c0/+c1
target-childOFFCHECK 68+g2
unused-speakersOFFCHECK 0+g5
ud-featuresOFFUD validation+u

vs --suppress

--suppress hides specific error codes from output. --disable prevents the validation from running at all.

For most users, the difference doesn’t matter. But for large corpus runs, skipping expensive checks (alignment, bullet-timing) saves time.

--suppress is the current workaround. --enable/--disable is the principled long-term solution.

Implementation Notes

The validation pipeline already has ValidationContext with flags like bullets_mode, ca_mode, enable_quotation_validation. Adding more flags is mechanical — the architecture supports it.

The work is:

  1. Add flags to ValidationContext / SharedValidationConfig
  2. Wire CLI --enable/--disable to the config
  3. Gate each validation category on its config flag
  4. Document which categories exist and what they control

CLAN CHECK Behaviors We Deliberately Don’t Implement

CHECKBehaviorWhy Not
81Bullet positionGrammar enforces structurally
89Wrong chars in bulletGrammar validates format
90Illegal time in bulletGrammar validates format
118Delimiter before bulletGrammar enforces order
11Symbol not in depfileWe don’t use depfile.cut (yet)
17Tier not in depfileSame
147Form marker not in depfileSame

These either can’t occur due to our grammar being stricter than CLAN’s parser, or require depfile.cut integration which is a separate project.

CLAN CHECK Behaviors We Haven’t Implemented Yet

CHECKBehaviorStatus
84Cross-speaker overlapDONE (E729)
85Gap between tiersE730 defined, not implemented
110Missing bullet (+c mode)E732 defined, not implemented
133Speaker self-overlap (timing)DONE (E704 + E731)
13Duplicate speaker declarationNot implemented
122@ID lang not in @LanguagesNot implemented
142Role mismatch @ID vs @ParticipantsNot implemented
153Age format (missing zero pad)Not implemented
157Media filename matchPartially implemented (E531)

Batchalign: reference

Look up syntax, contracts, and factual details.

CLI reference

The installed command is batchalign. Options below are checked against python/batchalign/cli/ for version 0.10.1. Run batchalign COMMAND --help for the help shipped with your installation.

batchalign [GLOBAL OPTIONS] COMMAND [COMMAND OPTIONS] [PATHS...]

Global options

OptionDefaultMeaning
--parallel N8Maximum concurrent input files; minimum 1
-v, --verboseOffIncrease verbosity; repeat for more detail
-q, --quietOffSuppress routine output and interactive credential prompts
--plainAutomaticUse the non-live renderer
--ansiAutomaticForce live rendering; --plain takes precedence if both are supplied
--helpShow help

Global options go before the command. Device, model, and engine options belong to their individual commands.

Input selection

Processing commands accept one or more paths, or -i/--input-list/--file-list FILE. Directories are scanned recursively. CHAT commands accept .cha and .chat; media discovery accepts .wav, .mp3, .m4a, .flac, .ogg, .mp4, .mov, and .m4v. Actual decoding support depends on the media backend.

batchalign morphotag first.cha second.cha -o tagged/
batchalign transcribe -i recordings.txt -o transcripts/ --lang eng

A list is a UTF-8 text file containing one path per line. Blank lines and lines whose first non-space character is # are ignored. List entries are resolved relative to the list’s directory. Positional paths are relative to the command’s working directory. Paths are deduplicated after resolving them; a repeated entry is processed once. Whitespace around list entries is trimmed.

-o/--out DIR selects an output directory. Relative subdirectories are mirrored from the common input root. Without it, CHAT transformation commands write beside or over their input as described below. See Command I/O.

CHAT commands (align, phonetic, diarize, morphotag, translate, utseg, ai, and compare) also accept -m/--media DIR for a separate media hierarchy:

batchalign align transcripts/ -m recordings/ -o aligned/

For transcripts/session/clip.cha, media is searched in recordings/session/, using the @Media basename first, then clip, with a supported audio or video extension. Missing matches report an error. A single transcript uses the media folder directly; multiple paths or an input list use their common input root, as output mirroring does. The explicit media folder takes precedence over media beside transcripts. Transcript headers and output locations are preserved.

Commands

CommandPurpose
transcribeRecording to CHAT transcript
alignForced alignment of CHAT against audio
phoneticAdd observed IPA to %pho using audio and phone-sequence DP
morphotagAdd %mor and %gra
utsegRevise utterance segmentation
translateAdd translation tiers
compareCompare CHAT against .gold.cha references
convertConvert media to WAV or MP3
diarizeAssign speakers to existing timed CHAT
aiApply an AI editing instruction to CHAT
cacheInspect or clear the result cache
versionPrint version and build information
daemonRun the separate HTTP service

transcribe

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--enginerevChoices: rev, whisper, chatwhisper, openai, funaudio, tencent, qwen3, aliyun, malayalam, google. ASR engine: rev | google | whisper | chatwhisper | openai | funaudio | tencent | qwen3 | malayalam.
--langRequiredISO-639-3 alpha_3 code: eng, cmn, yue, spa, … (Required.)
--modelUnsetASR model id (engine-specific default if omitted).
--diarize/--no-diarizeFalseRun speaker diarization with –diarize-engine after utterance segmentation.
--diarize-enginepyannote-aiChoices: pyannote-ai, pyannote. Diarization engine: pyannote-ai (cloud, default) or pyannote (local).
--num-speakers, -n2Expected speaker count (diarization hint).
--force-cpuFalseRun local inference, including utterance segmentation, on CPU (BA2’s –force-cpu).
--allow-mpsFalseExplicitly allow local ASR and utterance-segmentation models to use Apple MPS. Off by default because sustained MPS inference can be unstable; CHATWhisper remains float32 when selected.
--wor/--noworFalseInclude word-level timing (%wor and inline word bullets); omitted by default.

align

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--enginewav2vecChoices: wav2vec, whisper_fa, qwen. Forced-alignment engine: wav2vec | whisper_fa | qwen.
--modelUnsetFA model id (engine-specific default if omitted; wav2vec picks per the file language).
--force-cpuFalseRun the FA model on CPU (BA2’s –force-cpu). Needed for whisper_fa on Apple MPS, where Whisper’s bfloat16 attention kernel is unsupported.
--allow-mpsFalseExplicitly allow local alignment models to use Apple MPS. Off by default because sustained MPS inference can be unstable.
--utr-enginerevChoices: off, whisper, rev. Utterance Timing Recovery backend: rev | whisper | off. When non-off, runs Task.Utr before FA to recover utterance bullets on fully-untimed CHATs. Automatically skipped when any utterance already carries a bullet — UTR is intended for fully-untimed transcripts only.
--utr-modelUnsetUTR model id (only used when –utr-engine=whisper; default is openai/whisper-large-v3 to match BA2’s transcribe).

morphotag

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--retokenize/--no-retokenizeFalse
--clear-existing/--keep-existingTrueIf true (default), drop any pre-existing %mor:/%gra: tiers from each input before tagging so re-runs regenerate. Use –keep-existing to preserve them and let the engine skip already-tagged utterances.

utseg

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--stanza-fallback/--no-stanza-fallbackFalse
--languageen
--force-cpuFalseRun utterance segmentation on CPU.
--allow-mpsFalseUse Apple MPS for utterance segmentation when available.

translate

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--targetengTarget language code (ISO 639-3).
--enginegoogleChoices: google, nllb, tencent, aliyun.

compare

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails

convert

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--formatRequiredChoices: mp3, wav. Output format: mp3 or wav.

diarize

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
--enginepyannote-aiChoices: pyannote-ai, pyannote. Diarization engine: pyannote-ai (cloud) or pyannote (local).
--num-speakers, -n0Expected speaker count; zero auto-detects.

phonetic

Accepts timed CHAT and matching audio, using shared input selection and -o/--out. Install the phonetic extra for PhoneticXeus, Piper Plus G2P, and Epitran. Phonetic transcription requires Python 3.11 or newer. The first inference downloads the pinned model revision; building the CLI and displaying help do not download model weights.

just batchalign cli phonetic recording.cha --out phonetic-output --force-cpu

The command recognizes phones from audio and DP-aligns them against reference IPA pronunciations to recover word boundaries. %pho retains the observed IPA, including pronunciation differences. Existing %pho tiers are preserved. The input must have utterance timing bullets; use utr first when needed. Word-level forced alignment is not required.

Like Whisper forced alignment, phonetic inference groups consecutive utterances into approximately 20-second audio windows, then projects phones back to their original words and utterances. Gaps over two seconds and backwards timings start a new window; an utterance longer than 20 seconds stays whole. Words receiving no phones are marked … (uncoded) and reported with their utterance timing.

Windows of similar duration are batched using padding and real encoder lengths. Normalization is performed independently per window, and padded output frames are excluded from decoding. CPU defaults to one window at a time; CUDA defaults to two. Increase --batch-size to try higher GPU throughput, or decrease it to reduce memory use. Batched and single-window output can differ slightly because the upstream encoder’s convolution branches remain sensitive to padding.

OptionDefaultDetails
--pronunciationsNoneUTF-8 CSV with header word,ipa; one word or whole phonological unit and its IPA per row, overriding the generated pronunciation.
--force-cpuFalseUse CPU instead of automatic CUDA selection. MPS is not selected.
--batch-sizeCPU 1, CUDA 2Maximum audio windows per model batch; must be positive.

The task runner passes the primary @Languages code from CHAT. No separate language option is needed. Piper Plus G2P 0.2.0 handles English, Japanese, Mandarin Chinese, Korean, Spanish, French, Portuguese, and Swedish. Both cmn and zho select Mandarin; Cantonese (yue) is a separate language and uses the fallback.

Other languages use Epitran, with their default script resolved automatically (e.g. Russian rus-Cyrl, Hindi hin-Deva). A failure in a supported Piper backend is reported rather than silently switching providers. English does not require Flite’s lex_lookup or eSpeak; Python language packages are included in the extra. Language resources may download on first use.

The DP compares IPA directly, preserving distinctions such as nasalization, vowel length, and tone. Piper’s language-specific phone/tone labels are converted to IPA before comparison. References only determine grouping; they never replace the observed phones. Alternate scripts and code-switched words can use pronunciation overrides. Pass --pronunciations pronunciations.csv to supply dialect forms or words in unsupported languages. For example:

word,ipa
wug,wʌɡ
bonjour,bɔ̃ʒuʁ
the cat,ðəkæt

The header is required. Use standard CSV quoting for cells containing commas. Words are matched without case; empty cells and duplicate words are errors. An unknown pronunciation, invalid audio window, or alignment leaving a word without phones fails the file without overwriting it. Insertions between word anchors attach to the preceding word; review inferred boundaries, especially around reduced or atypical speech.

Python pipelines can compose recipes.phonetic(phonetic_backend=backend, utr_backend=...) with existing tasks. Custom backends implement the Phonetic marker and typed PhoneticInput/PhoneticOutput contract; the Rust runner owns CHAT extraction, result validation, and tier insertion.

ai

Accepts the shared input selection and -o/--out options above.

Option or argumentDefaultDetails
INSTRUCTIONRequiredInstruction applied to every utterance.
--enginedspyChoices: dspy.
--modelzai-org/GLM-5.2DSPy LM model string.
--max-tokens1024Maximum output tokens for the DSPy LM call.
--timeout30Per-utterance DSPy LM timeout in seconds.

daemon

Option or argumentDefaultDetails
--host127.0.0.1Bind address. Use 0.0.0.0 to accept off-host connections; default loopback-only so an unsecured daemon can’t be exposed accidentally.
--port8765Bind port.
--workers1Worker process count. Pinned to 1 by default — the in-memory job registry is per-process. Overriding requires a shared registry backend (not yet wired).
--log-levelinfouvicorn log level: critical/error/warning/info/debug/trace.
--access-log/--no-access-logTrueEmit HTTP access logs (one line per request).
--proxy-headers/--no-proxy-headersTrueTrust X-Forwarded-* headers from the upstream reverse proxy. Combine with –forwarded-allow-ips when fronted by nginx/ALB.
--forwarded-allow-ips127.0.0.1Comma-separated upstream IPs trusted for X-Forwarded-. Use ‘’ only when the daemon is behind a trusted proxy.
--graceful-timeout30Seconds to wait for in-flight requests to drain on SIGTERM.
--devFalseDevelopment mode: enable –reload, drop production defaults. Never use in production — disables clean shutdown handling.

cache

batchalign cache path
batchalign cache stats
batchalign cache clear [--yes]

See Manage the result cache.

version

batchalign version

Removed or unregistered interfaces

The current command registry does not expose serve, jobs, logs, setup, doctor, bench, benchmark, eval, or coref. A coreference recipe and backend interface exist in the Python API, but the module under cli/hidden/ is not registered as a public command. Old server and benchmark examples are not current CLI syntax.

Python API reference

The batchalign Python package exposes a Rust-backed Pipeline, task and input types, backend classes, and recipe functions. The CLI uses this API too.

For parsing, constructing, and editing transcripts directly, see Python CHAT documents. The CHATFile, Speaker, Utterance, and Media types use the Rust CHAT model and derive participant headers from utterance speaker choices.

Run a recipe

import batchalign as ba
from batchalign.inputs import chat_from_path

pipeline = ba.recipes.morphotag(
    stanza_backend=ba.StanzaBackend(),
    workers=4,
)
results = pipeline.run([chat_from_path("sample.cha")])

workers limits concurrent files. A recipe supplies a task sequence and backend instances to Pipeline; backend constructors hold model-specific settings. pipeline.run() returns outcomes. It does not perform the CLI’s output-file writing step. See the outcome type and serialization methods in crates/batchalign/batchalign-engine/src/py_outcome.rs when integrating output.

The ordinary return-value path prepares inputs and retains outcomes for the returned list. File concurrency alone therefore does not bound total memory for a large API call. The CLI uses the callback path to convert inputs within its dispatch window and write completed outcomes without retaining the full result set.

Cache policy

Recipes forward pipeline options such as cache:

pipeline = ba.recipes.morphotag(
    stanza_backend=ba.StanzaBackend(),
    cache=ba.CacheSpec.bypass(),
    workers=1,
)
PolicyReads saved resultsWrites new results
ba.CachePolicy.Use (default)YesYes
ba.CachePolicy.Bypass / ba.CacheSpec.bypass()NoNo
ba.CachePolicy.Refresh / ba.CacheSpec.refresh()NoYes

To use a separate database:

cache = ba.CacheSpec(path="/path/to/cache.lmdb", policy=ba.CachePolicy.Use)

The CLI’s cache-management commands target the default path returned by ba.default_cache_path(). They do not discover custom API cache paths.

API source

  • python/batchalign/__init__.py: exported types and lazy imports.
  • python/batchalign/recipes.py: standard task compositions.
  • python/batchalign/inputs.py: file-based input constructors.
  • python/batchalign/backends/: backend constructors and interfaces.
  • crates/batchalign/batchalign-engine/src/pipeline.rs: pipeline constructor and execution.

The old BA2 BatchalignPipeline and document model are not interchangeable with this API. Use the current types when migrating integrations.

Python CHAT documents

batchalign.chat exposes four Rust-backed types: CHATFile, Speaker, Utterance, and Media. Python imports these types from batchalign._core; parsing, validation, storage, and edits happen in Rust using the TalkBank model. CHATFile, Media, and Utterance are also available directly from batchalign. Import the CHAT Speaker from batchalign.chat; batchalign.Speaker remains the diarization backend base class.

Create a document

from batchalign.chat import CHATFile, Media, Speaker, Utterance

child = Speaker(
    "CHI",
    language="eng",
    corpus="example",
    role="Target_Child",
    name="Sam",
)
chat = CHATFile.new()
chat.media = Media("recording", media_type="audio")
chat.utterances.append(
    Utterance(child, "I want a cookie .", timing=(1000, 2500))
)

print(len(chat))  # 1: the number of utterances
print(chat.stringify())

Speaker(code, *, language, corpus, role="Participant", name=None) requires explicit language and corpus information. language accepts a language-code string or a list of codes for a multilingual speaker. Codes use CHAT’s ISO 639-3 form, such as eng, fra, and nld. Speaker values are immutable; replace an utterance’s speaker to change its participant metadata.

Utterance(speaker, text, *, timing=None) takes a Speaker and CHAT main-tier content, including its terminator. It does not take the *CHI: prefix. Timing is either None or a pair of nonnegative integer milliseconds with start less than end. Put timing in timing, rather than embedding a bullet in text.

An empty CHATFile.new() is an editable draft. Participant headers appear when utterances are added. No placeholder speaker, corpus, or language is invented.

Read and write

with open("input.cha", encoding="utf-8") as fd:
    chat = CHATFile.read(fd)

copy = CHATFile.parse(chat.stringify())
copy_from_lines = CHATFile.parse(chat.stringify().splitlines())

with open("output.cha", "w", encoding="utf-8") as fd:
    chat.write(fd)

assert str(chat) == chat.stringify()

parse accepts a string or a list of strings. List entries may include their line endings. read and write take open text streams and leave them open. They operate from the stream’s current position. Binary streams and paths are not accepted in place of text streams.

Serialization uses canonical CHAT formatting. Parsed dependent tiers, participant ID fields (including age, sex, group, education, and custom fields), and unrelated headers such as dates and comments are preserved. Comments retain their position relative to existing utterances.

utterance = chat.utterances[0]
print(utterance.speaker.code)
print(utterance.words)       # read-only tuple of word-timing units
print(utterance.timing)      # (start_ms, end_ms), or None
print(utterance.tiers["pho"])

utterance.timing = (1200, 2700)
utterance.tiers["com"] = "reviewed by the transcriber"

utterance.text, .speaker, and .timing are editable. .words is a read-only tuple obtained through the model’s word-timing traversal; it handles CHAT groups and annotations rather than splitting text on spaces. Edit .text to change the main tier.

.tiers is a live mapping from labels without % (for example pho, mor, gra, and com) to CHAT tier content strings. Read, assign, delete, iterate, and use len on it. Assigning an existing label replaces that tier in place. Edits validate all dependent tiers together; for example, %gra requires compatible %mor content. An invalid local edit raises before changing the utterance. Editing text can fail if existing dependent tiers no longer align.

chat.utterances supports iteration, integer indexing, read slices, append, insert, integer replacement and deletion, pop, and clear. Negative indices follow Python conventions. Slices return lists; slice assignment is not supported.

The collection stores live utterance objects. Appending an object and then editing that object updates the document. Inserting or removing other utterances does not invalidate existing handles. A removed utterance remains usable but no longer contributes to that document. Appending the same object to multiple documents shares its edits. Iteration takes a snapshot of the collection’s references, while the referenced utterances remain live.

Derived participant metadata

print(chat.speakers)                  # tuple of Speaker values
print(chat.languages)                 # tuple of distinct language codes
print(chat.corpora)                   # tuple of distinct corpus names
print(chat.utterances[0].speaker.languages)

These document properties are derived from referenced speakers in first-use order. Languages also include resolved utterance and word-level language choices. If regenerating the language order changes the file default, serialization adds an explicit language precode where needed to preserve an utterance’s meaning. These properties and their contents are read-only: assignment or mutation raises. @Participants, @ID, and @Languages are generated from those speakers. Silent participants are omitted from this Python API and its serialized output. Two utterances using the same speaker code must agree on participant metadata. Collection insertion and replacement reject conflicting speaker definitions.

Media and validation

Media(filename, *, media_type="audio", status=None) represents @Media. filename is a CHAT media basename without an extension. media_type accepts audio, video, or missing; status accepts None, missing, unlinked, or notrans. Media values are immutable.

chat.media = Media("recording", media_type="audio", status="unlinked")
print(chat.media.filename)
chat.media = None  # remove @Media

There is no generic header type or editable header mapping. Participant headers are derived; media has its own typed property. Other parsed headers are retained.

Wrong input types raise TypeError. Invalid CHAT or metadata values raise ValueError with model diagnostics; negative unsigned timestamps raise OverflowError. Missing tier labels raise KeyError, and out-of-range utterance indices raise IndexError.

Constructors and utterance edits validate local content and tier alignment. stringify, str, and write also validate document-wide constraints, including speaker consistency and media linkage. During editing, setting a media declaration before adding timed utterances is allowed; serializing the resulting document still requires valid CHAT. Timed transcripts require media; untimed transcripts declaring media should use an appropriate status such as unlinked.

This API is separate from pipeline inputs. To run a file through a recipe, use the input constructors described in the Python API reference.

Command inputs and outputs

This table describes the current local CLI. -o/--out selects a destination folder; relative subdirectories are mirrored from the selected inputs’ common root. Discovery accepts individual paths, recursive directories, and file lists. See Input selection.

CommandInputsOutputWithout -o
transcribeMedia; required --langCHATWrite .cha beside the recording
alignCHAT with resolvable mediaCHAT with word timingsUpdate source transcript
morphotagCHATCHAT with %mor/%graUpdate source transcript
utsegCHATCHAT with revised utterance boundariesUpdate source transcript
translateCHATCHAT with %eng translation tiersUpdate source transcript
aiCHAT and an instructionEdited CHATUpdate source transcript
compareCHAT with a sibling gold referenceComparison metricsWrite metrics beside inputs
diarizeTimed CHAT with resolvable mediaCHAT with revised speaker assignmentsUpdate source transcript
convertMedia and required --format wav or mp3Encoded mediaWrite beside input; same-format conversion uses .converted in the name

morphotag strips existing %mor/%gra from staged inputs by default; --keep-existing preserves them for the engine’s already-tagged check. convert rejects pre-existing targets and collisions between selected inputs.

The CLI’s per-command collectors and write_outcome in python/batchalign/cli/_common.py own these output decisions. Python API callers receive Outcome objects and choose when and where to write them.

Filesystem locations

DataLocation or resolution
Provider configuration~/.batchalign.ini (python/batchalign/config.py)
Default result cacheOS cache directory plus batchalign/cache.lmdb/; print with batchalign cache path
Input-list entriesRelative to the list file’s directory unless absolute
Positional input pathsRelative to the invocation directory unless absolute
CLI output-o/--out, or command-specific output beside the input
Downloaded modelsThe selected model library’s own cache/resource directory

There is no single Batchalign directory that owns all downloaded models. Stanza, Hugging Face, and other backends use their own loaders. The old BATCHALIGN_ANALYSIS_CACHE_DIR, BATCHALIGN_MEDIA_CACHE_DIR, and Rust server.yaml examples do not configure the current result cache.

Python callers select a result-cache path with CacheSpec(path=...). See Caching and Command I/O.

Processing provenance comments

Successful CHAT pipeline outputs contain a comment for each task in the pipeline’s task order. The current shape is:

@Comment: batchalign <compiled-git-sha> | <task>: <backend-name> | <UTC timestamp>

The placeholders above describe fields, not a literal header to add yourself. The backend name can encode model/runtime versions and settings. A missing backend name is recorded as <unknown>.

When a matching batchalign comment for the same task is present, stamping replaces it in place. Legacy batchalign3 stamps for the same task are also replaced with the current batchalign name. Comments for other tasks are preserved. Failed pipeline outcomes are not stamped as successful output for the whole task sequence. This records the pipeline configuration, not proof that every backend request ran freshly: cached results may have been reused.

ASR-generated transcripts also include @Comment: Unchecked output of ASR model. This is separate from build/task provenance and signals that the transcript needs review.

Implementation: engine pipeline.rs::stamp_run_provenance and core utils.rs::stamp_provenance. The earlier bracketed [ba3 command | ...] examples are not the format emitted by this implementation.

CHAT handling by the pipeline

The Rust core uses TalkBank’s CHAT model and parser. File input conversion enters through Chat::parse; task runners operate on the resulting typed state, and outcome writing serializes it. Python CLI code owns path selection and may stage input text before the Rust parse.

For morphology, staging removes existing %mor/%gra by default, while --keep-existing retains them. Translation emits %eng. Transcription creates a new transcript from media. See Command I/O for the complete input/output table and Morphosyntax for tier alignment checks.

For CHAT syntax, use the shared CHAT format reference. The old Rust-server parse-mode table does not describe this pipeline entrypoint.

CHAT options that affect processing

Header optionCurrent runner behavior
@Options: CAMorphosyntax skips Conversation Analysis transcripts
@Options: NoAlignForced alignment skips the transcript

These are task-specific checks, not a universal skip directive for every command. The CLI’s default morphology staging also removes the obsolete exact header @Options:\tmulti along with old %mor/%gra tiers.

Source: core taskrunners/morphosyntax.rs, taskrunners/fa.rs, and python/batchalign/cli/morphotag.py.

Language selection

Language selection is command-specific. A valid language identifier does not by itself establish that a selected model supports that language.

Entry pointLanguage source
transcribeRequired --lang, validated as ISO 639-3
morphotagCHAT @Languages: and applicable utterance-level language markers
alignLanguage inferred from selected CHAT input for backend construction
utsegBackend --language option, default en
translate--target selects output language, default eng

For transcription, examples include eng (English), spa (Spanish), cmn (Mandarin), and yue (Cantonese). python/batchalign/lang.py::LanguageCode normalizes whitespace and case and resolves three-letter identifiers using pycountry; backends choose the vendor-specific form they require.

Morphosyntax language routing

The Rust runner derives a language specification from CHAT. The Stanza backend resolves that specification to model language codes, groups requests by language configuration, and constructs a single-language or multilingual pipeline as needed. Its process-wide LRU retains at most two language/mode configurations. The CLI groups files by language headers to reduce model switching.

The current CLI does not expose the former --skipmultilang, --no-l2-morphotag, or --no-pos-hints switches. Older language experiments that use those switches must not be used as current operational instructions or accuracy guarantees.

For the implemented token and language workarounds, inspect python/batchalign/backends/morphosyntax/ud/lang.py, its language submodules, and the core morphology runner. For command syntax, see CLI reference.

Language reference index

Choose a language below for current routing notes and links to earlier studies. Model availability and language-code recognition are separate from analysis quality.

For the shared rules, see Language selection.

Coreference API

The Python API defines a Coref backend interface and a ba.recipes.coref(coref_backend=...) recipe. This build does not bundle a concrete coreference backend. The stub under cli/hidden/ is not registered as a public command.

An integration must supply its own implementation of the interface in python/batchalign/backends/base.py. See Add an inference backend and Python API.

Morphosyntax processing contract

The morphotag pipeline adds typed %mor and %gra tiers. Its implementation spans the Rust runner and Python Stanza backend; linguistic rendering is not entirely Rust-owned.

Input and language

The Rust runner reads CHAT, skips Conversation Analysis files marked @Options: CA, and collects utterances that need analysis. Language comes from @Languages: with utterance-level overrides such as [- hin] where applicable. The CLI has no morphotag --lang option.

By default, the CLI stages copies with existing %mor and %gra removed before parsing. --keep-existing leaves them in place for the runner’s already-tagged check. Clearing tiers does not bypass the backend result cache.

Backend processing

python/batchalign/backends/morphosyntax/stanza.py groups batch requests by language configuration. It prepares text, runs Stanza, and uses the Python ud/tokenize.py and ud/render.py modules for token handling and structured morphological/dependency output. The ud/ language modules contain additional language-specific behavior.

Preserve mode installs a tokenizer postprocessor to reconcile Stanza tokenization with the input. --retokenize selects the other mode and permits changes to word boundaries. The backend’s identity includes the mode and its rendering contract. Do not treat a model’s raw token count as the CHAT main-tier word count.

A failed batched model call can fall back to individual sentence processing. Pipeline initialization failures propagate as backend failures; an unsupported or broken model is not treated as a successful empty analysis for the whole file.

Applying results

The Rust runner converts the structured backend output into TalkBank model objects, aligns morphology and dependency tiers with try_align_mor_gra, and checks the candidate against the main tier before committing it. An utterance whose analysis cannot be aligned can be skipped with a warning; a completed file is not a guarantee that every utterance received new analysis.

%gra unit indices, heads, and relations originate in the backend output. The runner constructs typed relations and the CHAT writer serializes them. Avoid attributing current dependency-tree behavior to the retired server’s build_gra_and_validate path.

Scheduling and reuse

The per-file dispatch window is 128 utterances. Stanza’s default engine batch maximum is 128 with a 100 ms collection window. Result caching is enabled. See Performance and Caching.

Source locations

ConcernSource
CLI staging and optionspython/batchalign/cli/morphotag.py
Extraction, dispatch, typed injectioncrates/batchalign/batchalign-core/src/taskrunners/morphosyntax.rs
Protocol and cache-key fieldsCore proto/morphosyntax.rs
Stanza configuration and batchingpython/batchalign/backends/morphosyntax/stanza.py
Token reconciliation and renderingPython backend ud/tokenize.py, ud/render.py, language submodules

For a processing walkthrough, see Add morphology and grammar.

Dependency tiers (%gra)

A %gra item has the form index|head|relation. It describes the grammatical relation of one morphology unit to its head. Index 0 in the head position denotes the root convention used by the current backend output.

The Stanza renderer supplies structured indices, heads, and relation labels. The Rust morphology runner constructs typed grammatical relations and aligns %mor and %gra using TalkBank model alignment helpers before committing tiers. A rejected candidate can leave an utterance without new analysis and emit a warning.

The number of dependency units is not necessarily the number of whitespace- separated main-tier tokens: morphology can include clitic components. Consult the morphology contract and the shared dependent-tier reference.

Source: python/batchalign/backends/morphosyntax/ud/render.py and crates/batchalign/batchalign-core/src/taskrunners/morphosyntax.rs.

Forced-alignment pipeline

The alignment recipe composes optional utterance timing recovery (Utr) and forced alignment (Fa). The CLI supplies both backends by default, choosing wav2vec for FA and rev for recovery. Setting --utr-engine none omits recovery.

Timing recovery uses an ASR-capable backend to establish missing utterance timing. The UTR runner can skip work when the input is already timed. FA uses transcript content and prepared audio to request word alignments and applies timing to CHAT. Results travel through the same bounded backend queues and LMDB cache as other tasks. It does not rely on a remote worker process or a separate SQLite audio cache.

Ownership

ConcernSource
Engine/model options and language inferencepython/batchalign/cli/align.py
Task orderpython/batchalign/recipes.py::align
Timing recovery and overlap strategiescrates/batchalign/batchalign-core/src/taskrunners/utr/
FA preparation and result applicationCore taskrunners/fa.rs
Typed backend requests/resultsCore proto/utr.rs, proto/fa.rs
Local model implementationspython/batchalign/backends/fa/ and ASR backends supporting UTR

@Options: NoAlign causes the FA runner to skip alignment. For an operational walkthrough, see Align a transcript. For scheduling and RSS, see Performance.

Media conversion

batchalign convert accepts media paths and requires --format wav or mp3. The default recipe selects the native Rust conversion backend and bypasses the shared result cache. The output is an encoded media file, not a cached temporary artifact managed by an old server media-cache directory.

batchalign convert recordings/ --format wav -o wav-recordings/

The CLI rejects existing output targets, source-overwriting targets, and two inputs that would map to the same output. If a same-format conversion is written beside the input, the name includes .converted to keep it distinct.

Input discovery recognizes common audio/video suffixes, but recognition alone does not establish that every codec/container can be decoded. Consult the conversion backend and test a representative file.

Source: python/batchalign/cli/convert.py, recipes.py::convert, crates/batchalign/batchalign-core/src/taskrunners/convert.rs, and the core/engine native conversion backend modules.

TextGrid export

TextGrid is Praat’s annotation format. The current Batchalign CLI does not expose a TextGrid export command or the former batchalign_core Python export helper.

For CHAT-to-Praat conversion in the wider toolchain, see CHAT2PRAAT. Check that the input transcript has the timing required by the converter. Batchalign’s alignment workflow is described in Align a transcript.

English ASR text corrections

The Rust ASR cleanup code applies English-specific orthographic rules when the language is eng:

RuleExample
Capitalize the pronoun and supported contractionsi → I, i'm → I'm
Strip periods from an allowlist of abbreviationsDr. → Dr
Capitalize the first eligible word of an utterancehello world . → Hello world .

These modify spelling directly; they do not add a CHAT replacement annotation. The abbreviation rule runs before token splitting so a title’s period is not mistaken for an utterance boundary. It is an allowlist, not a general instruction to strip periods from arbitrary words or numbers.

The authoritative surfaces and exclusions are in crates/batchalign/batchalign-core/src/asr/cleanup.rs, including EN_I_CAP_REWRITES, EN_TITLE_PERIOD_SURFACES, and the pre/post-retokenization English correction functions. Their invocation order is in asr/prepare.rs and the ASR assembly path.

Automatic retrace detection

The ASR cleanup pass can mark earlier copies of an immediately repeated word sequence as retraces. Matching is case-insensitive; the word spelling is retained. It is a local repetition heuristic, not a detector of every conversational repair.

Fillers participate in sequence matching but are not themselves changed into retrace words. Repeating a filler alone therefore does not create a retrace mark. The minimum sequence length is two for the yue and zho language keys and one for other keys in the current implementation.

The pass sets typed word kinds. The CHAT builder uses those kinds to construct retrace annotations rather than embedding bracket syntax in word text. Review these automatic decisions against the recording.

Source: crates/batchalign/batchalign-core/src/asr/cleanup.rs::apply_retrace_detection. For CHAT notation, see Retraces and repetitions.

Wheel platform reference

The current Python wheel workflows build these native platforms:

PlatformCI runner
macOS ARM64macos-14
macOS x86-64macos-15-intel
Linux x86-64ubuntu-latest
Linux ARM64ubuntu-24.04-arm
Windows x86-64windows-latest

The extension wheel uses cp310-abi3. Linux builds target manylinux 2.28 through the wheel builder. The wheel smoke matrix installs and imports on Python 3.10. Optional model libraries can have narrower platform support than the core wheel.

The bootstrap scripts select Python 3.11 and install batchalign[all]. Desktop bundles are separate artifacts. See Install Batchalign and Package and release surfaces.

Source: .github/workflows/publish-pypi.yml, bazel-wheels.yml, and python/pyproject.toml.

Batchalign source map

LocationResponsibility
crates/batchalign/batchalign-core/Typed tasks, task runners, CHAT processing, ASR cleanup, protocol schemas
crates/batchalign/batchalign-engine/PyO3 runtime, pipelines, backend routing, batching, LMDB, outcomes
python/batchalign/cli/Typer CLI, input selection, terminal UI, output handling
python/batchalign/backends/Model and service adapters, including Stanza token processing and rendering
python/batchalign/recipes.pyStandard task compositions
python/batchalign/api.pyFastAPI recipe endpoints and in-memory job lifecycle
apps/batchalign/batchalign-gui/React/Tauri desktop application
bazel/python/Wheel, PyApp, and protocol build integration
just/batchalign.justProduct build, test, and CLI recipes

The Python package imports the extension as batchalign._core. Typed protocol schemas come from the Rust core; Bazel generates the Python wire types. TalkBank parser, model, and transform crates supply the CHAT representation used by the core. Their Git revision and version constraints are declared in crates/batchalign/batchalign-core/Cargo.toml; the root manifest does not centralize dependencies in a [workspace.dependencies] table.

For execution flow, see How a processing command runs.

Python versions and wheel tags

python/pyproject.toml declares Python >=3.10. The extension uses CPython’s stable ABI, with release wheels tagged cp310-abi3 for each supported platform. The wheel CI smoke test installs and imports the package on Python 3.10.

The user bootstrap scripts select Python 3.11 for batchalign[all]. This is separate from the minimum version supported by the core wheel. Optional model libraries can impose additional interpreter or platform constraints.

Use the bootstrap-selected interpreter for the documented end-user installation. A stable-ABI tag is not a claim that every model extra works on every newer or free-threaded Python build. See python/pyproject.toml, bootstrap/, and the wheel workflows before changing interpreter support.

Package and release surfaces

SurfaceSource of truth
Python distribution name, version, dependencies, scriptspython/pyproject.toml (batchalign)
Rust workspace versionRoot Cargo.toml
Rust dependencies and featuresEach crate’s Cargo.toml
Native wheel matrix.github/workflows/publish-pypi.yml
Installed wheel smoke checks.github/workflows/bazel-wheels.yml
Desktop bundleapps/batchalign/batchalign-gui/ and its Bazel targets
HTTP servicepython/batchalign/api.py, cli/daemon.py

The Python distribution provides the CLI, Python API, and compiled engine. A desktop bundle and a Python wheel are separate artifacts. Their versions and release procedures should not be inferred from obsolete server-era version tables.

For the release procedure, see Release the Python package.

Historical studies and decisions

These records describe earlier implementations, model probes, or proposals. They are useful for understanding why earlier choices were made, but are not current command instructions, support guarantees, or measurements of version 0.10.1. The links identify a fixed repository revision so the evidence does not drift.

For current behavior, use CLI reference, Morphosyntax, Language selection, and Pipeline architecture.

RecordScope
L2 Morphotag: Per-Word Code-Switching AnalysisPer-word code-switching design and its earlier evaluation
L2 Morphotag: Current StatusAn earlier implementation-status and accuracy report
Literature Review: Code-Switching Morphosyntactic AnalysisLiterature considered for the earlier code-switching design
Rev.AI Language Quality StrategyA proposed Rev.AI quality-assessment strategy
Stanza Limitations — Observed Defects with Version PinningVersion-specific model observations and former mitigation claims
Algorithms, Language, and Alignment MigrationA comparison of earlier BA2 and Rust-server algorithms
BA2 Architecture ReferenceThe BA2 implementation baseline
Batchalign2 CLI Reference (Baseline)The BA2 command-line baseline
Why Replace Python CHAT Handling with RustThe rationale for an earlier ownership migration
Arena Allocators (Evaluation — Not Used)An earlier allocator evaluation
Models Training Runtime ADRAn earlier model-training/runtime decision
Lenient ParsingAn earlier parser-policy decision
Trait-Based DispatchAn earlier algorithm-dispatch decision
Release Readiness State MachineAn earlier release-state proposal

Batchalign documentation audit

Audit baseline: fbaee727d9649f8d4bf1592433fb71f7acce7b12 (0.10.1 release source). Scope: every existing Markdown page under book/src/batchalign/, plus the shared runtime pages reached from Batchalign navigation. Supporting Chatter/CLAN/editor navigation was classified by Diátaxis; those products’ implementations were not re-audited as part of the Batchalign runtime cleanup.

Current CLI facts were checked against the Typer registry and command modules; pipeline, queue, and cache claims against the Rust engine; model/rendering ownership against Python backends; build and release instructions against the just recipes and CI workflows. Command-reference options were extracted from source and checked against the authoritative CLI help. Historical model results were not rerun or promoted to current accuracy guarantees.

The quick start retains its original command-window walkthrough and task order; changes there remove obsolete commands/options and correct cache/model claims. Consolidated pages retain their URLs as pointers. Historical research remains available through fixed-revision links.

PageDispositionCurrent destination or evidence
architecture/algorithm-visualizations.mdConsolidatedCurrent page; The former dashboard visualization endpoints are not part of the current service.
architecture/align-throughput.mdConsolidatedCurrent page; Worker pre-scaling and server admission heuristics are not the current CLI controls.
architecture/asr-token-pipeline.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
architecture/cache-override-guide.mdConsolidatedCurrent page; The current CLI does not expose –override-media-cache. Python callers choose a CacheSpec policy.
architecture/chat-parsing.mdConsolidatedCurrent page; The former server/PyO3 ownership map did not match the in-process engine.
architecture/command-contracts.mdConsolidatedCurrent page; The former validity-level table referred to the retired command orchestrators; current preconditions belong to each typed runner.
architecture/command-flowcharts.mdConsolidatedCurrent page; The former flag-routing charts described commands and server dispatch paths that are no longer registered.
architecture/command-lifecycles.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
architecture/incremental-processing.mdConsolidatedCurrent page; Current reuse comes from typed backend result keys; the old server diff/orchestrator description is not a current CLI contract.
architecture/job-state-machine.mdConsolidatedCurrent page; The former persistent Rust job state machine does not describe the FastAPI in-memory registry.
architecture/morphotag-invariants.mdConsolidatedCurrent page; The former MorOutcome and worker reconciliation description does not identify the current typed injection path.
architecture/number-expansion.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
architecture/observability.mdConsolidatedCurrent page; The old SQLite/Temporal job observability model is not implemented by the current CLI.
architecture/overlap-encoding.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
architecture/path-provenance.mdConsolidatedCurrent page; The old client/server path newtypes and remote media mappings are not the local CLI path contract.
architecture/pipeline-decisions.mdConsolidatedCurrent page; Existing decision types alone do not establish that all current runners emit the former review tiers and CLI controls.
architecture/preprocessing-postprocessing.mdConsolidatedCurrent page; The former claim that all linguistic processing lived in Rust was incorrect: Stanza token processing and rendering also live in Python.
architecture/progress-reporting.mdConsolidatedCurrent page; Current CLI progress uses Rust events and Python callbacks rather than the former server FileStatus stream.
architecture/provenance.mdConsolidatedCurrent page; The former bracketed ba3 format and server-owned injection sites do not match current stamping.
architecture/replacements-handling.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
architecture/server-architecture.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
architecture/server-model-loading.mdConsolidatedCurrent page; The former worker profiles and warmup configuration do not configure the current backends.
architecture/stanza-capability-registry.mdConsolidatedCurrent page; The current Stanza backend resolves language configurations directly; the former worker capability exchange is obsolete.
architecture/stanza-defect-mitigation-map.mdConsolidatedCurrent page; The former patch-point table cited deleted worker/inference modules; current tokenizer and renderer ownership is documented here.
architecture/test-server-lifecycle.mdConsolidatedCurrent page; Current product tests do not use the former shared Rust server fixture lifecycle.
architecture/time-transparency.mdConsolidatedCurrent page; The UX goal of visible progress remains useful, but the worker progress_v2 mechanism in the former page is obsolete.
architecture/type-driven-design.mdConsolidatedCurrent page; The former examples mixed current core types with removed server and worker-protocol types.
architecture/worker-failure-classification.mdConsolidatedCurrent page; The retired worker JSON protocol and server retry classifier do not describe in-process backend failures.
decisions/lenient-parsing.mdHistorical recordCurrent page; An earlier parser-policy decision; prior runtime/version evidence is not a current guarantee.
decisions/models-training-runtime-adr.mdHistorical recordCurrent page; An earlier model-training/runtime decision; prior runtime/version evidence is not a current guarantee.
decisions/release-state-machine.mdHistorical recordCurrent page; An earlier release-state proposal; prior runtime/version evidence is not a current guarantee.
decisions/trait-based-dispatch.mdHistorical recordCurrent page; An earlier algorithm-dispatch decision; prior runtime/version evidence is not a current guarantee.
developer/adding-commands.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/adding-engines.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/adding-language-support.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/api-stability.mdConsolidatedCurrent page; The previous assertion that no Python API exists was false. The package exposes pipelines, recipes, inputs, and backend interfaces.
developer/apple-mps-workarounds.mdConsolidatedCurrent page; The blanket MPS prohibition is obsolete: applicable commands now have an explicit –allow-mps option.
developer/arena-allocators.mdHistorical recordCurrent page; An earlier allocator evaluation; prior runtime/version evidence is not a current guarantee.
developer/backchannel-aware-alignment.mdConsolidatedCurrent page; The old –utr-strategy/–utr-fuzzy CLI controls are not registered; the UTR implementation has its own current strategy selection.
developer/building.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/chat-validation-failures.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/align.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/benchmark.mdConsolidatedCurrent page; The former benchmark orchestrator is not a current command entrypoint.
developer/commands/compare.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/coref.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/morphotag.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/transcribe.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/translate.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/commands/utseg.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/debugging-infrastructure.mdConsolidatedCurrent page; The current CLI does not promise the former always-on server failure dumps.
developer/decision-provenance.mdConsolidatedCurrent page; The former blanket review-tier emission contract is not exposed by the current CLI.
developer/gra-correctness-guarantee.mdConsolidatedCurrent page; The former guarantee cited a different generator; the current renderer and typed alignment checks are documented here.
developer/host-facts.mdConsolidatedCurrent page; The former host-facts/bootstrap coordinator is not part of this CLI execution path.
developer/http-body-limits.mdConsolidatedCurrent page; The former Rust route-body configuration is not the current FastAPI service contract; inspect its OpenAPI schema and route implementation.
developer/investigation-probe-harnesses.mdConsolidatedCurrent page; The former probe runner commands and server setup are not current test entrypoints.
developer/ipc-type-sync.mdConsolidatedCurrent page; Protocol schemas are generated from the core proto types through Bazel, not the removed batchalign-types worker_v2 tree.
developer/landing-status.mdConsolidatedCurrent page; The former migration landing checklist is no longer a source map for the current implementation.
developer/maturin-pyo3-surface.mdConsolidatedCurrent page; The old extension-module name, crate layout, and editable-install instructions were obsolete.
developer/memory-safety.mdConsolidatedCurrent page; The current engine bounds queues and active files; it does not enforce the former host-wide worker-startup memory leases.
developer/model-downloads-and-caching.mdConsolidatedCurrent page; The worker-loader inventory and audio-only SQLite cache claims were obsolete.
developer/non-english-workarounds.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/plugins.mdConsolidatedCurrent page; The current extension point is the backend interface and recipe composition, not the former worker capability registry.
developer/python-versioning.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/regression-fixtures.mdConsolidatedCurrent page; The former harness paths have changed; use the current product tests and representative CLI fixtures.
developer/release-checklist.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/release-contract.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/reliability-program.mdConsolidatedCurrent page; The former scorecard mixed proposed fleet procedures with claims about shipped tests.
developer/rust-cli-and-server.mdConsolidatedCurrent page; The current CLI and HTTP service are Python entrypoints around the Rust pipeline.
developer/rust-contributor-onboarding.mdConsolidatedCurrent page; Current contributor commands go through the repository just recipes and Bazel targets.
developer/rust-core.mdConsolidatedCurrent page; The extension is batchalign._core and its runtime lives in batchalign-engine, not the retired batchalign-pyo3 layout.
developer/rust-workspace-map.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/tauri-react-dashboard.mdConsolidatedCurrent page; The old cli-web-statuspage/dashboard-desktop paths and Rust server setup have been replaced.
developer/terminator-architecture.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
developer/testing-turmoil.mdConsolidatedCurrent page; The former network-simulation harness is not a current Batchalign test recipe.
developer/testing.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/tracing-and-debugging.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/upstream-defect-policy.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
developer/worker-protocol-v2.mdConsolidatedCurrent page; Backend calls cross the in-process typed engine boundary; the old worker_v2 transport is not the current interface.
developer/workflow-contributor-guide.mdConsolidatedCurrent page; ReleasedCommand and Rust command-family registration no longer own the CLI surface.
introduction.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
migration/algorithms-and-language.mdHistorical recordCurrent page; A comparison of earlier BA2 and Rust-server algorithms; prior runtime/version evidence is not a current guarantee.
migration/ba2-architecture-reference.mdHistorical recordCurrent page; The BA2 implementation baseline; prior runtime/version evidence is not a current guarantee.
migration/ba2-cli-reference.mdHistorical recordCurrent page; The BA2 command-line baseline; prior runtime/version evidence is not a current guarantee.
migration/ba2-compare-migration.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
migration/debugging-and-tracing.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
migration/developer-migration.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
migration/index.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
migration/persistent-state.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
migration/python-to-rust-rationale.mdHistorical recordCurrent page; The rationale for an earlier ownership migration; prior runtime/version evidence is not a current guarantee.
migration/user-migration.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/benchmarks.mdConsolidatedCurrent page; The former benchmark command is not registered in the current CLI.
reference/chat-format.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/chat-options.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/chinese-word-segmentation.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/command-io.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/cross-repo-dependency.mdConsolidatedCurrent page; Use the current workspace dependency declarations rather than the former sibling-checkout contract.
reference/english-transcribe-corrections.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/filesystem-paths.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/forced-alignment.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/gra-format.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/hebrew-morphosyntax.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/japanese-morphosyntax.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/l2-handling.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/l2-morphotag-literature.mdHistorical recordCurrent page; Literature considered for the earlier code-switching design; prior runtime/version evidence is not a current guarantee.
reference/l2-morphotag-status.mdHistorical recordCurrent page; An earlier implementation-status and accuracy report; prior runtime/version evidence is not a current guarantee.
reference/l2-morphotag.mdHistorical recordCurrent page; Per-word code-switching design and its earlier evaluation; prior runtime/version evidence is not a current guarantee.
reference/language-code-resolution.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/language-handling.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/language-specific-processing.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/languages/cantonese.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/dutch.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/french.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/hebrew.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/italian.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/japanese.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/malayalam.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/mandarin.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/overview.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/languages/portuguese.mdRevisedCurrent routing/source notes; older model probes linked at a fixed revision without revalidation claims.
reference/media-conversion.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/morphosyntax.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/morphotag-retokenization.mdConsolidatedCurrent page; The old –keeptokens flag and worker-side module paths are obsolete; current modes use –retokenize/–no-retokenize.
reference/multilingual.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/mwt-handling.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/nlp-engine-text-input.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/number-expansion.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/overlap-markers.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/platform-support.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/pos-hints.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/retokenization-overview.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/retrace-detection.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/revai-language-quality-strategy.mdHistorical recordCurrent page; A proposed Rev.AI quality-assessment strategy; prior runtime/version evidence is not a current guarantee.
reference/stanza-limitations.mdHistorical recordCurrent page; Version-specific model observations and former mitigation claims; prior runtime/version evidence is not a current guarantee.
reference/textgrid.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
reference/utterance-segmentation.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/whisper-asr.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
reference/wor-tier.mdConsolidatedCurrent page; The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.
user-guide/caching.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/cantonese-processing.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/cli-reference.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/align.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/benchmark.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/compare.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/coref.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/eval.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/morphotag.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/transcribe.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/translate.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/commands/utseg.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/dashboard.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/desktop-app.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/doctor.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/installation.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/model-downloads.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/performance.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/progress-and-feedback.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/provenance.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/python-api.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/quick-start.mdRevisedPreserved tutorial structure; corrected obsolete syntax and cache/model claims.
user-guide/rev-ai.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/review-tiers-guide.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/server-mode.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/server-setup.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/troubleshooting.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.
user-guide/worker-tuning.mdRevisedRewritten against current owning CLI, core/engine, backend, or build sources; obsolete claims removed.

Cantonese processing reference

The transcription language code is yue. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

The current Stanza language mapping uses Traditional Chinese. This does not establish Cantonese model accuracy. See Process Cantonese for command examples.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Cantonese study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Mandarin processing reference

The transcription language code is cmn. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

Use a Mandarin recording language code for transcription. Stanza mapping goes through ud/lang.py; the backend normalizes resolved zh to zh-hans. Do not assume every ISO alias has an alpha-2 mapping.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Mandarin study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Japanese processing reference

The transcription language code is jpn. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

The current Stanza configuration selects the combined package for Japanese. Japanese rendering helpers are in ud/ja/. Check word boundaries in both preserve and retokenize modes.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Japanese study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Hebrew processing reference

The transcription language code is heb. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

Review the rendered morphology and right-to-left text in an editor that supports the script. A model name from an older page is not an automatic current ASR selection.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Hebrew study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

French processing reference

The transcription language code is fra. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

French-specific rendering helpers are in python/batchalign/backends/morphosyntax/ud/fr/. Include contractions and clitic elisions in a review sample.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier French study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Italian processing reference

The transcription language code is ita. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

Italian handling spans ud/it/, the shared tokenizer, and renderer. The backend identity currently includes its native-MWT rendering contract; old Rust reconciler tables are not the current source map.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Italian study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Portuguese processing reference

The transcription language code is por. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

Check preposition/article contractions and apostrophe forms in the rendered morphology.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Portuguese study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Dutch processing reference

The transcription language code is nld. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

Contractions and apostrophes should be checked against the shared tokenizer and renderer.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Dutch study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Malayalam processing reference

The transcription language code is mal. For morphology, use the appropriate CHAT @Languages: header; there is no morphotag --lang option.

The CLI includes a malayalam ASR engine backed by Wav2Vec2 and a Malayalam segmentation implementation. That does not imply a usable Stanza morphology model. Verify the backend required by the task.

Language resolution and processor selection are defined in python/batchalign/backends/morphosyntax/ud/lang.py and stanza.py. Backend availability and linguistic quality must be checked for the actual model and input; a recognized language code alone is not a support guarantee.

See Language selection, Morphosyntax, and CLI reference.

The earlier Malayalam study contains model probes and implementation notes from a previous architecture. Its measurements and mitigation claims have not been revalidated by this documentation update.

Chatter: reference

Look up syntax, contracts, and factual details.

CLI Reference

Status: Current Last updated: 2026-05-11 15:39 EDT

The chatter CLI is the public command-line surface for talkbank-tools.

The following diagram shows the command dispatch structure. Each top-level command dispatches to a handler in the corresponding crate.

flowchart TD
    chatter(["chatter"])

    chatter --> validate["validate\n(chatter-cli)"]
    chatter --> normalize["normalize\n(chatter-cli)"]
    chatter --> tojson["to-json\n(talkbank-transform)"]
    chatter --> fromjson["from-json\n(talkbank-transform)"]
    chatter --> validateutseg["validate-utseg\n(chatter-cli)"]
    chatter --> showalign["show-alignment\n(chatter-cli)"]
    chatter --> watch["watch\n(chatter-cli)"]
    chatter --> lint["lint\n(chatter-cli)"]
    chatter --> clean["clean\n(chatter-cli)"]
    chatter --> newfile["new-file\n(chatter-cli)"]
    chatter --> cache["cache\n(stats, clear)"]
    chatter --> schema["schema\n(JSON Schema output)"]
    chatter --> clan["clan\n(clan-core)"]
    chatter --> debug["debug\n(overlap-audit, linker-audit,\nfind, sanitize, fix-s)"]

    clan --> freq["freq"]
    clan --> mlu["mlu"]
    clan --> ttr["ttr"]
    clan --> more["...all CLAN commands\n(see clan-reference)"]

Top-Level Commands

chatter validate PATH...
chatter normalize INPUT
chatter to-json INPUT
chatter from-json INPUT
chatter to-xml INPUT
chatter validate-utseg PRE_DIR POST_DIR
chatter show-alignment INPUT
chatter watch PATH
chatter lint PATH
chatter clean PATH
chatter new-file
chatter cache stats
chatter cache clear --prefix PATH
chatter schema
chatter clan ...
chatter debug ...

Use chatter --help or chatter <command> --help for the exact live surface.

validate

Validate CHAT file(s) or directory tree(s). Accepts multiple paths.

Usage: chatter validate [OPTIONS] <PATH>...
chatter validate file.cha                         # single file
chatter validate file1.cha file2.cha file3.cha    # multiple files
chatter validate corpus/                          # directory (recursive, parallel)
chatter validate file.cha corpus/ other.cha       # mix of files and directories
chatter validate corpus/ -f json                  # structured JSON output
chatter validate corpus/ --force                  # ignore cache, revalidate everything
chatter validate corpus/ --force --audit out.jsonl # bulk audit to JSONL file
chatter validate corpus/ --suppress xphon         # suppress named error group
chatter validate corpus/ --suppress E726,E727     # suppress specific error codes
chatter validate corpus/ -j 8                     # use 8 parallel workers
chatter validate corpus/ --max-errors 50          # stop after 50 errors

Options:

FlagDescription
-f, --format text|jsonOutput format (default: text)
--list-checksPrint every validation check with Active/Planned status, then exit (no <PATH> required)
--skip-alignmentSkip dependent-tier alignment checks
--forceIgnore cache, revalidate all files
-j, --jobs NParallel workers for directory mode (default: CPU count)
--quietOnly emit errors, suppress success messages
--max-errors NStop after N errors across all files
--roundtripTest serialization idempotency (developer tool)
--parser tree-sitter|re2cParser backend (default: tree-sitter; re2c is opt-in for faster batch validation)
--strict-linkersEnable strict cross-utterance linker pairing checks (E351–E355); off by default
--check-xphonRe-enable %xphon* cross-tier alignment checks (E725–E728); skipped by default
--audit FILEStream errors to JSONL file (bulk audit mode)
--suppress CODESSuppress error codes or groups (comma-separated)

Suppress groups: xphon expands to E725/E726/E727/E728 (%xphosyl/%xphoaln/%xmodsyl cross-tier alignment). These are suppressed by default since 2026-04-21; pass --check-xphon to include them. The --suppress flag can mix groups and codes: --suppress xphon,E316.

normalize

Serialize a CHAT file into canonical formatting.

chatter normalize input.cha
chatter normalize input.cha -o normalized.cha
chatter normalize input.cha --validate
chatter normalize input.cha --validate --skip-alignment

Flags:

  • -o, --output <PATH> — write to a file instead of stdout.
  • --validate — validate (including alignment by default) before writing the normalized output.
  • --skip-alignment — when paired with --validate, skip the dependent-tier alignment checks (still validates the rest).

normalize writes to stdout unless you pass -o/--output. There is no --in-place flag.

JSON Conversion

# Single file
chatter to-json input.cha                          # pretty-printed JSON to stdout
chatter to-json input.cha --compact                # minified JSON to stdout
chatter to-json input.cha -o output.json           # JSON to file

# Directory (recursive, preserves structure)
chatter to-json corpus/ --output-dir json/          # incremental by default (mtime check)
chatter to-json corpus/ --output-dir json/ --compact # minified output (saves disk)
chatter to-json corpus/ --output-dir json/ --force   # full rebuild
chatter to-json corpus/ --output-dir json/ --prune   # remove orphaned .json files
chatter to-json corpus/ --output-dir json/ --jobs 4  # parallel workers

# Reverse and schema
chatter from-json input.json -o output.cha
chatter schema
chatter schema --url

Single-file mode: to-json validates by default. Use --skip-validation, --skip-alignment, or --skip-schema-validation to bypass checks.

Directory mode: Walks recursively, converting each .cha to .json under --output-dir with the same relative path. Incremental by default: skips files whose JSON is already newer than the source. Use --force to rebuild all. Use --prune to remove .json files with no matching .cha (handles renames/deletions). Use --jobs N for parallel conversion (defaults to number of CPUs).

validate-utseg

Compare a pre-utseg corpus directory against the post-utseg directory and verify gated invariants — file count match, main-tier bullet preservation, and @Media linkage. Exits 0 on pass, 1 on regression. %wor drops on split utterances are reported informationally, not as failures.

chatter validate-utseg pre_corpus/ post_corpus/
chatter validate-utseg pre_corpus/ post_corpus/ --quiet

--quiet suppresses the per-file report and emits only the verdict line; the exit code still reflects pass/fail.

This is a CI-style check rather than a day-to-day workflow. See the align and utseg reference pages in the Batchalign section for the upstream pipeline that produces the post-utseg corpus.

Editing and Inspection Commands

show-alignment

Print the dependent-tier alignment for a CHAT file (debugging aid).

chatter show-alignment file.cha
chatter show-alignment file.cha -t mor          # one tier type
chatter show-alignment file.cha -t gra -c       # compact one-line-per-alignment output

Flags: -t/--tier <mor|gra|pho|sin> (omit to show all available tiers); -c/--compact (one line per alignment).

watch

Watch a CHAT file or directory and re-validate on every save.

chatter watch file.cha
chatter watch corpus/
chatter watch corpus/ --skip-alignment --clear

Flags: --skip-alignment (faster reruns); -c/--clear (clear the terminal between runs).

lint

Run lint checks and optionally auto-fix.

chatter lint corpus/
chatter lint corpus/ --fix
chatter lint corpus/ --fix --dry-run         # preview without modifying files
chatter lint corpus/ --skip-alignment

Flags: --fix (apply fixes); --dry-run (show what would change without writing); --skip-alignment.

clean

Show the cleaned text for each word (a debugging aid for the text-normalization pipeline).

chatter clean file.cha
chatter clean file.cha --diff-only       # only words where raw differs from cleaned
chatter clean file.cha --format json

Flags: --diff-only; --format text|json.

new-file

Create a new minimal valid CHAT file from defaults.

chatter new-file
chatter new-file -o starter.cha --speaker CHI --language eng
chatter new-file -o adult.cha -s MOT -l eng -r Mother

Flags: -o/--output <PATH> (stdout if omitted); -s/--speaker <CODE> (default CHI); -l/--language <ISO 639-3> (default eng); -r/--role <ROLE> (default Target_Child).

Cache Commands

chatter cache stats
chatter cache stats --json
chatter cache clear --prefix /path/to/corpus
chatter cache clear --all --dry-run

The validation cache lives under the platform cache directory and stores per-file validation results. validate --force refreshes cache state for the specified path.

clan

chatter clan is the single entry point for CLAN-style analysis and transform commands.

Examples:

chatter clan freq corpus/
chatter clan mlu corpus/ --per-file --format json
chatter clan phonfreq corpus/
chatter clan flo file.cha -o flo.cha

Many analysis commands share these options:

  • --speaker CODE / --exclude-speaker CODE
  • --gem LABEL / --exclude-gem LABEL
  • --include-word WORD / --exclude-word WORD
  • --range START-END
  • --per-file
  • --include-retracings
  • --format text|json|csv|clan

Legacy CLAN +flag syntax is accepted where documented by the command help, but chatter clan ... is the supported public interface going forward.

debug

Developer / debugging subcommands for CHAT analysis. Not intended for routine end-user workflows; surface and behavior may change between releases. Run chatter debug --help for the live list. Current subcommands include:

  • overlap-audit — analyze CA overlap markers (⌈⌉⌊⌋): pairing, temporal consistency, orphans.

  • linker-audit — audit linker / special-terminator usage across a corpus (cross-utterance pairing for +<, ++, +^, +", +,, +≋, +≈, plus +..., +/., +//., +"/. etc.).

  • find — filter CHAT files by @Languages and body content (token / substring counts) across a corpus tree; emits paths, JSONL, or CSV.

  • sanitize — strip contributor lexical content while preserving structure, for protected-corpus debugging. See the Sanitize user-guide page for the full workflow.

  • fix-s — normalize whole-utterance same-language @s runs into a [- lang] precode, clear the per-word @s markers (including those on fillers and nonwords), and append any missing explicit @s:LANG codes to @Languages. Trigger conditions and safety rules:

    • Every word-bearing item in the utterance — including fillers (&~, &-, &+), nonwords, and retraced material — must carry an explicit language marker AND every marker must resolve to the same target language. If a single filler such as &~dang3 lacks a marker, the utterance is left untouched (the predicate cannot prove it is monolingual).
    • Bare @s shortcuts on fillers must be cleared when the rewrite fires. A bare @s resolves relative to the surrounding tier language, so adding a [- LANG] precode without clearing the shortcut would flip the filler’s language to the precode target. fix-s clears the shortcut to keep the original meaning intact.
    • The pre-validation rule that catches the unrewritten pattern is E255 (whole-utterance same-language @s run); fix-s is the canonical repair. The companion warn-only E254 reports @s:LANG codes missing from @Languages; fix-s appends them.
    • True no-op on already-correct files: a file is rewritten only when a [- lang] conversion or @Languages repair can be proved necessary.

Exit Codes

CodeMeaning
0Success – all files valid, or command completed without errors
1Failure – validation errors found, parse errors, or command failed
2Usage error – invalid arguments or missing required options (from clap)

chatter validate exits with code 1 if any file has validation errors or parse errors. This makes it safe to use in scripts and CI pipelines:

chatter validate corpus/ --quiet --tui-mode disable || echo "Validation failed"

Use --quiet to suppress per-file success output while still relying on exit codes. Use --format json for machine-readable structured output (JSON objects go to stdout; exit code still reflects pass/fail).

Output Contracts

  • Text output is intended for humans.
  • JSON output is intended for automation and downstream tools.
  • Error codes and the JSON Schema are documented public contracts; see the Integrating section of this book.

Validation Errors

Status: Current Last updated: 2026-05-11 17:40 EDT

The CHAT validator produces diagnostics at two severity levels: errors (must fix) and warnings (should fix). Each diagnostic has an error code that links to detailed documentation.

Reading Error Output

The validator emits rich diagnostics that include the error code, a source-pointed snippet, and a suggested fix:

  × error[E304]: Missing speaker in main tier (line 15, column 3)

15 │ *	hello world .
   ·  ╰── here
   ╰────
  help: Add a speaker code between * and : (e.g., *CHI:)

Each diagnostic contains:

  • File path and location (line:column)
  • Severity — error or warning
  • Error code — E prefix for errors, W prefix for warnings, with a URL pointing at the per-code documentation page
  • Message — human-readable description
  • Suggestion — actionable fix guidance where available

Error Code Ranges

RangeCategoryExamples
E1xxUTF-8 and encodingE101: Invalid line format
E2xxWord-level contentE202: Missing form type after @, E203: Invalid form type marker, E207: Unknown annotation
E3xxMain tier (speakers, terminators, content)E301: Empty/missing main tier, E304: Missing speaker, E305: Missing terminator, E306: Empty utterance, E307: Invalid speaker, E308: Undeclared speaker
E4xxDependent tier structureE401: Duplicate dependent tier
E5xxHeadersE501: Duplicate header, E504: Missing @Participants, E505: Invalid @ID format
E6xxDependent tier validationE601: Invalid dependent tier, E604: %gra without %mor
E7xxAlignment (%mor, %gra, %pho, %wor)E705: Main/%mor count mismatch, E721: %gra index error
W1xx-W6xxWarningsW108: BOM detected, W601: Empty user-defined tier

Common Errors and Fixes

E304: Missing speaker code

A main tier line must have a speaker code after the *:

*CHI:	hello world .

An empty speaker code (*: hello .) triggers E304.

E308: Undeclared speaker

Every *SPEAKER: code must be listed in @Participants. Add the missing speaker to the header:

@Participants:	CHI Target_Child, MOT Mother

E505: Invalid @ID format

Check that pipe-separated fields are correct and the speaker code matches @Participants:

@ID:	eng|corpus|CHI|2;6.||||Target_Child|||

E705: Main/%mor alignment mismatch

The number of %mor items must match the number of alignable words on the main tier. Retraces, pauses, and events are not counted. The validator shows a columnar diff:

  Main tier       %mor tier
  ──────────────  ──────────────
  I               pro|I
  want            v|want
  to              inf|to
  go              v|go
  home            — ⊖

E714 / E715: %pho, %mod, or %wor count mismatch

The same two codes are reused for “too few” / “too many” count mismatches on %pho, %mod, and %wor.

For %wor, the main-tier side is a spoken-token inventory:

  • regular words and fillers count
  • fragments, nonwords, and xxx/yyy/www count
  • retrace does not change %wor membership
  • replacements keep the original spoken surface word for %wor

That context-sensitivity decides membership, not leniency. Once an item is in the %wor set, alignment is still strict 1:1. So if a filler like &-mm counts on the main tier and %wor omits it, E714 is the correct result.

So this is valid:

*CHI:	<one &+ss> [/] one play ground .
%wor:	one •321008_321148• ss •321148_321368• one •321809_321969• play •322049_322310• ground •322390_322890• .

But this is also valid:

*EXP:	&+ih <the what> [/] what's letter &+th is this ?
%wor:	ih •49063_49103• the •49103_49163• what •49183_50205• what's •50205_50405• letter •50405_50685• th •50886_50946• is •50946_51046• this •51086_51586• ?

And this is valid too:

*EXP:	what's is dis [: this] ?
%wor:	what's •37050_37471• is •37491_37631• dis •37631_38131• ?

E721: %gra sequential index error

%gra entries must have sequential 1-based indices: 1|...|... 2|...|... 3|...|...

Generated Error Documentation

Detailed documentation for every error code is auto-generated from the spec at book/src/operations/errors/. Each page includes:

  • Error description
  • Example input that triggers the error
  • Suggested fix
  • Which validation layer catches it (parser vs. validation)

Run just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs to regenerate error documentation after error spec changes.

CLAN Line Numbering

Status: Current Last updated: 2026-03-16

When you click “Open in CLAN” in the desktop app or press Enter in the TUI, chatter sends the error location to the CLAN editor. CLAN opens the file and places the cursor at the error. This usually works seamlessly, but there is one caveat: CLAN and chatter count lines differently.

Hidden Headers

CLAN hides five header types from its editor display:

HeaderPurpose
@UTF8Character encoding declaration
@PIDPersistent identifier
@FontDisplay font settings
@ColorWordsColor coding rules
@WindowWindow position/size

These headers are present in the .cha file but invisible in CLAN’s editor. CLAN’s line numbers skip them entirely. A file that starts with @UTF8 on line 1 will show @Begin as “line 1” in CLAN’s display, even though it’s actually line 2 in the file.

What Chatter Does

Chatter automatically adjusts line numbers before sending to CLAN:

  1. Compute the error’s line number in the source file
  2. Count how many hidden headers appear before that line
  3. Subtract the hidden count to get CLAN’s line number
  4. Send the adjusted line number to CLAN

This happens transparently — you don’t need to do anything.

Edge Case: Errors on Hidden Lines

If an error is on a hidden header itself (e.g., a malformed @UTF8 line), CLAN cannot navigate to it because CLAN doesn’t display that line. In this case, “Open in CLAN” will show an error message explaining why.

For Developers

The shared resolution logic lives in talkbank_model::resolve_clan_location(). Both the TUI and the desktop app call this function — it resolves line/column from byte offsets when needed and adjusts for hidden headers.

See clan_location.rs for the implementation and tests.

Library Usage

Status: Current Last updated: 2026-05-11 08:29 EDT

The TalkBank Rust crates can be used as dependencies in your own Rust projects for parsing, validating, and manipulating CHAT files. This page shows the most common entry points; the API reference on docs.rs (once published) is the authoritative source. Until then, treat the rustdoc comments inside each crate’s src/lib.rs as the source of truth.

Examples on this page are mirrored as a real Cargo test at crates/core/talkbank-transform/tests/book_library_usage_examples.rs. The book renders them as rust,ignore so mdbook doesn’t try to link against the workspace’s many compiled crate variants; the parallel test runs the same code under cargo test and is what catches API drift between this page and the libraries. If you edit either, update both.

Important: some legacy tree-sitter fragment helpers are synthetic rather than semantically honest. They can inject fragment input into boilerplate CHAT text and parse the resulting synthetic file. Prefer full-file parsing for real tree-sitter use, and do not treat legacy fragment helpers as the long-term fragment API. For direct-parser fragment semantics, use direct-parser-native tests instead of treating synthetic wrappers as the oracle.

Adding Dependencies

The TalkBank library crates are currently a source-first preview surface. There is not yet a dedicated crates.io publication workflow for them, so use git or path dependencies today:

[dependencies]
talkbank-model = { path = "../talkbank-tools/crates/core/talkbank-model" }
talkbank-transform = { path = "../talkbank-tools/crates/core/talkbank-transform" }
talkbank-parser = { path = "../talkbank-tools/crates/core/talkbank-parser" }

The published-crate workflow is tracked separately; once it lands these paths can become version = "X.Y" deps.

Parsing and Validating a CHAT File

The simplest entry point is parse_and_validate from talkbank-transform. It takes the source text and a ParseValidateOptions, returns a fully constructed ChatFile, or a PipelineError if parsing or validation failed.

extern crate talkbank_model;
extern crate talkbank_transform;
use talkbank_model::ParseValidateOptions;
use talkbank_transform::parse_and_validate;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let source = std::fs::read_to_string("file.cha")?;
let options = ParseValidateOptions::default().with_validation();
let chat_file = parse_and_validate(&source, options)?;

for utt in chat_file.utterances() {
    println!("Speaker: {}", utt.main.speaker);
}
Ok(())
}

ChatFile is generic over a ValidationState parameter; the parse_and_validate return defaults to the validated state. chat_file.utterances() returns an iterator over &Utterance derived from the file’s lines (utterances are interleaved with headers and comments in source order).

For batch workflows where parser construction overhead matters, reuse a single TreeSitterParser and call parse_and_validate_with_parser:

extern crate talkbank_model;
extern crate talkbank_parser;
extern crate talkbank_transform;
use talkbank_model::ParseValidateOptions;
use talkbank_parser::TreeSitterParser;
use talkbank_transform::parse_and_validate_with_parser;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let chat_files: Vec<std::path::PathBuf> = Vec::new();
let parser = TreeSitterParser::new()?;
let options = ParseValidateOptions::default().with_validation();

for path in &chat_files {
    let source = std::fs::read_to_string(path)?;
    let chat_file = parse_and_validate_with_parser(&parser, &source, options.clone())?;
    let _ = chat_file;
}
Ok(())
}

ParseValidateOptions also exposes with_alignment() (implies with_validation(), additionally validates cross-tier alignment for %mor, %gra, %pho, %wor) and with_strict_linkers() (enables E351–E355 self-completion/other-completion linker checks).

Working with the Model

ChatFile stores participants and language metadata as top-level fields populated from @Participants / @ID / @Languages headers during parsing. Utterances live in lines and are iterated via chat_file.utterances().

extern crate talkbank_model;
extern crate talkbank_transform;
use talkbank_model::DependentTier;
use talkbank_model::ParseValidateOptions;
use talkbank_transform::parse_and_validate;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let source = "\
@UTF8
@Begin
@Languages:\teng
@Participants:\tCHI Target_Child
@ID:\teng|test|CHI|||||Target_Child|||
*CHI:\thello world .
%mor:\tco|hello n|world .
@End
";
let chat_file = parse_and_validate(source, ParseValidateOptions::default().with_validation())?;

// Participant metadata is top-level on the ChatFile.
let _participants = &chat_file.participants;

// Iterate utterances and their dependent tiers.
for utt in chat_file.utterances() {
    for tier in &utt.dependent_tiers {
        if let DependentTier::Mor(mor_tier) = tier {
            for item in mor_tier.items() {
                println!("POS: {}, Lemma: {}", item.main.pos, item.main.lemma);
            }
        }
    }
}
Ok(())
}

DependentTier is a closed-set enum (Mor, Gra, Pho, Mod, Sin, Act, Add, Com, Err, Exp, Gpx, Int, Lan, …); match on the variants you care about and ignore the rest. MorTier::items() returns &[Mor]; each Mor has a main MorWord plus optional post-clitics.

Serializing to CHAT

Bring the WriteChat trait into scope and call to_chat_string() for a fully-rendered CHAT string, or write_chat(&mut writer) to stream into any std::fmt::Write.

extern crate talkbank_model;
extern crate talkbank_transform;
use std::fmt::Write as _;

use talkbank_model::ParseValidateOptions;
use talkbank_model::WriteChat;
use talkbank_transform::parse_and_validate;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let source = "@UTF8\n@Begin\n@Languages:\teng\n@Participants:\tCHI Target_Child\n@ID:\teng|test|CHI|||||Target_Child|||\n*CHI:\thello .\n@End\n";
let chat_file = parse_and_validate(source, ParseValidateOptions::default().with_validation())?;

// Convenience: render to a fresh String.
let chat_text = chat_file.to_chat_string();
assert!(chat_text.starts_with("@UTF8"));

// Streaming: write into any std::fmt::Write sink.
let mut output = String::new();
chat_file.write_chat(&mut output)?;
Ok(())
}

Serializing to JSON

Prefer the schema-validated helpers in talkbank_transform::json: to_json_pretty_validated checks the output against the JSON schema and catches drift between the data model and the schema. The unvalidated variants are a faster bypass when you’ve already validated upstream.

extern crate talkbank_model;
extern crate talkbank_transform;
use talkbank_model::ParseValidateOptions;
use talkbank_transform::json::to_json_pretty_validated;
use talkbank_transform::parse_and_validate;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let source = "@UTF8\n@Begin\n@Languages:\teng\n@Participants:\tCHI Target_Child\n@ID:\teng|test|CHI|||||Target_Child|||\n*CHI:\thi .\n@End\n";
let chat_file = parse_and_validate(source, ParseValidateOptions::default().with_validation())?;

let json = to_json_pretty_validated(&chat_file)?;
assert!(json.contains("\"speaker\""));
Ok(())
}

The schema for ChatFile lives at schemas/chat-file/chat-file.schema.json and is regenerated from the Rust types via node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js. For arbitrary serde values (not just ChatFile), to_json_unvalidated / to_json_pretty_unvalidated work the same way without the schema step.

Custom Error Handling

Lower-level parser entry points stream diagnostics through the ErrorSink trait. Implement it to collect, count, filter, or forward errors as they arrive — useful when you need finer-grained control than the Result<ChatFile, PipelineError> shape parse_and_validate returns.

extern crate talkbank_model;
use talkbank_model::ErrorSink;
use talkbank_model::ParseError;

struct MyErrorHandler;

impl ErrorSink for MyErrorHandler {
    fn report(&self, error: ParseError) {
        // Custom handling: log, filter, count, etc.
        eprintln!("[{}] {}", error.code, error.message);
    }
}

ErrorSink is Send + Sync, and a blanket &T: ErrorSink impl means borrowed references are sinks too — no Arc wrapper required. The built-in ErrorCollector (gathers into a Vec), ParseTracker (counts by severity), and NullErrorSink (discards) cover most common needs; implement ErrorSink directly for everything else.

Crate Selection Guide

NeedCrate
Data model types, error types, WriteChat, ErrorSinktalkbank-model
Tree-sitter CHAT parsing (low-level)talkbank-parser
Full pipeline (parse + validate + JSON, schema validation)talkbank-transform
CLAN analysis commands (FREQ, MLU, KIDEVAL, …)clan-core

talkbank-model is the foundation — every other crate depends on it. If all you need are the AST types and validation, model alone is enough. talkbank-transform brings parsing + JSON + caching but does not own CLAN command logic.

Batchalign3-Facing Surface

If you are building Batchalign3 or another external consumer, the stable surface is usually:

Batchalign3 needPrefer
Canonical full-file parsingtalkbank-parser
Parse/validate contracts and typed model accesstalkbank-model
Alignment-aware downstream consumers (align, compare, benchmark)talkbank-model alignment helpers plus the model AST
Whole-pipeline parse+validate+converttalkbank-transform

For batch workflows, keep parser instances reusable and keep alignment logic separate from parse semantics.

For CLAN analysis integration, prefer the library-owned execution boundary in clan-core instead of constructing command types ad hoc in outer crates. In practice that means:

  • use clan_core::framework::UtteranceRange and clan_core::framework::DiscoveredChatFiles for analysis input selection
  • parse raw outer-layer command names into clan_core::service::AnalysisCommandName at the boundary
  • use clan_core::service::AnalysisOptions and clan_core::service::AnalysisRequestBuilder when you need to translate raw outer-layer option bags into validated CLAN requests with library-owned defaults
  • use clan_core::service::AnalysisRequest to describe which CLAN analysis to run
  • use clan_core::service::AnalysisService when you need rendered or JSON analysis output from Rust code

That keeps CLI and editor integrations focused on adapting their own request shapes while the CLAN crate owns command construction and shared output behavior.

JSON Output Reference

Status: Reference Last updated: 2026-05-11 23:45 EDT

This document describes the structure of JSON produced by chatter to-json. For the formal JSON Schema, see JSON Schema.

Quick Start

# Default: parse + validate + align, pretty-printed, schema-checked
chatter to-json file.cha

# Write to file
chatter to-json file.cha -o file.json

# Skip validation (parse only, faster)
chatter to-json file.cha --skip-validation

# Skip alignment only
chatter to-json file.cha --skip-alignment

Validation and alignment are on by default. Use --skip-validation or --skip-alignment to opt out.

Top-Level Structure

{
  "lines": [ ... ]
}

A ChatFile is a flat list of lines. Each line has a line_type discriminator:

line_typeDescription
"header"File header (@Begin, @Languages, @Participants, etc.)
"utterance"Main tier + dependent tiers + alignment
"comment"@Comment: lines

Word Fields

Words are the fundamental unit. Every word in the main tier content array carries these fields:

FieldTypeAlways?Description
type"word"yesDiscriminator
raw_textstringyesExact text from the transcript, including all CHAT markers
cleaned_textstringyesNLP-ready text (shortenings restored, markers stripped)
contentarrayyesStructured breakdown of word parts (see below)
categorystringno"omission", "filler", "nonword", "fragment", "ca_omission"
form_typestringnoSpecial form code: "c", "d", "f", "x", etc.
langobjectnoLanguage marker (see Language-Switched example)
untranscribedstringno"unintelligible" (xxx), "phonetic" (yyy), "untranscribed" (www)

Word content items use "content" for the text value:

{ "type": "text", "content": "dog" }

Computed Fields

cleaned_text and untranscribed are computed from content during serialization. They do not exist as stored fields in the data model.

  • cleaned_text: Concatenates Text and Shortening elements from content. Excludes lengthening markers (:), stress markers, CA elements, overlap points, compound markers, and underline markers. Example: sit(ting) → "sitting".

  • untranscribed: Present only when cleaned_text is "xxx", "yyy", or "www".

Word Examples

Simple Word

dog
{
  "type": "word",
  "raw_text": "dog",
  "cleaned_text": "dog",
  "content": [{ "type": "text", "content": "dog" }]
}

Filler

&-uh
{
  "type": "word",
  "raw_text": "&-uh",
  "cleaned_text": "uh",
  "content": [{ "type": "text", "content": "uh" }],
  "category": "filler"
}

Untranscribed

xxx
{
  "type": "word",
  "raw_text": "xxx",
  "cleaned_text": "xxx",
  "content": [{ "type": "text", "content": "xxx" }],
  "untranscribed": "unintelligible"
}

Compound

ice+cream
{
  "type": "word",
  "raw_text": "ice+cream",
  "cleaned_text": "icecream",
  "content": [
    { "type": "text", "content": "ice" },
    { "type": "compound_marker", "content": { "span": { "start": 0, "end": 1 } } },
    { "type": "text", "content": "cream" }
  ]
}

Omission

0she
{
  "type": "word",
  "raw_text": "0she",
  "cleaned_text": "she",
  "content": [{ "type": "text", "content": "she" }],
  "category": "omission"
}

Nonword

&~baba
{
  "type": "word",
  "raw_text": "&~baba",
  "cleaned_text": "baba",
  "content": [{ "type": "text", "content": "baba" }],
  "category": "nonword"
}

Special Form

doggy@c
{
  "type": "word",
  "raw_text": "doggy@c",
  "cleaned_text": "doggy",
  "content": [{ "type": "text", "content": "doggy" }],
  "form_type": "c"
}

Language-Switched

maison@s:fra
{
  "type": "word",
  "raw_text": "maison@s:fra",
  "cleaned_text": "maison",
  "content": [{ "type": "text", "content": "maison" }],
  "lang": { "type": "explicit", "code": "fra" }
}

The lang field has variants: {"type": "shortcut"} (bare @s), {"type": "explicit", "code": "fra"} (@s:fra), and {"type": "multiple", "code": ["eng", "zho"]} (@s:eng+zho).

Utterances

An utterance line contains:

{
  "line_type": "utterance",
  "main": {
    "speaker": "CHI",
    "content": {
      "content": [ ... ],
      "terminator": { "type": "period" },
      "bullet": { "start_ms": 0, "end_ms": 3042 }
    }
  },
  "dependent_tiers": [ ... ],
  "alignments": { ... },
  "utterance_language": { "status": "resolved_default", "code": "eng" },
  "language_metadata": { ... }
}

Key structural points:

  • The utterance body is under "main", not "utterance".
  • content, terminator, and bullet are nested inside main.content.
  • terminator is an object with a type field ("period", "question", "exclamation", etc.), not a bare string.
  • bullet (utterance-level timing) is inside main.content, omitted when absent (not present as null).
  • dependent_tiers, alignments, utterance_language, and language_metadata are top-level siblings of main. Empty dependent_tiers and alignments are omitted when there is nothing to report.

Content Items

main.content.content is a heterogeneous array. Each item has a type discriminator:

TypeDescription
"word"A word token (see Word Fields above)
"event"Non-verbal action (&=laughs)
"pause"Timed or untimed pause ((.), (0.5))
"group"Bracketed group (<word word>)
"separator"Tag markers, linkers, etc.

Dependent Tiers

When present, dependent_tiers is an array of tagged objects:

"dependent_tiers": [
  {
    "type": "Mor",
    "data": {
      "tier_type": "Mor",
      "items": [
        {
          "main": { "pos": "pron", "lemma": "I" }
        },
        {
          "main": { "pos": "verb", "lemma": "want", "features": ["Fin", "Ind", "Pres"] }
        }
      ],
      "terminator": "."
    }
  },
  {
    "type": "Gra",
    "data": {
      "tier_type": "Gra",
      "relations": [
        { "index": 1, "head": 2, "relation": "NSUBJ" },
        { "index": 2, "head": 0, "relation": "ROOT" }
      ]
    }
  }
]
typeTierDescription
"Mor"%morMorphological analysis (POS tags, lemmas, features, clitics)
"Gra"%graGrammatical relations (dependency arcs)
"Pho"%phoPhonological transcription
"Sin"%sinSyntax tier
"Wor"%worWord-level timing (items with inline_bullet)
Other%xxxUser-defined dependent tiers

%wor Tier

The Wor tier contains word items with timing:

{
  "type": "Wor",
  "data": {
    "items": [
      {
        "kind": "word",
        "raw_text": "hello",
        "cleaned_text": "hello",
        "content": [{ "type": "text", "content": "hello" }],
        "inline_bullet": { "start_ms": 100, "end_ms": 300 }
      }
    ],
    "terminator": { "type": "period" }
  }
}

Note that %wor items use "kind" instead of "type" for their discriminator, since "type" is used by the tier envelope.

Alignment Data

When validation runs (the default), the alignments object contains:

  • units: per-tier index arrays (for internal bookkeeping)
  • Named tier pairs (e.g., mor, gra) with alignment mappings
"alignments": {
  "units": {
    "main_mor": [{"index": 0}, {"index": 1}],
    "main_pho": [{"index": 0}, {"index": 1}],
    "main_sin": [{"index": 0}, {"index": 1}],
    "main_wor": [{"index": 0}, {"index": 1}],
    "mor": [{"index": 0}, {"index": 1}]
  },
  "mor": {
    "pairs": [
      { "source_index": 0, "target_index": 0 },
      { "source_index": 1, "target_index": 1 }
    ],
    "errors": []
  }
}

Alignment links each main-tier word (source_index) to its corresponding dependent-tier item (target_index) by position. errors contains any alignment-level diagnostics (count mismatches, etc.) and is [] when alignment validated cleanly.

Headers

Headers use the header object with a type discriminator:

TypeHeaderKey Fields
"utf8"@UTF8—
"begin"@Begin—
"end"@End—
"languages"@Languagescodes
"participants"@Participantsentries (speaker_code, name, role)
"id"@IDlanguage, corpus, speaker, role, age, sex, …
"media"@Mediafilename, media_type, status
"comment"@Commenttext
"date"@Datedate
"options"@Optionsoptions (array of strings)

See the JSON Schema for the complete list of header types and fields.

Timing

Utterance-level timing appears in main.content.bullet:

"bullet": {
  "start_ms": 1234,
  "end_ms": 5678
}

Word-level timing (from %wor tier) appears in inline_bullet on individual words within the Wor dependent tier.

JSON Schema

Status: Current Last updated: 2026-05-11 23:47 EDT

talkbank-tools now generates JSON Schema from Rust-owned types with schemars for two different integration surfaces:

  • the ChatFile transcript model used by chatter to-json
  • the talkbank/analyze editor/server request contract shared by the VS Code extension and chatter-lsp

Keeping those schemas generated from the Rust source of truth lets cross-language integrations consume stable contracts without re-deriving the shapes by hand.

Available schemas

SchemaCanonical URLRepositoryGenerator
ChatFile transcript modelhttps://talkbank.org/schemas/v0.1/chat-file.jsonschemas/chat-file/chat-file.schema.jsoncargo test --test generate_schema
AnalyzeCommandPayload execute-command contracthttps://talkbank.org/schemas/v0.1/analyze-command.jsonschemas/chat-file/analyze-command.schema.jsoncargo test --test generate_analyze_command_schema

The generated schemas declare both $schema (JSON Schema 2020-12) and $id (the canonical URL above). External consumers that want to track the current transcript-model version should follow the v0.1 URL; there is no /latest/ alias in the generated artifacts.

Transcript schema: ChatFile

chatter to-json converts CHAT transcripts into a structured JSON form backed by the same ChatFile model used by the parser, validator, and serializer.

How chatter to-json uses it

By default, chatter to-json:

  • validates the CHAT input,
  • checks dependent-tier alignment unless --skip-alignment is passed, and
  • validates the emitted JSON against the schema unless --skip-schema-validation is passed.

Useful flags:

chatter to-json input.cha --skip-validation
chatter to-json input.cha --skip-alignment
chatter to-json input.cha --skip-schema-validation

chatter from-json deserializes JSON back into the internal ChatFile model and re-serializes it to CHAT format. The input should conform to this schema.

Roundtrip expectations

The CHAT-to-JSON-to-CHAT pipeline is intended to preserve the ChatFile model:

chatter to-json input.cha -o intermediate.json
chatter from-json intermediate.json -o output.cha
diff input.cha output.cha

Both directions go through the same typed model. When changing the parser, serializer, or schema generation, confirm roundtrip behavior with the existing roundtrip test suites rather than assuming byte-for-byte identity.

Using the schema externally

Validate JSON in Python

import json
import jsonschema
import urllib.request

schema_url = "https://talkbank.org/schemas/v0.1/chat-file.json"
schema = json.loads(urllib.request.urlopen(schema_url).read())

with open("transcript.json") as f:
    data = json.load(f)

jsonschema.validate(data, schema)

IDE autocompletion

{
  "$schema": "https://talkbank.org/schemas/v0.1/chat-file.json",
  "lines": [],
  "participants": {},
  "languages": [],
  "options": []
}

Generate types from the schema

Tools like quicktype, json-schema-to-typescript, and datamodel-code-generator can generate typed structs or classes from the schema for TypeScript, Python, Go, and other languages.

Editor/server contract schema: AnalyzeCommandPayload

The talkbank/analyze LSP command still travels through workspace/executeCommand, but its logical payload is now one typed object described by chatter_lsp::backend::contracts::AnalyzeCommandPayload:

{
  "commandName": "mlu",
  "targetUri": "file:///tmp/sample.cha",
  "options": {
    "words": true
  }
}

That schema is useful for:

  • external editor integrations that want the exact request contract
  • future TypeScript type generation or runtime validation
  • documenting the stable boundary between the extension and chatter-lsp

The Rust contract module also reuses library-owned types such as AnalysisCommandName, Gender, and DatabaseFilter so the transport schema stays aligned with the typed CLAN execution boundary.

The repo also keeps one concrete shared fixture for this contract at apps/vscode-extension/src/test/fixtures/analyzeCommandPayload.json. The TypeScript payload tests import that fixture directly, and tests/validate_analyze_command_fixture.rs validates the same JSON against schemas/chat-file/analyze-command.schema.json and then deserializes it through AnalyzeCommandPayload. That gives the contract one mechanically checked cross-language example in addition to the generated schema artifact itself.

Regenerating the schemas

After changing transcript-model types in talkbank-model:

cd talkbank-tools
cargo test --test generate_schema

After changing the editor/server analyze contract in crates/chatter/chatter-lsp/src/backend/contracts.rs:

cd talkbank-tools
cargo test --test generate_analyze_command_schema

This writes the checked-in schema artifacts in schema/. CI already checks that generated artifacts stay in sync.

Code references

  • schemas/chat-file/chat-file.schema.json — generated schema
  • schemas/chat-file/analyze-command.schema.json — generated talkbank/analyze contract schema
  • apps/vscode-extension/src/test/fixtures/analyzeCommandPayload.json — shared concrete analyze-command fixture
  • crates/core/talkbank-transform/src/json.rs — schema loading and validation
  • crates/core/talkbank-model/src/model/ — Rust data model
  • crates/chatter/chatter-lsp/src/backend/contracts.rs — Rust-owned editor/server transport contracts
  • tests/generate_schema/ — shared schema generation helpers
  • tests/generate_analyze_command_schema.rs — analyze-contract schema generation test
  • tests/validate_analyze_command_fixture.rs — fixture/schema validation test for the editor/server contract

Diagnostic and JSON Output Contract

Status: Current Last updated: 2026-05-11 23:37 EDT

This page documents the machine-readable JSON surfaces currently exposed by the top-level chatter CLI. It does not try to freeze every JSON payload emitted by chatter clan; those command-specific formats should be checked per command.

Stability policy

  • Treat field names documented here as the public contract.
  • Treat additional fields as additive unless this page says otherwise.
  • Treat message wording as human-facing text, not a stable machine contract.

chatter validate ... --format json

Both chatter validate FILE --format json and chatter validate DIR --format json emit newline-delimited JSON (NDJSON) on stdout, with the same record shapes in both modes:

  1. zero or more per-file records (one per validated file), then
  2. one final summary record.

A single-file invocation still emits a file record followed by a summary record — it is not a single-object surface.

Per-file records

Valid files:

{"type":"file","file":"/path/to/file.cha","status":"valid","cache_hit":false}

Invalid files (the errors array is opaque per-error JSON; the note field is appended when the validator stopped further checks because of structural errors):

{
  "type": "file",
  "file": "/path/to/file.cha",
  "status": "invalid",
  "error_count": 1,
  "errors": [
    {
      "code": "E502",
      "message": "Missing @End header at end of file",
      "severity": "Error"
    }
  ],
  "note": "Some additional checks may not have run because of structural errors. Fix the structural errors first, then re-validate."
}

Parser-failure files use "status":"parse_error" with an error string. Read-failure files use "status":"read_error" with an error string.

Summary record

{
  "type": "summary",
  "directory": "/path/to/dir",
  "total_files": 2,
  "valid": 1,
  "invalid": 1,
  "parse_errors": 0,
  "cache_hits": 0,
  "cache_misses": 2,
  "cache_hit_rate": 0.0,
  "cancelled": false
}

When --roundtrip is set, the summary also includes roundtrip_passed and roundtrip_failed counters.

Contract notes

  • The type field is stable: "file" or "summary".
  • For file records: file and status are stable; cache_hit is stable for valid records. error_count and errors are stable for invalid records.
  • For summary records: directory, total_files, valid, invalid, parse_errors, cache_hits, cache_misses, cache_hit_rate, and cancelled are stable.
  • status values currently observed: valid, invalid, parse_error, read_error. New status values may appear.
  • Errors do not include a byte-offset location field in the NDJSON surface; for byte-offset diagnostics use the LSP or the non-JSON renderer.
  • The note field on invalid file records is human-facing guidance and may be added or omitted between releases.
  • Exit code 0 means all files validated successfully; exit code 1 means at least one file failed or an I/O error occurred.

chatter to-json

chatter to-json emits the full ChatFile JSON model rather than a diagnostic summary. The authoritative contract for that output is the JSON Schema documented in JSON Schema.

Practical notes:

  • The JSON itself is the contract, not any validation status lines printed by the CLI.
  • Use -o/--output if you want only the JSON in a file.
  • Use --skip-validation, --skip-alignment, or --skip-schema-validation only when you explicitly want to bypass those checks.

chatter cache stats --json

Cache statistics emit one JSON object on stdout:

{
  "total_entries": 743,
  "cache_dir": "/Users/example/Library/Caches/talkbank-chat",
  "cache_size_bytes": 274432,
  "last_modified": "2026-03-09T13:05:31+00:00"
}

Contract notes:

  • total_entries, cache_dir, cache_size_bytes, and last_modified are stable.
  • last_modified is RFC 3339 / ISO 8601 text.

chatter clan ... --format json

CLAN analysis commands such as chatter clan freq --format json also provide JSON output, but those shapes are command-specific. Do not assume one shared schema across all CLAN commands. Check the command-specific documentation or the command’s --help output when you build an integration.

VS Code: reference

Look up syntax, contracts, and factual details.

Quick Reference

Status: Current Last updated: 2026-05-11 20:18 EDT

A comprehensive reference card for every keyboard shortcut, command, and setting in the TalkBank CHAT extension. Print this page or keep it open in a tab while working.

For installation, see Installation. For a guided tour, see Your First CHAT File.


Keyboard Shortcuts

All shortcuts below use macOS key names. On Windows and Linux, substitute Ctrl for Cmd.

Always Active (in any CHAT file)

ShortcutmacOSAction
Cmd+Shift+GCmd+Shift+GShow Dependency Graph
Cmd+Shift+EnterCmd+Shift+EnterPlay Media at Cursor
Cmd+Shift+/Cmd+Shift+/Play Media Continuously
Cmd+Shift+WCmd+Shift+WShow Waveform View
F8F8Rewind Media (configurable seconds)
Shift+F5Shift+F5Toggle Segment Loop
Alt+DownAlt+DownWalker: Next Utterance
Alt+UpAlt+UpWalker: Previous Utterance
Cmd+Shift+1Cmd+Shift+1Insert CA Special Character (compose mode)
Cmd+Shift+2Cmd+Shift+2Insert CHAT Special Character (compose mode)

Transcription Mode Active

These shortcuts are only active when transcription mode has been started via the Command Palette.

ShortcutmacOSAction
F4F4Stamp Timestamp Bullet and advance to new line

Coder Mode Active

These shortcuts are only active after starting Coder Mode.

ShortcutmacOSAction
Cmd+EnterCmd+EnterCoder: Next Utterance (advance to next uncoded)
Cmd+Shift+CCmd+Shift+CCoder: Insert Code without advancing

Review Mode Active

These shortcuts are only active after starting Review Mode. Number keys work only when the editor is not focused (i.e., focus is in the review panel).

ShortcutmacOSAction
Alt+]Alt+]Review: Next Flagged utterance
Alt+[Alt+[Review: Previous Flagged utterance
11Review: Rate Good
22Review: Rate Early
33Review: Rate Late
44Review: Rate Wrong
55Review: Skip

Standard VS Code Shortcuts (enhanced for CHAT)

These are built-in VS Code shortcuts that the extension enhances with CHAT-specific intelligence:

ShortcutAction
Cmd+.Quick Fix (automatic corrections for 21 error codes)
F2Rename Speaker Code (across all declarations and occurrences)
F12 / Cmd+ClickGo to Definition (speaker to @Participants, tier to main word)
Shift+F12Find All References (every occurrence of a speaker code)
Shift+Alt+FFormat Document (canonical CHAT style)
Cmd+TWorkspace Symbol Search (across all open CHAT files)
Cmd+Shift+ODocument Symbols (outline view by speaker and utterance)
Shift+Ctrl+RightSmart Selection: Expand (word, utterance, tier block, transcript)
Shift+Ctrl+LeftSmart Selection: Shrink

Command Palette Commands

Open the Command Palette with Cmd+Shift+P and type “TalkBank” to see all commands. Commands are listed below organized by category.

Media

CommandDescription
TalkBank: Play Media at CursorPlay the audio/video segment at the cursor position
TalkBank: Play Media ContinuouslyPlay from cursor to end, tracking current utterance
TalkBank: Stop Media PlaybackStop any active media playback
TalkBank: Rewind Media (2 s)Rewind by the configured number of seconds
TalkBank: Toggle Segment LoopLoop the current segment on/off
TalkBank: Show Waveform ViewOpen waveform visualization of linked media
TalkBank: Show Elicitation PictureDisplay associated elicitation image (Cookie Theft, etc.)
CommandDescription
TalkBank: Show Dependency GraphRender %gra dependency tree as a color-coded graph
TalkBank: Walker: Next UtteranceStep to the next utterance and play its segment
TalkBank: Walker: Previous UtteranceStep to the previous utterance and play its segment
TalkBank: Filter by Speaker…Show only selected speakers in a side-by-side panel
TalkBank: Find in Tier…Search within a specific CHAT tier, optionally by speaker

Analysis

CommandDescription
TalkBank: Run CLAN Analysis…Choose from 33 CLAN commands and view results in a styled panel
TalkBank: Run CLAN Analysis on Directory…Run any CLAN command across all .cha files in a folder
TalkBank: Run KidEval…Compare child language measures against normative databases
TalkBank: Run Eval…General assessment normative comparison
TalkBank: Run Eval-D (Dementia)…Dementia evaluation with DementiaBank databases

Editing

CommandDescription
TalkBank: Edit Participants…Edit @ID headers in an interactive table
TalkBank: Insert CA Special Character…Enter compose mode for Conversation Analysis symbols
TalkBank: Insert CHAT Special Character…Enter compose mode for CHAT/IPA symbols
TalkBank: Cancel Special Character InputCancel active compose mode

Transcription

CommandDescription
TalkBank: Start Transcription ModeBegin transcribing with media playback
TalkBank: Stamp Timestamp BulletInsert timing bullet at current playback position, advance to new line
TalkBank: Stop Transcription ModeEnd transcription mode
TalkBank: Configure Transcription Keybindings…Open keybinding editor for transcription keys (F4, F5, F8)

Coder

CommandDescription
TalkBank: Start Coder Mode…Load a .cut codes file and begin coding utterances
TalkBank: Stop Coder ModeEnd coder mode
TalkBank: Coder: Next UtteranceAdvance to the next uncoded utterance
TalkBank: Coder: Insert Code…Select and insert a code from the loaded codes file

Review

CommandDescription
TalkBank: Start Review ModeBegin reviewing flagged utterances
TalkBank: Stop Review ModeEnd review mode
TalkBank: Review: Next FlaggedJump to the next flagged utterance
TalkBank: Review: Previous FlaggedJump to the previous flagged utterance
TalkBank: Review: Rate GoodRate the current utterance as Good
TalkBank: Review: Rate EarlyRate the current utterance as Early (boundary too early)
TalkBank: Review: Rate LateRate the current utterance as Late (boundary too late)
TalkBank: Review: Rate WrongRate the current utterance as Wrong
TalkBank: Review: SkipSkip the current utterance without rating

Validation

CommandDescription
TalkBank: Validate FileRun validation on the current file
TalkBank: Validate DirectoryRun validation on a directory tree
TalkBank: Refresh ValidationRe-run validation for the Validation Explorer
TalkBank: Clear CacheClear cached validation results for the current file
TalkBank: Clear All Validation CacheClear the entire validation cache
TalkBank: View Cache StatisticsShow cache size and hit/miss statistics

Integration

CommandDescription
TalkBank: Open in CLANOpen the current file in the CLAN macOS application at the cursor position

Settings Reference

All settings are under the talkbank.* namespace. Open VS Code Settings (Cmd+,) and search for “talkbank” to see them all.

SettingDefaultRange / OptionsDescription
talkbank.transcription.defaultSpeaker"CHI"Any valid speaker codeDefault speaker code inserted on new utterance lines during transcription mode
talkbank.transcription.rewindSeconds20.5 – 30Seconds to rewind when using the Rewind Media command (F8)
talkbank.walker.autoPlaytruetrue / falseAutomatically play the media segment when stepping with the walker (Alt+Down/Alt+Up)
talkbank.walker.loopCount10 – 50Times to loop each segment during walker playback (0 = loop indefinitely until next step)
talkbank.walker.pauseSeconds00 – 10Seconds to pause between segments during continuous playback or walker stepping
talkbank.walker.walkLength00 – 100Utterances to play during continuous walker mode (0 = play all remaining segments)
talkbank.media.defaultSpeed10025, 50, 75, 100, 125, 150, 175, 200Default playback speed as a percentage (100 = normal, 50 = half, 200 = double)
talkbank.bullets.display"dim""dim", "hidden", "normal"How timing bullets are displayed: dim (35% opacity), hidden (invisible), or normal (full visibility)
talkbank.lsp.binaryPath(auto-detect)Absolute file pathOverride path to the chatter-lsp binary. Leave empty to auto-detect from PATH or target/ directory
talkbank.inlayHints.enabledtruetrue / falseShow inline annotations for alignment mismatches (e.g., [alignment: 3 main <-> 2 mor])
talkbank.validation.severity"all""all", "errorsOnly", "errorsAndWarnings"Filter which diagnostics are displayed: all, errors only, or errors and warnings

CLAN Analysis Commands (33 total)

These commands are available via TalkBank: Run CLAN Analysis… in the Command Palette or the right-click context menu. Commands marked with an asterisk (*) prompt for additional input before running.

Frequency and Length

CommandDescription
freqWord frequency counts
freqposWord frequency by part of speech
mluMean length of utterance (in morphemes)
mltMean length of turn
wdlenWord length distribution
wdsizeWord size (character count) distribution
maxwdLongest words
CommandDescription
kwal *Keyword and line search
combo *Boolean combination search
cooccurCo-occurrence analysis
distDistribution analysis
uniqUnique word list

Developmental Measures

CommandDescription
dssDevelopmental Sentence Score
ipsynIndex of Productive Syntax
kidevalChild language normative comparison
evalGeneral assessment normative comparison
sugarSyntactic Utterances and Grammatical Analysis Revised
vocdVocabulary diversity (D statistic)

Coding and Reliability

CommandDescription
codesCode frequency analysis
chainsChain analysis
keymap *Key remapping analysis
rely *Inter-rater reliability

Fluency and Phonology

CommandDescription
flucalcFluency calculation
phonfreqPhoneme frequency

Interaction

CommandDescription
chipChild imitation and practice
modrepModel and repetition analysis

Morphology and Tiers

CommandDescription
mortable *Morphology table
trnfixTranscript repair

Structure

CommandDescription
gemlistGem (activity) listing
timedurTimed duration analysis
script *Script execution
complexitySyntactic complexity
corelexCore lexicon analysis

Snippets

Type any of the listed prefixes and press Tab to expand. Each snippet has multiple equivalent prefixes — use whichever feels most natural.

PrefixesExpands To
@UTF8, header, newfileComplete new CHAT file template (@UTF8, @Begin, @Languages, @Participants, two @ID lines, an initial *CHI: line, @End)
@Participants, participant@Participants: header with two-speaker placeholder
@ID, id@ID: header with 10 pipe-delimited fields
*, utteranceMain tier line with speaker placeholder and trailing .
%mor%mor: dependent tier
%gra%gra: dependent tier
@Comment, comment@Comment: header
@Bg, @Eg, gemGem block (@Bg / @Eg pair around the cursor)

Settings Reference

Status: Current Last updated: 2026-04-16 13:37 EDT

All extension settings are accessible via File > Preferences > Settings (Cmd+, on macOS, Ctrl+, on Windows/Linux) and searching for “talkbank”. Settings can also be edited directly in settings.json.

Transcription

SettingTypeDefaultDescription
talkbank.transcription.defaultSpeakerstring"CHI"Speaker code inserted on new utterance lines during transcription mode. Change this when transcribing a conversation where the primary speaker is not the child (e.g., set to "PAR" for participant).
talkbank.transcription.rewindSecondsnumber2Number of seconds to rewind when pressing F8. Range: 0.5–30.

Walker and Playback

SettingTypeDefaultDescription
talkbank.walker.autoPlaybooleantrueAutomatically play the media segment when stepping through utterances with the walker (Alt+Down / Alt+Up). Set to false for silent navigation.
talkbank.walker.loopCountinteger1Number of times to loop each segment during walker playback. Range: 0–50. Set to 0 to loop indefinitely until the next step command.
talkbank.walker.pauseSecondsnumber0Seconds to pause between segments during continuous playback or walker stepping. Range: 0–10. Set to 0 for no pause.
talkbank.walker.walkLengthinteger0Number of utterances to play during continuous walker mode. Range: 0–100. Set to 0 to play all remaining segments to the end of the file.
talkbank.media.defaultSpeedinteger100Default playback speed as a percentage. Values: 25, 50, 75, 100, 125, 150, 175, 200. 100 = normal speed, 50 = half speed, 200 = double speed. The media panel toolbar also provides a speed slider for on-the-fly adjustment.

Display

SettingTypeDefaultDescription
talkbank.bullets.displaystring"dim"How timing bullets (15320_18450) are displayed in the editor. Options: "dim" (35% opacity – the default, keeps bullets visible but unobtrusive), "hidden" (completely invisible – useful when bullets are distracting during reading), "normal" (full visibility – useful when editing bullet values).
talkbank.inlayHints.enabledbooleantrueShow inlay hints for alignment count mismatches (e.g., [alignment: 3 main <> 2 mor]) and timing durations. Disable if you find the inline annotations distracting.

Validation

SettingTypeDefaultDescription
talkbank.validation.severitystring"all"Filter which validation diagnostics are displayed in the editor. Options: "all" (show errors, warnings, and informational diagnostics), "errorsOnly" (show only errors – useful for large files with many warnings), "errorsAndWarnings" (show errors and warnings, hide informational diagnostics).

Advanced

SettingTypeDefaultDescription
talkbank.lsp.binaryPathstring"" (empty)Absolute path to the standalone chatter-lsp binary. When empty (the default), the extension searches: (1) bundled binary at <extension>/server/chatter-lsp[.exe] (shipped with platform-specific VSIXes), (2) system PATH via which chatter-lsp, (3) dev-tree builds at <extension>/../target/{debug,release}/chatter-lsp. If none are found, an actionable error notification is shown. Set this only if you need to override the search.

Settings in JSON

To edit settings directly in settings.json, add entries like:

{
  "talkbank.transcription.defaultSpeaker": "PAR",
  "talkbank.transcription.rewindSeconds": 3,
  "talkbank.walker.autoPlay": true,
  "talkbank.walker.loopCount": 2,
  "talkbank.media.defaultSpeed": 75,
  "talkbank.bullets.display": "hidden",
  "talkbank.inlayHints.enabled": false,
  "talkbank.validation.severity": "errorsOnly",
  "talkbank.lsp.binaryPath": "/usr/local/bin/chatter-lsp"
}

Workspace vs. User Settings

All TalkBank settings can be set at either the User level (applies to all workspaces) or the Workspace level (applies only to the current workspace). Workspace settings override user settings.

This is useful when different corpora need different configurations. For example, a corpus where the primary speaker is the parent might have a workspace-level setting of "talkbank.transcription.defaultSpeaker": "PAR", while your user-level default remains "CHI".

Keyboard Shortcuts

Status: Current Last updated: 2026-03-30 13:40 EDT

This chapter lists all keyboard shortcuts provided by the TalkBank CHAT extension, grouped by feature area. All shortcuts are active only when a .cha file is open in the editor (editorLangId == chat), unless otherwise noted.

Media Playback

ActionmacOSWindows/LinuxWhen Active
Play Media at CursorCmd+Shift+EnterCtrl+Shift+EnterCHAT file open
Play Media ContinuouslyCmd+Shift+/Ctrl+Shift+/CHAT file open
Rewind Media (2 s)F8F8CHAT file open
Toggle Segment LoopShift+F5Shift+F5CHAT file open
Show Waveform ViewCmd+Shift+WCtrl+Shift+WCHAT file open

Walker Mode

ActionmacOSWindows/LinuxWhen Active
Walker: Next UtteranceAlt+DownAlt+DownCHAT file open
Walker: Previous UtteranceAlt+UpAlt+UpCHAT file open

Transcription Mode

ActionmacOSWindows/LinuxWhen Active
Stamp Timestamp BulletF4F4Transcription mode active

The F4 key is only active when transcription mode has been started via TalkBank: Start Transcription Mode (the talkbank.transcriptionActive context is set).

Coder Mode

ActionmacOSWindows/LinuxWhen Active
Coder: Next UtteranceCmd+EnterCtrl+EnterCoder mode active
Coder: Insert CodeCmd+Shift+CCtrl+Shift+CCoder mode active

These keys are only active when coder mode has been started via TalkBank: Start Coder Mode (the talkbank.coderActive context is set).

Review Mode

ActionmacOSWindows/LinuxWhen Active
Review: Next FlaggedAlt+]Alt+]Review mode active
Review: Previous FlaggedAlt+[Alt+[Review mode active
Review: Rate Good11Review mode active, not editing text
Review: Rate Early22Review mode active, not editing text
Review: Rate Late33Review mode active, not editing text
Review: Rate Wrong44Review mode active, not editing text
Review: Skip55Review mode active, not editing text

The number keys (1-5) for rating are active only when the editor does not have text focus (!editorTextFocus). This prevents accidental ratings while typing corrections. Click outside the text area (e.g., on the editor margin or the media panel) to enable the rating keys.

ActionmacOSWindows/LinuxWhen Active
Show Dependency GraphCmd+Shift+GCtrl+Shift+GCHAT file open

Special Character Input

ActionmacOSWindows/LinuxWhen Active
Insert CA Special CharacterCmd+Shift+1Ctrl+Shift+1Editor focused, CHAT file
Insert CHAT Special CharacterCmd+Shift+2Ctrl+Shift+2Editor focused, CHAT file

Standard VS Code Shortcuts (CHAT-Aware)

These are standard VS Code shortcuts that gain CHAT-specific behavior through the extension’s LSP:

ActionmacOSWindows/LinuxDescription
Quick FixCmd+.Ctrl+.Show available code actions (fixes for 21 error codes)
RenameF2F2Rename speaker code across the entire file
Find All ReferencesShift+F12Shift+F12Find all occurrences of a speaker code
Go to DefinitionF12 or Cmd+ClickF12 or Ctrl+ClickJump to speaker declaration or aligned tier
Document SymbolsCmd+Shift+OCtrl+Shift+ONavigate utterances by speaker in the outline
Workspace SymbolsCmd+TCtrl+TSearch across all open CHAT files
Format DocumentShift+Alt+FShift+Alt+FReformat to canonical CHAT form
Open ProblemsCmd+Shift+MCtrl+Shift+MShow all diagnostics in the Problems panel

Customizing Keyboard Shortcuts

To change any keybinding:

  1. Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P)
  2. Select Preferences: Open Keyboard Shortcuts
  3. Search for “talkbank” to see all extension keybindings
  4. Click the pencil icon next to any shortcut to assign a new key combination

Transcription Keybinding Configuration

The extension provides a dedicated command for configuring transcription keys:

  1. Open the Command Palette
  2. Select TalkBank: Configure Transcription Keybindings
  3. The keybindings editor opens, pre-filtered to show only TalkBank transcription commands (stamp, rewind, loop)

This is the recommended way to remap keys for foot pedals or other input devices.

Keybindings in keybindings.json

For manual configuration, add entries to your keybindings.json:

[
  {
    "key": "f9",
    "command": "talkbank.stampBullet",
    "when": "editorLangId == chat && talkbank.transcriptionActive"
  },
  {
    "key": "f10",
    "command": "talkbank.rewindMedia",
    "when": "editorLangId == chat"
  }
]

The when clause ensures the keybinding is only active in the appropriate context.

Cache Management

Status: Current Last updated: 2026-03-30 13:40 EDT

The extension uses a SQLite cache to store validation results, avoiding redundant re-validation of files that have not changed. This chapter covers the cache location, monitoring, and maintenance.

Cache Location

The cache database is stored in the OS-standard cache directory:

OSPath
macOS~/Library/Caches/talkbank-chat/
Linux~/.cache/talkbank-chat/
Windows%LocalAppData%\talkbank-chat\

The cache directory contains a SQLite database file. It is safe to delete this directory entirely – the extension will recreate it on the next validation run.

Status Bar Indicator

A cache indicator appears in the bottom-right of the VS Code status bar, showing the number of cached files (e.g., Cache: 95,247 files). This updates every 5 seconds by polling the chatter cache stats --json CLI command.

Click the status bar indicator to view detailed cache statistics in a popup.

View Cache Statistics

Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and select TalkBank: View Cache Statistics.

This displays a popup with:

  • Cache database size – the on-disk size of the SQLite file
  • Total cached entries – how many files have cached results
  • Valid / Invalid / Expired counts – breakdown of cache entry statuses
  • Cache hit rate – percentage of validation requests served from cache
  • Last updated timestamp – when the cache was last written to

Clearing the Cache

Clear a specific file or directory

In the Validation Explorer, right-click on a file or folder and select Clear Cache. This removes cached results only for the selected items, forcing revalidation on the next run.

Clear the entire cache

Open the Command Palette and select TalkBank: Clear All Validation Cache. This deletes all cached entries from the database.

You can also use the --force flag with the CLI to bypass the cache for a specific run:

chatter validate path/to/corpus/ --force

When to Clear the Cache

The cache is content-addressed: if a file changes, its cached result is automatically invalidated. You should not need to manually clear the cache under normal circumstances. Clear the cache when:

  • After upgrading chatter – a new version of the validator may produce different results for the same input. The cache does not automatically detect binary upgrades.
  • After grammar changes – if you are developing the tree-sitter grammar or the parser, cached results from the old grammar are stale.
  • If results seem stale – if validation results do not match what you expect after editing a file, clearing the cache rules out staleness as the cause.
  • If the cache becomes corrupted – SQLite corruption is rare but possible after a system crash. Deleting and recreating the cache is the simplest fix.

Cache Design

The cache is managed by the chatter CLI binary, not the extension’s TypeScript code. The extension’s CacheManager (cacheManager.ts) polls the CLI for statistics and invokes CLI commands for cache operations. The Validation Explorer also uses the CLI (chatter validate --format json) for bulk validation, and the CLI handles cache reads and writes transparently.

This design means the cache is shared between the VS Code extension and direct CLI usage. Validating files from the terminal benefits from the same cache, and vice versa.

LSP Protocol

Status: Current Last updated: 2026-04-16 22:12 EDT

This chapter documents all Language Server Protocol capabilities advertised by chatter-lsp. The capability set is declared in crates/chatter-lsp/src/backend/capabilities.rs via build_initialize_result(). Per-feature handlers live under backend/features/ (request-agnostic feature code) and backend/requests/ (request-dispatcher shims that pull out the parts of Backend each handler needs).

The twelve custom talkbank/* commands (also advertised through this mechanism) have their own per-endpoint reference at RPC Contracts; this chapter stays focused on the standard LSP surface.

Advertised Capabilities

The server advertises 23 standard LSP capabilities plus 12 custom commands.

Text Document Sync

CapabilityModeDescription
Text document syncIncrementalReceives incremental edits (not full document re-sends)

Text Document Features

CapabilityHandlerDescription
Hoverfeatures/hover.rsCross-tier alignment display for main, %mor, %gra, %pho, %sin tiers. Also covers headers and timing bullets.
Completionfeatures/completion.rsSpeaker codes from @Participants, tier prefixes, postcode suffixes, header names, bracket annotations. Triggers on *, %, +, @, [.
Definitionrequests/goto_definition.rsNavigate from speaker code to @Participants declaration, from dependent tier to aligned main tier word (chunks → host items via MorTier::item_index_of_chunk).
Referencesfeatures/references.rsFind all occurrences of a speaker code across @Participants, @ID headers, and main tier lines.
Document Highlightfeatures/highlights/Bidirectional cross-tier alignment highlighting. Click a word on any tier to highlight aligned items on all other tiers.
Document Symbolfeatures/document_symbol.rsTwo-level outline: transcript as Module, per-utterance String symbols labeled by speaker. Powers Cmd+Shift+O and the Outline view.
Code Actionfeatures/code_action.rsQuick fixes for 21 error codes (E241, E242, E244, E258, E259, E301, E305, E306, E308, E312, E313, E322, E323, E362, E501-E504, E506, E507, E604).
Formattingrequests/formatting.rsRe-serializes the document through the canonical CHAT serializer.
On-Type Formattingfeatures/on_type_formatting.rsAuto-inserts leading tab on continuation lines after tier prefix. Trigger character: :.
Renamefeatures/rename.rsRename speaker code across @Participants, @ID headers, and all main tier lines. Supports prepare rename.
Code Lensfeatures/code_lens.rsUtterance counts per speaker above @Participants (e.g., “CHI: 42 utterances”).
Semantic Tokenssemantic_tokens.rsFull and range-based semantic tokens. 11 token types (keyword, variable, string, comment, type, operator, number, function, tag, punctuation, error).
Diagnosticdiagnostics/Push (publishDiagnostics) and pull (textDocument/diagnostic, workspace/diagnostic) models.
Selection Rangefeatures/selection_range.rsSmart expand/shrink: word to utterance content to tier block to transcript.
Linked Editing Rangefeatures/linked_editing.rsSimultaneous editing of matching speaker codes.
Document Linkfeatures/document_link.rsMakes @Media: header values clickable links.
Folding Rangefeatures/folding_range.rsFold utterance blocks (main tier + dependent tiers) and the header block.
Inlay Hintfeatures/inlay_hints.rsAlignment count mismatch hints (e.g., [alignment: 3 main <> 2 mor]).

Workspace Features

CapabilityHandlerDescription
Workspace Symbolfeatures/workspace_symbol.rsCross-file search by speaker code and utterance content.
Did Change Configurationbackend/mod.rsResponds to settings changes from the client.

Execute Command

The server advertises 12 custom commands via workspace/executeCommand (the talkbank/* family). Per-endpoint reference: RPC Contracts. Architecture and sequence-diagram: Custom Commands.

Error contract

Feature handlers inside chatter-lsp return Result<_, LspBackendError> rather than stringly Result<_, String>. LspBackendError (backend/error.rs) is a thiserror enum with variants for tier/alignment state (MissingTier { tier: TierName }, TierAlignmentMissing { tier: TierName }, AlignmentMetadataMissing), document state (DocumentNotFound, JsonSerializeFailed(#[from] serde_json::Error)), parse state (ParseFailure { count, first_message }), executeCommand argument decoding (ArgumentMissing, ArgumentInvalid, InvalidUriParse, UriNotFilePath), regex (InvalidRegex), init failure (LanguageServicesUnavailable(#[from] BackendInitError)), external service failure (ExternalServiceFailed), and a wrapper for graph-specific failures (Graph(#[from] GraphEdgeError)).

Handlers that return typed errors get collapsed to the tower-lsp string form only at the protocol boundary (requests/*.rs). This means every feature can match on its classification without parsing free-form text.

Completion Trigger Characters

The completion provider activates on these trigger characters:

CharacterWhat it completes
*Speaker codes from @Participants
%Dependent tier prefixes (mor, gra, pho, sin, etc.)
+Postcode punctuation suffixes
@Header names (@Participants, @ID, @Languages, etc.)
[Bracket annotations

Semantic Token Types

The semantic tokens provider uses 11 token types, mapped to VS Code’s theme colors:

IndexTypeUsed For
0keywordHeaders (@Begin, @End), tier prefixes
1variableSpeaker codes (*CHI, *MOT)
2stringQuoted strings, word content
3commentComment lines
4typeType annotations, complex structures
5operatorPostcodes, morphological separators
6numberTiming values, indices
7functionDependent tier prefixes, special markers
8tagTier labels, annotation markers
9punctuationTerminators (. ? !), special punctuation
10errorSyntax errors, malformed tokens

Semantic tokens are computed on every request (full or range-based). The TextMate grammar (chat.tmLanguage.json) provides fallback highlighting before the LSP finishes starting.

Diagnostic Tags

The server uses LSP diagnostic tags for visual de-emphasis:

TagUsed For
UnnecessaryEmpty utterances, empty colons – shown with fade-out styling
DeprecatedDeprecated CHAT constructs

CLAN Feature Parity

Status: Current Last updated: 2026-04-16 22:16 EDT

This chapter summarizes the VS Code extension’s feature parity with the legacy CLAN macOS application. The extension does not just replicate CLAN — it surpasses it in many areas while faithfully implementing its core capabilities.

Summary

The VS Code extension implements all major CLAN features and adds over 30 capabilities that CLAN never had. A small number of CLAN features are not applicable in the VS Code context (they are handled natively by VS Code or are platform-specific).

Improvements Over CLAN

The following features exist in the VS Code extension but have no equivalent in the CLAN macOS application:

FeatureDescription
Cross-platformmacOS, Windows, and Linux with identical functionality. CLAN only runs on macOS.
Real-time diagnosticsErrors appear as you type with inline squiggles. CLAN shows errors only after an explicit check.
Corpus-scale validationValidate entire directory trees via the Validation Explorer with results cached in SQLite. CLAN validates one file at a time.
Quick-fix code actionsAutomatic fixes for 21 error codes via Cmd+.. CLAN has no equivalent.
Bidirectional cross-tier alignmentClick any word on any tier to highlight aligned counterparts across all tiers simultaneously. CLAN’s alignment display is one-directional and limited.
Alignment mismatch inlay hintsInline annotations ([alignment: 3 main <> 2 mor]) visible without running any command. CLAN only surfaces these in the error log.
Go to definitionF12 jumps from speaker codes to declarations, from dependent tiers to aligned main words.
Find all referencesShift+F12 lists every occurrence of a speaker code across declarations, headers, and tiers.
Rename speaker codeF2 renames a speaker across the entire file atomically. CLAN requires manual find-and-replace.
Linked editingEditing a speaker code simultaneously updates all matching occurrences.
Code lens (utterance counts)Per-speaker utterance counts above @Participants without running any analysis.
Smart selectionShift+Ctrl+Right/Left expands/shrinks by syntactic units (word, utterance, tier block, transcript).
Workspace symbol searchCmd+T searches across all open CHAT files by speaker and content.
Document links@Media: values are clickable links.
Speaker filteringSide-by-side filtered view of selected speakers only.
Code foldingCollapse utterance blocks and header blocks.
Breadcrumb navigationCmd+Shift+O structured outline of utterances by speaker.
On-type formattingContinuation lines auto-indented with tabs.
Diagnostic tagsEmpty utterances shown with fade-out styling.
Snippet templates8 tab-triggered CHAT structure templates.
CSV exportAnalysis results exportable to CSV for spreadsheets and statistical software.
Configurable severityFilter diagnostics to errors-only, errors+warnings, or all.
Offline graph renderingBundled Graphviz WASM, no internet needed.
Scoped findSearch within specific tiers and/or specific speakers, with regex.
Context menu organizationAnalysis, media, and navigation in logical submenus.
Proper data modelingAll CHAT operations use parsed AST, not string hacking.
Pull diagnosticsLSP 3.17 pull model alongside push model.
Waveform zoom/scroll100-2000% zoom with pointer-centered mouse wheel.
Playback speed control0.25x to 2x via slider in media panel.
Transcription key configurationRemap F4/F5/F8 for foot pedals via standard VS Code keybindings.

Features Fully Implemented from CLAN

These CLAN features have been faithfully ported to the VS Code extension:

CLAN FeatureVS Code Implementation
Syntax highlightingTextMate grammar + LSP semantic tokens (11 token types)
Validation (CHECK)Real-time diagnostics + Validation Explorer
Hover / Tier WindowtextDocument/hover for main, %mor, %gra, %pho, %sin tiers
Alignment displaytextDocument/documentHighlight with bidirectional range finders
Completion (auto-complete)textDocument/completion for speakers, tiers, postcodes, headers, brackets
Dependency graphtalkbank/showDependencyGraph + Graphviz WASM rendering
33 CLAN analysis commandstalkbank/analyze with generic result rendering
KidEval / Eval / Eval-DDedicated webview panels with language/activity grid, auto-detection from file headers, cascading database selection, and z-score comparison
Participant EditoridEditorPanel.ts with 10-column editable table
Media playback (Sonic mode)mediaPanel.ts with single and continuous play
Waveform viewwaveformPanel.ts with Web Audio API rendering
Walker modeAlt+Down/Up stepping with auto-play
Transcription modeF4 stamping with LSP-formatted bullets
Coding mode.cut file loading, hierarchical QuickPick, %cod insertion
Picture displaypicturePanel.ts with %pic: reference scanning
F5 loop / F8 rewindSegment loop toggle and configurable rewind
Special character inputCompose-key mode for 30+ CA symbols and 20+ CHAT marks
Open in CLANApple Events (macOS) and Windows messaging IPC
Format/tidytextDocument/formatting through canonical serializer
Document outlinetextDocument/documentSymbol two-level tree
Bullet displayConfigurable: dim (35%), hidden, or normal
Corpus-level analysisDirectory-level analysis via explorer context menu

Features Not Applicable to VS Code

These CLAN features are handled natively by VS Code or are platform-specific and not relevant:

CLAN FeatureVS Code Equivalent
Go to line dialogCmd+G (built-in)
Print supportVS Code print extensions
Font selectioneditor.fontFamily setting
Window managementVS Code’s built-in layout system
Options/preferences panelVS Code Settings UI
File input dialogVS Code’s native file picker
Commands windowCommand Palette (Cmd+Shift+P)
About dialogExtension info page
Services menumacOS-specific, not relevant cross-platform
Edit mode toggleVS Code’s read-only mode

Alignment Index Spaces

Status: Current Last updated: 2026-04-16 21:50 EDT

%mor / %gra / %pho / %sin alignment code in this extension crosses three integer index spaces that all look identical when typed as usize. Confusing them is the bug class the typed newtypes defined in talkbank_model::alignment::indices exist to prevent.

This page is the canonical source for which space each integer lives in, how the spaces convert into each other, and a worked example on a post-clitic utterance.

The three spaces

Newtype0- / 1-indexedOver which sequenceWhere it comes from
MorItemIndex0-indexedMorTier::items (one entry per %mor item, no post-clitic expansion)MorAlignmentPair.target_index on the main↔%mor alignment
MorChunkIndex0-indexedMorTier::chunks() (item main word, then each post-clitic, then the optional terminator)GraAlignmentPair.mor_chunk_index on the %mor↔%gra alignment
SemanticWordIndex11-indexedAuthor-written position in a %gra relationGrammaticalRelation::index, ::head

The %gra ROOT sentinel (head == 0) is not a chunk — it is represented separately as GraHeadRef::Root so the sentinel is a variant, not a magic number.

%% Verified against: crates/talkbank-model/src/alignment/indices.rs,
%% crates/talkbank-model/src/alignment/{mor,pho,sin,gra/types}.rs.
classDiagram
    class MainWordIndex {
        +new(usize)
        +as_usize() usize
    }
    class MorItemIndex {
        +new(usize)
        +as_usize() usize
    }
    class MorChunkIndex {
        +new(usize)
        +as_usize() usize
    }
    class GraIndex {
        +new(usize)
        +as_usize() usize
    }
    class PhoItemIndex
    class SinItemIndex
    class SemanticWordIndex1 {
        +new(usize) Result~Self~
        +to_chunk_index() MorChunkIndex
    }
    class GraHeadRef {
        <<enumeration>>
        Root
        Word(SemanticWordIndex1)
    }
    class MorAlignmentPair {
        source: Option~MainWordIndex~
        target: Option~MorItemIndex~
    }
    class GraAlignmentPair {
        mor_chunk_index: Option~MorChunkIndex~
        gra_index: Option~GraIndex~
    }
    MorAlignmentPair --> MainWordIndex : source
    MorAlignmentPair --> MorItemIndex : target
    GraAlignmentPair --> MorChunkIndex : source
    GraAlignmentPair --> GraIndex : target
    GraHeadRef --> SemanticWordIndex1 : Word variant carries
    SemanticWordIndex1 --> MorChunkIndex : to_chunk_index

Worked example

Consider the CHAT line

*CHI: it's cookies .
%mor: pron|it~aux|be n|cookie .
%gra: 1|2|SUBJ 2|0|ROOT 3|2|OBJ 4|2|PUNCT

The %mor tier has two items (pron|it~aux|be and n|cookie). The same tier has four chunks when MorTier::chunks() expands the items by their post-clitics and appends the terminator:

Chunk indexChunk kindWord / TextHost item index
0Mainpron|it0
1PostCliticaux|be0
2Mainn|cookie1
3Terminator.(none)

The %gra relations address this chunk sequence, 1-indexed. So 2|0|ROOT says “semantic word 2 (chunk 1 = aux|be) has head 0 (ROOT).”

Common pitfall this file exists to prevent: taking relation.index == 2, treating it as MorItemIndex(2) — which would be out of range (only two items exist) — or MorItemIndex(1) after a naïve - 1 — which would pick up n|cookie instead of aux|be. Neither maps the semantic word to its %mor lemma.

The correct projection is:

// 1. Semantic word index → chunk index via subtract-1.
let chunk_idx = (relation.index as usize).checked_sub(1)?;

// 2. Chunk → host item for anyone who needs main↔%mor alignment.
let host_item_idx = mor_tier.item_index_of_chunk(chunk_idx)?;

When to use which projection

NeedUse
“What lemma does this %gra relation refer to?”MorTier::chunk_at(chunk_idx) then chunk.lemma()
“Which main-tier word does this %gra relation correspond to?”MorTier::item_index_of_chunk(chunk_idx) then look up the main↔%mor pair
“Is this relation pointing at ROOT?”relation.head_ref() → match on GraHeadRef::Root
“Walk all chunks in order”mor_tier.chunks() (never hand-roll `items.iter().flat_map(

The other alignments

%pho, %mod, and %sin do not expand — they align 1:1 with main-tier alignable words. Their typed pairs are:

TierPair aliasSourceTarget
%morMorAlignmentPairMainWordIndexMorItemIndex
%graGraAlignmentPairMorChunkIndexGraIndex
%pho (and %mod)PhoAlignmentPairMainWordIndexPhoItemIndex
%sinSinAlignmentPairMainWordIndexSinItemIndex

%wor is not a structural alignment — it is a timing sidecar (WorTimingSidecar) with a Positional { count } / Drifted { main_count, wor_count } shape rather than a pair list. Count mismatches are tolerated rather than diagnosed.

See also

Custom LSP RPC Contracts

Status: Current Last updated: 2026-04-16 21:52 EDT

The extension drives twelve custom workspace/executeCommand endpoints exposed by chatter-lsp. This page is the per-endpoint reference: command identifier, request shape, response shape, TS caller, Rust handler. Everything here is verified against the current source on both sides — if an endpoint’s behavior drifts, update this page in the same commit.

One typical call at runtime

%% Verified against: apps/vscode-extension/src/lsp/executeCommandClient.ts,
%% crates/chatter-lsp/src/backend/requests/execute_command.rs,
%% crates/chatter-lsp/src/backend/execute_commands.rs.
sequenceDiagram
    participant TS as TS feature (e.g. analysis.ts)
    participant Client as TalkbankExecuteCommandClient
    participant LSP as tower-lsp dispatcher
    participant Decoder as ExecuteCommandRequest::parse
    participant Handler as feature handler (Rust)

    TS->>Client: executeStructured("talkbank/analyze", [payload])
    Client->>LSP: workspace/executeCommand JSON-RPC
    LSP->>Decoder: parse(params)
    Decoder-->>LSP: typed ExecuteCommandRequest::Analyze(...)
    LSP->>Handler: handle_analyze(&request)
    Handler-->>LSP: Result<serde_json::Value, LspBackendError>
    LSP-->>Client: JSON response (or stringified error)
    Client->>TS: decoded payload via Effect Schema

Twelve endpoints grouped by handler family.

Documents family

talkbank/showDependencyGraph

Generate a Graphviz DOT dependency graph for the utterance at a cursor position.

Rust requestDocumentPositionRequest { uri: Url, position: Position } in backend/execute_commands.rs
Rust handlergraph::build_dependency_graph_response(utterance, parse_state) in crates/chatter-lsp/src/graph/mod.rs
Rust responseDependencyGraphResponse::Dot { source } | Unavailable { reason }
TS callerexecuteCommandClient.getDependencyGraph (or similar — see graphPanel.ts)
TS response typeDependencyGraphResponse (Effect Schema discriminated union) in lsp/executeCommandPayloads.ts

Request layout: argument position 0 is the document URI, position 1 is the cursor as { line, character }. See the Cross-Tier Alignment and Dependency Graphs pages for user-facing behavior; the DOT output includes a stale baseline header label when the backend is serving a stale-baseline parse (see developer/architecture.md).

talkbank/getAlignmentSidecar

Produce the alignment-sidecar payload for one document — a JSON view of every utterance’s AlignmentSet usable by the extension’s alignment overlays.

Rust requestDocumentUriRequest { uri: Url }
Rust handlerbuild_alignment_sidecar(&uri, text, &chat_file) in backend/requests/alignment_sidecar.rs
Rust responseAlignmentSidecarDocument { schema_version, uri, utterances[] }
TS callerTalkbankExecuteCommandClient.getAlignmentSidecar
TS response typeAlignmentSidecarDocument in lsp/executeCommandPayloads.ts

alignments.wor on each utterance carries synthetic positional pairs {source_index: i, target_index: i} when the backing WorTimingSidecar is Positional, and is empty when it is Drifted or absent. See alignment-indices.md.

Analysis family

talkbank/analyze

Run one CLAN analysis command (freq, mlu, kideval, …) and return JSON.

Rust requestAnalyzeRequest = AnalyzeCommandPayload (shared with clan-core)
Rust handlerhandle_analyze(&request) in backend/analysis.rs
Rust responseserde_json::Value — the analysis command’s native JSON output
TS callerTalkbankExecuteCommandClient.analyze(request)
TS request typeAnalyzeCommandRequest in lsp/executeCommandPayloads.ts

Payload carries command_name, target_uri, and an AnalysisOptions struct with every per-command flag. See the Command Reference page for the command-name catalog.

talkbank/kidevalDatabases · talkbank/evalDatabases

Discover normative databases in a CLAN library directory. Returns the same shape for both; the extension surfaces them in different UIs.

Rust requestDiscoverDatabasesRequest { library_dir: PathBuf }
Rust handlerhandle_discover_databases(&request) in backend/analysis.rs
Rust responseVec<AvailableDatabase>
TS callerTalkbankExecuteCommandClient.kidevalDatabases / .evalDatabases
TS response typeAvailableDatabase[] in lsp/executeCommandPayloads.ts

Participants family

talkbank/getParticipants

Extract @ID participant entries from a document.

Rust requestDocumentUriRequest { uri: Url }
Rust handlerhandle_get_participants(&backend, &request) in backend/participants.rs
Rust responseVec<ParticipantEntry>
TS callerTalkbankExecuteCommandClient.getParticipants
TS response typeParticipantEntry[]

talkbank/formatIdLine

Format one @ID header line from field values. Pure formatting — does not touch a document.

Rust requestIdLineFieldsRequest { language, corpus, speaker, age, sex, group, ses, role, education, custom }
Rust handlerhandle_format_id_line(&request) in backend/participants.rs
Rust response{ formatted: String }
TS callerTalkbankExecuteCommandClient.formatIdLine
TS request typeIdLineFields

ChatOps family

talkbank/getSpeakers

Extract declared speaker metadata from @Participants.

Rust requestDocumentUriRequest { uri: Url }
Rust handlerhandle_get_speakers(&backend, &request) in backend/chat_ops/speakers.rs
Rust responseVec<SpeakerInfo>
TS callerTalkbankExecuteCommandClient.getSpeakers
TS response typeSpeakerInfo[]

talkbank/filterDocument

Filter a document down to selected speakers. Returns the filtered document text for use as a virtual document.

Rust requestFilterDocumentRequest { uri: Url, speakers: Vec<String> }
Rust handlerhandle_filter_document(&backend, &request) in backend/chat_ops/filter_document.rs
Rust response{ text: String }
TS callerTalkbankExecuteCommandClient.filterDocument

talkbank/getUtterances

Return utterance metadata used by coder mode (speaker, text, range).

Rust requestDocumentUriRequest { uri: Url }
Rust handlerhandle_get_utterances(&backend, &request) in backend/chat_ops/utterances.rs
Rust responseVec<UtteranceInfo>
TS callerTalkbankExecuteCommandClient.getUtterances
TS response typeUtteranceInfo[]

talkbank/formatBulletLine

Format one timing-bullet insertion for the transcription mode’s F4 insertion key.

Rust requestFormatBulletLineRequest { prev_ms, current_ms, speaker }
Rust handlerhandle_format_bullet_line(&request) in backend/chat_ops/format_bullet.rs
Rust responseFormattedBulletLine { bullet, new_line }
TS callerTalkbankExecuteCommandClient.formatBulletLine
TS response typeFormattedBulletLine

talkbank/scopedFind

Execute semantically scoped search in one document (main tier only, all tiers, specific dependent tier, with optional regex).

Rust requestScopedFindRequest { uri, query, scope, speakers[], regex }
Rust handlerhandle_scoped_find(&backend, &request) in backend/chat_ops/scoped_find.rs
Rust responseVec<ScopedFindMatch>
TS callerTalkbankExecuteCommandClient.scopedFind
TS request typeScopedFindRequest · TS response type ScopedFindMatch[]

Error contract

All handlers return Result<serde_json::Value, LspBackendError>. The dispatcher in backend/requests/execute_command.rs serializes successes to the JSON-RPC result; failures are returned as a string-valued result (one-field wire format) so the extension can render them inline even when no LSP Diagnostic applies. LspBackendError variants include ArgumentMissing, ArgumentInvalid, InvalidUriParse, UriNotFilePath, DocumentNotFound, LanguageServicesUnavailable(BackendInitError), and the command-agnostic ExternalServiceFailed { service, reason }.

The TS side wraps every call in three typed error classes:

  • ExecuteCommandRequestError — transport failed (disconnected, timeout)
  • ExecuteCommandResponseError — wrong payload shape
  • ExecuteCommandServerError — server returned a string-valued error

See apps/vscode-extension/src/lsp/executeCommandErrors.ts for the tagged error family and Webview Message Contracts for the panel-side decoding patterns.

Advertised command list

The LSP advertises its twelve command names during initialize via ExecuteCommandName::advertised_commands(). A unit test in backend/execute_commands.rs::tests pins the list against the manifest so adding a command without updating the advertisement is a compile-and-test failure.

Webview Message Contracts

Status: Current Last updated: 2026-04-16 21:54 EDT

Each webview panel exchanges JSON messages with the extension host over VS Code’s Webview.postMessage / onDidReceiveMessage channels. This page is the per-panel contract reference: message unions each direction, decoder location, HTML/JS renderer paths.

Contracts are defined via Effect Schema on the extension side so incoming payloads decode through a typed boundary (see effectBoundary.ts). Panels that render DOT graphs or static images do not need a full message contract and are listed at the end.

Seven panels

PanelWebview → ExtensionExtension → WebviewContract module
analysisExportCsvMessagePanelErrorMessagewebviewMessageContracts.ts
idEditorIdEditorPanelSaveMessageIdEditorPanelEntriesMessage | IdEditorPanelSavedMessage | PanelErrorMessagewebviewMessageContracts.ts
media{segmentChanged, timestamp, stopped}{rewind, setLoop, requestTimestamp, seekTo}webviewContracts/mediaPanelContract.ts
waveformWaveformPanelSeekMessageWaveformPanelHighlightSegmentMessagewebviewMessageContracts.ts
kideval{discoverDatabases, runAnalysis}{fileInfo, databases, results, error}webviewMessageContracts.ts
graph(none — one-shot DOT)(none — one-shot DOT render)graphPanel.ts inline
picture(none)(none)picturePanel.ts inline

Shared shape: PanelErrorMessage

Every panel that can surface an extension-side failure uses the same error-envelope type:

interface PanelErrorMessage {
    command: 'error';
    message: string;
}

createIdEditorErrorMessage(msg) and createKidevalErrorMessage(msg) are thin constructors in webviewMessageContracts.ts.

Analysis panel

CLAN analysis results viewer. One message direction: webview asks the extension to export a visible table as CSV.

// Webview → Extension
interface ExportCsvMessage {
    command: 'exportCsv';
    csv: string;      // Pre-rendered CSV text
    filename: string; // Suggested save-as filename
}
type AnalysisPanelWebviewMessage = ExportCsvMessage;

// Extension → Webview
type AnalysisPanelExtensionMessage = PanelErrorMessage;
  • Decoder: decodeAnalysisPanelWebviewMessage(value) → AnalysisPanelWebviewMessage in webviewMessageContracts.ts.
  • Renderer: src/webview/analysisPanel.html + analysisPanel.js.
  • Panel controller: src/analysisPanel.ts.

ID editor panel

Edit @ID header entries for all participants in a document.

// Webview → Extension
interface IdEditorPanelSaveMessage {
    command: 'save';
    entries: IdLineFields[]; // All participant entries
}

// Extension → Webview
interface IdEditorPanelEntriesMessage {
    command: 'entries';
    entries: ParticipantEntry[];
}
interface IdEditorPanelSavedMessage {
    command: 'saved';
}
type IdEditorPanelExtensionMessage =
    | IdEditorPanelEntriesMessage
    | IdEditorPanelSavedMessage
    | PanelErrorMessage;
  • Decoder: decodeIdEditorPanelWebviewMessage in webviewMessageContracts.ts.
  • Constructors: createIdEditorEntriesMessage, createIdEditorSavedMessage, createIdEditorErrorMessage.
  • Renderer: src/webview/idEditorPanel.{html,js}.
  • Panel controller: src/idEditorPanel.ts.
  • Upstream data: talkbank/getParticipants (RPC); save translates entries back into @ID lines via talkbank/formatIdLine.

Media panel

Audio/video playback tied to utterance segments. Most message-rich panel. Contract lives in its own module (webviewContracts/mediaPanelContract.ts) after KIB-006.

// Webview → Extension
type MediaPanelWebviewMessage =
    | { command: 'segmentChanged'; index: number }
    | { command: 'timestamp'; ms: TimestampMs }
    | { command: 'stopped' };

// Extension → Webview
type MediaPanelExtensionMessage =
    | { command: 'rewind'; seconds: number }
    | { command: 'setLoop' }
    | { command: 'requestTimestamp' }
    | { command: 'seekTo'; ms: TimestampMs };
  • Decoder: decodeMediaPanelWebviewMessage in webviewContracts/mediaPanelContract.ts.
  • Constructors: createMediaRewindMessage, createMediaSetLoopMessage, createMediaRequestTimestampMessage, createMediaSeekToMessage.
  • Renderer: src/webview/mediaPanel.{html,js}.
  • Panel controller: src/mediaPanel.ts.

TimestampMs is a branded number newtype from utils/bulletParser.ts; the decoder brands validated incoming values through toTimestampMs.

Waveform panel

Web Audio API waveform visualization; click-to-seek + segment highlight.

// Webview → Extension
interface WaveformPanelSeekMessage {
    command: 'seek';
    ms: TimestampMs;
}
type WaveformPanelWebviewMessage = WaveformPanelSeekMessage;

// Extension → Webview
interface WaveformPanelHighlightSegmentMessage {
    command: 'highlightSegment';
    index: number;
}
type WaveformPanelExtensionMessage = WaveformPanelHighlightSegmentMessage;
  • Decoder: decodeWaveformPanelWebviewMessage.
  • Constructor: createWaveformHighlightSegmentMessage(index).
  • Renderer: src/webview/waveformPanel.{html,js}.
  • Panel controller: src/waveformPanel.ts.

KidEval panel

Normative-assessment comparison against CLAN databases.

// Webview → Extension
type KidevalPanelWebviewMessage =
    | { command: 'discoverDatabases'; libraryDir: string }
    | { command: 'runAnalysis'; databasePath: string; ...options };

// Extension → Webview
type KidevalPanelExtensionMessage =
    | { command: 'fileInfo'; fileName: string }
    | { command: 'databases'; databases: AvailableDatabase[]; filter: AnalysisDatabaseFilter }
    | { command: 'results'; data: unknown }
    | PanelErrorMessage;
  • Decoder: decodeKidevalPanelWebviewMessage.
  • Constructors: createKidevalFileInfoMessage, createKidevalDatabasesMessage, createKidevalResultsMessage, createKidevalErrorMessage.
  • Renderer: src/webview/kidevalPanel.{html,js}.
  • Panel controller: src/kidevalPanel.ts.
  • Upstream RPC: discoverDatabases message triggers talkbank/kidevalDatabases; runAnalysis triggers talkbank/analyze with kideval command name.

Graph panel (no contract)

Renders a Graphviz DOT string from the LSP’s talkbank/showDependencyGraph endpoint. One-shot: the extension posts the DOT source into the webview, which renders it with @hpcc-js/wasm Graphviz. No response channel; the webview has no state the extension needs.

  • Panel controller: src/graphPanel.ts.
  • Renderer: src/webview/graphPanel.{html,js}.
  • Upstream: talkbank/showDependencyGraph (see RPC Contracts).

Picture panel (no contract)

Static image viewer for elicitation pictures (Cookie Theft, picture description tasks). No JSON messages; the extension builds an HTML page inline with one <img> tag pointing at a webview resource URI.

  • Panel controller: src/picturePanel.ts.
  • Renderer: built inline from TypeScript (no external HTML file).

Decoding pattern

Every decoder uses the shared decodePanelMessageWithSchema helper in effectBoundary.ts, which runs the Effect Schema against the payload and throws StructuredPayloadDecodeError with a panel-labeled reason on mismatch. Panel controllers translate the error into a PanelErrorMessage sent back to the webview.

Adding a new panel

  1. Add the panel’s message types + schemas + decoder + constructors. New panels should live under src/webviewContracts/<panel>PanelContract.ts (not in the umbrella webviewMessageContracts.ts); the umbrella file still exists as a re-export facade for historical shapes but is not the right home for new contracts. See KIB-006.
  2. Add the panel controller under src/<panel>Panel.ts — follow the singleton pattern from mediaPanel.ts (createOrShow, panel disposal hooks, asset URI resolution via panelAssets.ts).
  3. Add the panel HTML + JS under src/webview/<panel>Panel.{html,js}.
  4. Register the open-panel command in src/activation/commands/<family>.ts via registerEffectCommand.
  5. Declare the command in package.json contributes.commands.

Command Catalog

Status: Current Last updated: 2026-04-16 21:56 EDT

Every user-invocable VS Code command this extension contributes, grouped by feature area. Commands are declared in package.json’s contributes.commands; registrations live in src/activation/commands/ (the 41 effect commands) and src/activation/validation.ts (the 4 validation-explorer commands).

Total contributed commands: 45.

Command IDs all use the talkbank. prefix; the corresponding LSP RPC endpoints (12 of them) use the talkbank/ prefix instead — they are not directly user-invocable. See RPC Contracts for those.

Editor features

CommandTitleHandler
talkbank.showDependencyGraphShow Dependency Graphcommands/editor.ts → graphPanel.ts
talkbank.showWaveformShow Waveform Viewcommands/editor.ts → waveformPanel.ts
talkbank.showPictureShow Elicitation Picturecommands/editor.ts → picturePanel.ts
talkbank.editParticipantsEdit Participants…commands/editor.ts → idEditorPanel.ts
talkbank.openInClanOpen in CLANcommands/editor.ts → clanIntegration.ts

Media playback

CommandTitleKeybindingHandler
talkbank.playBulletPlay Media at CursorCmd+Shift+Entercommands/media.ts
talkbank.playContinuousPlay Media ContinuouslyCmd+Shift+/commands/media.ts
talkbank.stopPlaybackStop Media Playbackcommands/media.ts
talkbank.rewindMediaRewind Media (2 s)F8commands/media.ts
talkbank.loopSegmentToggle Segment LoopF5commands/media.ts

Walker mode

CommandTitleKeybindingHandler
talkbank.walkerNextWalker: Next UtteranceAlt+Downcommands/media.ts
talkbank.walkerPrevWalker: Previous UtteranceAlt+Upcommands/media.ts

Transcription mode

CommandTitleKeybindingHandler
talkbank.startTranscriptionStart Transcription Modecommands/media.ts
talkbank.stampBulletStamp Timestamp BulletF4commands/media.ts
talkbank.stopTranscriptionStop Transcription Modecommands/media.ts
talkbank.configureTranscriptionKeysConfigure Transcription Keybindings…commands/utility.ts

CLAN analysis

CommandTitleHandler
talkbank.runAnalysisRun CLAN Analysis…commands/analysis.ts → analysisPanel.ts
talkbank.runAnalysisOnDirectoryRun CLAN Analysis on Directory…commands/analysis.ts
talkbank.runKidevalRun KidEval…commands/analysis.ts → kidevalPanel.ts
talkbank.runEvalRun Eval…commands/analysis.ts → kidevalPanel.ts
talkbank.runEvaldRun Eval-D (Dementia)…commands/analysis.ts → kidevalPanel.ts
CommandTitleHandler
talkbank.filterBySpeakerFilter by Speaker…commands/utility.ts
talkbank.scopedFindFind in Tier…commands/utility.ts

Coder mode

CommandTitleHandler
talkbank.startCoderStart Coder Mode…commands/utility.ts → coderPanel.ts
talkbank.stopCoderStop Coder Modecommands/utility.ts
talkbank.coderNextCoder: Next Utterancecommands/utility.ts
talkbank.coderInsertCodeCoder: Insert Code…commands/utility.ts

Review mode

CommandTitleHandler
talkbank.startReviewStart Review Modecommands/utility.ts
talkbank.stopReviewStop Review Modecommands/utility.ts
talkbank.reviewNextReview: Next Flaggedcommands/utility.ts
talkbank.reviewPrevReview: Previous Flaggedcommands/utility.ts
talkbank.reviewGoodReview: Rate Goodcommands/utility.ts
talkbank.reviewEarlyReview: Rate Earlycommands/utility.ts
talkbank.reviewLateReview: Rate Latecommands/utility.ts
talkbank.reviewWrongReview: Rate Wrongcommands/utility.ts
talkbank.reviewSkipReview: Skipcommands/utility.ts

Special characters

CommandTitleHandler
talkbank.composeCAInsert CA Special Character…commands/editor.ts
talkbank.composeChatInsert CHAT Special Character…commands/editor.ts
talkbank.cancelComposeCancel Special Character Inputcommands/editor.ts

Validation explorer

Registered directly via vscode.commands.registerCommand in src/activation/validation.ts rather than the effect runtime — the validation explorer tree view owns its own command surface because the tree view’s data model drives context-menu contributions.

CommandTitleHandler
talkbank.validateFileValidate Filevalidation.ts
talkbank.validateDirectoryValidate Directoryvalidation.ts
talkbank.refreshValidationRefresh Validationvalidation.ts
talkbank.clearCacheClear Cachevalidation.ts

Cache management

CommandTitleHandler
talkbank.viewCacheStatisticsView Cache Statisticscommands/utility.ts
talkbank.clearAllCacheClear All Validation Cachecommands/utility.ts

Registration patterns

All 41 non-validation commands register through registerEffectCommand(context, name, effect) in src/activation/commands/*.ts. The effect runtime wraps each handler so failures become typed Effect errors rather than unhandled promise rejections, and the same effect value can compose with other features’ effects without ad-hoc try/catch orchestration. See ADR-002 for the rationale.

A command-registration sanity test (src/activation/commands/__tests__/) walks every contributes.commands entry and asserts one of two things:

  1. The command name is in the effect-registration table, or
  2. The command name is in the validation-explorer allow-list.

Adding a command to package.json without registering it is therefore a compile-or-test failure, not a silent runtime miss.

CHAT format: reference

Look up syntax, contracts, and factual details.

CHAT Format Overview

Status: Reference Last updated: 2026-05-11 21:51 EDT

CHAT (Codes for the Human Analysis of Transcripts) is a standardized transcription format for spoken language data, developed by Brian MacWhinney as part of the CHILDES and TalkBank projects. It is the most widely used format in child language research and conversational analysis.

File Anatomy

Every CHAT file follows this structure:

@UTF8
@Begin
@Languages:	eng
@Participants:	CHI Target_Child, MOT Mother
@ID:	eng|corpus|CHI|2;6.||||Target_Child|||
@ID:	eng|corpus|MOT|||||Mother|||
*MOT:	what do you want ?
%mor:	ADV|what AUX|do PRON|you VERB|want ?
%gra:	1|4|LINK 2|4|AUX 3|4|SUBJ 4|0|ROOT 5|4|PUNCT
*CHI:	I want cookie .
%mor:	PRON|I VERB|want NOUN|cookie .
%gra:	1|2|SUBJ 2|0|ROOT 3|2|OBJ 4|2|PUNCT
@End

A CHAT file consists of:

  1. @UTF8 — required first line, declares UTF-8 encoding
  2. @Begin — marks the start of the transcript
  3. Headers — lines starting with @ that provide metadata (participants, languages, IDs, etc.)
  4. Utterances — blocks consisting of:
    • A main tier (line starting with *SPEAKER:) containing the transcribed speech
    • Zero or more dependent tiers (lines starting with %tier:) containing annotations
  5. @End — marks the end of the transcript

Key Conventions

  • Tab separation: a tab character separates the tier prefix from its content (e.g., *CHI:⟶content)
  • Terminators: every utterance ends with a terminator (., ?, !, or special forms like +...)
  • Line continuation: long lines wrap with a tab at the start of continuation lines
  • Speaker codes: short identifiers — the validator accepts up to seven characters from A-Z, 0-9, _, -, '; three uppercase letters is the convention (e.g., CHI, MOT, FAT, INV)
  • Media linking: timestamps link transcripts to audio/video via bullet markers

CHAT vs Other Formats

FeatureCHATPraat TextGridELAN EAF
Morphological tiersBuilt-in (%mor, %gra)NoNo
Dependency syntaxBuilt-in (%gra)NoNo
Standardized POSUD-style via %morNoNo
Word-level alignment%wor tierInterval-basedInterval-based
Error recoveryTree-sitter GLRN/AN/A

References

Headers

Status: Reference Last updated: 2026-05-11 20:30 EDT

Headers are lines beginning with @ that provide metadata about the transcript. They appear between @Begin and the first utterance (though some headers like @Comment can appear anywhere).

Required Headers

@UTF8

Must be the very first line of every CHAT file. Declares UTF-8 encoding.

@UTF8

@Begin / @End

Mark the start and end of the transcript body. Every CHAT file must have exactly one @Begin and one @End.

@Participants

Declares all speakers in the transcript. Format: CODE [Name] Role, comma-separated. The role is required; the name is optional, so each entry is either CODE Role or CODE Name Role.

@Participants:	CHI Target_Child, MOT Mother, FAT Father
@Participants:	CHI Alex Target_Child, MOT Mary Mother

In the first line, Target_Child, Mother, and Father are roles, not names. In the second line, Alex and Mary are optional names sitting between the speaker code and the role.

Speaker codes are short identifiers — the validator accepts up to seven characters from A-Z, 0-9, _, -, and '. The convention is three uppercase letters; the most common codes are:

  • CHI — target child
  • MOT — mother
  • FAT — father
  • INV — investigator
  • OBS — observer

@ID

Provides detailed metadata for each participant. One @ID line per participant.

@ID:	eng|corpus|CHI|2;6.||||Target_Child|||

Fields (pipe-separated): language, corpus, speaker code, age, sex, group, SES, participant role, education, custom field.

Age format: years;months.days (e.g., 2;6. = 2 years, 6 months).

SES field: ethnicity (White, Black, Asian, Latino, Pacific, Native, Multiple, Unknown), socioeconomic code (UC, MC, WC, LI), or combined with comma separator (e.g., White,MC).

Optional Headers

@Languages

Declares the language(s) used in the transcript.

@Languages:	eng, fra

@Date

Recording date in DD-MON-YYYY format.

@Date:	15-JAN-2024

@Location

Where the recording took place.

@Location:	Boston, MA, USA

@Situation

Description of the recording context.

@Situation:	free play with toys in lab

@Activities

Activities during the recording.

@Activities:	toyplay, reading

@Comment

Free-form comments. Can appear anywhere in the file (before, between, or after utterances).

@Comment:	child was tired during this session

@Media

Links the transcript to an audio or video file.

@Media:	session01, audio

@Transcriber / @Coder

Identifies who created or coded the transcript.

@Transcriber:	JDS
@Coder:	ABC

Header Ordering

Headers should follow this conventional order:

  1. @UTF8 (required, first line)
  2. @Begin (required)
  3. @Languages
  4. @Participants (required)
  5. @ID lines (one per participant)
  6. Other metadata headers (@Date, @Location, etc.)
  7. @Comment lines (can also appear later)

Validation

The parser validates header structure including:

  • @UTF8 must be the first non-empty line
  • @Begin and @End are required and must appear exactly once
  • @Participants is required and must declare all speakers used in utterances
  • @ID participant codes must match @Participants declarations
  • Age format validation in @ID lines

Utterances

Status: Reference Last updated: 2026-05-11 23:22 EDT

An utterance is the fundamental unit of a CHAT transcript. It consists of a main tier (the transcribed speech) followed by zero or more dependent tiers (annotations).

Main Tier

The main tier begins with *SPEAKER: followed by a tab and the utterance content, ending with a terminator.

*CHI:	I want a cookie .

Speaker Codes

Speaker codes are short identifiers (up to seven characters from A-Z, 0-9, _, -, '; three uppercase letters is the convention) matching a code declared in @Participants:

@Participants:	CHI Target_Child, MOT Mother
*MOT:	what do you want ?
*CHI:	cookie .

Terminators

Every utterance must end with a terminator:

TerminatorMeaning
.Declarative (period)
?Question
!Exclamation
+...Trailing off
+..?Trailing-off question
+/.Interruption
+//.Self-interruption
+/?Interrupted question
+!?Broken question
+"/.Quotation follows on next line

Line Continuation

Long utterances wrap to the next line with a leading tab:

*MOT:	well I think that we should probably go to
	the store and get some more cookies .

Content Items

The content between *SPEAKER: and the terminator consists of content items separated by whitespace:

  • Words — regular words, potentially with annotations
  • Groups — bracketed content like <word word> for overlap, retrace, etc.
  • Special forms — pauses (.), events &=laughs, fillers &-uh
  • Separators — commas , and other punctuation

Words

Words are the primary content unit. See Word Syntax for full details.

Groups

Angle brackets < > group words for annotations:

*CHI:	<I want> [/] I want cookie .

Common group annotations:

  • [/] — partial retrace (speaker repeats the same words)
  • [//] — full retrace (speaker restarts with different words)
  • [///] — multiple retracing (multiple false starts)
  • [/-] — reformulation (speaker rephrases with different structure)
  • [?] — uncertain transcription

Special Forms

*CHI:	um (.) I want &-uh cookie .
  • (.) — short pause
  • (..) — medium pause
  • (...) — long pause
  • (1.5) — timed pause in seconds
  • &=laughs — paralinguistic event
  • &-uh — filler

Media Linking

Utterances can include media timestamps (bullets) that link to audio/video:

*CHI:	I want cookies . •1234_5678•

The numbers represent start and end times in milliseconds. The bullets delimiting the pair render as • in most editors; on disk they are the NAK control character (U+0015). See grammar/grammar.js rule bullet.

Dependent Tiers

See Dependent Tiers for documentation on %mor, %gra, %pho, %wor, and other annotation tiers that follow the main tier.

Retraces and Repetitions

Status: Current Last updated: 2026-05-11 23:16 EDT

Retraces mark content that the speaker said but then corrected, repeated, or abandoned. They are one of the most consequential constructs in CHAT because they affect how every dependent tier aligns to the main tier.

CHAT Syntax

A retrace has two parts: the retraced content (what the speaker said first) and the correction (what follows). The retraced content is marked with a trailing bracket code:

MarkerNameMeaning
[/]Partial repetitionSpeaker repeats the same words
[//]Full correctionSpeaker restarts with different words
[///]Multiple correctionMultiple false starts
[/-]ReformulationSpeaker rephrases with different structure

Single-Word Retraces

When only one word is retraced, no angle brackets are needed:

*CHI: I [/] I want that .
*CHI: ana [//] an .
*MOT: the book [/-] the magazine is here .

Group Retraces

When multiple words are retraced, angle brackets delimit the scope:

*MOT: <the dog> [//] the cat ran .
*CHI: <I want> [/] I need cookie .
*CHI: <I want the> [///] give me that .

Retraces with Replacements

A retraced word often has a replacement [: target] and/or error code [* code]. This is common in aphasia and child language corpora where the speaker produces an incorrect form:

*PAR: tika@u [: kitty] [* p:n] [//] kitty is nice .
%mor: noun|kitty aux|be-Fin-Ind-Pres-S3 adj|nice-S1 .

*PAR: lɛɾɪ@u [: later] [* p:n] [//] later in the day .
%mor: adv|late adp|in det|the-Def-Art noun|day .

*CHI: male [: female] [* s:r] [/] male [: female] [* s:r] .
%mor: adj|female-S1 .

In each case, the retraced word (before the [//] or [/]) is excluded from %mor alignment. Only the correction (after the marker) is counted.

Data Model

Retraces are a first-class variant of UtteranceContent:

flowchart TD
    UC["UtteranceContent"]
    UC --> Word
    UC --> RW["ReplacedWord"]
    UC --> Retrace
    UC --> AG["AnnotatedGroup"]
    UC --> Other["...20 other variants"]

    Retrace --> BC["BracketedContent"]
    Retrace --> RK["RetraceKind"]
    BC --> BIW["BracketedItem::Word"]
    BC --> BIRW["BracketedItem::ReplacedWord"]

    style Retrace fill:#f96,stroke:#333

The Retrace struct wraps the retraced content in a BracketedContent container, which can hold any combination of words, replaced words, and other content items:

// crates/core/talkbank-model/src/model/content/retrace.rs
pub struct Retrace {
    pub content: BracketedContent,          // the retraced words
    pub kind: RetraceKind,                  // Partial, Full, Multiple, Reformulation
    pub is_group: bool,                     // <word> [/] vs word [/]
    pub annotations: Vec<ContentAnnotation>,// non-retrace annotations after marker
    pub span: Span,
}

Why First-Class?

Before the retrace refactor, retraces were represented as annotations on words or groups. This meant every match on content had to inspect annotation lists to determine whether a word was retraced. This led to a class of bugs where retraced content was accidentally included in alignment counting, word extraction, or retokenization.

Making Retrace a top-level UtteranceContent variant means:

  1. The compiler enforces handling. Every match on UtteranceContent must have a Retrace arm. Forgetting to handle retraces is a compile error, not a silent runtime bug.
  2. Domain-aware gating is centralized. The content walker checks the Retrace variant once, not at every annotation-inspection site.
  3. Alignment counting is simple. The count function returns 0 for Retrace in Mor domain, no annotation inspection needed.

Parser Conversion

The tree-sitter grammar parses retrace markers ([/], [//], etc.) as annotations on word_with_optional_annotations. The Rust parser converts them to structural Retrace nodes in parse_word_content():

flowchart LR
    subgraph "Tree-sitter CST"
        WOA["word_with_optional_annotations"]
        SW["standalone_word"]
        BA["base_annotations"]
        RP["retrace_partial / retrace_complete / ..."]
        WOA --> SW
        WOA --> BA
        BA --> RP
    end

    subgraph "Rust Model"
        RET["UtteranceContent::Retrace"]
        BC2["BracketedContent"]
        W2["Word or ReplacedWord"]
        RET --> BC2
        BC2 --> W2
    end

    WOA -->|"parse_word_content()\n(word.rs)"| RET

Three cases in parse_word_content():

  1. Word + retrace (I [/]) — wrap Word in BracketedItem::Word inside Retrace
  2. Word + replacement + retrace (tika@u [: kitty] [* p:n] [//]) — build ReplacedWord, then wrap in BracketedItem::ReplacedWord inside Retrace
  3. Word + replacement, no retrace (tika@u [: kitty]) — emit bare ReplacedWord

Group retraces (<content> [/]) are handled in group/parser.rs via the same structural wrapping.

Alignment Behavior

Retraces interact differently with each dependent tier domain:

flowchart TD
    RT["Retrace node\n(e.g. 'tika@u [: kitty] [* p:n] [//]')"]

    RT -->|"Mor domain"| SKIP["SKIP\n(return 0)\nNot morphologically analyzed"]
    RT -->|"Pho domain"| COUNT["COUNT\nPhonologically produced"]
    RT -->|"Sin domain"| COUNT2["COUNT\nGesturally produced"]
    RT -->|"Wor domain"| COUNT3["RECURSE\napply retrace-aware %wor leaf rule"]

    style SKIP fill:#faa,stroke:#333
    style COUNT fill:#afa,stroke:#333
    style COUNT2 fill:#afa,stroke:#333
    style COUNT3 fill:#afa,stroke:#333

Why %mor skips retraces: The %mor tier represents the morphological analysis of what the speaker meant to say. Retraced content is a false start or error — it was produced phonologically but is not part of the intended linguistic structure. The correction after the retrace marker carries the morphological analysis.

Why %pho/%sin/%wor include retraces: These tiers document what was actually produced — the sounds, gestures, and timing of the speech as it happened, including false starts. The retrace was physically spoken, so it appears in these tiers.

For %wor, retrace ancestry does not change leaf-level membership:

  • spoken word tokens count both inside and outside retrace
  • that includes fillers, fragments, nonwords, and untranscribed placeholders
  • overlap annotations do not affect %wor membership

Exact corpus-shaped contrast:

*CHI:	<one &+ss> [/] one play ground .
%wor:	one •321008_321148• ss •321148_321368• one •321809_321969• play •322049_322310• ground •322390_322890• .

*CHI:	&+ih <the what> [/] what's letter &+th is this ?
%wor:	ih •49063_49103• the •49103_49163• what •49183_50205• what's •50205_50405• letter •50405_50685• th •50886_50946• is •50946_51046• this •51086_51586• ?

Implementation

Counting: count_alignable_item() in alignment/helpers/count.rs:

UtteranceContent::Retrace(retrace) => {
    if domain == TierDomain::Mor {
        0  // excluded from morphological alignment
    } else {
        count_bracketed_alignable_content(&retrace.content, domain, true)
    }
}

Walking: walk_words() in alignment/helpers/walk/mod.rs:

UtteranceContent::Retrace(retrace) => {
    if !matches!(domain, Some(TierDomain::Mor)) {
        walk_bracketed_content(&retrace.content.content, domain, f);
    }
}

%wor generation and overlap counting still use dedicated recursive helpers, but now for %wor-specific sequencing details like replacement handling rather than for retrace-sensitive membership.

Validation

Cross-Utterance Retrace Validation

The retrace validators in validation/retrace/ check:

  • Collection: collection/utterance.rs and collection/bracketed.rs walk the content tree to find all Retrace nodes
  • Detection: detection.rs provides utterance_item_has_retrace() for quick retrace presence checks

Alignment Validation (E705)

E705 fires when the main tier has more alignable items than %mor. If retraces are correctly parsed as Retrace nodes, they are excluded from the count and E705 does not fire. If a retrace is accidentally parsed as a bare ReplacedWord (the bug fixed in c90b9bf), it is counted and triggers a false E705.

Regression Tests

tests/retrace_replaced_word_regression.rs contains 6 targeted tests:

TestPatternVerifies
single_word_retrace_with_replacement_fullword [: repl] [* err] [//]Retrace wraps ReplacedWord
single_word_retrace_with_replacement_partialword [: repl] [* err] [/]Partial retrace with replacement
single_word_retrace_with_replacement_multipleword [: repl] [* err] [///]Multiple retrace with replacement
single_word_retrace_with_replacement_no_error_markerword [: repl] [///]No [*] still produces Retrace
single_word_retrace_without_replacementword [//]Baseline (no replacement)
retrace_with_replacement_does_not_cause_e705Full pipeline with %morNo false E705

Reference corpus entries: resources/corpus/reference/annotation/retrace.cha

See Also

Replacements

Status: Current Last updated: 2026-05-11 23:14 EDT

A replacement is a CHAT annotation [: ...] that pairs a single spoken word on the main tier with one or more “intended” words. It records both what the speaker actually said and what the analysis should treat the utterance as containing.

*CHI:	wanna [: want to] go .
*CHI:	dis [: this] is fun .
*CHI:	rocking+house [: rocking+horse] [*] ?

This page is the canonical reference for what replacements mean in TalkBank — both as a CHAT-manual construct and as a typed AST in this repo. The most important load-bearing fact, which the rest of the page expands on:

Replacements are word-level, not group-level. Each tier domain chooses one side of the pair: %mor analyzes the replacement (right side); %wor, %pho, %sin align to the original (left side). %gra follows %mor.

CHAT Syntax

Word-Level Scope

A replacement attaches to a single standalone_word on the main tier and contains one or more replacement words inside the brackets:

*CHI:	gonna [: going to] eat lunch .
*CHI:	dis [: this] toy .
*CHI:	rocking+house [: rocking+horse] [*] ?

The grammar rules are word_with_optional_annotations and replacement in grammar/grammar.js — grep for the rule names rather than line numbers so this stays accurate as the grammar evolves. Replacement words can be separated by whitespace, so [: going to] is a single replacement of gonna with two words.

There Is No Group-Level Replacement

<dat is> [: that is] is not valid CHAT. A replacement does not attach to a group; it attaches to a single word. The grammar enforces this by typing: ReplacedWord.word: Word, never Group. To replace words inside a group, attach the replacement to the inner word:

*CHI:	<dat [: that] is> [/] is broken .

This shape — replacement inside a group inside a retrace — is legal because each annotation operates at its own scope.

There Is No [::] Form

Some literature on CHILDES tooling references a [::] annotation; it does not exist in this repo’s grammar, parser, or model, and is not defined by the current CHAT manual. Only [:] exists. If you encounter [::] in legacy data, treat it as a parse error to investigate, not a construct to support.

The Per-Domain Alignment Rule

This is the rule contributors most often get wrong. Different tier domains align to different sides of a replacement pair:

TierSide aligned toRationale
%morreplacement (right)Morphosyntactic analysis annotates the target form, not the error
%grareplacement (right)Grammatical relations align to %mor’s structure
%wororiginal (left)Word-level timing is for what was actually spoken
%phooriginal (left)Phonological transcription describes what was actually spoken
%sinoriginal (left)Spelling-in-actual describes the original surface form

The mnemonic: the replacement encodes the intended form (what the speaker meant or what a corrected transcript would read). Tiers analyzing intent (%mor/%gra) use the replacement; tiers documenting realization (%wor/%pho/%sin) use the original.

flowchart LR
    spoken["Original word\n(left of [:)\n'dis'"]
    target["Replacement words\n(inside [: ])\n'this'"]

    spoken -->|"%wor (timing)"| wor["%wor: dis"]
    spoken -->|"%pho (phonology)"| pho["%pho: dɪs"]
    spoken -->|"%sin (spelling)"| sin["%sin: dis"]
    target -->|"%mor (UD parse)"| mor["%mor: pron|this"]
    target -->|"%gra (paired with %mor)"| gra["%gra: 1|0|ROOT"]

For multi-word replacements like gonna [: going to], the rule generalizes consistently:

  • %wor / %pho / %sin produce one entry — for gonna.
  • %mor produces two entries — for going and to.
  • %gra produces two entries — paired to the two %mor items.

The alignment-counting code that enforces this is in alignment/units.rs — look for the UtteranceContent::ReplacedWord arm. The full table of per-domain rules is in resources/spec/docs/ALIGNMENT_RULES.md.

Rust AST

A replacement is modeled as a first-class UtteranceContent variant, not as a flag on Word:

// crates/core/talkbank-model/src/model/annotation/replacement.rs
pub struct ReplacedWord {
    pub word: Word,                       // left side: original spoken word
    pub replacement: Replacement,         // right side: 1+ intended words
    pub scoped_annotations: ReplacedWordAnnotations,
}

Two consequences of this shape:

  1. A replacement is a wrapper around a Word, not a kind of Word. ReplacedWord lives as its own variant of UtteranceContent (and BracketedItem), holding an inner word: Word plus the replacement payload. Contrast with retraces: Retrace is also a variant of UtteranceContent/BracketedItem, but it wraps a group of content (a single word or a <...> group), not a single Word. Different mechanism, different scope — same top-level slot in the AST.
  2. The walk_words() content walker yields WordItem::ReplacedWord as a distinct leaf (defined in crates/core/talkbank-model/src/alignment/helpers/walk/mod.rs). Domain-aware extraction code branches on this leaf type and chooses original or replacement per the table above.

Validation

Each Replacement Word Is Validated Like a Main-Tier Word

The replacement is a Vec<Word>. Each Word inside it goes through the same validator that runs on main-tier words:

*CHI:	dog [: C-3PO] .

This produces [E220] "C-3PO" is not a legal word in language(s) "eng": numeric digits not allowed — exactly as if C-3PO had appeared on the main tier directly. The replacement does not provide an escape from word-level validation. The implementation is in replacement.rs:117-202.

This is critical for any code generating replacements programmatically: do not assume [: ...] lets you smuggle arbitrary text past the word validator. If your producer emits a replacement, both sides must be CHAT-legal under the utterance’s declared language.

Replacement-Specific Error Codes

Three error codes are specific to replacements and do not apply to main-tier words:

CodeMeaning
E208Empty replacement [:] (no words provided between : and ])
E390Replacement contains an omission (0prefix form) — disallowed inside replacements
E391Replacement contains untranscribed material (xxx, yyy, www) — disallowed inside replacements

The principle: a replacement must be a concrete intended form. Empty, omitted, or unintelligible content defeats that purpose.

Interactions with Other Annotations

Replacements and Retraces Are Orthogonal

A retrace ([/], [//], [///], [/-]) and a replacement ([:]) are distinct annotations operating at different structural levels:

  • Retraces wrap content (a single word or a group). They are first- class UtteranceContent variants and represent post-hoc speaker correction.
  • Replacements attach inside a Word slot via ReplacedWord. They are editorial metadata about an individual spoken word.

Both can coexist:

*CHI:	<dat [: that] is> [/] is broken .   (replacement inside retrace)

A retrace cannot live inside a replacement (the grammar wraps replacements around standalone_word, not arbitrary content).

Replacements and Error Coding

Error codes follow the replacement and operate on the replaced word as a unit:

*CHI:	rocking+house [: rocking+horse] [*] ?

Here [*] marks rocking+house as containing a phonological/lexical error; the [: rocking+horse] records the intended form. The two annotations cooperate: the replacement encodes what was meant, the error code classifies how it deviates. Implementation: scoped_annotations field on ReplacedWord.

Common Misconceptions

These are bugs we have repeatedly written down then forgotten — recording them here so future contributors don’t reinvent them.

  1. “[: ...] lets me put any text I want.” No. Each replacement word is validated. [: C-3PO] fails E220 in English just as C-3PO would.
  2. “[:] is the right mechanism for ASR sanitization.” Usually no. ASR-introduced normalization typically wants [% ...] (free- form comment) or [= ...] (free-form explanation), neither of which validates word grammar. Use [:] only when you have a concrete CHAT-legal intended form.
  3. “%mor analyzes the original.” No. %mor analyzes the replacement. This is the correction’s morphology, not the error’s.
  4. “%wor count must equal %mor count.” No. For gonna [: going to], %wor has 1 entry and %mor has 2. They align to different sides. The validator’s per-domain rule respects this.
  5. “<a b> [: c d] is a group-level replacement.” No. Group-level replacements don’t exist. Either replace inside (<a [: c] b [: d]>) or rephrase the transcription.

Source Citations

ConcernFile:line
Grammar rule (replacement)grammar/grammar.js:1341-1352
Word-with-replacement rulegrammar/grammar.js:1063-1071
ReplacedWord structcrates/core/talkbank-model/src/model/annotation/replacement.rs (search pub struct ReplacedWord)
Per-domain alignmentcrates/core/talkbank-model/src/model/file/utterance/metadata/alignment/units.rs (search UtteranceContent::ReplacedWord)
Replacement validationcrates/core/talkbank-model/src/model/annotation/replacement.rs (search impl ... Validate for ReplacementWords)
Reference corpus exampleresources/corpus/reference/annotation/errors-and-replacements.cha
CHAT manualhttps://talkbank.org/0info/manuals/CHAT.html#Replacement_Scope

See Also

  • Retraces and Repetitions — the orthogonal post-hoc correction mechanism.
  • The %mor Tier — UD-syntax morphosyntactic analysis that aligns to the replacement form.
  • Word Syntax — the word grammar that replacement words must satisfy.
  • Dependent Tiers — overview of %mor, %wor, etc., with their alignment relationships.

Untranscribed Markers: xxx, yyy, www

Status: Reference Last updated: 2026-05-11 23:21 EDT

CHAT reserves three short word-level markers for material the human transcriber cannot or chose not to render as words on the main tier. Each one has a specific meaning. Tools that emit CHAT — including ASR pipelines, format converters, and editor heuristics — must respect those meanings, because every downstream consumer (researchers, validators, and aggregate-statistics tools like CLAN’s freq, kideval, mlu) reads them at face value.

MarkerMeaningEmitter
xxxTranscriber listened to the audio and could not make out what was said. The speech is unintelligible to the human ear at this point.Human transcriber only.
yyyTranscriber heard a discrete utterance but could not write it as ordinary CHAT words. Used when the surface form resists orthography (mumbled, slurred, foreign with no equivalent). The phonetic content typically appears on the %pho tier.Human transcriber only.
wwwTranscriber chose not to transcribe this stretch — usually for privacy, off-topic content, or because the segment is irrelevant to the corpus’s purpose.Human transcriber only.

The shared property: each marker is the human transcriber telling later readers something specific about their experience listening to the audio. None of them mean “tooling could not process this token”.

Why this matters

When a researcher loads a CHAT corpus and counts xxx occurrences, the result is a measure of human listening difficulty: it tells them how much of the audio resisted human transcription. That number feeds into methodology decisions (“can we get reliable MLU from this corpus?”, “what’s the noise floor on this child’s speech?”, “should we re-record in a quieter environment next time?”). It is a load-bearing signal in language-development research.

If an ASR pipeline emits xxx whenever it can’t sanitize a token — for example, substituting xxx for any word that fails CHAT validation under a strict language profile — every xxx count in the corpus becomes a meaningless mixture of “human couldn’t tell” and “pipeline gave up”. Researchers then reading those counts are silently misled. The signal is destroyed for the entire history of that corpus, because the corruption is indistinguishable from real unintelligibility once committed.

The same reasoning applies to yyy and www. A converter or post-processor that emits any of these three markers because the tooling couldn’t handle a token is committing semantic vandalism against the whole field.

Rules for tooling

  1. Never emit xxx, yyy, or www from a tool to mean “could not process”. These markers are reserved for human transcriber judgment.
  2. When a token cannot be validated as legal CHAT under the declared language, prefer one of:
    • Pass the token through verbatim and let the CHAT validator (or CLAN’s check) flag it for human review. The transcriber listens, decides, and corrects.
    • Fail loud — abort the file rather than emit corrupted output.
    • Apply only purely orthographic, semantically null repairs (e.g., stripping a stray boundary quote mark from "My). These are safe because no information is lost.
  3. Never sanitize a token by replacing it with one of the three markers. That is exactly the corrupting behavior this document prohibits.
  4. Never delete a token to “fix” a validation failure. Deletion loses data without any flag.

What ASR pipelines should do instead

The transcript_from_asr_utterances gate (defined in crates/core/talkbank-transform/src/asr_postprocess/cleanup.rs, exercised by the pipeline assembled in crates/core/talkbank-transform/src/build_chat/) demonstrates the right pattern:

  1. The pipeline silently fixes only orthographically inarguable problems (Stage 2c boundary-quote strip).
  2. For tokens that fail language-level validation but are structurally legal CHAT (e.g., C-3PO under English: tree-sitter accepts the digit-hyphen compound but Word::validate fires E220 “numeric digits not allowed”), the gate falls back to the structural-only construction path. The token ships verbatim. The downstream full-file validator and check fire E220 on the same word, the file ends up in the human review queue, and the transcriber listens to the audio and decides what was actually said.
  3. For tokens that fail structural parsing (tree-sitter rejects), the gate fails loud — emitting malformed CHAT would corrupt the file beyond the validator’s ability to flag it.

The division of labor is: ASR fixes only what’s mechanically unambiguous; CHECK and the human transcriber handle everything that requires judgment about what the speaker said.

  • xxx / yyy / www survive the transcript through all NLP passes (morphotag, utseg, translate, coref) without re-interpretation. Tools that walk the AST treat them as opaque tokens — they have no POS tag, no lemma, no dependency parent, no translation.
  • %wor excludes all three (no phoneme sequence to align). %pho may reference yyy directly because the phonetic content is the whole point of the marker.
  • See word-syntax.md for grammar; this document is the policy reference for who is allowed to emit them and why.

Postcodes ([+ ...])

Status: Reference Last updated: 2026-05-11 23:01 EDT

A postcode is a tagged annotation token that attaches to an utterance as a whole and appears after the terminator. The canonical CHAT syntax is [+ <text>]. Postcodes carry researcher / analysis tags about the utterance — whether it should be excluded from analysis, how it should be coded, what kind of speech act it represents — without modifying the utterance’s word content.

Syntax and Scope

*CHI:   I want cookie .  [+ exc]
*MOT:   what did you say ?  [+ imp]
*CHI:   no I don't want it !  [+ neg] [+ trn]

Three structural facts to internalize:

  1. Postcodes attach to the utterance, not to a word. They sit after the terminator, on the main tier, alongside (but distinct from) any utterance-level bullet. Unlike word-scoped annotations ([: ...] replacement, [% ...] comment, [= ...] explanation, [* ...] error code), a postcode does not modify the interpretation of any single word — it tags the whole utterance.
  2. Multiple postcodes may follow a single terminator. They are ordered, but the order is not semantically privileged.
  3. The body is free-form text. The CHAT word grammar is not applied to postcode contents. Researchers can write arbitrary tags, codes, descriptions, comments, or analytic notes. The model stores the raw text and leaves interpretation to downstream tooling and conventions.

Common Postcodes — Empirical Survey

The postcode vocabulary is open-ended: the CHAT format imposes no closed set, and an audit of every [+ ...] token across a JSON-mirrored snapshot of the TalkBank corpora (~99k files, 23+ data-repo families) found 488 distinct values in active use.

The findings split into three tiers ranked by repo spread (in how many distinct corpus families the code appears) — the more useful ranking than raw count, because high-count codes can be concentrated in a single corpus.

Tier 1 — Cross-corpus codes (in 7+ repos)

These are the conventions every CHAT consumer should expect to encounter across collections:

PostcodeRepo spreadTotal occurrencesMeaning
[+ gram]13~3,100Grammatical — utterance is grammatically well-formed for purposes of the analysis.
[+ exc]9~26,900Exclude utterance from analysis. The utterance is preserved in the transcript but tagged so analytic tools (CLAN’s freq, mlu, etc.) skip it.
[+ bch]9~10,000Backchannel — listener-side acknowledgement (mhm, yeah) that should not be counted as a substantive turn.
[+ trn]7~3,800Translation utterance.

Tier 2 — Multi-corpus protocol codes (in 4-6 repos)

Codes deployed across several CHILDES sub-collections, typically encoding picture-narration / story-reading / imitation experimental conditions. Substantial raw counts (often tens of thousands), but their meaning is set by the originating protocol — consult per-corpus documentation rather than assuming a global definition:

PostcodeRepo spreadTotal occurrences
[+ SR]5~31,000
[+ IN]5~24,500
[+ PI]5~22,700
[+ R]4~16,200
[+ I]4~10,500
[+ nv]4~3,300
[+ imit]4~3,200

Tier 3 — Single-corpus and long-tail codes

About 80% of the 488 distinct values appear in one repo only. The single-corpus codes include high-volume protocol vocabularies (e.g. [+ uncued] ~19,500 in one repo, [+ NAC] ~3,500 in one repo, [+ diary] ~2,800 in a Romance/Germanic diary-study collection, [+ noatt] ~2,300 in one repo, [+ inter-utter-switch] ~720 flagging code-switching turns).

The long tail also includes researcher-private notes, typos that survived check, and per-study coding schemes. Tooling MUST treat any unknown postcode value as opaque text — the corpus author may know what it means, the format does not.

Caveats

  • Numbers are from a snapshot audit and will drift as corpora are added or revised. Treat the broad shape (open vocabulary, ~4 truly cross-corpus codes, ~10 multi-corpus protocol codes, ~hundreds of single-corpus or long-tail codes) as the load-bearing finding, not the exact counts.
  • “Repo spread” counts data-repo families, not individual files. Two corpora curated by the same group inside one data-repo count as one for spread; researchers using the same code in two different family-of-corpora packages count as two.
  • The CHAT manual remains the source of truth for standard conventions. The empirical survey above shows what is actually deployed; when ingesting a new corpus, consult its own documentation for the postcodes in use.

What Postcodes Are NOT

Postcodes are easy to confuse with several other CHAT annotation forms because they all use square brackets. The differences are substantive and load-bearing.

FormScopeBody validationPurpose
[+ ...]Utterance-level (this doc)None — free textResearcher / analysis tag attached to the whole utterance
[: ...]Word-levelReplacement words ARE validated as CHAT wordsSanctioned-form correction of the preceding word (see replacements.md)
[% ...]Word-levelNone — free textFree-form comment about the preceding word or local span
[= ...]Word-levelNone — free textExplanation of unclear / non-standard speech (often paired with xxx / yyy placeholders)
[* ...]Word-levelNone — error code textError coding for the preceding word, optionally with a structured code

Two consequences worth pinning down explicitly:

  • A postcode cannot carry per-word semantics. If you want to attach a comment, replacement, or error code to a single word, use the appropriate word-scoped form. Stretching a postcode to mean “this word is X” loses the per-word position downstream tools depend on.
  • A word-scoped annotation cannot tag an utterance. If you want to mark an entire utterance for exclusion or translation, use a postcode. A [% exclude this] after a word does not mean “exclude the utterance” to any consumer.

Not Postcodes: Quotation Markers

Quotation marking in CHAT is not a postcode form. The constructs +"/. (quotation end), +"/, and +" (quotation linkers / continuations) are tier-level terminators and linkers, not [+ ...] postcodes — the grammar rule postcode in grammar/grammar.js is strictly [+ <text>], and the quotation forms live under separate grammar rules (quoted_new_line, linker_quotation_follows).

See Utterances → Terminators for the syntactic forms, and the talkbank-model::validation::cross_utterance validator family (gated by ValidationContext::enable_quotation_validation) for the cross-utterance balance checks.

A walker in talkbank-model::validation::utterance::quotation (check_quotation_balance) does scan the postcode list for text "/ and "/., but a sweep over the data-json corpus mirror (101,414 files, 2026-05-11) returned zero such postcodes — that code path is effectively dead, retained presumably as defence against hand-edited oddities. The real quotation-balance work happens in the cross-utterance family above.

Position in the AST

An utterance’s main tier is MainTier, whose content: TierContent field carries the actual tier payload — including postcodes — as a typed list:

pub struct MainTier {
    pub speaker: SpeakerCode,
    pub content: TierContent,
    // spans omitted for brevity
}

pub struct TierContent {
    pub linkers: TierLinkers,                  // utterance-leading +<, ++, etc.
    pub language_code: Option<LanguageCode>,   // [- code]
    pub content: TierContentItems,             // word-level items (newtype over Vec<UtteranceContent>)
    pub terminator: Option<Terminator>,        // ., ?, !, +..., etc.
    pub postcodes: TierPostcodes,              // [+ ...] tokens after the terminator
    pub bullet: Option<Bullet>,                // optional terminal media bullet
    // content_span omitted for brevity
}

(See talkbank-model/src/model/content/main_tier.rs and tier_content.rs for the exact shape; the same TierContent type is shared by dependent tiers, so the postcode slot exists on every tier even though only main-tier postcodes are conventional.)

Because postcodes live at the utterance level, the per-word traversal helpers (walk_words, walk_words_mut) do not visit them. Code that needs to read or rewrite postcodes accesses the list directly.

The model stores postcode text as SmolStr and preserves it verbatim through CHAT roundtrips. Downstream tooling — including CLAN command implementations such as freq, mlu, kideval — is responsible for interpreting individual postcode values per its own conventions.

Tooling Rules

Tools that emit or consume CHAT must respect the scope distinction.

  • Emitters: when adding a researcher tag to an utterance, attach a Postcode to the utterance’s MainTierContent, not a ContentAnnotation to a word. Both serialize, but only the former reaches downstream consumers as utterance-level metadata.
  • Consumers: when reading utterance-level tags (e.g., implementing an “exclude” filter), iterate main.content.postcodes on each utterance — not the word-level annotations in UtteranceContent. The two lists are populated by different parser branches and have different semantics.
  • Round-trip preservers (extract→modify→inject pipelines such as the NLP injection passes in crates/batchalign-*): preserve the postcode list unchanged. None of the standard NLP passes have a reason to add, remove, or reorder postcodes.

References

Dependent Tiers

Status: Reference Last updated: 2026-04-09 08:40 EDT

Dependent tiers appear on lines beginning with % immediately after an utterance. They provide annotations linked to the main tier content.

CHAT defines four structural categories of dependent tiers:

  1. Structured linguistic tiers — parsed into typed AST nodes with word-level alignment
  2. Phon phonological tiers — syllabification and segmental alignment from the Phon project
  3. Bullet-content tiers — free-form text with optional inline timing markers
  4. Text tiers — plain text with no structural alignment

Structured Linguistic Tiers

These tiers have rich, parsed representations in the data model. Each token aligns 1-to-1 with an alignable word on the main tier (excluding retraces, pauses, and events). Terminators (., ?, !) must match the main tier terminator.

%mor — Morphological Analysis

The %mor tier carries part-of-speech tags, lemmas, and morphological features for each word on the main tier. See The %mor Tier for full documentation covering the UD-style format, data model, divergences from Universal Dependencies, and migration from traditional CHAT MOR.

Format: POS|lemma[-Feature]*, with ~ separating post-clitics.

*CHI:	she's eating cookies .
%mor:	PRON|she~AUX|be-Pres-S3 VERB|eat-Prog NOUN|cookie-Plur .

%gra — Grammatical Relations

The %gra tier encodes dependency syntax using Universal Dependencies relation labels. Each entry has the format index|head|relation, where indices are 1-based and head 0 indicates ROOT.

*CHI:	I want cookies .
%mor:	PRON|I VERB|want NOUN|cookie-Plur .
%gra:	1|2|SUBJ 2|0|ROOT 3|2|OBJ 4|2|PUNCT

The %gra tier aligns with %mor chunks (clitics expand into multiple chunks). Validation checks sequential indices (E721), ROOT structure (E722 missing root, E723 multiple roots), and circular dependencies (E724).

%pho / %mod — Phonological Transcription

The %pho tier records actual pronunciation; %mod records target/model pronunciation. Both use the same format: space-separated phonetic tokens aligned 1-to-1 with main tier words.

*CHI:	I want three cookies .
%pho:	aɪ wɑnt fwi kʊkiz .
%mod:	aɪ wɑnt θri kʊkiz .

Phonological tiers support IPA, UNIBET, X-SAMPA, or custom notation systems. They are used for child language, speech disorders, L2 learning, and dialectal variation studies.

Parsing strategy: We deliberately parse only the minimal word/group-level structure in %pho and %mod needed for coarse alignment with the main tier. The full IPA phoneme content is stored as opaque strings — deep phonological analysis is handled by Phon, and we avoid duplicating that work. The Phon extension tiers (%modsyl, %phosyl, %phoaln) follow the same strategy.

%sin — Gesture and Sign Annotation

The %sin tier codes gestures and signs aligned with speech. Each token is either 0 (no gesture) or g:referent:type (e.g., g:ball:dpoint for a deictic point at a ball).

*CHI:	that ball .
%sin:	g:ball:dpoint 0 .

Multiple simultaneous gestures use bracket grouping: 〔g:toy:hold g:toy:shake〕.

%wor — Word Timing

The %wor tier carries word-level timing annotations for media synchronization. Words may include inline bullets with millisecond timestamps. Word text is display-only (“eye candy”) — timing data comes from the bullet fields.

%wor is not a flat “all tokens except punctuation” tier. It follows a word-level alignment rule:

  • Regular words count.
  • Fillers (&-um, &-uh, &-you_know) count — they are real spoken words with known phoneme sequences.
  • Fragments (&+...) do NOT count — incomplete phoneme sequences; the FA engine cannot reliably anchor partial phonological material.
  • Nonwords (&~...) do NOT count — interactional/gestural sounds without stable lexical phoneme content for alignment.
  • Untranscribed placeholders (xxx, yyy, www) do NOT count — they have no known phoneme sequence; CTC forced alignment cannot produce timings for unknown material.
  • Replacements keep the original spoken word slot for %wor; the replacement text matters for %mor, not %wor. If the original slot is untranscribed or a fragment/nonword, it is still excluded.
  • Retrace scope does not change %wor membership.
  • Overlap markers do not change %wor membership.

%wor is a timing-annotation tier. Its word count equals the number of Wor-domain words and may differ from a naive main-tier word count. There is no downstream positional indexing into %wor; the %wor count is not validated against the main-tier word count.

*CHI:	I want cookies .
%wor:	I want cookies .

Exact corpus-shaped contrast:

*CHI:	<one &+ss> [/] one play ground .
%wor:	one •321809_321969• play •322049_322310• ground •322390_322890• .
# &+ss is a fragment — excluded from %wor regardless of retrace context.

*EXP:	&+ih <the what> [/] what's letter &+th is this ?
%wor:	the •49103_49163• what •49183_50205• what's •50205_50405• letter •50405_50685• is •50946_51046• this •51086_51586• ?
# Fragments &+ih and &+th excluded; regular words remain.

*EXP:	what's is dis [: this] ?
%wor:	what's •37050_37471• is •37491_37631• dis •37631_38131• ?

*CHI:	xxx snack .
%wor:	snack •884668_885168• .
# xxx has no phoneme sequence — excluded from %wor; only snack appears.

*CHI:	&~um a boat .
%wor:	a •1073779_1073799• boat •1076861_1077361• .
# &~um is a nonword — excluded from %wor.

*CHI:	&-mm [<] bananas are good .
%wor:	mm •1949506_1949566• bananas •1949566_1949766• are •1949846_1949987• good •1950067_1950567• .
# &-mm is a filler — included in %wor (real spoken word with alignable phoneme sequence).
flowchart TD
    A["Main-tier word candidate"] --> B{"Timestamp token /\nomission / empty?"}
    B -->|Yes| OUT["Excluded from %wor"]
    B -->|No| C{"Untranscribed?\n(xxx/yyy/www)"}
    C -->|Yes| OUT
    C -->|No| D{"Fragment or nonword?\n(&+ or &~)"}
    D -->|Yes| OUT
    D -->|No| IN["Counts for %wor\n(word or filler &-)"]

    style IN fill:#afa,stroke:#333
    style OUT fill:#faa,stroke:#333

Phon Phonological Tiers

These tiers originate from the Phon project and provide syllable-annotated phonological transcription and segmental alignment. They were originally serialized as %x-prefixed user-defined tiers (%xmodsyl, %xphosyl, %xphoaln) and are being promoted to official CHAT tiers. Phon stores phonological data in its own XML format; the CHAT representation is generated by PhonTalk.

%modsyl / %phosyl — Syllabified Phonology

%modsyl is a syllabified version of %mod (target pronunciation); %phosyl is a syllabified version of %pho (actual pronunciation). Each phoneme is annotated with a syllable position code (N=nucleus, O=onset, C=coda, etc.). Words are space-separated and align 1-to-1 with the corresponding %mod or %pho tier.

*CHI:	the best .
%mod:	ðə bɛst .
%modsyl:	ð:Oə:N b:Oɛ:Ns:Ct:C .
%pho:	ðə bɛs .
%phosyl:	ð:Oə:N b:Oɛ:Ns:C .

Alignment: Content-based — stripping position codes (:N, :O, :C, etc.) and stress markers (ˈ, ˌ) from %modsyl should yield the same phonemes as %mod. Same for %phosyl → %pho.

%phoaln — Phone Alignment

%phoaln provides segmental alignment between target and actual IPA, showing phoneme-by-phoneme correspondence. Each pair uses source↔target notation; ∅ marks insertions or deletions.

*CHI:	the best .
%phoaln:	ð↔ð,ə↔ə b↔b,ɛ↔ɛ,s↔s,t↔∅

Alignment: Positional, word-by-word — word N in %phoaln aligns with word N in both %mod and %pho.

Parsing strategy: Same as %pho/%mod — we parse just enough structure for alignment (word boundaries for %modsyl/%phosyl, alignment pairs for %phoaln). IPA phoneme content is treated as opaque strings.

Validation (E725–E728)

Because these are derived views, word counts must match between each syllabification tier and its parent IPA tier:

CheckError code
%modsyl word count ≠ %mod word countE725
%phosyl word count ≠ %pho word countE726
%phoaln word count ≠ %mod word countE727
%phoaln word count ≠ %pho word countE728

These checks are gated on ParseHealth — if either tier in a pair has parse errors, the alignment check is suppressed to avoid false positives.

Known PhonTalk Export Issue

The PhonTalk XML→CHAT converter writes %mod/%pho through a OneToOne alignment path that maps IPA words to orthography words and silently drops extras. The syllabification tiers (%modsyl, %phosyl, %phoaln) bypass this path and include all IPA words. In child phonology data where children produce more IPA words than orthographic targets (~4% of Phon corpus files), this creates tier-to-tier word count mismatches. The mismatches originate in the Phon XML source data (orthography↔IPA word count discrepancies) and are inconsistently handled during CHAT export. This is being investigated in collaboration with the Phon team.

Bullet-Content Tiers

These tiers contain free-form text with optional embedded timing markers (•START_END•) and picture references (•%pic:"file.jpg"•). They do not align word-by-word with the main tier.

TierPurpose
%actPhysical actions, gestures, non-verbal behaviors
%codResearch-specific coding (semantic roles, thematic coding, error classification)
%comComments, annotations, and contextual notes
%expExplanations or expansions of ambiguous/incomplete speech
%addAddressee identification in multi-party conversations
%spaSpeech act coding (request, assertion, question, directive)
%sitSituational context or setting description
%gpxExtended gesture position coding
%intIntonational contours and prosodic patterns

%cod remains bullet-content in the shared TalkBank AST, but current clan-core coding commands derive a semantic %cod item stream on top of that parsed content so selectors such as <w4> scope the following code instead of being counted as codes themselves.

Example with timing:

*CHI:	gimme that .
%act:	reaches toward shelf
%com:	child is pointing to picture

Text Tiers

These tiers contain plain text with no bullets, timing, or structural alignment:

TierPurpose
%altAlternative transcriptions
%cohCohesion annotation
%defDefinitions
%engEnglish translations (for non-English transcripts)
%errError annotations
%facFacial expressions
%floFlow annotation
%glsGlosses
%ortOrthographic representations
%parParalinguistic information
%timTiming information

User-Defined Tiers

Tiers prefixed with %x (e.g., %xcod, %xact) are user-defined dependent tiers. They are preserved during parsing and roundtrip but receive no structural validation beyond basic format checks.

The %mor Tier: Morphological Analysis

Status: Reference Last updated: 2026-05-11 20:35 EDT

The %mor (morphological) dependent tier provides word-by-word morphosyntactic annotation aligned with the main tier. Each main-tier word receives a morphological code specifying part of speech, lemma, and grammatical features.

Format Overview

*CHI:	I want cookies .
%mor:	pron|I-Prs-Nom-S1 verb|want-Fin-Ind-Pres-S1 noun|cookie-Plur .

Each %mor item has the structure POS|lemma[-Feature]*, where:

  • POS — part-of-speech category (noun, verb, pron, det, aux, etc.)
  • | — pipe separator (always present)
  • Lemma — base form of the word (cookie, be, I). May contain language-specific compound or derivational boundary markers (see Compound Lemma Boundaries below)
  • Features — zero or more morphological features, each preceded by - (-Plur, -Fin-Ind-Pres-S3)

Items are space-separated and terminate with a punctuation marker (., ?, !, etc.).

The UD MOR Format

TalkBank’s %mor tier uses a format inspired by Universal Dependencies (UD) but adapted to CHAT conventions. We call this the UD MOR format to distinguish it from the older CLAN-era MOR format.

The UD MOR format was introduced via batchalign’s Stanza-based morphosyntax pipeline. Stanza produces standard UD analysis (UPOS, lemma, morphological features, dependency relations), and the Rust mapping layer converts this to CHAT %mor and %gra tiers. The new format has been adopted for all new corpus annotation.

Structure: Flat POS|lemma[-Feature]*

Every morphological word is flat — a single POS tag, a single lemma, and a linear chain of features:

POS|lemma[-Feature1][-Feature2][-Feature3]...

There are no compounds, prefixes, subcategories, or nested structures in the UD MOR format. The entire morphological analysis of a word is captured by the POS+lemma+features triple.

Examples:

Word%mor codePOSLemmaFeatures
dognoun|dognoundog(none)
dogsnoun|dog-PlurnoundogPlur
runningverb|run-Part-Pres-SverbrunPart, Pres, S
isaux|be-Fin-Ind-Pres-S3auxbeFin, Ind, Pres, S3
Ipron|I-Prs-Nom-S1pronIPrs, Nom, S1
thedet|the-Def-ArtdettheDef, Art

Multi-Word Tokens (Clitics)

English contractions and similar multi-word tokens (MWTs) are represented using the tilde (~) separator for post-clitics:

*CHI:	it's red .
%mor:	pron|it~aux|be-Fin-Ind-Pres-S3 adj|red .

Here it's is a single main-tier word that expands to two morphological words: pron|it (main) and aux|be-Fin-Ind-Pres-S3 (post-clitic). The ~ indicates the two MOR words are fused into one orthographic token.

Each clitic counts as its own chunk for %gra alignment — pron|it~aux|be-Fin-Ind-Pres-S3 produces 2 chunks, each needing its own grammatical relation.

Terminator

The %mor tier ends with a terminator that matches the main tier’s utterance terminator:

*CHI:	what is that ?
%mor:	pron|what aux|be-Fin-Ind-Pres-S3 det|that ?

The terminator (., ?, !, +..., etc.) counts as one chunk for %gra alignment.

How It Diverges from UD

The UD MOR format is UD-inspired but not UD-compliant. Several deliberate adaptations make it fit CHAT conventions while preserving most UD information. This section catalogs every divergence.

1. POS Tags Are Lowercased UPOS

UD uses uppercase UPOS tags (NOUN, VERB, PRON). CHAT uses lowercase (noun, verb, pron). This is a lossless, trivially reversible surface change.

UD UPOSCHAT POS
NOUNnoun
VERBverb
AUXaux
PRONpron
DETdet
ADJadj
ADVadv
ADPadp
PROPNpropn
INTJintj
CCONJcconj
SCONJsconj
NUMnum
PARTpart
Xx

2. Feature Values Are Flat, Not Key=Value (Currently)

UD represents morphological features as key=value pairs: Number=Plur, Tense=Past, Person=3. The current CHAT convention drops the keys and uses only the values: -Plur, -Past, -S3.

This is the most significant divergence from UD, because:

  • Information loss: Plur could in principle be Number=Plur or Degree=Plur (though in practice the UD feature value set has no real ambiguities).
  • Collapsed person/number: UD Person=3|Number=Sing becomes -S3 — a combined code that cannot be mechanically decomposed back to its UD components.
  • Feature ordering: Features appear in a conventional order determined by the generation pipeline, not in UD’s alphabetical order.

The data model now supports key=value features. The MorFeature type has an optional key field — when present, the feature serializes as Key=Value (e.g., -Number=Plur); when absent, it serializes as just the value (e.g., -Plur). This is forward-compatible: existing flat features parse and serialize identically, and if batchalign’s mapper begins emitting Key=Value features, they flow through the parser and model without any format changes.

3. Multi-Value Features: Commas Preserved

UD encodes multi-value features with commas: PronType=Int,Rel (the word is both interrogative and relative). In CHAT %mor, the comma is preserved within the feature value:

-Int,Rel

This is treated as a single feature value "Int,Rel". The grammar accepts commas within feature values, and the model stores them as-is. No decomposition occurs — the model faithfully records the string that appears in the %mor tier.

Historical note: Earlier documentation described a “comma-stripping” convention where PronType=Int,Rel became -IntRel (concatenated without separator). The current grammar and parser preserve the comma. Existing corpus data using the concatenated form (-IntRel) also parses correctly — it’s simply treated as the flat value "IntRel".

4. Dependency Relations Are Uppercase with Dash Subtypes

The %gra tier (not %mor, but closely related) uses uppercase relation names with dashes for subtypes, where UD uses lowercase with colons:

UDCHAT %gra
nsubjNSUBJ
acl:relclACL-RELCL
obl:tmodOBL-TMOD

This is lossless — case and separator are trivially reversible.

5. ROOT Head Convention

In UD, the root word has head=0. In %gra, two conventions coexist:

  • UD convention: head=0 (e.g., 3|0|ROOT) — the standard we now emit
  • Legacy TalkBank convention: head=self (e.g., 3|3|ROOT) — found in older corpus data

The parser and validator accept both forms. New output uses head=0.

6. No XPOS, No DEPREL Subtypes in %mor

UD provides both UPOS (universal POS) and XPOS (language-specific POS). CHAT %mor uses only UPOS-equivalent tags — there is no XPOS field. Language-specific POS distinctions are not represented.

Similarly, UD’s fine-grained dependency relation subtypes (e.g., nsubj:pass) appear in %gra as NSUBJ-PASS, but the %mor tier itself contains no dependency information.

7. No Morpheme Segmentation

Traditional CHAT MOR formats (CLAN-era) supported morpheme-level segmentation with compound markers (+), prefix markers (#), and suffix chains (-SUFFIX&type). The UD MOR format does not use any of these — each word is analyzed as a flat POS+lemma+features triple.

The grammar still accepts some of these legacy markers for backward compatibility with older corpus data, but the canonical UD MOR format does not produce them.

Compound Lemma Boundaries

Several UD treebanks use special characters inside lemmas to mark morphological boundaries. These are meaningful linguistic annotations preserved in the CHAT %mor lemma field when possible.

Known Markers Across Languages

LanguageMarkerMeaningExample LemmaIn %mor
Estonian=Compound boundarymaja=uks (house-door)noun|maja=uks — preserved
Basque!Derivational boundarypartxi!se (share + derivation)noun|partxi!se-Ine — preserved
Finnish#Compound boundaryjää#kaappi (ice-cabinet)noun|jää_kaappi — mangled (# → _)

= and ! pass through the cleaning pipeline because they are not reserved CHAT %mor syntax characters. # is reserved in traditional CHAT MOR for prefix markers (e.g., v|#un#do), so the sanitizer replaces it with _.

Gotcha: = ambiguity with legacy CLAN translation glosses. Legacy CLAN %mor tiers use = for translation glosses (e.g., n|perro=dog), a convention predating UD adoption. The parser treats = identically in both cases — it is preserved as part of the lemma string. This means legacy n|perro=dog parses successfully but the translation semantics are lost: the model stores perro=dog as a single lemma, indistinguishable from an Estonian compound like maja=uks. Since we cannot reliably disambiguate the two uses without language-specific context, legacy translation glosses are silently absorbed into the lemma. Files with legacy =translation syntax still parse and round-trip correctly, but the translation information is not semantically accessible. This affects corpora that predate our UD MOR adoption and lack Stanza coverage for their language.

Multi-Word Expression Lemmas (Stanza _ Convention)

Stanza uses underscores in lemmas to represent multi-word expressions across many languages: New_York, parce_que (French), pick_up (English), a_causa_di (Italian). The current cleaning pipeline strips underscores entirely (New_York → NewYork), which is a known data quality issue and should be treated as an open data-quality limitation of the current mapper.

Multi-Value Features (Commas in Feature Values)

UD encodes multi-value features with commas: PronType=Int,Rel means a word is both interrogative and relative. These commas appear in the CHAT %mor feature suffix and are preserved as-is:

pron|wat-Int,Rel

This is sometimes mistaken for a compound lemma marker, but commas in UD always appear in the feature column (CONLLU column 6), never in the lemma column (CONLLU column 3). In CHAT %mor, they appear after the - feature separator, not inside the lemma. The grammar, both parsers, and the data model all accept commas in feature values. See Section 3: Multi-Value Features above.

Future Direction

The current handling of compound lemma boundaries is inconsistent across languages. A possible future improvement is a unified Unicode separator character that would normalize all compound/derivational boundary markers (=, !, #, and potentially _) into a single convention. This has not been implemented as of 2026-03-02 and requires a design decision on which character to use and whether to preserve the original markers in a structured field.

Data Model

The Rust data model in talkbank-model represents %mor tiers with these types:

MorTier

The top-level tier container:

pub struct MorTier {
    pub tier_type: MorTierType,    // MorTierType::Mor
    pub(crate) items: MorItems,    // Vec<Mor> wrapper; accessed via accessor methods
    pub terminator: Terminator,    // typed terminator (.`, `?`, `!`, `+...`, etc.)
    pub span: Span,                // source location
}

Mor (Item)

One item aligned with one main-tier word:

pub struct Mor {
    pub main: MorWord,                        // required main word
    pub post_clitics: SmallVec<[MorWord; 2]>, // optional ~clitics
}

MorWord

A single morphological word (POS + lemma + features):

pub struct MorWord {
    pub pos: PosCategory,                    // e.g., "noun"
    pub lemma: MorStem,                      // e.g., "dog"
    pub features: SmallVec<[MorFeature; 4]>, // e.g., [Plur]
}

MorFeature

A morphological feature with optional key:

pub struct MorFeature {
    key: Option<Arc<str>>,  // e.g., Some("Number") or None
    value: Arc<str>,        // e.g., "Plur"
}

Construction examples:

// Flat feature (current convention)
MorFeature::new("Plur")         // key=None, value="Plur"
MorFeature::new("S3")           // key=None, value="S3"
MorFeature::new("Int,Rel")      // key=None, value="Int,Rel"

// Keyed feature (UD-standard, forward-compatible)
MorFeature::new("Number=Plur")  // key=Some("Number"), value="Plur"
MorFeature::new("Tense=Past")   // key=Some("Tense"), value="Past"

// Explicit constructors
MorFeature::flat("Plur")
MorFeature::with_key_value("Number", "Plur")

Lossless roundtrip guarantee: MorFeature::new auto-detects the = delimiter. Features without = are flat; features with = split into key+value. Serialization reproduces the original format exactly — flat features stay flat, keyed features keep their key.

PosCategory and MorStem

Both are interned Arc<str> newtypes for memory efficiency:

pub struct PosCategory(pub Arc<str>);  // interned via pos_interner()
pub struct MorStem(pub Arc<str>);      // interned via stem_interner()

Common values (noun, verb, the, a, be, etc.) are pre-populated in the interner. Cloning is O(1) — atomic reference count increment.

Memory Layout

The model uses SmallVec for inline storage of common cases:

  • Mor.post_clitics: SmallVec<[MorWord; 2]> — most words have 0-1 clitics
  • MorWord.features: SmallVec<[MorFeature; 4]> — most words have 0-4 features
  • MorFeature key and value are Arc<str> — interned for deduplication

For a typical 30-word utterance with %mor, the model allocates approximately 30 Mor items, each with 1 MorWord and 0-4 MorFeature values. The interning system ensures that repeated POS tags, stems, and feature values share a single allocation across the entire file.

Grammar

The tree-sitter grammar for %mor is defined in grammar.js. The relevant rules:

mor_content → mor_word (mor_post_clitic)*
mor_post_clitic → tilde mor_word
mor_word → mor_pos pipe mor_lemma (mor_feature)*
mor_feature → hyphen mor_feature_value
mor_feature_value → /[^\.\?\|\+~\-\s\r\n]+/

Key design decisions:

  • mor_feature_value accepts = and !: The regex [^\.\?\|\+~\-\s\r\n]+ matches any characters except the MOR structural delimiters. This means Number=Plur parses as a single mor_feature_value node. The split on = happens in the model layer, not the grammar — following the “parse, don’t validate” principle.
  • mor_feature_value accepts ,: Multi-value features like Int,Rel parse as a single node.
  • No compound/prefix rules: The grammar has no rules for + (compounds) or # (prefixes) in the UD MOR format. These are legacy CHAT MOR features not used in UD-style output.

Parser

The tree-sitter parser produces MorTier from CHAT text. It is GLR-based and error-recovering, producing a CST that the Rust talkbank-parser crate walks to construct MorTier. Used by the CLI, LSP, and batchalign. High-frequency values (PosCategory, MorStem) are interned via Arc<str> during construction.

The resources/corpus/reference/ set is the correctness gate for %mor parsing — every file must parse and round-trip cleanly. The file count grows as new constructs are added; run find resources/corpus/reference -name '*.cha' | wc -l to get the live total.

Validation

The %mor tier undergoes several validation checks:

Content Validation (E711)

Every MorWord is checked for:

  • Empty POS: |lemma with no POS before the pipe
  • Empty lemma: pos| with no lemma after the pipe
  • Empty feature: bare - separator with no feature text

Main-tier Alignment (E705 / E706)

The %mor tier must align 1-to-1 with the main tier’s alignable words (excluding pauses, events, and other non-word content). The number of Mor items must equal the number of alignable main-tier words. The validator emits E705 MorCountMismatchTooFew when %mor has fewer items than the main tier and E706 MorCountMismatchTooMany when it has more. Terminator-mismatch errors are emitted separately as E707 (presence) and E716 (value).

GRA Alignment (E720)

When both %mor and %gra tiers are present, the number of %gra relations must equal the number of %mor chunks (including clitics and the terminator). A mismatch emits E720 MorGraCountMismatch. This is computed via MorTier::count_chunks().

(%gra’s own internal validators — E708 malformed relation, E709 invalid index, E712 word-index out of range, E713 head-index out of range, E721 non-sequential index, E722 no ROOT, E723 multiple ROOTs, E724 circular dependency — are documented in Dependent Tiers § %gra.)

JSON Serialization

The MorTier serializes to JSON using serde. MorFeature serializes as a plain string ("Plur" or "Number=Plur"), so the JSON schema is simply "type": "string". Example:

{
  "tier_type": "Mor",
  "items": [
    {
      "main": {
        "pos": "pron",
        "lemma": "I",
        "features": ["Prs", "Nom", "S1"]
      }
    },
    {
      "main": {
        "pos": "verb",
        "lemma": "want",
        "features": ["Fin", "Ind", "Pres", "S1"]
      }
    },
    {
      "main": {
        "pos": "noun",
        "lemma": "cookie",
        "features": ["Plur"]
      }
    }
  ],
  "terminator": "."
}

When key=value features are present, they serialize with the key included:

"features": ["Number=Plur", "Tense=Past"]

The JSON schema for MorFeature is "type": "string" regardless of whether keys are present.

Migration from Traditional CHAT MOR

What Changed

The traditional CHAT MOR format (CLAN-era) used a complex, hierarchically structured notation:

%mor:	pro:sub|I v|want n|cookie-PL .

Key differences from the UD MOR format:

AspectTraditional CHAT MORUD MOR
POS tagsCLAN categories (pro:sub, v, n, adj, adv)Lowercased UPOS (pron, verb, noun, adj, adv)
POS subtypesColon-separated (pro:sub, det:art, v:aux)Flat (subtypes dropped or encoded differently)
FeaturesCLAN suffix system (-PL, -PAST, -3S, -PRES)UD feature values (-Plur, -Past, -S3, -Pres)
Compounds+ separator (`n+n|black+n|bird`)
Prefixes# separator (`v#un#do`)
Morpheme segmentationFull segmentation (v|eat&PAST)Not used (features are abstract, not morphemic)
Translations= separator (n|perro=dog)Not present in base format (separate mechanism)

What the Model Removed

The UD MOR redesign (2026) removed the following types from the data model:

  • MorSuffix — suffix with type discriminant (fusional, derivational, etc.)
  • MorCompound — compound word with + separator
  • MorPrefix — prefix with # separator
  • MorSubcategory — POS subcategory after colon
  • AnnotatedChunk — chunk with optional translation
  • Chunk — enum of word/compound/terminator

These were replaced by the flat MorWord { pos, lemma, features } structure. The model went from ~12 types to 4 (MorTier, Mor, MorWord, MorFeature).

Backward Compatibility

The grammar still accepts many traditional CHAT MOR constructs (colons in POS tags, etc.) because the reference corpus contains files in both formats. The parser produces the same flat MorWord regardless — legacy constructs are mapped to the simplified structure during parsing.

What Stays the Same

Despite the format changes, fundamental CHAT conventions remain:

  • Pipe (|) separates POS from lemma
  • Hyphen (-) introduces features
  • Tilde (~) marks post-clitics
  • Space separates items
  • Terminator ends the tier
  • 1-to-1 alignment with main tier words

Toward Full UD Compatibility

The current format is UD-inspired but not UD-compliant. Here is a roadmap of what would be needed for full lossless UD round-tripping:

Already Supported

  • POS tags (UPOS equivalents)
  • Lemmas
  • Feature values (flat and key=value)
  • MWT expansions (clitics)
  • Dependency relations (via %gra)

Gaps Remaining

  1. Feature keys: The model supports Key=Value features, but batchalign’s mapper currently emits flat values only. When the mapper switches to emitting Number=Plur instead of just Plur, the parser, model, and serializer handle it automatically with no code changes.

  2. Person+Number composites: UD has separate Person=3 and Number=Sing features. CHAT combines them into -S3 (3rd person singular). Decomposing S3 back to Person=3|Number=Sing would require a lookup table or a convention change.

  3. Multi-value feature delimiter: UD uses commas (PronType=Int,Rel). CHAT preserves these commas in the feature value, but the semantic structure (two separate values) is not explicitly modeled. The model treats Int,Rel as an opaque string.

  4. XPOS: UD provides language-specific POS tags (XPOS) alongside universal tags (UPOS). CHAT %mor has no XPOS field. This information is simply not represented.

  5. Morpheme-level analysis: UD’s MISC field can encode morpheme boundaries and glosses. CHAT’s UD MOR format does not attempt morpheme segmentation — features are abstract grammatical categories, not morphemic decompositions.

The Path Forward

The model is designed so that moving toward UD compliance requires no breaking changes:

  • MorFeature already supports Key=Value — just needs the mapper to emit keys
  • PosCategory is an opaque string — could hold XPOS in a separate field if needed
  • JSON schema uses "type": "string" for features — adding keys doesn’t break consumers
  • The grammar already accepts = in feature values — no grammar changes needed

The migration can happen incrementally: the mapper starts emitting key=value features, existing flat data continues to parse identically, and corpus files can be upgraded at their own pace.

Phon Tiers (%xmodsyl, %xphosyl, %xphoaln)

Status: Reference Last updated: 2026-05-11 22:05 EDT

The Phon extension tiers provide syllable-level phonological annotation and segmental alignment between target and actual IPA transcription. They are produced by the Phon application and exported to CHAT via PhonTalk.

Note on the x prefix: These tiers currently use the %x prefix (%xmodsyl, %xphosyl, %xphoaln) to indicate they are user-defined extension tiers, following the Phon project’s convention. When these tiers are officially adopted into the CHAT standard, the x prefix will be dropped.

Background

Phon is a software program for phonological analysis of child language, second language acquisition, and speech disorders. It stores phonological data in its own XML session format with structured IPA transcription, syllabification, and phone-level alignment. When Phon data is exported to CHAT, three extension tiers carry this phonological annotation:

  • %xmodsyl — syllabified version of %mod (target/model IPA)
  • %xphosyl — syllabified version of %pho (actual IPA)
  • %xphoaln — phone-by-phone alignment between target and actual IPA

These tiers are derived views of the parent IPA tiers. The syllabification tiers store the same phoneme sequence as %mod/%pho but with syllable structure annotations applied. There is no independent alignment — the syllabification is a reannotation of the same phone sequence.

Tier Format

%xmodsyl / %xphosyl — Syllabified Phonology

Each phoneme is annotated with a syllable constituent position code:

CodeMeaning
NNucleus (vowel center)
OOnset (syllable-initial consonant)
CCoda (syllable-final consonant)
DLeft appendix
EAmbisyllabic / right appendix
RRime / rhotic

Format: phone:Position pairs concatenated within each syllable, with . separating syllables within a word and spaces separating words.

*CHI:	I want three cookies .
%mod:	aɪ wɑnt θri kʊkiz .
%xmodsyl:	a:Dɪ:D w:Oɑ:Nn:Ct:C θ:Oɹ:Oi:N k:Oʊ:Nk:Oɪ:Nz:C .
%pho:	aɪ wɑn fwi kʊkiz .
%xphosyl:	a:Dɪ:D w:Oɑ:Nn:C f:Ow:Oi:N k:Oʊ:Nk:Oɪ:Nz:C .

Pauses appear as (.) in the syllabification tiers, matching their representation in %mod/%pho:

%pho:	aɪ (.) wɑnt .
%xphosyl:	a:Dɪ:D (.) w:Oɑ:Nn:Ct:C .

%xphoaln — Phone Alignment

Shows phoneme-by-phoneme correspondence between target (%mod) and actual (%pho) IPA using target↔actual notation. Multiple pairs within a word are comma-separated. ∅ marks deletions (target phone with no actual counterpart) or insertions (actual phone with no target counterpart).

*CHI:	the best .
%mod:	ðə bɛst .
%pho:	ðə bɛs .
%xphoaln:	ð↔ð,ə↔ə b↔b,ɛ↔ɛ,s↔s,t↔∅ .

The %xphoaln tier aligns positionally with both %mod and %pho — word N in %xphoaln corresponds to word N in both parent tiers.

Alignment Semantics

Because the syllabification tiers are derived views, word counts must match:

  • %xmodsyl word count = %mod word count
  • %xphosyl word count = %pho word count
  • %xphoaln word count = %mod word count = %pho word count

Mismatches are flagged as errors E725–E728. See Alignment Architecture for implementation details.

Parsing Strategy

We parse the structure that alignment validation needs, but not the IPA content itself:

  • %xmodsyl / %xphosyl: word boundaries are recognized for word-count alignment; the phoneme:Position sequence inside each word stays as an opaque string (talkbank-model::dependent_tier::phon::SylTier’s words: Vec<NonEmptyString>).
  • %xphoaln: each word is parsed into a Vec<AlignmentPair>, where AlignmentPair { source: Option<NonEmptyString>, target: Option<NonEmptyString> } represents one source↔target mapping and None represents ∅. Segment topology (which target phone maps to which actual phone, and where insertions / deletions occur) is therefore queryable in code; the IPA characters themselves remain opaque.

Deep phonological analysis is Phon’s domain — we avoid duplicating that work.

Phon XML Source Format

In Phon’s native XML format, phonological data is stored as structured elements:

<ipaTarget>
  <pho>
    <pw>
      <ph scType="onset"><base>θ</base></ph>
      <ph scType="nucleus"><base>ɹ</base></ph>
      <ph scType="nucleus"><base>i</base></ph>
    </pw>
  </pho>
</ipaTarget>

Each <pw> (phonological word) element contains <ph> elements with syllable constituent types (scType). Pauses appear as <pause> elements at the same level as <pw> inside <pho>.

The <alignment> element provides phone-level mappings between target and actual using index-based <pm> (phone map) entries.

Data Quality Notes

A small percentage of Phon corpus XML records have an orthography↔IPA word-count mismatch — i.e. the number of <pw> (phonological word) elements in <ipaTarget> / <ipaActual> differs from the number of <w> elements in <orthography>. This is expected in child phonology data: children may produce extra syllables, partial words, or over-productions relative to the target.

For current counts on the data repos in ~/0tb/data/, run:

python3 scripts/analysis/scan_phon_mismatches.py ~/0tb/data

It reports total XML files scanned, files with at least one mismatched record, and total mismatched records across the corpus.

The PhonTalk CHAT export handles this discrepancy inconsistently:

  1. %mod/%pho are written through a OneToOne alignment path that maps IPA words to orthography words — extras are silently dropped
  2. %xmodsyl/%xphosyl/%xphoaln are written directly from the raw IPATranscript — all IPA words are included

This produces CHAT files where %xmodsyl may have more words than %mod, triggering E725–E728 validation errors.

This is being investigated in collaboration with the Phon team.

Word Syntax

Status: Reference Last updated: 2026-05-11 23:33 EDT

Words are the primary content unit on the main tier. CHAT defines several word types and annotation mechanisms.

Standalone Words

Most words are simple tokens separated by whitespace:

*CHI:	I want a cookie .

Words can contain Unicode characters for any language:

*CHI:	ich möchte Kekse .

Compounds

Compound words join multiple elements with +:

*CHI:	I want ice+cream .

Special Word Forms

Shortened Forms

Parentheses mark omitted portions of a word:

*CHI:	(be)cause I want it .

The full form is because; the child produced cause.

Replacements

Square brackets with colon mark what the speaker actually meant:

*CHI:	I goed [: went] to the store .

The speaker said “goed” but the intended word was “went”.

Language Markers

The @s: suffix marks a word’s language in multilingual transcripts:

*CHI:	I want a Keks@s:deu .

Other @ markers:

  • @l — letter
  • @c — child-invented form
  • @f — family-specific word
  • @n — neologism
  • @o — onomatopoeia
  • @b — babbling
  • @wp — word play
  • @si — signed word

Annotations

Words and groups can carry post-positioned annotations in square brackets:

Error Marking

*CHI:	he goed [*] to school .

[*] marks an error. More specific error codes can follow: [* m:+ed].

Explanations

*CHI:	that one [= the red ball] .

[= text] provides an explanation or gloss.

Replacements

*CHI:	I wanna [: want to] go .

[: text] marks the target/intended form.

Best Guess

*CHI:	I want the birfer [?] .

[?] marks uncertain transcription.

Events and Actions

Paralinguistic Events

Events marked with &= describe non-speech sounds:

*CHI:	&=laughs I want cookie .
*CHI:	&=coughs .

Fillers

Fillers are marked with &-:

*CHI:	&-um I want &-uh cookie .

Interposed Speech (Other Speaker)

Brief background speech from a different speaker is marked with the &*SPK:text prefix — it captures the interjection without creating a full turn line:

*CHI:	I want &*MOT:careful a cookie .

This says CHI was speaking and MOT briefly said “careful” mid-turn. If the intervention is substantial enough to constitute its own turn, transcribe it as a separate *MOT: utterance instead. Model: crates/core/talkbank-model/src/model/content/other_spoken.rs.

(Note: [^ text] is a freecode — a standalone free-form researcher annotation that sits as its own content item on the main tier (variant of UtteranceContent::Freecode, sibling of Word and Group — it is NOT attached to any word). See grammar/grammar.js rule freecode and crates/core/talkbank-model/src/model/content/utterance_content/. Used for transcriber notes that are independent of any single word; for notes about a single word use [% text] or [= text] instead.)

Pauses

*CHI:	I (.) want (..) a (...) cookie .
*CHI:	I (1.5) want a cookie .
  • (.) — short pause
  • (..) — medium pause
  • (...) — long pause
  • (N.N) — timed pause in seconds

Overlap

Overlapping speech between speakers uses angle brackets and overlap markers:

*MOT:	do you want <a cookie> [>] ?
*CHI:	<cookie> [<] !
  • [>] — follows the overlap (this speaker started first)
  • [<] — overlaps the previous speaker

Retrace and Repetition

Groups followed by retrace markers indicate speech disfluencies:

*CHI:	<I want> [/] I want a cookie .
*CHI:	<I want> [//] I need a cookie .
*CHI:	<I want a> [///] give me a cookie .
  • [/] — partial retrace (speaker repeats the same words)
  • [//] — full retrace (speaker restarts with different words)
  • [///] — multiple retracing (multiple false starts)
  • [/-] — reformulation (speaker rephrases with different structure)

The CHAT Word

Status: Current Last updated: 2026-05-11 23:31 EDT

“Word” is the most complex and most misunderstood concept in CHAT. This chapter documents what a word actually is, how the grammar parses it, and how the Rust model represents it. If you maintain this codebase, you will encounter word-level bugs. This chapter exists so you can understand them.

The Fundamental Rule

Whitespace delimits words. Contiguous non-whitespace characters form one word token. This applies everywhere on the main tier.

*CHI:   hello world .
        ^^^^^              word: "hello"
              ^^^^^        word: "world"

The grammar uses extras: $ => [] – no implicit whitespace. Whitespace nodes (whitespaces, space) are explicit in the CST. Tree-sitter does not skip whitespace between tokens. This is the foundation of every tokenization decision in the grammar.

There are no exceptions to this rule. Every ambiguity described in this chapter is resolved by applying this rule consistently.

Word Structure

A word in the grammar is standalone_word – a sequence of an optional prefix, a required body, optional suffixes, and an optional POS tag.

The following diagram shows the full decomposition. All named nodes are separate CST children.

flowchart TD
    sw["standalone_word\n(grammar.js, prec.right 6)"]

    zero["zero\n'0' -- omission prefix"]
    wp["word_prefix\n'&amp;-' filler | '&amp;~' nonword | '&amp;+' fragment"]

    wb["word_body\n(required)"]
    fm["form_marker\n@b, @c, @d, @z:label, ..."]
    wls["word_lang_suffix\n@s, @s:eng, @s:eng+fra"]
    pos["pos_tag\n$n, $v, $adj, ..."]

    sw -->|"optional prefix"| zero & wp
    sw -->|"required"| wb
    sw -->|"optional suffix"| fm & wls
    sw -->|"optional"| pos

    ws["word_segment\npure spoken text"]
    short["shortening\n'(text)' omitted sound"]
    sm["stress_marker\nprimary or secondary"]
    len["lengthening\n':' one or more colons"]
    op["overlap_point\none of four brackets"]
    cae["ca_element\nsingle CA marker"]
    cad["ca_delimiter\npaired CA marker"]
    ub["underline_begin\ncontrol char pair"]
    ue["underline_end\ncontrol char pair"]
    cm["'+'\ncompound marker"]

    wb -->|"children (any order)"| ws & short & sm & len & op & cae & cad & ub & ue & cm

In the grammar (search grammar/grammar.js for the standalone_word and word_body rules), the structure is:

standalone_word: $ => prec.right(6, seq(
  optional(choice($.word_prefix, $.zero)),
  $.word_body,
  optional($.form_marker),
  optional($.word_lang_suffix),
  optional($.pos_tag),
)),

word_body: $ => prec.right(choice(
  seq(
    choice($.word_segment, $.shortening, $.stress_marker),
    repeat(choice($.word_segment, $.shortening, $.stress_marker, $._word_marker)),
  ),
  seq(
    choice($.overlap_point, $.ca_element, $.ca_delimiter, $.underline_begin),
    choice($.word_segment, $.shortening, $.stress_marker),
    repeat(choice($.word_segment, $.shortening, $.stress_marker, $._word_marker)),
  ),
)),

word_body has two branches:

  1. Standard start: the word begins with word_segment, shortening, or stress_marker, followed by any number of body children.
  2. Marker-initial: the word begins with a structural marker (overlap, CA, underline), but that marker must be immediately followed by text content. This prevents degenerate words like a standalone overlap marker from forming a valid standalone_word.

Lengthening and + (compound marker) are excluded from starting a word body. This is how standalone : falls through to separator(colon) – see Section 5 (Tokenization Ambiguities) below.

The word_segment Purity Invariant

word_segment contains ONLY pure spoken text. All structural markers are separate typed children in word_body, never consumed by word_segment.

This is a hard invariant with three consequences:

  1. cleaned_text() never scans for markers. It concatenates Text and Shortening elements. No stripping needed.
  2. Validation finds ALL markers by type. Overlap markers, CA elements, and underline pairs are always WordContent variants, regardless of position within the word.
  3. Editors get typed CST nodes. Syntax highlighting, bracket matching, and hover info work on individual markers, not opaque substrings.

How it works

word_segment is a DFA token at prec(5) with a regex that excludes all structural characters. The exclusions are generated from the symbol registry (grammar/src/generated_symbol_sets.js) – never hand-written.

word_segment: $ => token(prec(5, seq(
  WORD_SEGMENT_FIRST_RE,    // generated: excludes structural chars + '0' at start
  WORD_SEGMENT_REST_RE,     // generated: excludes structural chars
))),

Full exclusion table

Every character in this table is excluded from word_segment and becomes a separate typed node in the CST.

CategoryCharactersCST node type
Overlap markers⌈ ⌉ ⌊ ⌋overlap_point
CA elements↑ ↓ ≠ ∾ ⁑ ⤇ ∙ Ἡ ↻ ⤆ca_element
CA delimiters∆ ∇ ° ▁ ▔ ☺ ♋ ⁇ ∬ Ϋ ∮ ↫ ⁎ ◉ §ca_delimiter
Stress markersˈ ˌstress_marker
Colons:lengthening
Underline markers\x02\x01, \x02\x02underline_begin / underline_end
Brackets[ ] < > ( ) { }structural (annotations, groups)
Punctuation. ! ? , ; +terminators, separators, compound
CHAT prefixes@ $ & * %headers, events, speakers
Intonation contours⇗ ↗ → ↘ ⇘ ≈ ≋ ∞ ≡content-level markers
Group delimiters‹ › " " 〔 〕pho/sin groups, quotes
Control chars\x01-\x08, \x15bullets, underline

First-character-only exclusion: 0 is excluded from the first character of word_segment (it is the omission prefix). 0 in non-initial positions is valid: 200, h0me, abc0 all parse correctly.

The Rust Data Model

Word struct

The Word struct (crates/core/talkbank-model/src/model/content/word/word_type.rs) is the canonical typed representation:

pub struct Word {
    pub span: Span,
    pub word_id: Option<SmolStr>,
    pub(crate) raw_text: SmolStr,
    pub content: WordContents,
    pub category: Option<WordCategory>,
    pub form_type: Option<FormType>,
    pub lang: Option<WordLanguageMarker>,
    pub part_of_speech: Option<SmolStr>,
    pub inline_bullet: Option<Bullet>,
}

Key fields:

  • raw_text: the exact text from the input, including all markers. Used for roundtrip serialization.
  • content: a WordContents (SmallVec-backed sequence of WordContent elements). This is the structured decomposition. Most words have 1-2 elements; SmallVec avoids heap allocation for the common case.
  • category: optional prefix (Omission, CAOmission, Filler, Nonword, PhonologicalFragment).
  • form_type: optional @ suffix (@c child-invented, @d dialect, @z:label user-defined, etc.).
  • lang: optional @s language marker (Shortcut, Explicit, Multiple, Ambiguous).
  • part_of_speech: optional $ tag.

WordContent enum

WordContent (crates/core/talkbank-model/src/model/content/word/content.rs) is the enum of everything that can appear inside a word body. Each variant maps directly to a grammar node.

Grammar nodeWordContent variantRust typeExample
word_segmentTextWordText(NonEmptyString)hello, want
shorteningShorteningWordShortening(NonEmptyString)(be) in (be)cause
overlap_pointOverlapPointOverlapPoint⌈, ⌉2
ca_elementCAElementCAElement↑, ↓
ca_delimiterCADelimiterCADelimiter∆, °
stress_markerStressMarkerWordStressMarkerˈ primary, ˌ secondary
lengtheningLengtheningWordLengthening { count: u8 }: = 1, :: = 2, ::: = 3
(caret in word)SyllablePauseWordSyllablePause^ in o^ver
underline_beginUnderlineBeginUnderlineMarker\x02\x01
underline_endUnderlineEndUnderlineMarker\x02\x02
+ (compound)CompoundMarkerWordCompoundMarker+ in ice+cream
~ (clitic boundary)CliticBoundaryWordCliticBoundary~ in le~ha

cleaned_text()

Word::cleaned_text() derives NLP-ready text from content by concatenating only Text and Shortening variants:

pub fn compute_cleaned_text(&self) -> String {
    let mut result = String::new();
    for item in &self.content {
        match item {
            WordContent::Text(t) => result.push_str(t.as_ref()),
            WordContent::Shortening(s) => result.push_str(s.as_ref()),
            _ => {}
        }
    }
    result
}

This works because the purity invariant guarantees that Text elements never contain structural markers. There is nothing to strip.

Examples:

Inputcontentcleaned_text()
hello[Text("hello")]hello
(be)cause[Shortening("be"), Text("cause")]because
no::[Text("no"), Lengthening(2)]no
ice+cream[Text("ice"), CompoundMarker, Text("cream")]icecream
le~ha[Text("le"), CliticBoundary, Text("ha")]leha
ja^ja[Text("ja"), SyllablePause, Text("ja")]jaja
he↑llo[Text("he"), CAElement(PitchUp), Text("llo")]hello
°soft°[CADelimiter(Softer), Text("soft"), CADelimiter(Softer)]soft
ˈhello[StressMarker(Primary), Text("hello")]hello
⌈hello⌉[OverlapPoint(TopBegin), Text("hello"), OverlapPoint(TopEnd)]hello

The result is cached via OnceLock on first access.

What is included in cleaned_text vs what is stripped

The following table is the complete inventory of how every word-internal element contributes to (or is excluded from) cleaned_text(). This must match what NLP pipelines (Stanza, etc.) expect as input.

WordContent variantCharacter(s)In cleaned_text?Rationale
Textspoken textYESThe actual word
Shortening(be)YESShortened form is still spoken
CompoundMarker+NoStructural boundary, not spoken
CliticBoundary~NoMorphological boundary, not spoken
SyllablePause^NoPause between syllables, not spoken
Lengthening: :: :::NoProsodic marker, not spoken
StressMarkerˈ ˌNoProsodic marker, not spoken
OverlapPoint⌈ ⌉ ⌊ ⌋NoTiming marker, not spoken
CAElement↑ ↓ ≠ ∾ ⁑ ⤇ ∙ Ἡ ↻ ⤆NoProsodic annotation
CADelimiter∆ ∇ ° ▁ ▔ ☺ ♋ ⁇ ∬ Ϋ ∮ ↫ ⁎ ◉ §NoVoice quality annotation
UnderlineBegin\x02\x01NoFormatting marker
UnderlineEnd\x02\x02NoFormatting marker

Characters that stay in word_segment (ARE spoken text):

  • Letters (all Unicode)
  • Digits (in non-initial position; 0 in initial = omission prefix)
  • Hyphen (-) — part of word text, e.g., ice-cream, self-conscious
  • Apostrophe (') — contractions, e.g., don't, it's
  • Hash (#) — appears in some transcription conventions
  • Underscore (_) — compound boundary in some conventions

Characters NOT in word_segment (excluded by symbol registry): See the full exclusion table in Precedence Decisions in the grammar docs.

Comparison with batchalign2

batchalign2’s annotation_clean() (60 lines of .replace() calls) strips all the same characters that our grammar excludes from word_segment. Key differences:

  1. Parentheses: ba2 COMMENTED OUT the strip. We handle them as Shortening — the content inside parens IS included in cleaned_text.
  2. IPA characters (ạ ā ʔ ʕ ʰ): ba2 incorrectly strips them. We correctly keep them — they are real phonetic content.
  3. Hyphen (-): ba2 strips it. We keep it in word_segment because hyphen is a valid word character (contractions, compounds, morphological suffixes in %mor tier).

Our design eliminates the need for character-by-character stripping entirely. cleaned_text() is a simple concatenation of Text + Shortening elements, with zero scanning.

The Six Tokenization Ambiguities

CHAT was designed for human readability, not machine parsing. Six characters have context-dependent meanings that the grammar must disambiguate. Full details with proof grammars are in grammar/docs/tokenization-rules.md and grammar/docs/precedence-decisions.md. What follows is a summary for orientation.

1. Overlap markers (⌈⌉⌊⌋)

Adjacent to text = part of the word. Space-separated = standalone overlap_point.

Yeah⌋⌈2 hey      ONE word: "Yeah⌋⌈2"
Yeah ⌋ ⌈2 hey    three tokens: "Yeah", ⌋, ⌈2

Maximal munch at prec(5) makes word_segment consume adjacent overlap characters. Overlap markers are only recognized as overlap_point when space-separated on both sides.

2. Zero/omission prefix (0)

Adjacent to word body = omission prefix. Space-separated = action marker.

0die              ONE word: standalone_word(zero, word_body("die"))
0 die             TWO tokens: nonword(zero), word("die")

standalone_word at prec.right(6) beats nonword at prec(1). The extras: [] setting prevents whitespace from being skipped between zero and word_body. The zero token is inlined directly into standalone_word (not through word_prefix) because tree-sitter’s precedence does not propagate through intermediate rules. This was proven empirically with a minimal test grammar – see grammar/docs/precedence-decisions.md.

3. CA parenthetical vs shortening

In CA mode (@Options: CA), a fully parenthesized word (word) is an uncertain/omitted word (CAOmission), semantically equivalent to 0word. Partially parenthesized hel(lo) is always a shortening.

@Options: CA
*CHI:   (ja) .            CAOmission: uncertain "ja"
*CHI:   hel(lo) .         Shortening: "(lo)" is the shortened part

Distinguishing these requires file-level context (@Options header). The parser sets WordCategory::CAOmission when the word is fully parenthesized in CA mode. Isolated parser.parse_word_fragment() calls cannot determine CA mode – they need a FragmentSemanticContext.

4. Colon – lengthening vs separator

Inside a word (after text): prosodic lengthening. Standalone: separator.

no::              ONE word: Text("no") + Lengthening(2)
hello : world     separator(colon)

The DFA always produces lengthening for : (higher precedence). But word_body rejects lengthening as a first element, so standalone : cannot form a valid word and falls through to separator(colon). This is the “constrain the parser, not the DFA” pattern.

5. Plus (+) – compound vs terminator vs linker

Inside a word: compound marker. At line end: terminator prefix. At line start: linker prefix.

ice+cream         ONE word with compound marker
and then +...     terminator: trailing_off (prec 10 beats prec 5)
+< but I +/.      linker: lazy_overlap, terminator: interruption

Terminators and linkers use prec(10), which beats word_segment at prec(5). No valid CHAT word ends with + – the grammar enforces this by structure.

6. Bracket annotations vs plain brackets

Bracket annotations ([= text], [=! text], [% text]) use prec(8) prefix tokens to beat generic bracket handling.

Historical Context: The Coarsening and Its Reversal

The original structured grammar (pre-coarsening)

The original grammar (preserved in grammar/docs/pre-coarsening-grammar.js.reference) had all word-internal markers as children of word_content:

// Pre-coarsening: every marker was a child of word_content
word_content: $ => choice(
  $.word_segment,
  $.shortening,
  $.stress,
  $.colon,
  $.caret,
  $.tilde,
  $.plus,
  $.overlap_point,
  $.ca_element,
  $.ca_delimiter,
  $.underline_begin,
  $.underline_end,
),

The coarsening decision

At one point, standalone_word was coarsened into an opaque token – a single DFA regex that consumed the entire word as one undifferentiated string. The rationale was:

  • Simpler grammar with fewer tree-sitter conflicts.
  • A Chumsky-based direct parser in Rust would re-parse the opaque token into structured WordContent elements.

This worked but had costs:

  • Two parsers (tree-sitter + Chumsky) with independent bugs.
  • Validation could not find structural markers without re-parsing.
  • Editors got one opaque node instead of typed children.
  • cleaned_text() had to scan for and strip marker characters.

The reversal (Chumsky elimination)

When the Chumsky direct parser was eliminated (making tree-sitter the sole parser), the structured word grammar was restored. The key decisions:

  1. All marker characters were re-excluded from word_segment using the symbol registry as the single source of truth.
  2. Each marker type became a separate CST child in word_body.
  3. The WordContent enum in the Rust model was aligned 1:1 with the grammar nodes.
  4. The word_segment purity invariant was established as a TDD gate.

The result is one parser, one source of truth for exclusions, and typed markers from grammar through model.

Testing: The word_segment Purity Gate

The purity invariant — each structural marker produces a separate CST child rather than being consumed by word_segment — is enforced by a group of tree-sitter corpus tests under grammar/test/resources/corpus/word/. Each *_in_word_lint.txt file embeds a structural marker inside a word and asserts the CST splits the word appropriately:

Test fileInputAsserts
overlap_in_word_lint.txtbutt⌈er⌉word_segment, overlap_point, word_segment, overlap_point
ca_element_in_word_lint.txtCA element inside a wordword_segment, ca_element, word_segment
ca_delimiter_in_word_lint.txtCA delimiter pair around a wordca_delimiter, word_segment, ca_delimiter
lengthening.txt, lengthening_between_segments.txtno::, etc.word_segment, lengthening
stacked_ca_markers.txtMultiple adjacent CA markers in one wordEach marker is its own CST child

Underline and stress invariants are covered by corpus tests elsewhere in grammar/test/corpus/ and by the parser-equivalence tests in crates/core/talkbank-parser-tests/. The historical word_segment_purity.txt consolidated 8 named tests in one file; it was retired in commit fdceeac2 when the corresponding constructs were given their own per-construct test files (this is the new layout that just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs produces from the spec sources).

How to add a new purity-style test

If you add a new structural marker to the grammar:

  1. Add its characters to the symbol registry (resources/spec/symbols/symbol_registry.json).
  2. Run node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js to regenerate the exclusion sets.
  3. Add a spec in resources/spec/constructs/ that embeds the marker inside a word; run just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs so a per-construct test fixture is created in grammar/test/resources/corpus/word/. Verify the CST output names each marker as its own child.
  4. Run the full verification sequence:
    cd grammar && tree-sitter generate && tree-sitter test
    cargo nextest run -p talkbank-parser
    cargo nextest run -p talkbank-parser-tests
    bazel build //... && bazel test //...
    

Key Source Files

FileWhat it defines
grammar/grammar.jssearch for standalone_word, word_body, word_segment, _word_marker
grammar/src/generated_symbol_sets.jsCharacter exclusion sets (generated, do not edit)
grammar/test/resources/corpus/word/*_in_word_lint.txt, lengthening*.txt, stacked_ca_markers.txtPer-construct purity-invariant gate tests (replaced the consolidated word_segment_purity.txt retired in fdceeac2)
grammar/docs/tokenization-rules.mdThe 6 tokenization ambiguities with full examples
grammar/docs/precedence-decisions.mdPrecedence proofs (zero, colon, purity invariant)
grammar/docs/pre-coarsening-grammar.js.referenceHistorical: the grammar before coarsening
crates/core/talkbank-model/src/model/content/word/word_type.rsWord struct
crates/core/talkbank-model/src/model/content/word/content.rsWordContent enum (12 variants)
crates/core/talkbank-model/src/model/content/word/word_contents.rsWordContents (SmallVec-backed sequence)
crates/core/talkbank-model/src/model/content/word/category.rsWordCategory enum (5 variants)
crates/core/talkbank-model/src/model/content/word/form.rsFormType enum (22 variants)
crates/core/talkbank-model/src/model/content/word/language.rsWordLanguageMarker enum (4 variants)

Symbols

Status: Reference — partially inaccurate, see warning on the CA section Last updated: 2026-05-11 23:19 EDT

WARNING: The “CA (Conversation Analysis) Delimiters” section below erases a parser-level distinction. The symbol registry (resources/spec/symbols/symbol_registry.json) splits these characters into three grammatical categories with different parser treatment: ca_delimiter_symbols, ca_element_symbols, and word_segment_forbidden_*. ↑ and ↓ attach to a word (book↑ is one token); →, ↗, ↘ are separators that the parser splits as their own nodes. Lumping them together as “CA delimiters” with one semantic is misleading. Until this page is rewritten to mirror the registry, trust the registry. See <workspace>/docs/architecture-review-2026-05-01.md §6 stale-doc entry for this page.

CHAT uses a rich set of symbols for transcription conventions. This page documents the symbol categories and the symbol registry that drives both the grammar and the Rust crates.

Symbol Registry

The authoritative symbol definitions live in resources/spec/symbols/symbol_registry.json. This JSON file is the single source of truth — it generates:

  • Character sets for the tree-sitter grammar (grammar.js)
  • Rust constants for the model and validation crates
  • Validation rules for the spec tool

After any change to the symbol registry, run:

node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js

Symbol Categories

Terminators

Punctuation that ends an utterance:

SymbolNameUsage
.PeriodDeclarative
?QuestionInterrogative
!ExclamationExclamatory
+...Trailing offIncomplete utterance
+..?Trailing-off questionQuestion trails off
+/.InterruptionSpeaker interrupted by another
+//.Self-interruptionSpeaker interrupts self
+/?Interrupted questionQuestion interrupted
+!?Broken questionExclamation-question
+"/.Quoted new lineQuotation continues on next line

CA (Conversation Analysis) Delimiters

Symbols used in conversation analysis notation:

SymbolMeaning
↑Rising pitch
↓Falling pitch
→Level pitch
↗Rise-fall
↘Fall-rise
≋Creaky voice
∙Micropause

CA Elements

Symbols that appear within conversation analysis annotations (prosodic marking, stress, etc.).

Word Segment Characters

Characters that are forbidden at the start of words, forbidden in the rest of words, or forbidden throughout. These define the lexical boundaries of what constitutes a “word” in CHAT.

The grammar uses these sets to construct the word-matching regex patterns. Characters like [, ], <, >, (, ) are structural delimiters and cannot appear inside words.

Event Segment Characters

Characters forbidden in event descriptions (&=event content). Events have slightly different lexical rules than words.

Language Codes

CHAT uses ISO 639-3 three-letter language codes in @Languages headers and @s: word markers:

@Languages:	eng, fra
*CHI:	I want a croissant@s:fra .

Common codes: eng (English), fra (French), deu (German), spa (Spanish), zho (Mandarin), jpn (Japanese).

Special Markers

@ Markers (Word-Level)

MarkerMeaning
@s:LANGSecond language word
@lLetter
@cChild-invented form
@fFamily-specific word
@nNeologism
@oOnomatopoeia
@bBabbling
@wpWord play
@siSigned word

& Markers (Events and Fillers)

PrefixMeaning
&=Paralinguistic event (e.g., &=laughs)
&-Filler (e.g., &-um)
&+Phonological fragment (e.g., &+sh)
&~Nonword (e.g., &~mama)
&*Other speaker’s speech event (e.g., &*MOT:word — speech attributed to another speaker)

Scope Markers

MarkerMeaning
[/]Partial retrace — speaker repeats the same words
[//]Full retrace — speaker restarts with different words
[///]Multiple retracing — multiple false starts
[/-]Reformulation — speaker rephrases with different structure
[*]Error
[?]Best guess
[>]Overlap follows
[<]Overlap precedes
[= text]Explanation
[: text]Replacement

CLAN: reference

Look up syntax, contracts, and factual details.

CHECK — CHAT File Validation

Status: Current Last updated: 2026-05-11 17:22 EDT

CHECK validates CHAT files for structural correctness, checking headers, tier formatting, bracket matching, bullet consistency, speaker declarations, and more.

Usage

chatter clan check file.cha
chatter clan check +c0 file.cha          # Full bullet check
chatter clan check +e file.cha           # List all error numbers
chatter clan check +e6 file.cha          # Only report error 6
chatter clan check -e6 file.cha          # Exclude error 6
chatter clan check +g2 file.cha          # Check CHI has Target_Child
chatter clan check +g5 file.cha          # Check for unused speakers
chatter clan check +u file.cha           # Check UD features on %mor

Options

CLAN flagModern flagDescription
+c0--bullets 0Full bullet consistency check
+c1--bullets 1Check for missing bullets only
+e--list-errorsList all 161 error numbers and exit
+eN--error NOnly report error number N (repeatable)
-eN--exclude-error NExclude error number N (repeatable)
+g1(no-op)Prosodic delimiters (always recognized)
+g2--check-targetVerify CHI has Target_Child role
+g3(partial)Word detail checks (via parser)
+g4--check-idCheck for missing @ID tiers (on by default)
+g5--check-unusedCheck for unused speakers
+u--check-udValidate UD features on %mor tier

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.3.5 (Unique Options, CHECK) for PI review. Note: CHECK’s +d is warning-suppression, not output formatting — semantically very different from FREQ/KWAL/COMBO’s +d.

NCLAN behavior (verbatim from manual)
+d (no number)“Attempts to suppress repeated warnings of the same type.”
+d1“Suppress ALL repeated warnings of the same type.”

Open questions for PI review

  1. CHECK’s +d shape is orthogonal to the other commands’ --display-mode. Mapping CHECK’s +d to --suppress-repeats (boolean) or --max-per-error N (numeric) would be more honest than overloading --display-mode.
  2. The talkbank-tools chatter validate already exposes --max-errors N (stop after N errors total, across files). That’s different from CLAN’s “suppress repeats of the same type” semantics. Should --display-mode 1 for CHECK be a new flag, or a variant of --max-errors?
  3. Both +d and +d1 are about suppression — the manual text doesn’t distinguish their behaviors clearly. “Attempts to suppress” (+d) vs “Suppress ALL” (+d1) — is the former rate-limited or heuristic? PI input needed on what CLAN actually does at runtime.

Output Format

CHECK output matches CLAN’s format:

*** File "sample.cha": line 12.
*CHI:	doggy wanna play .
[E501] Illegal word character in 'wanna' (47)

Each error shows the file path and line number, the offending tier text, and the error message with CHECK’s numbered error code in parentheses.

Errors that don’t map to a CHECK number show our internal code in brackets instead:

*** File "sample.cha": line 5.
@Participants:	CHI Child
Missing role for CHI — expected format: CODE Name Role [E312]

CHECK vs chatter validate

See CHECK vs chatter validate for a detailed comparison of these two validation tools.

Differences from CLAN

  • Parsing: Uses tree-sitter grammar instead of CLAN’s character-by-character parser. More rigorous and consistent; catches structural errors that CHECK sometimes misses.
  • Error numbering: CLAN CHECK uses flat numbers 1-161. We map our typed error codes to CHECK numbers where correspondence exists; unmapped errors get number 0.
  • Two-pass vs single-pass: CLAN runs check_OverAll then check_CheckRest. Our parser combines both into a single streaming parse+validate pipeline.
  • depfile.cut: CLAN reads depfile.cut for tier/code templates. We validate against the CHAT specification directly.
  • Bug fixes: Several CHECK errors in the original are unreachable or duplicate (e.g., errors 51, 96 are commented out). We skip those.
  • +g1: Always a no-op — our parser recognizes prosodic delimiters by default.
  • +g3: Partially implemented through existing word validation.

FREQ – Word Frequency

Status: Current Last updated: 2026-05-11 17:02 EDT

Purpose

Counts word tokens and types and computes type-token ratio (TTR). The legacy manual describes FREQ as one of CLAN’s most powerful and easiest-to-use programs, producing word-frequency counts and lexical-diversity measures over selected files and speakers.

In clan-core, FREQ counts words on the main tier by default, or morphemes from the %mor tier when --mor is set.

See the CLAN manual for the original FREQ command specification.

Usage

chatter clan freq file.cha
chatter clan freq --speaker CHI file.cha
chatter clan freq --format json corpus/
chatter clan freq --mor file.cha
chatter clan freq --include-word "the" file.cha

+k / --case-sensitive is currently non-functional. The legacy +k flag rewrites to --case-sensitive (see crates/clan/clan-core/src/clan_args.rs:104), but no clap field consumes that token in the current Freq/CommonAnalysisArgs structs, so passing it produces a parse error. Word matching is case-insensitive today.

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--include-word <WORD>+s"word"Only count matching word
--exclude-word <WORD>-s"word"Skip matching word
--gem <LABEL>+g"label"Restrict to gem segment
--range <START-END>+z25-125Utterance range
--case-sensitive+kCurrently non-functional — see callout above
--format <FMT>–Output format: text, json, csv, clan
--mor–Count morphemes from %mor tier instead of words from main tier

CLAN Equivalence

CLAN commandRust equivalent
freq file.chachatter clan freq file.cha
freq +t*CHI file.chachatter clan freq file.cha --speaker CHI
freq +s"the" file.chachatter clan freq file.cha --include-word "the" (case-sensitive matching not currently supported — see callout above)
freq *.chachatter clan freq corpus/

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. The legacy rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N, but no clap field consumes that token today. This section drafts the per-N table from CLAN manual §7.10.15 (Unique Options, FREQ) verbatim, for PI review and subsequent TDD implementation. Tracked in <workspace>/docs/superpowers/plans/2026-05-11-clan-rewriter-honor-three-flags.md Phase 3.

FREQ uses +d to switch output format, not to vary verbosity. Each value of N selects a different report shape. Quoted from CLAN manual §7.10.15:

NCLAN behavior (verbatim from manual)
+d (no number)“Perform a particular level of data analysis. By default, the output consists of all selected words found in the input data file(s) and their corresponding frequencies.” (Equivalent to no-flag default.)
+d0“Output provides a concordance with the frequencies of each word, the files and line numbers where each word, and the text in the line that matches.”
+d1“Outputs each of the words found in the input data file(s) one word per line with no further information about frequency. Later this output could be used as a word list file for kwal or combo programs.”
+d2“Output is sent to a file in a form that can be opened directly in Excel. To do this, you must include information about the speaker roles you wish to include in the output spreadsheet.” (Manual example: `freq +d2 +t@ID=“*
+d3“Essentially the same as that for +d2, but with only the statistics on types, tokens, and the type–token ratio. Word frequencies are not placed into the output.” (Note: +d2 and +d3 assume +f; no need to pass it explicitly.)
+d4“Allows you to output just the type–token information.”
+d5“Output all words you are searching for, including those that occur with zero frequency. … Can be combined with other +d switches.”
+d6“When used for searches on the main line, outputs matched forms with a separate tabulation of replaced forms, errors, partial omissions, and full forms.” Also +d6 +sm|n*,o% on %mor line: produces separate counts per part-of-speech instantiation.
+d7“Links forms on a ‘source’ tier with their corresponding words on a ‘target’ tier.” Default source is %mor; pass a tier name to change source. Items on the two tiers must be in one-to-one correspondence. +c5 swaps source ↔ target.
+d8“Outputs words and frequencies of cross tabulation of one dependent tier with another.”

Open questions for PI review

  1. +d0: emits a concordance — overlaps with KWAL semantically. Should chatter’s freq --display-mode 0 reuse the kwal output path internally, or produce its own concordance shape?
  2. +d1: word-list output suitable as input to kwal +s@file. Should the file be auto-named (<basename>.fre?) or printed to stdout by default?
  3. +d2/+d3: “form that can be opened directly in Excel” maps to --format csv in chatter. Is this duplication acceptable, or should --display-mode 2 imply --format csv (and conflict-error otherwise)?
  4. +d4: “type-token information only” — same content as the existing text/json default, minus the word frequencies. Add a new Truncated variant to the output struct, or emit a CSV row with just types/tokens/TTR?
  5. +d5: combinable with other +d values. How should this combine in clap — a Vec<DisplayMode> rather than scalar Option<u8>?
  6. +d6/+d7/+d8: deeply specific to %mor and cross-tier tabulation. Are these in scope for chatter’s freq, or are they future work (probably alongside or instead of mortable / freqpos)?

The +dCN form (capital C plus a number — “output only words used by <, <=, =, => or > than N percent of speakers”) is a separate flag from plain +dN; the rewriter does not currently handle +dC.... It would get its own clap field (--speaker-percentage-style) rather than overload --display-mode.

Output

Per-speaker frequency tables with:

  • Word frequency counts (sorted by count descending, then alphabetically)
  • Total types (unique words) and tokens (total words)
  • TTR (type-token ratio = types / tokens)

Example output (text)

Speaker: CHI
  the       12
  I         8
  want      6
  a         5
  go        4
  ...
Types: 45
Tokens: 127
TTR: 0.354

Example output (JSON)

{
  "speakers": {
    "CHI": {
      "words": { "the": 12, "I": 8, "want": 6, ... },
      "types": 45,
      "tokens": 127,
      "ttr": 0.354
    }
  }
}

Word Normalization

Words are grouped using NormalizedWord, which lowercases and strips compound markers (+) for counting purposes, while preserving the original CLAN display form (with +) for output. This means wanna+go and Wanna+Go are counted as the same word.

Differences from CLAN

Word identification

The legacy manual says FREQ ignores xxx, www, and words beginning with 0, &, +, -, or # by default, and also ignores header and code tiers unless selected. CLAN implements much of this with character-level string-prefix matching:

if (word[0] == '0') continue;     // omitted words
if (word[0] == '&') continue;     // fillers/nonwords
if (word[0] == '+') continue;     // terminators

Our implementation uses AST-based is_countable_word(), which checks semantic type rather than string prefixes. This is more precise – a filler (&-um) and a phonological fragment (&+fr) have distinct semantic types in our model, even though CLAN lumps them together under the & prefix.

Manual features not yet mirrored directly

The legacy manual documents several advanced FREQ workflows, including +s@file lexical-group lists, %mor/%gra combined search with +d7, and multilingual searches. Some of those behaviors are covered in clan-core through broader filtering infrastructure, but the command chapter should not imply one-for-one flag parity unless explicitly implemented.

Output ordering

Output is deterministic via sorted collections (count descending, then alphabetically). CLAN’s ordering can vary across runs.

Output formats

Supports text, JSON, and CSV formats. CLAN produces text only. Use --format clan for character-level CLAN-compatible output.

Multi-file behavior

Results are merged across files by default (+u behavior). CLAN requires explicit +u flag. Use chatter clan freq dir/ for recursive directory traversal (CLAN uses shell globs).

Golden test parity

Verified against CLAN C binary output. 100% parity.

GEMFREQ – Word Frequency Within Gem Segments

Status: Current Last updated: 2026-05-11 17:43 EDT

Purpose

Computes word frequency restricted to utterances inside @G:-labeled gem segments. gemfreq is a CLAN compatibility alias for the more general freq --gem. The behavior is identical to running freq with a required --gem filter; the alias exists so legacy CLAN scripts that invoke gemfreq directly keep working.

The legacy CLAN manual entry is at GEMFREQ.

Usage

gemfreq requires the --gem/-g option — there is no implicit “all gems” mode (unlike freq, where --gem is optional).

chatter clan gemfreq --gem story file.cha
chatter clan gemfreq -g story file.cha               # short form
chatter clan gemfreq --gem story --gem retell file.cha   # multiple gems

Options

The flag set is identical to freq: --mor, --speaker / --exclude-speaker, --include-word / --exclude-word, --exclude-gem, --range, --per-file, --include-retracings, --format, plus the universal verbosity / TUI / theme flags.

The only behavioral difference is that gemfreq rejects invocations without at least one --gem/-g argument; freq treats --gem as an optional restriction.

For full per-flag descriptions, output formats, word-normalization rules, and CLAN-equivalence tables, see freq.md.

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.14 (Unique Options, GEMFREQ) for PI review.

Important divergence flag. In CLAN, GEMFREQ’s +d table is identical to GEM’s (legal CHAT format + annotation), not identical to FREQ’s (output format selector with 9 values). chatter’s gemfreq currently inherits the freq common-args, so any --display-mode N implementation has to decide which semantics apply — and that’s worth pulling apart before clap touches it.

NCLAN behavior (verbatim from manual)
+d0“Produces simple output that is in legal chat format.”
+d1“Adds information to the legal chat output regarding file names, line numbers, and @ID codes.”

Open questions for PI review

  1. GEMFREQ in CLAN borrows GEM’s +d, not FREQ’s. Should chatter follow CLAN’s CLAN-faithful behavior (treat --display-mode 0 as the GEM-style legal-CHAT output), or should the chatter gemfreq inherit FREQ’s --display-mode table (since they share common-args)?
  2. If we honor CLAN’s split, gemfreq is the only analyze command whose +d differs from its lexically-similar parent. That feels like a CLAN historical accident worth deviating from in chatter — but it’s a deviation, not a faithful port.

When to use gemfreq vs freq --gem

Functionally these are the same call. Pick gemfreq when:

  • you are porting a legacy CLAN script that invokes gemfreq and want byte-compatible-looking command lines, or
  • you want the command name to surface “gem-restricted” intent immediately to readers of the script.

Pick freq --gem story when:

  • you are writing new scripts that may need to mix gem-restricted and unrestricted analysis under the same command, or
  • you want to omit --gem to fall back to whole-file frequency.

Reference

Implementation: Gemfreq is a separate clap subcommand variant in the ClanCommands enum (not a #[command(alias = "...")]); the required --gem constraint is enforced via a clap ArgGroup with required(true). The dispatcher at crates/chatter/chatter-cli/src/commands/clan/compatibility.rs::96 routes the parsed arguments to run_analysis_and_print with AnalysisCommandName::Freq, so behavior past the parse boundary is identical to a freq --gem … invocation. See freq.md for the complete reference.

MLU – Mean Length of Utterance

Status: Current Last updated: 2026-05-11 17:14 EDT

Purpose

Calculates mean length of utterance in morphemes from the %mor tier. When no %mor tier is available and --words was not passed, reports “utterances = 0, morphemes = 0” (matching CLAN behavior – no fallback to word counting).

See the CLAN manual for the original MLU command specification.

Usage

chatter clan mlu file.cha
chatter clan mlu --speaker CHI file.cha
chatter clan mlu --words file.cha
chatter clan mlu --format json corpus/

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--words–Count words from main tier instead of morphemes from %mor
--gem <LABEL>+g"label"Restrict to gem segment
--range <START-END>+z25-125Utterance range
--format <FMT>–Output format: text, json, csv, clan

CLAN Equivalence

CLAN commandRust equivalent
mlu file.chachatter clan mlu file.cha
mlu +t*CHI file.chachatter clan mlu file.cha --speaker CHI

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. The rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N, but no clap field consumes that token today. Drafted from CLAN manual §7.21.2 (Unique Options, MLU) for PI review. Plan: <workspace>/docs/superpowers/plans/2026-05-11-clan-rewriter-honor-three-flags.md Phase 3.

MLU’s +d table is small — two N-values, both Excel-friendly output formats. Quoted from CLAN manual §7.21.2:

NCLAN behavior (verbatim from manual)
+d (no number)“You can use this switch, together with the ID specification to output data for Excel.” Example: mlu +d +tCHI sample.cha produces a one-line @ID-keyed record: en|sample|CHI|1;10.4|female|||Target_Child|| 5 7 1.400 0.490 (fields: @ID, utterance count, morpheme count, MLU, MLU std dev). Requires @ID headers per participant.
+d1“This level of the +d switch outputs data in another systematic format, with data for each speaker on a single line. However, this form is less adapted to input to a statistical program than the output for the basic +d switch. Also, this switch works with the +u switch, whereas the basic +d switch does not.” Example: *CHI: 5 7 1.400 0.490.

Open questions for PI review

  1. +d (no number) maps cleanly to --format csv in chatter. Should --display-mode 0 (or absent N) imply --format csv, or remain a separate axis?
  2. +d1 is “less adapted to statistical input” yet combinable with +u. That combinability is the differentiating feature; should chatter expose it as a --display-mode merged-by-speaker enum variant?
  3. The +d output requires @ID headers per participant. Should --display-mode error early if @ID rows are missing for any matched speaker, or fall back to the speaker-code-only form silently?

Algorithm

For each utterance with a %mor tier:

  1. Count 1 per stem (the base morpheme word)
  2. Count +1 per bound morpheme suffix – but ONLY these 7 suffix strings: PL, PAST, Past, POSS, PASTP, Pastp, PRESP
  3. Count +1 per clitic stem (~ separated)
  4. Count clitic suffixes using the same 7-string rule
  5. Fusional features (&PRES, &INF, etc.) do NOT count

Per speaker, compute:

  • Number of utterances
  • Total morphemes
  • MLU (mean = total morphemes / utterances)
  • Standard deviation (population SD, dividing by n)
  • Range (min, max morphemes per utterance)

Brown’s Morpheme Rules

This was a key discovery during parity verification. CLAN only counts 7 specific suffix strings as bound morphemes:

SuffixMeaning
PLPlural
PASTPast tense
PastPast tense (alternate)
POSSPossessive
PASTPPast participle
PastpPast participle (alternate)
PRESPPresent participle

All other suffixes (including fusional features like &PRES, &INF, &3S) are ignored for MLU counting. This matches Brown’s (1973) original operationalization of “morpheme” for child language analysis.

Example

Given %mor: pro|I v|want-PAST det|a n|cookie-PL:

  • pro|I = 1 stem = 1
  • v|want-PAST = 1 stem + 1 suffix (PAST) = 2
  • det|a = 1 stem = 1
  • n|cookie-PL = 1 stem + 1 suffix (PL) = 2
  • Total: 6 morphemes

Output

Speaker: CHI
  Utterances: 42
  Morphemes: 168
  MLU: 4.000
  SD: 1.732
  Range: 1-9

Differences from CLAN

Standard deviation

Uses population SD (dividing by n), not sample SD (dividing by n-1). Verified against CLAN output – CLAN uses population SD too.

Morpheme counting

Uses parsed %mor tier structure (MorWord features and post-clitics) rather than text splitting on spaces and delimiters. The semantic result is identical thanks to applying Brown’s 7-suffix rule, but the mechanism is type-safe.

No %mor tier behavior

When no %mor tier exists and --words was not passed, reports 0 utterances for the speaker (matching CLAN). Does not silently fall back to word counting.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output.

MLT – Mean Length of Turn

Status: Current

Purpose

Calculates mean length of turn in utterances and words. A “turn” is a maximal consecutive sequence of utterances by the same speaker; the turn boundary is detected when a different speaker produces the next utterance.

See the CLAN manual for the original MLT command specification.

Usage

chatter clan mlt file.cha
chatter clan mlt --speaker CHI file.cha
chatter clan mlt --format json corpus/

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--gem <LABEL>+g"label"Restrict to gem segment
--range <START-END>+z25-125Utterance range
--format <FMT>–Output format: text, json, csv, clan

CLAN Equivalence

CLAN commandRust equivalent
mlt file.chachatter clan mlt file.cha
mlt +t*CHI file.chachatter clan mlt file.cha --speaker CHI

Algorithm

  1. Walk utterances in file order
  2. Group consecutive utterances by the same speaker into turns
  3. For each speaker, compute:
    • Number of turns
    • Total utterances and total words
    • MLT-u: mean turn length in utterances
    • MLT-w: mean turn length in words
    • SD: standard deviation of words per utterance (population SD, dividing by n)

Turn boundaries

A turn boundary occurs when a different speaker produces the next utterance. For example:

*CHI: I want a cookie .          <- turn 1 (CHI)
*CHI: please .                   <- still turn 1 (CHI)
*MOT: here you go .              <- turn 2 (MOT)
*CHI: thank you .                <- turn 3 (CHI)

CHI has 2 turns (3 utterances), MOT has 1 turn (1 utterance).

Output

Speaker: CHI
  Turns: 15
  Utterances: 42
  Words: 127
  MLT (utterances): 2.800
  MLT (words): 8.467
  SD: 3.217

Differences from CLAN

Standard deviation

Uses population SD (dividing by n), matching CLAN. This was verified during parity testing.

SD basis

The SD is computed over per-utterance word counts, not per-turn totals. This matches CLAN’s behavior, which was confirmed through golden test comparison.

Turn detection

Operates on parsed speaker codes from the AST rather than raw text line prefixes. Functionally identical but type-safe.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output.

VOCD – Vocabulary Diversity (D Statistic)

Status: Current Last updated: 2026-04-28 23:16 EDT

Purpose

Computes the D statistic for lexical diversity using bootstrap sampling of type-token ratios (TTR). The D statistic provides a more stable measure of vocabulary diversity than raw TTR because it accounts for sample size effects.

See the CLAN manual for the original VOCD command specification.

Usage

chatter clan vocd file.cha
chatter clan vocd --speaker CHI file.cha
chatter clan vocd --format json file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--format <FMT>–Output format: text, json, csv, clan

CLAN Equivalence

CLAN commandRust equivalent
vocd file.chachatter clan vocd file.cha
vocd +t*CHI file.chachatter clan vocd file.cha --speaker CHI

Algorithm

Overview

VOCD fits empirical type-token data to a theoretical curve, finding the D parameter that best explains the relationship between sample size and vocabulary diversity.

Steps

  1. Collect tokens: Gather all countable word tokens per speaker from the main tier
  2. Bootstrap sampling (3 independent trials):
    • For each sample size N from 35 through 50:
      • Draw 100 random samples of N tokens (without replacement)
      • Compute mean TTR across the 100 samples
    • Fit the empirical (N, TTR) curve to the theoretical D-curve using gradient-descent least-squares optimization
    • Record the optimal D value
  3. Report: Per-trial D values and their average

Theoretical TTR Curve

TTR(N) = (D/N) * [sqrt(1 + 2*N/D) - 1]

This models the expected type-token ratio for a sample of size N given a lexical diversity parameter D. Higher D means greater diversity.

Interpretation

D valueInterpretation
< 30Low lexical diversity
30-70Typical range for young children
70-100Typical range for older children/adults
> 100High lexical diversity

(Values are approximate and depend on the population.)

Output

Per-speaker D statistic with per-trial breakdown tables:

Speaker: CHI
  Trial 1:
    N    samples  TTR     std_dev   D
    35   100      0.743   0.045     42.1
    36   100      0.735   0.043     42.3
    ...
    50   100      0.680   0.038     43.0
    D = 42.5
  Trial 2: D = 41.8
  Trial 3: D = 43.2
  Average D = 42.5

Differences from CLAN

Stochastic variation

Because VOCD uses random sampling, D values may differ slightly between runs and between CLAN and our implementation. This is expected behavior, not a bug. Differences of +/- 5 are normal.

Fusional feature stripping

Fusional features (&PRES, &INF, etc.) are stripped from lemmas in %mor echo output. This ensures clean lemma display when VOCD echoes the morphological tier for insufficient-token warnings.

Word identification

Uses AST-based is_countable_word() instead of CLAN’s string-prefix matching. Token collection operates on parsed AST content rather than raw text.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output (within expected stochastic variation).

DSS – Developmental Sentence Scoring

Status: Current

Purpose

Assigns point values to utterances based on grammatical complexity, using a configurable rule file that defines pattern-matching rules for morphosyntactic categories. DSS is a clinical tool developed by Laura Lee and Susan Canter for evaluating children’s grammatical development by scoring complete sentences on eight grammatical categories.

Scoring requires a %mor dependent tier on each utterance. Utterances without %mor are silently skipped.

See the CLAN manual for the original DSS command specification and the full rule set.

Usage

chatter clan dss --speaker CHI file.cha
chatter clan dss --rules english.scr file.cha
chatter clan dss --max-utterances 100 file.cha
chatter clan dss --format json file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--exclude-speaker <CODE>Exclude speaker
--rules <PATH>Custom DSS rules file (.scr)
--max-utterances <N>Maximum utterances to score (default: 50)
--format <FMT>Output format: text, json, csv, clan

Scoring Categories

DSS scores utterances on eight grammatical categories:

  1. Indefinite pronouns / noun modifiers (it, this, that)
  2. Personal pronouns (I, me, my, mine, you, your)
  3. Main verbs (uninflected, copula, auxiliary)
  4. Secondary verbs (non-finite: infinitives, gerunds, participles)
  5. Negation (no, not, can’t, don’t)
  6. Conjunctions (and, but, or, if, because)
  7. Interrogative reversals (is he, can you, do they)
  8. Wh-questions (who, what, where, when, why, how)

Each category earns 1-8 points based on developmental complexity. A sentence point is awarded if the utterance is a complete grammatical sentence.

Algorithm

  1. Parse each utterance’s %mor tier for POS-tagged morphemes
  2. Match morpheme patterns against category rules
  3. For each category, award points for the highest-scoring matched pattern
  4. Award sentence point for complete sentences (heuristic: subject + verb POS)
  5. Sum across categories + sentence point = utterance score
  6. DSS = mean score across scored utterances

Output

Per-speaker DSS total with per-category breakdown and per-utterance scores.

Differences from CLAN

Built-in rules

The default rules are a simplified subset of the canonical DSS rule set (10 categories). For full clinical scoring, supply a complete .scr rules file via --rules. When a rules file is not provided, DSS produces approximate scores suitable for screening but not clinical reporting.

Sentence-point assignment

Uses a heuristic (presence of subject + verb POS tags in %mor) rather than full syntactic analysis. This may under-award sentence points for syntactically complex but structurally unusual utterances.

Maximum utterances

Defaults to 50 per speaker (configurable via --max-utterances). CLAN also defaults to 50 but the implementation differs in how utterances are selected when the sample exceeds the maximum.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

Verified against CLAN C binary output.

EVAL – Language Sample Evaluation

Status: Current

Purpose

Comprehensive morphosyntactic analysis computing lexical diversity, grammatical category counts, error rates, and MLU. EVAL was originally designed for clinical evaluation of adult aphasic speech samples (Saffran, Berndt & Schwartz, 1989) and produces a detailed profile of morphosyntactic abilities.

Requires a %mor dependent tier for morpheme-level metrics. Word-level metrics are computed from the main tier regardless.

See the CLAN manual for the original EVAL command specification.

Usage

chatter clan eval file.cha
chatter clan eval --speaker CHI file.cha
chatter clan eval --format json file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--format <FMT>–Output format: text, json, csv, clan

EVAL has no command-specific flags beyond the shared CommonAnalysisArgs set; the per-speaker normative-comparison aspect lives in EVAL-D (DementiaBank norms) and KIDEVAL (child norms), invoked as separate subcommands.

Metrics

EVAL produces a comprehensive profile per speaker:

Lexical measures

  • Utterances: Total utterance count
  • Total words: All countable words
  • NDW: Number of different words (types)
  • TTR: Type-token ratio (types / tokens)

MLU

  • MLU-w: Mean length of utterance in words
  • MLU-m: Mean length of utterance in morphemes (from %mor)

Part-of-speech counts (from %mor)

  • Nouns, verbs, auxiliaries, modals
  • Prepositions, adjectives, adverbs
  • Conjunctions, determiners, pronouns

Inflectional morphology (from %mor)

  • Plurals (PL)
  • Past tense (PAST)
  • Present participle (PRESP)
  • Past participle (PASTP)

Error and ratio measures

  • Word errors: Count of [*] markers
  • Open/closed class ratio: Content words vs function words

Differences from CLAN

Word and morpheme identification

Uses AST-based is_countable_word() and typed POS categories instead of CLAN’s string-prefix matching. POS classification operates on structured MorWord types rather than parsing POS tag strings at analysis time.

Error extraction

[*] error markers are extracted from parsed AST annotations (the ErrorMarker node type) rather than raw text pattern matching. This ensures accurate counting even with complex nested annotations.

Output formats

Supports text, JSON, and CSV. CLAN produces text only. JSON output provides structured access to all metrics for programmatic use.

Golden test parity

Verified against CLAN C binary output.

EVAL-D variant

chatter clan eval-d is identical to EVAL but uses DementiaBank protocol norms instead of AphasiaBank norms for normative comparison. See EVAL-D for details.

EVAL-D – Language Sample Evaluation (DementiaBank)

Status: Current

Purpose

EVAL-D is a variant of EVAL used for data collected with the DementiaBank protocol. The analysis logic is identical – only the normative comparison database differs.

EVAL uses AphasiaBank norms; EVAL-D uses DementiaBank norms.

Usage

chatter clan eval-d file.cha
chatter clan eval-d --speaker PAR file.cha
chatter clan eval-d --format json file.cha

Options

All options are identical to EVAL.

Differences from CLAN

  • Same as EVAL – see EVAL differences.
  • The EvalVariant::Dialect config flag selects DementiaBank norms automatically.

Implementation

EVAL-D is not a separate command module. It reuses EvalCommand with EvalConfig { variant: EvalVariant::Dialect, .. }. The variant determines which .cut database directory to use for normative comparison.

KIDEVAL – Child Language Evaluation

Status: Current

Purpose

Produces a comprehensive child language evaluation report by combining multiple analysis methods into a single per-speaker summary. KIDEVAL is designed for evaluating children’s language development and aggregates results from several individual CLAN commands.

See the CLAN manual for the original KIDEVAL command specification.

Usage

chatter clan kideval file.cha
chatter clan kideval --speaker CHI file.cha
chatter clan kideval --format json file.cha
chatter clan kideval --dss-rules english.scr file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--exclude-speaker <CODE>Exclude speaker
--dss-rules <PATH>Custom DSS rules file (.scr)
--ipsyn-rules <PATH>Custom IPSYN rules file
--format <FMT>Output format: text, json, csv, clan

KIDEVAL does not expose per-component utterance caps as CLI flags (despite the standalone DSS / IPSYN commands having --max-utterances of their own); the combined report uses each component’s built-in default (DSS 50, IPSYN 100). To override, run those components separately and aggregate.

Combined Metrics

KIDEVAL produces a single report combining:

MetricSourceDetails
MLU (words and morphemes)Main tier + %morSee MLU
NDW / TTRMain tier word types/tokensSee FREQ
DSS score%mor tierSee DSS
VOCD (D statistic)Main tier wordsSee VOCD
IPSyn score%mor tierSee IPSYN
POS category counts%mor tierNouns, verbs, auxiliaries, etc.
Error counts[*] markersWord-level errors

This is the primary tool for clinical assessment of child language samples, providing a comprehensive profile in a single command invocation.

Differences from CLAN

VOCD simplification

KIDEVAL uses a simplified TTR-based D estimate rather than the full bootstrap sampling approach used by the standalone VOCD command. This trades precision for speed when computing the combined report.

IPSYN rules

Uses the built-in simplified rule subset unless a custom rules file is provided via --ipsyn-rules. For full 56-rule coverage, supply the official IPSYN rules file.

DSS rules

Uses the built-in simplified rule subset unless a custom rules file is provided via --dss-rules. For full clinical scoring, supply a complete .scr rules file.

AST-based analysis

All component analyses share the same AST-based infrastructure, ensuring consistent word identification and morpheme counting across all metrics. In CLAN, each component command has its own independent word-filtering logic, which can lead to subtle inconsistencies.

Golden test parity

Verified against CLAN C binary output.

IPSYN – Index of Productive Syntax

Status: Current

Purpose

Computes a syntactic complexity score by awarding points for distinct syntactic structures observed in a child’s utterances. Each structure type (rule) can earn at most 2 points – one per distinct utterance in which the structure appears. The total across all rules yields the IPSyn score.

See the CLAN manual for the original IPSYN command specification.

Usage

chatter clan ipsyn file.cha
chatter clan ipsyn --speaker CHI file.cha
chatter clan ipsyn --rules ipsyn.rules file.cha
chatter clan ipsyn --max-utterances 100 file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--exclude-speaker <CODE>Exclude speaker
--rules <PATH>Custom IPSYN rules file
--max-utterances <N>Maximum utterances to analyze (default: 100)
--format <FMT>Output format: text, json, csv, clan

Rule Categories

Rules are organized into four categories:

CategoryCodeDescriptionExample structures
Noun PhraseNNoun phrase complexityTwo-word NP, article+noun, possessive
Verb PhraseVVerb phrase complexityCopula, auxiliary, modal, infinitive
QuestionQQuestion formationYes/no, wh-question, tag question
SentenceSSentence structureConjoined, embedded, relative clause

The full English IPSyn has ~56 rules. The built-in default set provides a representative subset.

Algorithm

  1. For each utterance, serialize the %mor tier to text
  2. Match each rule pattern against the serialized %mor content
  3. For each rule, record the first two distinct utterances that match (max 2 points per rule)
  4. Sum all rule scores across categories
  5. Report total score and per-category subtotals

Scoring example

If rule N1 (“Two-word NP”) matches in utterances 3 and 7, it earns 2 points. If it only matches in utterance 3, it earns 1 point. If it never matches, 0 points.

Output

Per-speaker IPSyn total score with per-category subtotals (N, V, Q, S) and optional per-rule detail.

Differences from CLAN

Rule set

The built-in rule set is a simplified subset. For full 56-rule coverage, supply the official IPSYN rules file via --rules.

Pattern matching

Uses substring-based matching on the serialized %mor tier text rather than structured POS/morpheme matching. This produces equivalent results for most patterns but may differ for edge cases involving complex morphological structures.

Maximum utterances

Defaults to 100 (matching CLAN). Configurable via --max-utterances.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

Verified against CLAN C binary output.

FLUCALC – Fluency Calculation

Status: Current

Purpose

Detects and quantifies disfluencies in speech transcripts, producing per-speaker counts of stuttering-like disfluencies (SLD) and typical disfluencies (TD). FLUCALC is the standard tool in CLAN for analyzing fluency in stuttering research.

See the CLAN manual for the original FLUCALC command specification.

Usage

chatter clan flucalc file.cha
chatter clan flucalc --speaker CHI file.cha
chatter clan flucalc --format json file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--exclude-speaker <CODE>Exclude speaker
--format <FMT>Output format: text, json, csv, clan

FLUCALC has no command-specific flags beyond the shared CommonAnalysisArgs set. There is no --syllable-mode switch in the current CLI; output measures are word-based.

Disfluency Categories

Stuttering-Like Disfluencies (SLD)

TypeCHAT notationExampleStatus
Prolongation: within wordwa:ntImplemented
Broken word^ notationbase^ballImplemented
Whole-word repetitionConsecutive identical wordsI I wantImplemented
Part-word repetition––Partial
Block––Partial

Typical Disfluencies (TD)

TypeCHAT notationExampleStatus
Phrase repetition[/]I want [/] I wantImplemented
Revision[//]I want [//] I needImplemented
Filled pause&- prefix&-uh, &-umImplemented
Phonological fragment&+ prefix&+frImplemented

Output measures

All counts are reported as:

  • Raw values: Total count per disfluency type
  • Percentages: Per 100 words

Algorithm

  1. For each utterance, walk the AST content nodes
  2. Identify disfluency markers:
    • Retrace groups ([/], [//]) from parsed AST annotations
    • Fillers and fragments from word category annotations
    • Prolongations and broken words from within-word notation
    • Whole-word repetitions from consecutive identical countable words
  3. Accumulate per-speaker counts by disfluency category
  4. Compute percentages relative to total words (or syllables)

Differences from CLAN

Detection method

Some categories (specifically [/] and [//] retrace markers) are currently counted via substring matching on the serialized CHAT text rather than fully through the parsed AST. This produces equivalent results but is a known area for future improvement.

Part-word repetitions and blocks

Counted via CHAT notation markers rather than acoustic analysis. Full detection of these categories requires audio-linked analysis that is beyond the scope of text-based transcript processing.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

Verified against CLAN C binary output.

SUGAR – Sampling Utterances and Grammatical Analysis Revised

Status: Current Last updated: 2026-05-12 13:38 EDT

Purpose

Computes language sample analysis metrics from %mor and %gra tiers, providing a quick clinical assessment of grammatical complexity. SUGAR is designed as a time-efficient alternative to more detailed scoring systems like DSS or IPSYN.

See the CLAN manual for the original SUGAR command specification.

Usage

chatter clan sugar file.cha
chatter clan sugar --speaker CHI file.cha
chatter clan sugar --format json file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--exclude-speaker <CODE>Exclude speaker
--format <FMT>Output format: text, json, csv, clan

SUGAR has no command-specific flags beyond the shared CommonAnalysisArgs set. The minimum-utterance threshold (50) is a fixed internal default; there is no --min-utterances switch in the current CLI.

Metrics

MetricDescriptionSource
MLU-SMean Length of Utterance in morphemes%mor tier
TNWTotal Number of Words (tokens with POS tags)%mor tier
WPSWords Per SentenceUtterances containing verbs
CPSClauses Per Sentence%gra subordination relations

Algorithm

  1. For each utterance, count morphemes and words from %mor
  2. Detect verb-containing utterances using POS tags: v, verb, cop, aux, mod, part (crates/clan/clan-core/src/commands/sugar.rs:198 — both v and the longer verb form are accepted)
  3. For verb utterances with %gra, count subordinate clauses via grammatical relations (COMP, CSUBJ, CMOD, etc.)
  4. Compute per-speaker ratios at finalization:
    • WPS = total words / number of verb utterances
    • CPS = total clauses / number of verb utterances

Minimum utterance threshold

If a speaker has fewer than min_utterances (default: 50), the sample is flagged as insufficient. This ensures statistical reliability of the computed ratios.

Differences from CLAN

Verb detection

Uses mapped POS tags from the parsed %mor tier structure. CLAN may use a slightly different POS tag set for verb identification. Both implementations identify the same core verb categories.

Post-clitic %mor chunks are included in verb detection, so clitic-bearing items still contribute when the verb-like chunk appears only after ~.

Clause counting

Uses %gra subordination relations only (dependency structure). CLAN’s clause detection may use additional heuristics beyond grammatical relations.

Morpheme counting

Morpheme counts are computed from typed %mor structure, including post-clitics and their features.

Minimum utterance threshold

The Rust implementation uses a fixed internal default of 50, matching CLAN. There is currently no CLI flag to override this.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

Verified against CLAN C binary output.

MORTABLE – Morphological Category Cross-Tabulation

Status: Current Last updated: 2026-05-12 11:09 EDT

Purpose

Produces a per-speaker frequency table of morphosyntactic categories by matching POS tags from the %mor tier against patterns defined in a language-specific script file.

Requires a language script file (e.g., eng.cut) that defines patterns and their labels for categorizing morphemes from the %mor tier. Each rule line contains a quoted label and +/- prefixed POS patterns. Rules can be grouped as OR (first match wins) or AND (all must match).

See the CLAN manual for the original MORTABLE command specification.

Note on -f short flag. Both --script and --format declare -f as their short flag in crates/chatter/chatter-cli/src/cli/args/clan_commands.rs:260 and :345. clap currently accepts this — chatter clan mortable --help runs successfully and -f test.cut resolves to --script — so the previous warning that the command was “unusable” no longer applies. Verified 2026-05-12 by invoking chatter clan mortable --help and chatter clan mortable -f test.cut file.cha. Prefer the long form --script / --format in scripts to avoid ambiguity in future clap upgrades.

Usage

chatter clan mortable --script eng.cut file.cha
chatter clan mortable --script eng.cut --speaker CHI file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--script <PATH>Language script file (.cut) – required
--format <FMT>Output format: text, json, csv, clan

Differences from CLAN

  • POS matching: Operates on parsed %mor tier data rather than raw text line scanning.
  • POS matching detail: POS tags are read directly from typed %mor items instead of reparsing serialized %mor content.
  • Script file format: Compatible with CLAN’s .cut files.
  • Output formats: Supports text, JSON, and CSV formats.
  • Deterministic ordering: BTreeMap ordering ensures deterministic output across runs.
  • Golden test parity: Verified against CLAN C binary output.

KWAL – Keyword And Line

Status: Current Last updated: 2026-05-11 17:10 EDT

Purpose

Searches for clusters containing specified keywords and displays the matching lines with context. The legacy manual gives KWAL a dedicated section and describes it as operating on “clusters”: the main tier plus the selected dependent tiers associated with that line.

In clan-core, keywords are currently matched against countable words on the main tier, with the matched utterance shown in context.

Usage

chatter clan kwal -k want file.cha
chatter clan kwal -k want --speaker CHI file.cha
chatter clan kwal -k want -k cookie file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
-k <WORD> / --keyword <WORD>Keyword to search for (repeatable)
--format <FMT>Output format: text, json, csv, clan

CLAN Equivalence

CLAN commandRust equivalent
kwal +s"want" file.chachatter clan kwal file.cha -k want
kwal +s"want" +t*CHI file.chachatter clan kwal file.cha -k want --speaker CHI

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. The rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N, but no clap field consumes that token today. This table is drafted from CLAN manual §7.17.5 (Unique Options, KWAL) verbatim for PI review. Tracked in <workspace>/docs/superpowers/plans/2026-05-11-clan-rewriter-honor-three-flags.md Phase 3.

KWAL uses +d to switch the output shape (plain CHAT, with filenames, Excel form, etc.). Quoted from CLAN manual §7.17.5:

NCLAN behavior (verbatim from manual)
+d (no number)“Normally, kwal outputs the location of the tier where the match occurs. When the +d switch is turned on you can [output] in these formats: … outputs legal CHAT format.”
+d1“Outputs legal CHAT format plus file names and line numbers.”
+d2“Outputs file names once per file only.”
+d3“Outputs ONLY matched items.”
+d30“Outputs ONLY matched items without any defaults removed. The +d30 and the +d3 switches can be combined.”
+d99“Convert ‘word [x 2]’ to ‘word [/] word’ and so on.”
+d4“Outputs for Excel.”
+d40“Outputs for Excel, repeating the same tier for every keyword match.”
+d7“Compares items across dependent tiers.” Example: kwal +d7 +s@|-cop +sROOT +t%gra +t%mor t.cha

Open questions for PI review

  1. +d (no number): the manual implies the default behavior already prints match location; +d changes the format to legal CHAT. Does --display-mode 0 (or --display-mode chat) feel right as a chatter spelling? An enum-valued flag would be more honest than a 0..99 numeric range.
  2. +d30 is “+d3 + don’t strip defaults” — combinable. Maps to --display-mode matched --no-strip-defaults or --display-mode 30?
  3. +d99 is conceptually orthogonal to the others (it’s a transformation, not an output shape). Worth splitting into a separate --expand-repetition flag rather than overloading --display-mode.
  4. +d4/+d40 for Excel: same Excel question as FREQ — overlap with --format csv.
  5. +d7 cross-tier comparison: deeply specific. In scope for the first --display-mode pass, or future work alongside freqpos / mortable?

Output

Each matching utterance with:

  • Speaker code
  • Full utterance text
  • File path (for multi-file searches)
  • Match count summary per keyword

Differences from CLAN

  • Manual intent: KWAL is a cluster-oriented search command, not just a main-tier keyword matcher.
  • Search: Operates on parsed AST word content rather than raw text lines.
  • Word identification: Uses AST-based is_countable_word() instead of CLAN’s string-prefix matching.
  • Scope reduction: The legacy manual describes richer tier-selection and output-shaping behavior, including cluster searches over selected dependent tiers and %mor/%gra combined searches with +d7. The current implementation is narrower.
  • Output formats: Supports text, JSON, and CSV formats (CLAN produces text only).
  • Golden test parity: Verified against CLAN C binary output.

COMBO – Boolean Keyword Search

Status: Current Last updated: 2026-05-11 17:20 EDT

Purpose

Searches for utterances matching boolean combinations of keywords. Supports AND (+) and OR (,) logic with case-insensitive substring matching. This is the primary search tool for finding utterances containing specific words or word combinations.

See the CLAN manual for the original COMBO command specification.

Usage

chatter clan combo -S "want+cookie" file.cha
chatter clan combo -S "want,milk" file.cha
chatter clan combo -S "want+cookie" --speaker CHI file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
-S, --search <EXPR>Search expression (required, repeatable; multiple --search flags combined with OR)
--format <FMT>Output format: text, json, csv, clan

Search Syntax

  • + between terms means AND (all terms must be present in the utterance)
  • , between terms means OR (at least one term must be present)
  • Terms are case-insensitive substring matches against countable words
  • Multiple --search flags are combined with OR (any expression matching counts)
  • AND takes precedence if both + and , appear in one expression

CLAN Equivalence

CLAN commandRust equivalent
combo +s"want^cookie" file.chachatter clan combo file.cha -S "want+cookie"
combo +s"want|milk" file.chachatter clan combo file.cha -S "want,milk"
combo +s"want^cookie" +t*CHI file.chachatter clan combo file.cha -S "want+cookie" --speaker CHI

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.7.10 (Unique Options, COMBO) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“Normally, combo outputs the location of the tier where the match occurs. When the +d switch is turned on you can output only each matched sentence in a simple legal chat format.”
+d1“Outputs legal chat format along with line numbers and file names.”
+d2“Outputs files names once per file only.”
+d3“Outputs legal chat format, but with only the actual words matched by the search string, along with @Comment headers that are ignored by other programs.”
+d4“Use of the +d4 switch was described in the previous section.” (Manual cross-reference; resolution pending.)
+d7“Search for words linked between two tiers.”

Open questions for PI review

  1. +d/+d1/+d2 parallel KWAL’s +d/+d1/+d2 almost exactly. Worth defining a shared --display-mode enum across search-style commands (KWAL + COMBO) with the same variant names?
  2. +d3 “matched words plus @Comment headers” is COMBO-specific. Probably a separate enum variant.
  3. +d4: manual cross-references the “previous section”. This needs PI clarification — the immediately-previous section is general COMBO description, not a +d-table.
  4. +d7 (cross-tier linkage) overlaps with FREQ +d7. If both commands’ +d7 is “compare two tiers”, the enum variant name should match.

Output

Each matching utterance with:

  • Source filename
  • Speaker code
  • Full utterance text (CHAT format)
  • Summary counts of matching vs. total utterances

Differences from CLAN

  • Operator syntax: CLAN uses ^ for AND and \| for OR; this implementation uses + and , respectively for shell-friendliness.

CODES – Code Frequency Table

Status: Current

Purpose

Tabulates the frequency and distribution of coding annotations found on %cod: dependent tiers, organized by speaker. This is useful for analyzing hand-coded behavioral or discourse annotations attached to transcripts.

Codes on %cod: tiers typically use colon-separated hierarchical structure (e.g., AC:DI:PP), but this implementation treats each whitespace-delimited token as a single code string without parsing the internal hierarchy.

The CLAN manual does not appear to contain a standalone CODES section. The closest direct evidence is the CHAINS section, which documents coding tiers and the codes.ord file used to order $-prefixed codes.

Usage

chatter clan codes file.cha
chatter clan codes --speaker CHI file.cha
chatter clan codes --format json file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--max-depth <N>Maximum depth of code parsing (0 = all levels)
--format <FMT>Output format: text, json, csv, clan

Output

Per-speaker frequency tables listing each code and its count, plus a per-speaker total and a grand total across all speakers.

Differences from CLAN

  • Manual coverage gap: the CLAN manual has no standalone CODES section, so this chapter is based on implemented behavior, golden tests, and nearby coding-tier documentation such as CHAINS.
  • Code extraction: Codes are extracted from a clan-local semantic %cod layer built from the parsed AST, not from raw line text.
  • Selector handling: %cod extraction preserves optional selectors like <w4> or <w4-5> as item scope rather than counting them as standalone codes.
  • Hierarchy handling: Each whitespace-delimited token is treated as a single code string; colon-separated hierarchy is preserved but not parsed into sublevels.
  • Output formats: Supports text, JSON, and CSV formats (CLAN produces text only).
  • Deterministic ordering: BTreeMap ordering ensures deterministic output across runs.
  • Golden test parity: Verified against CLAN C binary output.

COMPLEXITY — Syntactic Complexity Ratio

Status: Current Last updated: 2026-05-12 10:07 EDT

Computes syntactic complexity by counting subordinating dependency relations in the %gra tier.

Usage

chatter clan complexity file.cha
chatter clan complexity corpus/ --speaker PAR
chatter clan complexity file.cha --format json

What It Measures

COMPLEXITY reads the %gra dependency tier and counts clause-embedding relations that indicate syntactic subordination. The complexity ratio is the number of subordinating relations divided by total tokens (non-punctuation).

UD Relations (auto-detected)

Per crates/clan/clan-core/src/commands/complexity.rs:10:

RelationDescription
CSUBJClausal subject
CCOMPClausal complement
XCOMPOpen clausal complement
ACLAdnominal clause
ADVCLAdverbial clause modifier
APPOSApposition
EXPLExpletive

Legacy CLAN Relations (auto-detected)

RelationDescription
CSUBJClausal subject
COMPComplement
CPREDClausal predicate
CPOBJClausal object of preposition
COBJClausal object
CJCTClausal adjunct
XJCTNon-finite clausal adjunct
NJCTNominal adjunct
CMODClausal modifier
XMODNon-finite clausal modifier

Output

Per speaker:

  • Count of each subordinating relation
  • Tokens: total matched complexity relations
  • TotalTokens: all non-PUNCT tokens
  • Ratio: Tokens / TotalTokens

Requirements

Requires a %gra dependent tier. Utterances without %gra are skipped.

Differences from CLAN

  • Auto-detects UD vs legacy relation names (CLAN requires compile-time config)
  • Uses typed AST GraTier with GrammaticalRelation entries
  • Supports JSON and CSV output
  • Sub-relations (e.g., CSUBJ:pass) are properly matched to their base relation

CORELEX — Core Vocabulary

Status: Current

Purpose

Identifies “core” vocabulary items that appear above a frequency threshold. Core vocabulary analysis is used in clinical assessment to evaluate whether a child’s lexicon includes expected high-frequency words.

Usage

chatter clan corelex file.cha
chatter clan corelex --speaker CHI file.cha
chatter clan corelex --threshold 5 file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--threshold <N>Minimum frequency for core classification (default: 3)
--format <FMT>Output format: text, json, csv

Output

  • Core word list (frequency >= threshold) sorted by frequency descending
  • Non-core word list
  • Core/total ratio and percentage
  • Per-word speaker count (how many speakers used each word)

Differences from CLAN

  • Word identification uses AST-based is_countable_word().
  • Output supports text, JSON, and CSV formats.
  • Core/non-core classification uses shared NormalizedWord for consistency.

CHAINS – Clause Chain Analysis

Status: Current Last updated: 2026-05-11 17:30 EDT

Purpose

Analyzes sequences of interactional codes on a single specified coding tier. In the legacy manual, CHAINS is described as tracking hand-entered codes on one coding tier, with %cod, %spa, and %sit given as examples, and with dependent-tier periods used to mark correspondence to [^c] clause units on the main tier.

In clan-core, a “chain” is a run of consecutive utterances by the same speaker that all contain a given code item on the selected coding tier. When the code disappears or the speaker changes, the chain is flushed and its length is recorded.

Reports chain count, average/min/max length, and standard deviation per code and speaker.

See the CLAN manual for the original CHAINS command specification.

Usage

chatter clan chains file.cha
chatter clan chains --speaker CHI file.cha
chatter clan chains --tier cod file.cha

Options

OptionDescription
--speaker <CODE>Include speaker
--tier <NAME>Tier label to read codes from (default: cod)
--format <FMT>Output format: text, json, csv, clan

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.1.2 (Unique Options, CHAINS) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“Use this switch to change zeroes to spaces in the output.” Example: chains +d +t%spa chains.cha +s$nia:%
+d1“Works the same as +d, while also displaying every input line in the output.”

Open questions for PI review

  1. “Change zeroes to spaces” is a CLAN text-formatting artifact — columnar output where 0 marks absence. Does chatter’s typed output already render absence as empty/space? If so, --display-mode 0 may be a no-op in chatter and the flag should error rather than silently accept.
  2. +d1 “displays every input line”: this is a verbose-trace mode, not an output-shape change. Map to --include-input boolean instead of overloading --display-mode?

Output

Per code and speaker:

  • Number of separate chains
  • Average chain length
  • Standard deviation of chain lengths (sample, N-1)
  • Minimum chain length
  • Maximum chain length

Differences from CLAN

  • Manual intent: The legacy manual frames CHAINS as a coding-tier command, not a generic dependent-tier tokenizer.
  • Speaker-change detection: Flushes all open chains for the previous speaker, matching CLAN’s behavior of treating chains as speaker-scoped.
  • Coding-tier semantics: CHAINS operates on %cod-style coding items, not %gra.
  • Code extraction: For %cod, chains now read a clan-local semantic %cod item stream derived from the parsed AST rather than flattening the tier back to whitespace text. For other tiers passed via --tier, extraction remains content-token based.
  • Manual constraint not yet fully enforced: The manual examples all use coding tiers. Non-%cod --tier fallback still tokenizes tier text; this should eventually be restricted to tiers with explicit coding semantics.
  • Standard deviation: Uses the sample (N-1) formula rather than population.
  • Golden test parity: Verified against CLAN C binary output.

CHIP – Child/Parent Interaction Profile

Status: Current Last updated: 2026-05-11 17:18 EDT

Purpose

Analyzes interaction patterns between a child speaker and their conversational partners. Categorizes successive utterance pairs to measure imitation, repetition, and overlap. CHIP is commonly used in child language research to quantify how much a child imitates or echoes their interlocutor.

Usage

chatter clan chip file.cha
chatter clan chip --speaker CHI file.cha
chatter clan chip --format json file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--format <FMT>–Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. The rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N, but no clap field consumes that token today. Drafted from CLAN manual §7.4.5 (Unique Options, CHIP) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“Using +d with no further number outputs only coding tiers, which are useful for iterative analyses.”
+d1“Using +d1 outputs only summary statistics, which can then be sent to a statistical program.”

Open questions for PI review

  1. CHIP’s +d shape (coding-tier-only vs summary-only) is orthogonal to the FREQ/KWAL/MLU shape (output format selectors). Should the --display-mode enum’s variants be CHIP-specific (coding-tiers / summary) or share a name space with FREQ’s variants?
  2. “Useful for iterative analyses” implies the coding-tier output is intended to be piped to another chatter clan command. Should chatter prefer making this the default JSON output shape, with --display-mode summary collapsing to just the matrix?

Interaction Categories

For each adjacent utterance pair (speaker A followed by speaker B):

CategoryCondition
Exact repetitionB’s words are identical to A’s (order-independent)
OverlapB shares >= 50% of words with A (smaller unique-word set as denominator)
No overlapB shares < 50% of words with A

Only cross-speaker adjacency is considered; consecutive utterances by the same speaker do not produce interaction records. Adjacency state is reset at file boundaries.

Output

36-measure matrix format structurally matching CLAN:

  • ADU (adult) / CHI (child) / ASR (adult-speech-related) / CSR (child-speech-related) columns
  • Per directed speaker pair (MOT->CHI is distinct from CHI->MOT)
  • Counts and percentages for each interaction category
  • Grand totals across all pairs

Current implementation status (2026-05-12): the matrix header, row labels, and ADU/CHI/ASR/CSR column layout are implemented and written by chip.rs:246-260. The actual measure values are written as zeros — chip.rs:253 carries a comment “currently all zeros — full computation not yet implemented.” So the structural parity is in place but the analytical numbers are not yet computed. See §Golden test parity below for the consequence.

Echo behavior

When displaying matched utterances, CHIP echoes:

  • Main tier text
  • %mor tier (if present)

It does not echo %gra tiers, matching CLAN’s behavior.

Differences from CLAN

Matrix format

Uses the exact 36-measure matrix format with ADU/CHI/ASR/CSR columns. Header, row labels, and column structure match CLAN character-for-character; per §Output, the measure-value cells are currently emitted as zeros pending full implementation.

Echo content

Echoes main tier + %mor only (not %gra tiers), matching CLAN.

Word identification

Uses AST-based is_countable_word() instead of CLAN’s string-prefix matching. Overlap comparison operates on parsed word content, not raw text.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

No CHIP-specific golden tests are currently checked in (rg 'chip_golden|chip.*golden' returns nothing in tests/ and crates/clan/clan-core/). The previous “100% parity” claim was incorrect given the stub measure values described above. Once the 36 measures are computed, golden tests should be added against a real CLAN CHIP run.

COOCCUR — Word Co-occurrence (Bigram) Counting

Status: Current Last updated: 2026-05-11 17:32 EDT

Purpose

Counts adjacent word pairs (bigrams) across utterances. For each utterance, every pair of consecutive countable words is recorded as a directed bigram. Pairs are directional: (“put”, “the”) and (“the”, “put”) are counted separately.

COOCCUR is part of the FREQ family of commands and is useful for studying word collocations and sequential patterns in speech.

Usage

chatter clan cooccur file.cha
chatter clan cooccur file.cha --speaker CHI

Options

OptionCLAN flagDescription
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.8.1 (Unique Options, COOCUR) for PI review. Manual uses CLAN’s COOCUR spelling; chatter’s subcommand is cooccur.

NCLAN behavior (verbatim from manual)
+d (no number)“Strip the numbers from the output data that indicate how often a certain cluster occurred.”

Open questions for PI review

  1. +d here removes frequency counts from the output, leaving just the cluster strings. That’s a content-stripping switch, not a format selector. Map to --no-counts boolean or --display-mode tokens-only enum variant?
  2. There is no +d1/+d2/… documented for COOCUR — only the bare +d. The clap --display-mode N shape may want a Option<u8> where None = default, Some(0) = bare +d form.

Output

  • Table of adjacent word pairs with co-occurrence counts
  • Default sort: by frequency descending, then alphabetically
  • CLAN output: sorted alphabetically by pair display form
  • Summary: unique pair count, total pair instances, total utterances

Differences from CLAN

  • Word identification uses AST-based is_countable_word() instead of CLAN’s string-prefix matching
  • Bigram extraction operates on parsed AST content rather than raw text
  • Output supports text, JSON, and CSV formats (CLAN produces text only)
  • Deterministic output ordering via sorted collections
  • Golden test parity: Verified against CLAN C binary output

DIST – Word Distribution Across Turns

Status: Current Last updated: 2026-05-11 17:33 EDT

Purpose

Counts turns and tracks for each word the first and last turn in which it appears. DIST is part of the FREQ family of commands and is useful for studying when words first appear and how their usage is distributed across a conversation.

Usage

chatter clan dist file.cha
chatter clan dist --speaker CHI file.cha
chatter clan dist --format json file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--format <FMT>–Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.9.1 (Unique Options, DIST) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“Output data in a form suitable for statistical analysis.”

DIST’s +d7 is mentioned in passing in the manual as part of the FREQ-family +d7 cross-tier comparison.

Open questions for PI review

  1. “Form suitable for statistical analysis” maps cleanly to --format csv in chatter. Should +d translate directly to --format csv at rewrite time (drop the --display-mode translation entirely for DIST), or honour both?
  2. The DIST +d7 mention is a stub — is DIST genuinely a +d7 user, or is the manual cross-referencing FREQ’s +d7?

Output

Global word list (sorted alphabetically by display form) with:

  • Occurrence count across all turns
  • First turn number (1-based) in which the word occurs
  • Last turn number (omitted if same as first)
  • Total number of turns in the transcript

Turn Definition

Every utterance is its own turn, regardless of whether the speaker changed. This matches CLAN’s behavior, which was verified during parity testing. There is no speaker-continuity grouping – each utterance increments the turn counter.

This is different from how turns are defined in MLT (where consecutive utterances by the same speaker form a single turn).

Differences from CLAN

Turn counting

Every utterance = one turn (no speaker-continuity grouping), matching CLAN exactly.

Word identification

Uses AST-based is_countable_word() instead of CLAN’s string-prefix matching.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output.

FREQPOS — Word Frequency by Position

Status: Current Last updated: 2026-05-11 17:35 EDT

Purpose

Counts how often each word appears in initial, final, other (middle), or one-word positions within utterances. FREQPOS is part of the FREQ family of commands and is useful for studying positional word preferences – for example, whether a child tends to place certain words at the beginning or end of utterances.

Position Classification

  • Initial: first word of a multi-word utterance
  • Final: last word of a multi-word utterance
  • Other: any middle word of a multi-word utterance (3+ words)
  • One-word: the sole word in a single-word utterance

Usage

chatter clan freqpos file.cha
chatter clan freqpos file.cha --speaker CHI

Options

OptionCLAN flagDescription
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.12.1 (Unique Options, FREQPOS) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“Count words in either first, second, or other positions. The default is to count by first, last, and other positions.”

Open questions for PI review

  1. FREQPOS’s +d switches the position-classification scheme from “first / last / other / one-word” (default) to “first / second / other”. That’s not a display change — it’s a bucketing change. Map to --positions <first-last|first-second> enum rather than --display-mode.
  2. If we keep the --display-mode translation, +d (no number) corresponds to a single behavior (the alternative bucketing), so --display-mode 0 is the only valid value. The clap field should probably be an enum with two variants (Default, FirstSecondOther), not a numeric Option<u8>.

Output

Global word list (sorted alphabetically by display form) with positional breakdown (initial/final/other/one-word counts per word), followed by aggregate position totals.

Differences from CLAN

  • Word identification uses AST-based is_countable_word() instead of CLAN’s string-prefix matching
  • Position classification operates on parsed AST word lists rather than raw text token splitting
  • Output supports text, JSON, and CSV formats (CLAN produces text only)
  • Deterministic output ordering via sorted collections
  • Golden test parity: Verified against CLAN C binary output

GEMLIST — List Gem Segments

Status: Current

Purpose

Lists all gem segments (@Bg/@Eg bracketed regions) found in CHAT files, reporting the label, utterance count, and participating speakers for each gem.

See the CLAN manual for the original GEM command specification.

Usage

chatter clan gemlist file.cha
chatter clan gemlist file.cha --speaker CHI

Options

OptionCLAN flagDescription
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Output

Per gem label:

  • Number of utterances within the gem scope
  • Number of occurrences (how many @Bg/@Eg pairs with this label)
  • Speakers who produced utterances within the gem
  • Source files containing this gem

Implementation Note

Gem boundaries (@Bg/@Eg) are interleaved headers in ChatFile.lines. Since the parser does not populate Utterance.preceding_headers, this command scans the full line array in end_file() rather than relying on per-utterance callbacks.

Differences from CLAN

  • Gem boundary detection operates on parsed Header variants from the AST rather than raw text line matching
  • Output supports text, JSON, and CSV formats (CLAN produces text only)
  • Deterministic output ordering via sorted collections
  • Golden test parity: Verified against CLAN C binary output

KEYMAP — Contingency Tables for Coded Data

Status: Current

Purpose

Builds contingency tables for coded interactional data. The legacy manual describes KEYMAP as choosing initiating or beginning codes on a specific coding tier, then examining all codes on that same tier in the next utterance.

In clan-core, given a set of keyword codes, KEYMAP tracks each keyword occurrence on a specified coding tier and records what code items appear in the immediately following utterance, broken down by speaker.

See the CLAN manual for the original KEYMAP command specification.

Usage

chatter clan keymap file.cha --keyword code1 --keyword code2
chatter clan keymap file.cha --keyword code1 --tier spa
chatter clan keymap file.cha -k code1 -k code2          # short form

Options

OptionCLAN flagDescription
-k, --keyword <code>—Primary code to track. Pass repeatedly to track multiple codes. Required.
--tier <name>—Tier label to read codes from (default: cod)
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Output

Per speaker per keyword:

  • Total keyword occurrences
  • Following codes with speaker attribution and frequency counts

Differences from CLAN

  • Manual intent: The legacy manual explicitly treats KEYMAP as a coding-tier command and says that only symbols beginning with $ are considered on that tier; all other strings are ignored.
  • Code extraction for %cod now uses a clan-local semantic %cod item layer derived from the parsed AST rather than flattened tier text
  • Selector handling: %cod selectors such as <w4> and <w4-5> are treated as item scope, not as stand-alone codes, when deriving keyword and following-code items.
  • Manual constraint not yet fully enforced: KEYMAP currently retains a generic non-%cod tier fallback. The manual suggests tighter coding-tier semantics than that fallback provides.
  • Keyword matching is case-insensitive by default
  • Output supports text, JSON, and CSV formats
  • Deterministic ordering via BTreeMap
  • Golden test parity: Verified against CLAN C binary output

MAXWD – Longest Words

Status: Current Last updated: 2026-05-11 17:34 EDT

Purpose

Finds the longest words used by each speaker, reporting a ranked table of unique words sorted by character length descending. Word length is measured in characters after normalization (lowercasing, stripping + compound markers and ' apostrophes for CLAN compatibility).

Usage

chatter clan maxwd file.cha
chatter clan maxwd --speaker CHI file.cha
chatter clan maxwd --limit 50 file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--limit <N>–Maximum number of words to show (default: 20)
--format <FMT>–Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.19.1 (Unique Options, MAXWD) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“The +d level of this switch produces output with one line for the length level and the next line for the word.”
+d1“Produces output with only the longest words, one per line, in order, and in legal chat format.”

Open questions for PI review

  1. +d is a two-line-per-result format (length on one line, word on next). chatter’s current MAXWD output is a single-line table. Should --display-mode 0 produce the two-line legacy form, or should we treat the table form as the modern default and only honour +d1 (legal CHAT format)?
  2. +d1 “legal chat format” suggests the output is itself a CHAT file. That’s a transform-flavoured output, not analyze. The chatter approach might be to route --display-mode 1 to an explicit chatter clan maxwd-extract transform rather than overloading the analyze command.

Output

Per speaker:

  • Table of longest words sorted by length descending (up to limit)
  • All occurrences with line numbers (matching CLAN)
  • Maximum word length
  • Mean word length
  • Total and unique word counts

Differences from CLAN

Occurrence reporting

Reports all occurrences with line numbers, matching CLAN’s output format exactly.

Word normalization

Length is measured after stripping + (compound markers) and ' (apostrophes), matching CLAN’s character counting behavior.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output.

MODREP — Model/Replica Comparison

Status: Current

Purpose

Compares the model (target) pronunciation on the %mod tier with the actual (replica) pronunciation on the %pho tier, tracking word-by-word mappings between model forms and replica forms. This is used in phonological analysis to assess how closely a speaker’s productions match the adult target forms.

See the CLAN manual for the original MODREP command specification.

Usage

chatter clan modrep file.cha
chatter clan modrep file.cha --speaker CHI

Algorithm

  1. For each utterance with both %mod and %pho tiers:
    • Extract word lists from both tiers (flattening groups)
    • Pair words positionally (model word N <-> replica word N)
    • Record each (model, replica) pair in a frequency map per speaker
  2. Report per-speaker tables of model words with their replica variants and frequency counts

Options

OptionCLAN flagDescription
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Output

Per-speaker listing of model words, each with its set of replica variants and their frequency counts, sorted alphabetically by model word.

Differences from CLAN

  • Model and replica extraction uses parsed %mod and %pho tier structures from the AST rather than raw text line parsing
  • Word pairing operates on typed PhoWord content rather than string splitting
  • Output supports text, JSON, and CSV formats (CLAN produces text only)
  • Deterministic output ordering via sorted collections
  • Golden test parity: Verified against CLAN C binary output

PHONFREQ — Phonological Frequency Analysis

Status: Current Last updated: 2026-05-12 11:13 EDT

Purpose

Counts individual phone (character) occurrences from %pho tier content, tracking positional distribution within each phonological word: initial (first character), final (last character), and other (middle positions). Counts alphabetic characters (Unicode, including IPA) plus the + compound marker; stress marks (ˈ, ˌ), length marks (ː), digits, and other non-letter symbols are skipped (crates/clan/clan-core/src/commands/phonfreq.rs:178).

See the CLAN manual for the original PHONFREQ command specification.

Usage

chatter clan phonfreq file.cha
chatter clan phonfreq file.cha --speaker CHI

Options

OptionCLAN flagDescription
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Output

Per-phone frequency with positional breakdown (initial/final/other), sorted alphabetically by phone character.

Differences from CLAN

  • Phone extraction uses parsed %pho tier structure from the AST rather than raw text character scanning
  • Positional classification operates on typed PhoWord content
  • Output supports text, JSON, and CSV formats (CLAN produces text only)
  • Deterministic output ordering via sorted collections
  • Golden test parity: Verified against CLAN C binary output

RELY — Inter-Rater Reliability (Cohen’s Kappa)

Status: Current Last updated: 2026-05-11 17:45 EDT

Purpose

Compares two parallel CHAT files for coder agreement. The legacy manual gives RELY five functions: coder agreement, Cohen’s kappa, student-vs-master evaluation, rough transcript overlap on the main line, and selective dependent-tier merging.

The current clan-core implementation focuses on the coding-tier comparison use case: it compares coded data on a specified dependent tier (default %cod) across two files to compute per-code agreement statistics, overall agreement percentage, and Cohen’s kappa coefficient.

See the CLAN manual for the original RELY command specification.

Usage

chatter clan rely file1.cha file2.cha
chatter clan rely file1.cha file2.cha --tier spa

Algorithm

  1. Parse both input files and extract codes per utterance from the specified tier
  2. Align utterances by position (index)
  3. For each aligned pair, count per-code agreements (minimum of the two counts for each code in that utterance)
  4. Compute overall observed agreement (Po) and expected agreement (Pe) for Cohen’s kappa: k = (Po - Pe) / (1 - Pe)

Options

OptionCLAN flagDescription
--tier <name>—Tier label to compare (default: cod)
--format <fmt>—Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.24.1 (Unique Options, RELY) for PI review. RELY’s +d is a multi-dispatch flag with three distinct forms — bare, +dmN, and +dN (numeric kappa) — that can’t all fit into a single scalar --display-mode N.

FormCLAN behavior (verbatim from manual)
+d (bare)“Compute percentage agreement. By default, this is based only on the main line. To compute percentage agreement on a dependent tier, such as %cod, you should add the -t* switch to exclude the main line and then use +t%cod to include just this dependent tier.”
+dmN“Compute student correctness. (+dm1 — first file is control, +dm2 second file is control.)”
+dN“Compute Cohen’s kappa coefficient, where N is the number of categories.”

Open questions for PI review

  1. The three forms are modes, not numeric levels. Should chatter expose them as an enum (--mode percentage|student|kappa) with a separate --categories N for the kappa case, rather than try to overload --display-mode?
  2. +dN collides with the universal --display-mode N shape used by FREQ/KWAL/etc. The rewriter at clan_args.rs:101 doesn’t currently distinguish RELY’s +dN from FREQ’s +dN. Resolving this requires the rewriter to be subcommand-aware (similar to how +g1..+g5 are already CHECK-aware).
  3. +dmN is even more specific — small-N integer that picks which file is the control. The current rewriter has no +dm case; adding it requires explicit per-command branching.

Output

  • Per-code agreement statistics (count in each file, agreed count, agreement percentage)
  • Overall agreement percentage
  • Cohen’s kappa coefficient

Differences from CLAN

  • RELY requires two-file input and does not use the standard AnalysisCommand trait; it is invoked directly
  • Manual intent: The legacy manual gives special semantics for coding tiers such as %cod and %spa, and documents +c1 as comparing only the main part of a colon-delimited code.
  • Code extraction for %cod now uses a clan-local semantic %cod item layer derived from the parsed AST
  • Selector handling: %cod selectors such as <w4> and <w4-5> are preserved as item scope, not counted as compared code values.
  • Scope reduction: The current implementation does not yet cover all five legacy RELY functions described in the manual, and it does not yet implement the documented +c1 colon-prefix comparison mode.
  • Output supports text, JSON, and CSV formats
  • Golden test parity: Verified against CLAN C binary output

SCRIPT — Compare Utterances to a Template

Status: Current Last updated: 2026-05-11 17:47 EDT

Purpose

Compares subject CHAT data against an ideal template file to compute accuracy metrics: words produced vs. expected, correct matches, omissions (in template but not produced), and additions (produced but not in template). Useful for evaluating scripted language samples such as picture descriptions or story retells.

See the CLAN manual for the original SCRIPT command specification.

Usage

chatter clan script file.cha --template template.cha
chatter clan script corpus/ --template template.cha --speaker CHI

Algorithm

  1. Parse the template CHAT file and build a word frequency map (ideal counts)
  2. For each subject utterance, accumulate word frequency counts
  3. At finalization, compute per-word matches (minimum of ideal and actual), omissions, and additions

Options

OptionCLAN flagDescription
--template <path>—Path to template/script file (required)
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.25.5 (Unique Options, SCRIPT) for PI review.

NCLAN behavior (verbatim from manual)
+d (no number)“Outputs default results in SPREADSHEET format.”
+d1“Outputs ratio of words and utterances over time duration.”
+d10“Outputs above, +d1, results in SPREADSHEET format.”

Open questions for PI review

  1. +d (spreadsheet) overlaps with --format csv exactly. Should +d rewrite to --format csv instead of --display-mode 0?
  2. +d10 = +d1 + spreadsheet — a combinator. The numeric encoding 10 mixes a “ratio mode” with the “spreadsheet” output format on one axis. Better factored as --display-mode ratio --format csv (two orthogonal flags) rather than --display-mode 10.
  3. +d1 requires “time duration” data — implies bullet timings or @Duration headers in the input. Should chatter validate that prerequisite at clap-parse time or fail at runtime with a clear error?

Output

Per file:

  • Words produced by subject
  • Words expected from template
  • Correct words (matched)
  • Omitted words (in template but not produced)
  • Added words (produced but not in template)
  • Percentage correct

Overall totals across all files.

Differences from CLAN

  • Template file is parsed into a typed AST (not raw text comparison)
  • Word matching uses NormalizedWord for case-insensitive comparison
  • Omissions and additions are computed from frequency maps rather than positional alignment, which may produce different results when word order matters
  • Output supports text, JSON, and CSV formats
  • Golden test parity: Verified against CLAN C binary output

TIMEDUR – Time Duration

Status: Current

Purpose

Computes time duration statistics from media timestamp bullets attached to utterances. Utterances without bullet timing are silently skipped.

See the CLAN manual for the original TIMEDUR command specification.

Usage

chatter clan timedur file.cha
chatter clan timedur --speaker CHI file.cha
chatter clan timedur --format json file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--format <FMT>–Output format: text, json, csv

Output

Per speaker:

  • Number of timed utterances
  • Total duration (formatted as HH:MM:SS.mmm)
  • Mean utterance duration
  • Min/max duration

Plus a corpus-wide summary:

  • Total timed utterances across all speakers
  • Total duration
  • Recording span (earliest start to latest end)
  • Speaker interaction matrix (overlap and gap analysis)

Differences from CLAN

Timestamp extraction

Uses parsed media bullet structures from the AST (Bullet { start_ms, end_ms }) rather than raw byte scanning in text. This is more robust against formatting variations.

Interaction matrix header

The interaction matrix header includes a leading space, matching CLAN exactly. This was verified during golden test parity work.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output.

TRNFIX — Compare Two Dependent Tiers

Status: Current

Purpose

Compares two dependent tiers (default: %mor and %trn) word-by-word across all utterances, reporting unique mismatch pairs with frequency counts and an overall accuracy percentage. Useful for verifying tier consistency after automatic annotation or manual correction.

When tiers have different lengths for a given utterance, missing positions are represented as the null symbol ∅ (empty set).

Usage

chatter clan trnfix file.cha
chatter clan trnfix file.cha --tier1 mor --tier2 gra

Options

OptionCLAN flagDescription
--tier1 <name>—First tier to compare (default: mor)
--tier2 <name>—Second tier to compare (default: trn)
--speaker <code>+t*CODERestrict to specific speaker
--format <fmt>—Output format: text, json, csv

Output

  • Table of unique mismatch pairs with frequency counts
  • Total items compared
  • Total mismatched items
  • Accuracy percentage

Differences from CLAN

  • Tier content is compared from parsed AST data rather than raw text
  • %trn is treated as an alias of %mor, and %grt as an alias of %gra
  • %mor/%gra token comparison preserves typed token boundaries from the AST rather than comparing whitespace-split serialized payload strings.
  • Length mismatches are handled with explicit ∅ null symbols
  • Configurable tier names (CLAN uses fixed %mor/%trn comparison)
  • Output supports text, JSON, and CSV formats
  • Golden test parity: Verified against CLAN C binary output

UNIQ – Report Repeated Lines

Status: Current

Purpose

Identifies and counts duplicate lines (both @header and *speaker utterance lines, lowercased) across all input files. Matches CLAN behavior of including all line types in the frequency table.

See the CLAN manual for related CLAN command specifications.

Usage

chatter clan uniq file.cha
chatter clan uniq --sort file.cha
chatter clan uniq --format json corpus/

Options

OptionCLAN FlagDescription
--sort-oSort output by descending frequency
--speaker <CODE>+t*CHIRestrict to specific speaker
--format <FMT>–Output format: text, json, csv

Output

  • Table of unique line texts with frequency counts (headers + utterances + dependent tiers)
  • Total lines processed and number of unique lines
  • Optional frequency-descending sort

What Gets Counted

UNIQ counts all line types, including:

  • @header lines
  • *speaker utterance lines
  • %dependent tier lines (including %mor and %gra)
  • Multi-line headers are split and counted individually

This matches CLAN’s behavior of including dependent tiers in the frequency table and splitting multi-line headers for counting.

Differences from CLAN

Dependent tier inclusion

Includes %mor/%gra dependent tiers in counts, matching CLAN.

Multi-line header splitting

Splits multi-line headers for counting, matching CLAN.

Unicode sort order

1 accepted divergence: Unicode sort order for U+230A (LEFT FLOOR character). C-locale strcoll() places this character differently than Rust’s byte-order sorting. Result: a single line position swap with identical content and counts. This is a cosmetic difference with no impact on analysis.

Line text extraction

Uses the parsed AST and WriteChat serialization rather than raw text line reading. This ensures consistent normalization.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

99% parity (1 accepted Unicode sort order divergence).

WDLEN – Word Length Distribution

Status: Current

Purpose

Computes six distribution tables matching CLAN’s output format. WDLEN provides detailed histograms of word and utterance lengths, useful for studying vocabulary complexity and utterance structure development.

See the CLAN manual for the original WDLEN command specification.

Usage

chatter clan wdlen file.cha
chatter clan wdlen --speaker CHI file.cha
chatter clan wdlen --format json file.cha

Options

OptionCLAN FlagDescription
--speaker <CODE>+t*CHIInclude speaker
--exclude-speaker <CODE>-t*CHIExclude speaker
--format <FMT>–Output format: text, json, csv

Six Distribution Sections

SectionWhat it measuresSource
1. Word lengths in charactersCharacter count per wordMain tier
2. Utterance lengths in wordsWord count per utteranceMain tier
3. Turn lengths in utterancesUtterances per turnMain tier
4. Turn lengths in wordsWords per turnMain tier
5. Word lengths in morphemesMorphemes per word (stem + Brown’s suffixes)%mor tier
6. Utterance lengths in morphemesMorphemes per utterance (POS + stem + Brown’s suffixes)%mor tier

Each section shows a histogram (value -> count), mean, and total.

Differences from CLAN

Brown’s morpheme rules

Sections 5 and 6 use distinct counting methods:

  • Section 5: stem + Brown’s suffix count (no POS tag counted). Clitic pairs (~) are merged as one word.
  • Section 6: POS tag + stem + Brown’s suffix count. POS is counted only for the main word (not clitics).

Brown’s suffix strings: PL, PAST, Past, POSS, PASTP, Pastp, PRESP (same 7 as MLU).

Character counting

CLAN strips apostrophes before counting character length. Our implementation matches this behavior.

Speaker ordering

CLAN outputs speakers in reverse encounter order (an artifact of its C linked-list prepend pattern). Our implementation replicates this ordering for parity.

CLAN appends </Table></Worksheet></Workbook> XML tags at the end of output. Our implementation matches this for parity.

Output formats

Supports text, JSON, and CSV. CLAN produces text only.

Golden test parity

100% parity with CLAN C binary output.

WDSIZE — Word Size Distribution

Status: Current Character-length histogram for word stems from the %mor tier.

Usage

chatter clan wdsize file.cha
chatter clan wdsize corpus/ --speaker CHI
chatter clan wdsize file.cha --main-tier    # Use main tier words instead
chatter clan wdsize file.cha --format json

What It Measures

WDSIZE counts the character length of each word stem extracted from the %mor tier and produces a histogram showing how many words of each length appear. By default it uses morphological stems (lemmas); with --main-tier it uses surface forms from the main tier.

Output

Per speaker:

  • Character-length histogram (length → count)
  • Total words measured
  • Mean word size in characters

Differences from WDLEN

FeatureWDSIZEWDLEN
Default source%mor stemsMain tier words
Sections1 (character lengths only)6 (chars, words/utt, turns, morphemes)
Morpheme countingNoYes (sections 5-6)

Differences from CLAN

  • Uses typed MorTier items with MorWord.lemma rather than raw string parsing
  • Supports JSON and CSV output
  • Falls back to main tier words when %mor is absent

CHSTRING – String Replacement Using a Changes File

Status: Current

Purpose

Reimplements CLAN’s chstring command, which reads a changes file containing find/replace pairs (alternating lines) and applies text substitutions to main-tier words. Replacements are applied to all word nodes, including words inside annotated groups, replacement forms, and bracketed groups.

See the CLAN manual for the original command documentation.

Usage

chatter clan chstring --changes changes.cut file.cha

Options

OptionTypeDefaultDescription
-c, --changespath(required)Path to the changes file containing find/replace pairs
-o, --outputpathstdoutOutput CHAT file path

Changes File Format

The changes file contains alternating lines of find and replace strings:

find_text1
replace_text1
find_text2
replace_text2

The file must have an even number of non-empty lines. CLAN looks for changes.cut in the current directory by default; chatter clan chstring requires the path to be passed explicitly via --changes.

Behavior

For each utterance in the file, the transform walks all word nodes on the main tier – including words inside annotated groups, replacement forms, and bracketed groups – and applies find/replace substitutions from the changes file.

Differences from CLAN

  • Operates on the parsed AST rather than raw text, ensuring structural integrity of the CHAT file after substitution.
  • Does not support CLAN’s regex-based pattern matching in the changes file.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).
  • Golden test parity: Verified against CLAN C binary output.

COMBTIER – Combine Duplicate Dependent Tiers

Status: Current

Purpose

Combines duplicate dependent tiers within an utterance. The legacy manual describes COMBTIER narrowly: it corrects the case where transcribers create several %com lines, combining two %com lines into one by removing the second header and moving its material onto the first %com line.

clan-core generalizes that behavior to any selected dependent-tier label, while preserving the underlying tier variant whenever the selected tier has a supported AST model.

This is useful for cleaning up files where duplicate tiers were introduced during manual editing or automated annotation.

Usage

chatter clan combtier --tier com file.cha

Options

OptionTypeDefaultDescription
--tierstring(required)The tier label to combine (e.g., com for %com:, spa for %spa:)
--separatorstring" "Separator between combined tier contents

Behavior

For each utterance, the transform finds all dependent tiers matching the specified label. If two or more matching tiers exist, their text content is concatenated using the configured separator and the result replaces the first occurrence. All subsequent duplicates are removed.

Utterances with zero or one matching tier are left unchanged.

Differences from CLAN

  • Manual intent is narrower: the legacy manual only documents %com combination explicitly. clan-core supports other selected tier labels too.
  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).
  • Tier-kind preservation: Combining bullet/text dependent tiers such as %cod: and %com: preserves their actual tier variants instead of degrading them to user-defined tiers.

COMPOUND – Normalize Compound Word Formatting

Status: Current

Purpose

Reimplements CLAN’s COMPOUND command, which normalizes compound word notation in CHAT files. In CHAT, compound words are joined with + (e.g., ice+cream). This command converts dash-joined compounds to the canonical plus notation.

Usage

chatter clan compound file.cha

Options

COMPOUND has no command-specific flags — only the input path positional and the optional shared -o/--output (default: stdout). The dash→plus normalization is unconditional: there is no --dash-to-plus switch in the current CLI.

Behavior

The transform walks all main-tier word nodes and converts dash-joined compounds to plus notation (e.g., ice-cream becomes ice+cream).

Operations performed:

  • Normalize dash-joined compounds to plus notation: ice-cream -> ice+cream
  • Preserves filler prefixes (&-uh) and omission prefixes (0word)
  • Only converts when all parts are purely alphabetic

The transform recurses into annotated words, replacement forms, groups, and annotated groups.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

DATACLEAN – Fix Common CHAT Formatting Errors

Status: Current

Purpose

Reimplements CLAN’s DataCleanUp command, which fixes spacing and formatting issues in CHAT files. Because these are text-level formatting concerns that operate below the AST level, the AST transform is a no-op; the actual logic operates on serialized CHAT text via clean_chat_text() and the end-to-end run_dataclean() function.

Usage

chatter clan dataclean file.cha

Options

DATACLEAN has no command-specific flags. It accepts a single positional input path plus the optional shared -o/--output from CommonAnalysisArgs (default: stdout). The full set of fixes always runs together; there is no --spacing-only switch — to apply only the text-spacing repairs you would have to pre-filter the input or post-edit the output.

Behavior

The following fixes are applied to non-header lines:

  • Missing space before [ brackets
  • Missing space after ] brackets
  • Tab characters inside lines (converted to spaces)
  • Bare ... without + prefix (converted to +...)
  • #long converted to ##
  • Header lines (@-prefixed) are left untouched

Differences from CLAN

  • Operates on serialized text (post-parse) rather than raw input, since these are formatting concerns below the AST level.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> text fixups -> write).

DATES – Age Computation from Birth and Date Headers

Status: Current

Purpose

Reimplements CLAN’s dates command, which computes the age of each participant at the time of transcription by subtracting @Birth dates from the file-level @Date header. Computed ages are inserted as @Comment: Age of CHI is Y;M.D headers after the @ID/@Birth block.

See the CLAN manual for the original command documentation.

The computed age uses CHAT format: years;months.days (e.g., 2;6.15 for two years, six months, and fifteen days).

Usage

chatter clan dates file.cha

Options

This command has no configurable options.

Behavior

  1. Collects the @Date header value and all @Birth headers with their participant codes.
  2. Computes each participant’s age by subtracting the birth date from the file date.
  3. Inserts @Comment: Age of <PARTICIPANT> is <age> headers into the file.
  4. If no @Date header is present, the file is left unchanged.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

DELIM – Add Missing Utterance Terminators

Status: Current

Purpose

Reimplements CLAN’s delim command, which ensures every main tier has a terminator. Utterances missing a terminator (., ?, !) receive a default period (.). This is typically used as a repair step for files imported from external formats that lack CHAT punctuation conventions.

Usage

chatter clan delim file.cha

Options

This command has no configurable options.

Behavior

For each utterance in the file, if the main tier lacks a terminator, a period (.) is inserted as the default terminator.

Utterances that already have a terminator (., ?, or !) are left unchanged.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).
  • 4 accepted divergences: CLAN writes an empty file when no changes are needed; we always write the full file. This is intentional – the output is always a valid CHAT file.
  • Golden test parity: 4 accepted divergences (empty-file behavior).

FIXIT – Normalize CHAT Formatting

Status: Current

Purpose

Normalizes CHAT file formatting by re-serializing through the parser. Fixes inconsistent spacing, malformed tier prefixes, and other formatting issues.

Usage

chatter clan fixit file.cha
chatter clan fixit file.cha -o normalized.cha

Behavior

Since the parse-serialize pipeline produces canonically formatted output, FIXIT is effectively a roundtrip: parse the file, then serialize the resulting AST. Any formatting inconsistencies are corrected during serialization.

Differences from CLAN

  • Uses full AST roundtrip rather than heuristic text manipulation.
  • Files that fail to parse produce an error rather than attempting partial text-level fixes.
  • Output is the canonical CHAT serialization, which may reorder some whitespace or normalize header formatting.

FIXBULLETS – Fix Timing Bullet Consistency

Status: Current

Purpose

Repairs timing bullets that link CHAT to audio or video. The legacy manual describes FIXBULLETS more broadly: converting old-format bullets to new format, inserting @Media, merging multiple bullets, adding language tags, and shifting global timing offsets.

clan-core now supports the AST-safe subset of that behavior: main-tier monotonic bullet repair, global millisecond offsets on parsed bullet timings, and tier-scoped bullet repair using parsed tier kinds.

Usage

chatter clan fixbullets file.cha
chatter clan fixbullets file.cha --offset 800
chatter clan fixbullets file.cha --tier cod
chatter clan fixbullets file.cha --exclude-tier com

Options

  • --offset N Shift parsed bullet timings by N milliseconds. Negative offsets fail if they would move a parsed bullet before 0.
  • --tier S Restrict processing to selected tier kinds such as cod, %cod, or * for main tiers.
  • --exclude-tier S Exclude selected tier kinds from processing.

Behavior

The transform iterates through the parsed AST and:

  1. Shifts parsed bullet timings on supported AST locations: main-tier terminal bullets, main-tier inline word/internal bullets, bullet-content dependent tiers such as %act/%cod/%com, and %wor inline bullets.
  2. Enforces non-overlapping, monotonic timing windows on main-tier terminal bullets.
  3. Preserves duration when repairing an overlapping main-tier terminal bullet, using a minimum duration of 1 ms.

Utterances without main-tier terminal bullets are skipped for monotonic tracking.

Differences from CLAN

  • Current supported scope: FIXBULLETS supports global offsets and tier-scoped AST-native bullet repair on parsed bullet locations, including bullet-bearing @Comment headers.
  • Scope reduction remains: the CLAN manual documents a broader repair tool than the current implementation provides.
  • Not yet implemented: old-to-new bullet conversion, @Media insertion, multi-bullet merge (+b), and +l language-tag insertion described in the manual.
  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

FLO – Simplified Fluent Output

Status: Current

Purpose

Reimplements CLAN’s flo command, which generates a %flo: dependent tier containing a simplified, “fluent” version of each utterance’s main line.

See the CLAN manual for the original command documentation.

Usage

chatter clan flo file.cha

Options

This command has no configurable options.

Behavior

Processing steps:

  1. Strips all header lines (no @UTF8, @Begin, @End, etc.)
  2. Adds a %flo: dependent tier to each utterance containing the simplified main line: just countable words plus the terminator
  3. Strips retrace targets (words/groups before [/], [//], [///], [/-] — the four RetraceKind variants per crates/core/talkbank-model/src/model/content/retrace.rs)
  4. Strips non-countable words (xxx/yyy/www, 0word, &~frag, &-um)
  5. Strips events (&=thing) and pauses
  6. For replaced words ([: form]), uses the replacement (corrected form)
  7. Keeps existing dependent tiers (%mor, %gra, etc.)

The %flo: tier is inserted at position 0 (before other dependent tiers).

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

GEM – Extract Gem Segments

Status: Current Last updated: 2026-05-11 17:42 EDT

Purpose

Extracts material within gem boundaries. The legacy manual gives GEM a dedicated section; in clan-core, it extracts utterances and their dependent tiers that fall within @Bg/@Eg gem boundaries, producing a new CHAT file containing only the gem-scoped content.

See the CLAN manual for the original GEM command specification.

Usage

chatter clan gem file.cha
chatter clan gem --gem story file.cha

CLAN Equivalence

CLAN commandRust equivalent
gem file.chachatter clan gem file.cha
gem +g"story" file.chachatter clan gem --gem story file.cha

Options

OptionCLAN FlagDescription
--gem <LABEL>+g"label"Extract only gem segments matching this label

Without --gem, all gem segments in the file are extracted.

Display Modes (+dN / --display-mode N) — DRAFT, awaiting PI review

Status: drafted from CLAN manual; not yet implemented. Rewriter at crates/clan/clan-core/src/clan_args.rs:101 translates +dN → --display-mode N; no clap field consumes it today. Drafted from CLAN manual §7.13 (GEM, in-section +d note) for PI review.

NCLAN behavior (verbatim from manual)
+d0“Produces simple output that is in legal chat format.”
+d1“Adds information to the legal chat output regarding file names, line numbers, and @ID codes.”

Open questions for PI review

  1. GEM is a transform command in chatter (writes a new CHAT file). +d0 “legal CHAT format” is GEM’s default behavior in chatter — so --display-mode 0 would be a no-op. Should the flag error on --display-mode 0 (already-default), accept it silently, or simply not be plumbed for GEM at all?
  2. +d1 adds filenames/line numbers/@ID codes — that’s annotation metadata not normally in a CHAT file. Map to a separate --annotate boolean rather than --display-mode 1?

Behavior

The transform scans for @Bg: (begin gem) and @Eg: (end gem) header boundaries. All utterances between a matching @Bg/@Eg pair are included in the output, along with their dependent tiers. The gem boundary headers themselves are preserved. File-level headers and participant metadata are carried through unchanged.

Differences from CLAN

  • Gem boundary detection operates on parsed Header variants from the AST rather than raw text line matching for @BG:/@EG:.
  • Handles both @Bg:/@Eg: (mixed case) and @BG:/@EG: (uppercase).
  • Without --gem filter, extracts all gem segments. With --gem, extracts only matching labels.

INDENT – Align CA Overlap Markers

Status: Current

Purpose

Aligns overlap markers in Conversation Analysis (CA) transcripts. The legacy manual describes INDENT simply as a program for realigning overlap marks in CA files, and notes that the files must use a fixed-width font such as CAFont.

clan-core aligns closing overlap markers (⌊, U+230A) by column position with their matching opening overlap markers (⌈, U+2308) on a preceding speaker tier.

Usage

chatter clan indent file.cha
chatter clan indent file.cha -o aligned.cha

Algorithm

  1. Parse the file into tiers (speaker prefix + content text)
  2. For each main tier (*SPK:), scan for opening overlap markers ⌈ and record their column positions and optional numeric suffixes
  3. Scan up to 30 subsequent tiers from different speakers for closing overlap markers ⌊
  4. Match open/close pairs by numeric suffix (or sequentially if unnumbered)
  5. Insert or remove spaces before the closing marker to align columns
  6. Report unmatched markers as warnings

Example

Before:

*CHI:	I want ⌈ cookies ⌉ .
*MOT:	⌊ yeah ⌋ okay .

After:

*CHI:	I want ⌈ cookies ⌉ .
*MOT:	       ⌊ yeah ⌋ okay .

Numbered overlaps (⌈1, ⌈2, etc.) are matched by their numeric suffix, allowing multiple simultaneous overlaps to be aligned independently.

Differences from CLAN

  • Manual intent: INDENT is a layout command, not a semantic CHAT analysis command.
  • Operates on UTF-8 text using Rust’s char-based column counting rather than C byte-level scanning.
  • Uses the text-based transform pattern (no AST round-trip) to preserve original formatting outside of overlap alignment.
  • Maximum 10 alignment passes (CLAN’s goto beginAgain loop has no bound, causing infinite loops on some inputs).
  • Column counting treats each Unicode scalar value as width 1.

LINES – Add or Remove Sequential Line Numbers

Status: Current

Purpose

Adds or removes line numbers for display. The legacy manual describes LINES as inserting line numbers based on CLAN’s “Show Line Numbers” display and using +n to remove them.

Since line numbering is a display concern rather than a structural CHAT operation, the AST transform is a no-op; the actual logic operates on serialized CHAT text via add_line_numbers(), remove_line_numbers(), and the end-to-end run_lines() function.

Line numbers are formatted as 5-character right-aligned integers prefixed to non-header lines. Header lines (@Begin, @Languages, etc.) are not numbered.

Usage

chatter clan lines file.cha
chatter clan lines --remove file.cha

Options

OptionTypeDefaultDescription
--removeboolfalseRemove existing line numbers instead of adding them

Behavior

Adding line numbers: Each non-header line receives a 5-character right-aligned sequential number prefix (e.g., 1 *CHI: ...). Header lines (@-prefixed) are not numbered.

Removing line numbers: Strips the first 6 characters (5-digit number + space) from non-header lines.

Differences from CLAN

  • Manual intent: LINES is a display/layout command, so text-level processing is intentional here.
  • Operates on serialized text (post-parse) rather than raw input, since line numbering is a display concern.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> text fixups -> write).

LONGTIER – Remove Line Continuation Wrapping

Status: Current

Purpose

Removes physical continuation-line wrapping so each logical tier occupies one line in the output file. The legacy manual describes LONGTIER as removing line wraps on continuation lines so that each main tier and each dependent tier is on one long line.

In CHAT format, long tiers are conventionally wrapped with a newline followed by a tab. LONGTIER folds those continuations back into a single line.

Usage

chatter clan longtier file.cha
chatter clan longtier file.cha -o unwrapped.cha

Behavior

Folds any line starting with a tab character into the preceding tier line, replacing the newline+tab with a single space. The result has one line per tier with no continuation wrapping.

Differences from CLAN

  • Manual intent: LONGTIER is below the AST layer; it is about physical line wrapping, not CHAT semantics.
  • Operates on raw text rather than partial parsing, making it robust against malformed files that might not parse cleanly.
  • Normalizes all newlines to \n (handles \r\n and \r).
  • Multiple leading tabs on continuation lines are all consumed.

LOWCASE – Lowercase All Words on Main Tiers

Status: Current

Purpose

Reimplements CLAN’s lowcase command, which converts all words on main tiers to lowercase. Speaker codes, headers, and dependent tiers are preserved unchanged. The transformation recurses into annotated words, replaced words, groups, and annotated groups.

See the CLAN manual for the original command documentation.

Usage

chatter clan lowcase file.cha

Options

This command has no configurable options.

Behavior

For each utterance, the transform lowercases all word surfaces on the main tier. The transformation recurses into:

  • Plain words
  • Annotated words (inner word)
  • Replaced words (both original and replacement forms)
  • Groups (bracketed content)
  • Annotated groups (inner bracketed content)

Speaker codes, header lines, and dependent tiers are not modified.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

MAKEMOD – Generate %mod Tier from Pronunciation Lexicon

Status: Current Last updated: 2026-05-12 11:05 EDT

Purpose

Reimplements CLAN’s MAKEMOD command, which looks up each countable word on main tiers in a pronunciation lexicon (CMU dictionary format) and generates a %mod dependent tier with the phonemic transcription. Words not found in the lexicon are marked with ???.

Usage

chatter clan makemod --lexicon cmulex.cut file.cha

Options

OptionTypeDefaultDescription
-l, --lexiconpath(required)Path to the pronunciation lexicon file
--all-alternativesboolfalseShow all alternative pronunciations (default: first only)
-o, --outputpathstdoutOutput CHAT file path

External Data

Requires a CMU-format lexicon file. CLAN ships a cmulex.cut in its lib/ directory and uses that by default; chatter clan makemod does not bundle a lexicon, so --lexicon must be passed explicitly.

Format: WORD phoneme1 phoneme2 ... (one entry per line). Lines starting with # or % are treated as comments. Words with (N) suffix (variant number like READ(2)) are treated as pronunciation alternatives for the base word.

Behavior

For each utterance, the transform:

  1. Extracts countable words from the main tier (using the framework’s countable_words() utility).
  2. Looks up each word in the loaded pronunciation lexicon (case-insensitive).
  3. Builds a %mod dependent tier with the phonemic transcriptions. Words not found are marked ???.
  4. Appends the %mod tier to the utterance’s dependent tiers.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

ORT – Orthographic Conversion via Dictionary Lookup

Status: Current Last updated: 2026-05-12 11:11 EDT

Purpose

Reimplements CLAN’s CONVORT command, which applies orthographic conversion rules from a dictionary file to main-tier words. When a word is modified, the original main-tier text is preserved on a %ort: dependent tier for reference.

Usage

chatter clan ort --dictionary ort.cut file.cha

Options

OptionTypeDefaultDescription
-d, --dictionarypath(required)Path to the orthographic conversion dictionary
-o, --outputpathstdoutOutput CHAT file path

External Data

Requires an orthographic conversion dictionary. CLAN uses ort.cut from its lib/ directory by default; chatter clan ort does not bundle a dictionary, so --dictionary must be passed explicitly. Format: from_word to_word (one pair per line, tab or space separated). Lines starting with # or ; are treated as comments. Lookups are case-insensitive.

Behavior

For each utterance, the transform:

  1. Serializes the original main tier content for preservation.
  2. Applies dictionary-based word substitutions on the main tier.
  3. If any words were modified, inserts a %ort: dependent tier containing the original (pre-conversion) main-tier text.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

POSTMORTEM – Pattern-Matching Rules for %mor Post-Processing

Status: Current Last updated: 2026-05-12 11:28 EDT

Purpose

Reimplements CLAN’s POSTMORTEM command, which applies pattern-matching and replacement rules to dependent tiers (typically %mor:). Rules are applied sequentially, and wildcard tokens (*) match any single token. The replacement side uses $- to copy the matched wildcard text.

The CLAN manual does not appear to contain a standalone POSTMORTEM command section. It is mentioned indirectly as part of the mor *.cha pipeline that runs MOR, PREPOST, POST, POSTMORTEM, and MEGRASP to produce %mor and %gra.

Usage

chatter clan postmortem --rules postmortem.cut file.cha
chatter clan postmortem --rules rules.cut --target-tier spa file.cha

Options

OptionTypeDefaultDescription
-r, --rulespath(required)Path to the rules file
--target-tierstring"mor"Target tier label to apply rules to
-o, --outputpathstdoutOutput CHAT file path

External Data

Requires a rules file. CLAN uses postmortem.cut from its lib/ directory by default; chatter clan postmortem does not bundle a rules file, so --rules must be passed explicitly. Format: from_pattern => to_replacement (one rule per line, using => or ==> as the separator). Lines starting with # or ; are comments.

Wildcards: * in the pattern matches any single token. $- in the replacement copies the matched wildcard text.

Behavior

For each utterance, the transform:

  1. Finds the target dependent tier (default: %mor:).
  2. If the target is a user-defined text tier, tokenizes its content.
  3. Applies each rule sequentially, matching patterns and performing substitutions.
  4. Stores the modified result back on the tier.

Differences from CLAN

  • Manual coverage gap: the CLAN manual only mentions POSTMORTEM indirectly through the MOR pipeline, so this chapter cannot yet rely on a standalone legacy command spec.
  • Typed %mor safety: If a rule would change a parsed %mor tier, POSTMORTEM fails explicitly until an AST-based %mor rewrite exists, rather than degrading typed morphology into user-defined text.
  • User-defined target tiers are still supported as text rewrite targets.
  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

QUOTES – Extract Quoted Text to Separate Utterances

Status: Current

Purpose

Reimplements CLAN’s QUOTES command.

This is a relatively uncommon command used for discourse analysis of reported speech.

Usage

chatter clan quotes file.cha

Options

This command has no configurable options.

Behavior

The Rust port now inspects the parsed CHAT AST directly.

  • If no quote-extraction postcode ([+ "]) is present, the command is a semantic no-op and emits normalized CHAT.
  • If [+ "] is present, the command fails with an explicit error. clan-core does not silently fall back to post-serialization string manipulation for this transform.

Differences from CLAN

  • Does not currently implement CLAN’s text-level extraction rewrite for [+ "].
  • Fails explicitly on unsupported quote-extraction postcodes instead of attempting a lossy raw-text rewrite.

REPEAT – Mark Utterances Containing Revisions

Status: Current Last updated: 2026-05-12 11:31 EDT

Purpose

Reimplements CLAN’s repeat command, which adds a [+ rep] postcode to utterances from a target speaker that contain revision markers. Only utterances that do not already have [+ rep] are modified.

Usage

chatter clan repeat --speaker CHI file.cha

Options

OptionTypeDefaultDescription
--speakerspeaker code(required)Target speaker to process. Only utterances from this speaker are checked.

Revision Markers Detected

Per is_revision_kind() at crates/clan/clan-core/src/transforms/repeat.rs:81-87, three of the four RetraceKind variants trigger [+ rep]:

  • [//] — full retrace (RetraceKind::Full)
  • [///] — multiple retracing (RetraceKind::Multiple)
  • [/-] — reformulation (RetraceKind::Reformulation)

Note: Simple partial repetition ([/], RetraceKind::Partial) does not trigger the [+ rep] marker — that’s the “non-revision” case. (Earlier versions of this doc listed a [/?] “uncertain retracing” marker as the fourth case; there is no such marker — the grammar has exactly four retrace tokens, all listed above and at book/src/chat-format/retraces.md.)

Behavior

For each utterance from the target speaker, the transform checks whether the main-tier content contains any revision markers. If revision markers are found and the utterance does not already have a [+ rep] postcode, one is appended.

Utterances from other speakers are left unchanged.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

RETRACE – Add %ret Dependent Tier with Verbatim Main-Tier Copy

Status: Current

Purpose

Reimplements CLAN’s retrace command, which adds a %ret: dependent tier to each utterance containing a verbatim serialized copy of the main-tier content (including retrace markers, pauses, events, etc.). This serves as a reference tier preserving the original utterance text before other transforms modify it.

See the CLAN manual for the original command documentation.

Usage

chatter clan retrace file.cha

Options

This command has no configurable options.

Behavior

For each utterance, the transform:

  1. Serializes the main tier to its full CHAT text representation.
  2. Extracts the content portion (after *SPEAKER:\t).
  3. Creates a %ret: user-defined dependent tier containing the verbatim content.
  4. Inserts the %ret: tier at position 0 (before other dependent tiers).

All headers are preserved. Existing dependent tiers are kept.

Differences from CLAN

  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

ROLES — Rename Speakers

Status: Current Last updated: 2026-05-12 13:33 EDT

Purpose

Renames speaker codes throughout a CHAT file: in @Participants, @ID headers, and all main-tier speaker prefixes. Used to standardize speaker codes across a corpus.

Usage

chatter clan roles --rename "EXP=INV" file.cha
chatter clan roles --rename "Child=CHI" --rename "Mother=MOT" file.cha

Options

OptionDescription
-r, --rename "OLD=NEW"Rename speaker OLD to NEW (required, can be repeated). Splits on the first =; see crates/chatter/chatter-cli/src/commands/clan/transforms.rs:172 for the parser.
-o, --outputOutput CHAT file path (default: stdout).

Behavior

Speaker codes are renamed in all structural locations:

  • @Participants header entries
  • @ID header speaker fields
  • Main-tier speaker prefixes (*OLD: becomes *NEW:)

Differences from CLAN

  • Operates on the typed AST rather than raw text.
  • Speaker codes are renamed in all structural locations via AST manipulation.

TIERORDER – Reorder Dependent Tiers to Canonical Order

Status: Current Last updated: 2026-05-12 13:40 EDT

Purpose

Reorders dependent tiers into a consistent order. The legacy manual describes TIERORDER as putting dependent tiers into a consistent alphabetical order, with /lib/fixes/tierorder.cut able to control the order.

clan-core sorts dependent tiers on each utterance according to its built-in canonical ordering.

Usage

chatter clan tierorder file.cha

Options

This command has no configurable options.

Behavior

Dependent tiers are sorted into the following canonical order (per the tier_order() priority function at crates/clan/clan-core/src/transforms/tierorder.rs:57-100):

  1. Linguistic analysis tiers (priorities 0-5): %mor → %gra → %pho → %mod → %wor → %sin

  2. Phon project syllabification/alignment tiers (priorities 6-8): %xmodsyl → %xphosyl → %xphoaln

  3. Behavioral/descriptive tiers (priorities 10-18): %act → %cod → %com → %spa → %gpx → %sit → %exp → %int → %add

  4. Simple text tiers (priorities 20-30): %alt → %coh → %def → %eng → %err → %fac → %flo → %gls → %ort → %par → %tim

  5. User-defined tiers (priority 100): %x* (anything beyond the standard set)

  6. Unsupported tiers (priority 101): tiers that hit the grammar’s catch-all but aren’t recognized CHAT tiers

Utterances with zero or one dependent tier are left unchanged.

Differences from CLAN

  • Manual configurability not yet mirrored: The legacy manual describes tierorder.cut as controlling tier order. The current implementation uses a built-in ordering instead.
  • Operates on AST rather than raw text.
  • Uses the framework transform pipeline (parse -> transform -> serialize -> write).

TRIM — Remove Dependent Tiers

Status: Current

Purpose

Removes selected dependent tiers from a CHAT file while preserving headers, main tiers, and all other file structure. The CLAN manual describes TRIM as a shorthand for removing coding tiers, such as %mor, without changing anything else in the transcript.

Usage

chatter clan trim file.cha --exclude-tier mor
chatter clan trim file.cha --exclude-tier '*'
chatter clan trim file.cha --tier cod

Options

OptionCLAN FlagDescription
--tier <NAME>+t%NAMEKeep only selected dependent tier(s)
--exclude-tier <NAME>-t%NAMERemove selected dependent tier(s)

Differences from CLAN

  • Legacy intent preserved: TRIM follows the tier-removal behavior described in CLAN.html, rather than extracting utterance or gem ranges.
  • Operates on the typed AST rather than the KWAL-style text-output workaround shown in the legacy manual.
  • --tier / --exclude-tier operate on dependent-tier labels only. Headers and main tiers are always preserved.
  • Supports * as a wildcard dependent-tier selector and normalizes %trn to %mor and %grt to %gra.

CHAT2TEXT — CHAT to Plain Text

Status: Current

Purpose

Converts CHAT files into plain text lines suitable for downstream text analysis. The legacy manual describes CHAT2TEXT as converting a CHAT file to text lines for concordance programs such as AntConc, and says it is implemented as an alias of FLO with +cr +t*.

clan-core implements the same broad intent directly by extracting plain spoken text from the parsed CHAT AST.

Usage

chatter clan chat2text file.cha
chatter clan chat2text --include-speaker file.cha

Options

OptionDescription
--include-speakerPrefix each line with speaker code (e.g., “CHI: hello world”)

Output

Plain text with one utterance per line. All CHAT annotations are stripped:

  • Bracketed annotations ([/], [: replacement], [*])
  • Timing bullets
  • Terminators (., ?, !)
  • Fillers (&-um), fragments (&+fr), events (&=laughs)
  • Untranscribed markers (xxx, yyy, www)
  • Omitted words (0word)

Differences from CLAN

  • Manual intent: the CLAN manual does have a dedicated CHAT2TEXT section, and it describes the command as a FLO-based alias rather than as a separate semantic engine.
  • Uses AST-based content extraction for reliable annotation stripping.
  • Does not model the legacy implementation literally as flo +cr +t*; it performs the plain-text extraction directly.

CHAT2ELAN – CHAT to ELAN XML Conversion

Status: Current

Purpose

Converts CHAT files into ELAN annotation format (.eaf). The output is a valid ELAN XML file with time-aligned tiers and annotations derived from CHAT main tiers and their timing bullets.

See the CLAN manual for the original CHAT2ELAN command documentation.

Usage

chatter clan chat2elan file.cha
chatter clan chat2elan --media-extension wav file.cha

CLAN Equivalence

CLAN commandRust equivalent
chat2elan +e.wav file.chachatter clan chat2elan file.cha

Options

OptionCLAN FlagDescription
--media-extension <EXT>+e.EXTInclude a MEDIA_DESCRIPTOR element referencing a media file with the same basename and the given extension

Without --media-extension, the output omits media references.

Output

A standard-compliant EAF 3.0 XML file. Each CHAT speaker becomes an ELAN tier (using the full speaker code as tier ID). Utterances with timing bullets become time-aligned annotations. The time slot pool is derived from all utterance start/end timestamps.

Differences from CLAN

  • CLAN requires the +e flag to specify a media extension; the Rust version omits media references by default.
  • Uses typed AST traversal for utterance extraction rather than text parsing.
  • Generates standard-compliant EAF 3.0 XML.
  • Speaker codes are used directly as tier IDs (no truncation).

CHAT2PRAAT – CHAT to Praat TextGrid

Status: Current

Purpose

Converts CHAT files to Praat TextGrid format for acoustic/phonetic analysis. Each speaker becomes a separate interval tier, with timed utterances mapped to intervals.

This is the reverse of PRAAT2CHAT. Both conversions are implemented in the same module (praat2chat).

Usage

chatter clan chat2praat file.cha
chatter clan chat2praat file.cha -o output.TextGrid

Behavior

  • Each speaker in the CHAT file becomes a separate interval tier
  • Only timed utterances (those with bullet timing) are included
  • Timing bullets are converted from milliseconds to seconds
  • Speaker codes become tier names
  • Utterance text is extracted with annotations stripped
  • If no timed utterances exist, an empty string is returned

Output format

Produces standard Praat TextGrid long format:

File type = "ooTextFile"
Object class = "TextGrid"

xmin = 0
xmax = 5.042652
tiers? <exists>
size = 2
item []:
    item [1]:
        class = "IntervalTier"
        name = "CHI"
        ...

Differences from CLAN

  • Uses the typed AST to extract timing and text, rather than string scanning
  • Bidirectional conversion in one module (CLAN has separate praat2chat and chat2praat binaries)
  • Deterministic speaker tier ordering via BTreeMap

CHAT2SRT – CHAT to Subtitle Conversion

Status: Current

Purpose

Converts CHAT files to subtitle format for captioned video. The legacy manual describes CHAT2SRT as converting a CHAT file to SRT for video captioning and then walks through a GUI workflow using external subtitle software.

This book focuses on the CLI semantics only: clan-core converts timed CHAT utterances to SRT or WebVTT subtitle entries.

Usage

chatter clan chat2srt input.cha

Output Formats

FormatFunctionTimestamp style
SRTchat_to_srt()HH:MM:SS,mmm
WebVTTchat_to_vtt()HH:MM:SS.mmm

Options

OptionDefaultDescription
(none)This converter operates on a parsed ChatFile and has no additional configuration options.

Input Format

Standard CHAT (.cha) files with timing bullets on utterances. Utterances without timing bullets are excluded from the output.

Output

Numbered subtitle blocks with timestamps derived from CHAT timing bullets and cleaned text content (no CHAT markers, annotations, or speaker codes).

Example SRT output:

1
00:00:01,000 --> 00:00:03,500
hello world

2
00:00:04,200 --> 00:00:06,800
how are you

Differences from CLAN

  • GUI material intentionally omitted here: The legacy manual’s Subtitle Writer walkthrough belongs in the TalkBank VS Code extension docs, not in this CLI command chapter.
  • Uses typed AST for subtitle text extraction
  • Produces valid, well-formed SRT/WebVTT output
  • Additionally supports WebVTT output format
  • Manual feature not yet mirrored: The legacy manual documents using +t%glo to caption from the gloss tier. This chapter should not imply that arbitrary dependent-tier caption sourcing is available unless explicitly implemented.

Reference

See the CLAN manual for the original CHAT2SRT command documentation.

CHAT2VTT – CHAT to WebVTT Subtitle Conversion

Status: Current Last updated: 2026-05-02 03:00 EDT

Purpose

Converts a CHAT file to WebVTT subtitle format for captioned video. WebVTT (.vtt) is the W3C-standard subtitle format used by HTML5 <track> elements; it is closely related to SRT but uses . instead of , as the millisecond separator and includes a WEBVTT header.

Internally, chat2vtt is a thin CLI surface over the same converter crate as chat2srt: both subcommands take a CHAT file with timing bullets and emit numbered subtitle blocks, just in different file formats. The shared infrastructure lives at crates/clan/clan-core/src/converters/chat2srt.rs (chat_to_vtt() for this subcommand, chat_to_srt() for SRT).

Usage

# Write WebVTT to stdout
chatter clan chat2vtt input.cha

# Write to a file
chatter clan chat2vtt input.cha -o input.vtt

Options

OptionDefaultDescription
-o, --output <PATH>stdoutDestination file. Omit to print to stdout.

The subcommand takes a single positional <PATH> argument (one CHAT file at a time). It does not accept directory inputs, file lists, or the CommonAnalysisArgs filtering flags (--speaker, --gem, etc.) — WebVTT conversion is structural, not analytic.

Input Format

Standard CHAT (.cha) files with timing bullets (•start_end•) on utterances. Utterances without timing bullets are excluded from the output, since WebVTT requires every cue to carry a timestamp.

Output

Numbered subtitle cues with HH:MM:SS.mmm --> HH:MM:SS.mmm timestamps and cleaned text content (no CHAT markers, annotations, or speaker codes). The output begins with the WEBVTT header line that the WebVTT specification requires.

WEBVTT

1
00:00:01.000 --> 00:00:03.500
hello world

2
00:00:04.200 --> 00:00:06.800
how are you

Difference from chat2srt

The two converters share extraction logic and only differ in the output formatting layer:

SubcommandOutput formatTimestamp separatorHeader
chat2srtSRT (.srt), (e.g. 00:00:01,000)none
chat2vttWebVTT (.vtt). (e.g. 00:00:01.000)WEBVTT

For the rationale on which utterances are emitted vs skipped, the shape of cleaned text, and what’s intentionally not included from CLAN’s GUI-driven Subtitle Writer flow, see chat2srt. Everything documented there about extraction also applies here; only the serialization differs.

Reference

The CLAN manual section CHAT2SRT covers the original SRT-conversion workflow. WebVTT is an extension on the Rust side; the manual does not document a separate CHAT2VTT binary.

ELAN2CHAT – ELAN XML to CHAT Conversion

Status: Current Last updated: 2026-05-12 11:27 EDT

Purpose

Converts ELAN annotation files (.eaf) into CHAT format. ELAN uses a time-aligned annotation format stored as XML, with time slots referenced by alignable annotations within tiers.

Usage

chatter clan elan2chat input.eaf

Options

OptionDefaultDescription
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "elan_corpus" (crates/clan/clan-core/src/converters/elan2chat.rs:230); there is no CLI flag to override it.

Conversion Details

  • ELAN tier IDs are mapped to CHAT speaker codes (first 3 characters, uppercased)
  • Time slots are resolved to millisecond timing bullets
  • Annotations are merged across tiers and sorted by start time
  • All speakers are assigned the Unidentified participant role

Input Format

ELAN XML (.eaf) files containing <TIER> elements with <ALIGNABLE_ANNOTATION> entries referencing <TIME_SLOT> elements for timing.

Output

A well-formed CHAT file with @UTF8, @Begin/@End headers, @Languages, @Participants, and @ID headers for each discovered speaker. Each ELAN annotation becomes a timed utterance.

Implementation Note

Uses simple string-based XML parsing to avoid adding a quick-xml dependency. Sufficient for well-formed ELAN files.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

Reference

See the CLAN manual for the original ELAN2CHAT command documentation.

LAB2CHAT – LAB Timing Labels to CHAT Conversion

Status: Current Last updated: 2026-05-12 11:25 EDT

Purpose

Converts LAB (label) timing files into CHAT format. LAB files contain time-aligned word or segment labels commonly used in speech research tools (e.g., HTK, Kaldi).

Usage

chatter clan lab2chat input.lab

Options

OptionDefaultDescription
-s, --speaker"SPK"Speaker code for all utterances
-L, --language"eng"ISO 639 language code for the @Languages header (note: uppercase -L because lowercase -l would conflict)
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "lab_corpus" (crates/clan/clan-core/src/converters/lab2chat.rs:110); there is no CLI flag to override it.

Supported Formats

  • Three-column: start_time end_time label (times in seconds)
  • Two-column: time label (end time inferred from the next entry)

Input Format

Plain text files with whitespace-separated columns. Silence markers (sil, sp, #) are skipped during conversion. Comment lines starting with # and blank lines are ignored.

Example:

0.0 0.5 hello
0.5 1.2 world
1.2 1.5 sil

Output

A well-formed CHAT file where each non-silence label becomes a separate utterance with timing bullets derived from the LAB timestamps (converted from seconds to milliseconds).

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

LENA2CHAT – LENA Device XML to CHAT Conversion

Status: Current

Purpose

Converts LENA (Language Environment Analysis) device output files (.its format) into CHAT format. LENA XML contains segment-level annotations with speaker types and timing information but no actual transcribed words.

Usage

chatter clan lena2chat input.its

Options

OptionDefaultDescription
language"eng"ISO 639 language code for the @Languages header
corpus"lena_corpus"Corpus name for the @ID header

Speaker Mapping

LENA segment types are mapped to CHAT speaker codes:

LENA typeCHAT speakerDescription
CHN/CXNCHIChild near/far
FAN/FAFMOTFemale adult near/far
MAN/MAFFATMale adult near/far
OLN/OLFOTHOther child overlap
TVN/TVFENVTV/electronic media
NON/NOFENVNoise
SIL(skipped)Silence

Input Format

LENA .its XML files containing segment-level annotations with speaker type attributes and timing information.

Output

A well-formed CHAT file where each LENA segment becomes a timed utterance. Since LENA does not provide transcribed words, all utterances use xxx (untranscribed) as placeholder text, with optional word count annotation.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

LIPP2CHAT – LIPP Phonetic Profile to CHAT Conversion

Status: Current Last updated: 2026-05-12 11:25 EDT

Purpose

Converts LIPP (Logical International Phonetics Programs) phonetic profile data into CHAT format. Each entry becomes an utterance, and phonetic transcriptions are placed on %pho dependent tiers.

Usage

chatter clan lipp2chat input.lipp

Options

OptionDefaultDescription
-s, --speaker"CHI"Speaker code for all utterances
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "lipp_corpus" (crates/clan/clan-core/src/converters/lipp2chat.rs:80); there is no CLI flag to override it.

Input Format

Tab-separated word and phonetic transcription, one pair per line:

cat    kaet
dog    dog

Lines starting with # are treated as comments. Single words without a tab-separated phonetic field are imported without a %pho tier.

Output

A well-formed CHAT file where each LIPP entry becomes an utterance on the main tier. When a phonetic transcription is present, it is placed on a %pho dependent tier attached to the utterance.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

PLAY2CHAT – PLAY Annotation to CHAT Conversion

Status: Current Last updated: 2026-05-12 11:15 EDT

Purpose

Converts PLAY (Phonological and Lexical Acquisition in Young children) annotation files into CHAT format.

Usage

chatter clan play2chat input.play

Options

OptionDefaultDescription
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "play_corpus" (crates/clan/clan-core/src/converters/play2chat.rs:92); there is no CLI flag to override it. If you need a different corpus name, post-edit the generated @ID lines or call play_to_chat_with_options() from Rust.

Input Format

Tab-separated fields: speaker, start_time, end_time, text. Times are in milliseconds and may be empty. Lines starting with # or % are skipped. Lines with fewer than 2 tab-separated fields are ignored.

Example:

CHI	1000	3500	hello world
MOT	4200	6800	how are you

Output

A well-formed CHAT file with headers and participants. Unique speakers are automatically collected and registered as CHAT participants with the Unidentified role. Each PLAY entry becomes an utterance, with timing bullets when start/end times are provided.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

PRAAT2CHAT – Praat TextGrid Bidirectional Conversion

Status: Current Last updated: 2026-05-12 11:29 EDT

Purpose

Converts between Praat TextGrid files and CHAT format. TextGrid files contain time-aligned interval tiers widely used in phonetic research.

Usage

chatter clan praat2chat input.TextGrid

Conversion Functions

DirectionFunctionDescription
TextGrid to CHATpraat_to_chat()Convert TextGrid intervals to CHAT utterances
CHAT to TextGridchat_to_praat()Convert timed CHAT utterances to TextGrid intervals

Options

OptionDefaultDescription
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "praat_corpus" (crates/clan/clan-core/src/converters/praat2chat.rs:200); there is no CLI flag to override it. Same pattern as the other converters in this directory.

TextGrid Format Support

Both long (normal) and short TextGrid formats are supported. Tier names are mapped to CHAT speaker codes (first 3 characters, uppercased). Empty intervals and point tiers are skipped. Untimed utterances are excluded from CHAT-to-TextGrid conversion.

Input Format

Praat TextGrid files (.TextGrid) containing interval tiers with time-aligned text segments.

Output

TextGrid to CHAT: A well-formed CHAT file with timing bullets derived from interval boundaries. Each non-empty interval becomes a timed utterance.

CHAT to TextGrid: A Praat TextGrid file with one interval tier per speaker, containing text from timed utterances.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output
  • Supports bidirectional conversion (CHAT to TextGrid and TextGrid to CHAT)
  • Handles both long and short TextGrid formats

Reference

See the CLAN manual for the original PRAAT2CHAT command documentation.

RTF2CHAT – Rich Text Format to CHAT Conversion

Status: Current Last updated: 2026-05-12 13:35 EDT

Purpose

Converts Rich Text Format (RTF) files into CHAT format by stripping RTF formatting commands and extracting plain text content.

Usage

chatter clan rtf2chat input.rtf

Options

OptionDefaultDescription
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "rtf_corpus" (crates/clan/clan-core/src/converters/rtf2chat.rs:212); there is no CLI flag to override it. Same pattern as the other converters in this directory.

Processing Steps

  1. RTF stripping: Removes control words, groups, font/color/stylesheet tables, and converts Unicode escapes (\uN?) to characters. Handles \par (newline) and \tab (tab).
  2. Turn extraction: Looks for CHAT-style speaker prefixes (*CHI:, *MOT:) in the plain text. If none are found, all text is assigned to a default SPK speaker.
  3. CHAT construction: Builds a proper ChatFile with headers, participants, and utterances.

Input Format

RTF (.rtf) files, optionally containing CHAT-style speaker prefixes embedded in the rich text. Standard RTF control sequences are supported including font tables, color tables, stylesheets, Unicode escapes, and nested groups.

Output

A well-formed CHAT file. If the RTF contains CHAT-style speaker codes (*CHI:, *MOT:, etc.), those are preserved as proper CHAT speaker codes. Otherwise, all text is assigned to a default SPK speaker.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

SALT2CHAT – SALT Transcription to CHAT Conversion

Status: Current Last updated: 2026-05-12 13:36 EDT

Purpose

Converts SALT (Systematic Analysis of Language Transcripts) transcription files into CHAT format. SALT is a widely used clinical transcription system with its own conventions for speaker codes, morpheme annotations, and error marking.

Usage

chatter clan salt2chat input.slt

Options

OptionDefaultDescription
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "salt_corpus" (crates/clan/clan-core/src/converters/salt2chat.rs:193); there is no CLI flag to override it. Same pattern as the other converters in this directory.

Speaker Mapping

SALT codeCHAT speakerRole
CCHITarget_Child
EEXAInvestigator
PPAR(Parent)
IINV(Investigator)

SALT Annotation Stripping

SALT-specific annotations are removed during conversion:

  • Morpheme codes (word/3s –> word)
  • Error markers (word* –> word)
  • Maze markers ((word) –> skipped)
  • Comment markers ({...}, [...] –> skipped)
  • Bound morpheme markers (_word –> word)

Input Format

SALT transcription files with header lines (starting with $ or +) followed by speaker-prefixed utterance lines. SALT uses single-letter speaker codes and inline annotation conventions.

Output

A well-formed CHAT file with SALT speakers mapped to standard CHAT speaker codes, SALT-specific annotations stripped, and proper CHAT headers generated. Header metadata (participant name, age, gender, context) is extracted from SALT $ lines when available.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

SRT2CHAT – SRT Subtitle to CHAT Conversion

Status: Current Last updated: 2026-05-12 13:37 EDT

Purpose

Parses SRT (SubRip) subtitle files and converts them to CHAT format, mapping each subtitle block to an utterance with timing bullets derived from the SRT timestamps.

Usage

chatter clan srt2chat input.srt

Options

OptionDefaultDescription
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The speaker code ("SPK") and corpus name ("srt_corpus") in @ID headers are both hardcoded at crates/clan/clan-core/src/converters/srt2chat.rs:152; there are no CLI flags to override them. Same pattern as the other converters in this directory.

Input Format

SRT files consist of numbered blocks separated by blank lines:

1
00:00:01,000 --> 00:00:03,000
Hello world

2
00:00:04,200 --> 00:00:06,800
How are you

Timestamps use HH:MM:SS,mmm format (both comma and period separators are accepted). Multi-line subtitle text within a block is joined with spaces.

Output

A well-formed CHAT file where each SRT subtitle block becomes a timed utterance. Timing bullets are derived from the SRT timestamps (converted to milliseconds). All utterances are assigned to the configured speaker code.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output
  • Accepts both comma and period as millisecond separators in timestamps

TEXT2CHAT – Plain Text to CHAT Conversion

Status: Current Last updated: 2026-05-12 13:39 EDT

Purpose

Converts plain text files into CHAT format by splitting on sentence-ending punctuation (., ?, !) and assigning all utterances to a default speaker. This is the simplest converter, useful for bootstrapping CHAT files from raw text.

Usage

chatter clan text2chat input.txt

Options

OptionDefaultDescription
-s, --speaker"SPK"Speaker code for all utterances
-l, --language"eng"ISO 639 language code for the @Languages header
-o, --outputstdoutOutput CHAT file path

The corpus name in @ID headers is hardcoded to "text_corpus" (crates/clan/clan-core/src/converters/text2chat.rs:37); there is no CLI flag to override it. Same pattern as the other converters in this directory.

Input Format

Plain text files. Newlines within the input are treated as spaces (not sentence boundaries). The text is split into utterances at sentence-ending punctuation (., ?, !).

Output

A well-formed CHAT file where each sentence becomes an utterance. Sentence terminators are preserved as CHAT terminators (period, question mark, exclamation point). Trailing text without punctuation receives a default period terminator.

Differences from CLAN

  • Uses typed AST for CHAT generation
  • Produces valid, well-formed CHAT output

Flag Translation Guide

Status: Current Last updated: 2026-05-12 04:20 EDT

Complete mapping between legacy CLAN +flag/-flag syntax and modern --flag equivalents. Both styles are accepted by chatter clan. Translation lives in crates/clan/clan-core/src/clan_args.rs.

Speaker and tier filters

CLANModernDescription
+t*CHI--speaker CHIInclude speaker
-t*CHI--exclude-speaker CHIExclude speaker
+t%mor--tier morInclude dependent tier
-t%gra--exclude-tier graExclude dependent tier
+t@ID="..."--id-filter "..."Filter by @ID fields

Word filters

CLANModernDescription
+s<word>--include-word <word>Include word
-s<word>--exclude-word <word>Exclude word
+k--case-sensitiveCase-sensitive matching

Gem and range

CLANModernDescription
+g<label>--gem <label>Include gem segment
-g<label>--exclude-gem <label>Exclude gem segment
+z25-125--range 25-125Utterance range

Display and output

CLANModernDescription
+dN--display-mode NDisplay mode
+fEXT--output-ext EXTOutput file extension
+u (CHECK)--check-udValidate UD features (CHECK-specific)
+u (other commands)(no-op)Legacy “merge speakers”; chatter always merges, so this is a no-op
+r6--include-retracingsCount retraced material

Context

CLANModernDescription
+wN--context-after NContext lines after match
-wN--context-before NContext lines before match

CLAN Manual Audit

Status: Current This appendix tracks how the CLAN manual covers the commands currently implemented in clan-core.

The purpose of this audit is to keep the book honest about command intent:

  • when the manual has a command section, that section is the starting point for legacy semantics
  • when the manual is silent or incomplete, the command chapter should say so explicitly
  • GUI-oriented legacy material should move to the TalkBank VS Code extension docs, not this CLI book

Status legend

StatusMeaning
SectionThe legacy manual has a recognizable standalone command section.
IndirectThe command is mentioned or implied, but not documented as a standalone section.
MissingNo clear standalone coverage was found in the legacy manual.

Validation and Analysis Commands

CommandManual statusNotes
CHECKSectionNeeds a dedicated modernization pass.
FREQSectionPresent in CLAN.html.
MLUSectionPresent in CLAN.html.
MLTSectionPresent in CLAN.html.
VOCDSectionPresent in CLAN.html.
DSSSectionPresent in CLAN.html; manual also references the MOR pipeline.
EVALSectionPresent in CLAN.html.
EVAL-DMissingNo standalone section was found in the published CLAN manual.
KIDEVALSectionPresent in CLAN.html.
IPSYNSectionPresent in CLAN.html.
FLUCALCSectionPresent in CLAN.html.
SUGARMissingNo standalone section was found in the published CLAN manual.
MORTABLESectionPresent in CLAN.html.
KWALSectionPresent in CLAN.html.
COMBOMissingNo standalone section was found in the published CLAN manual.
CODESMissingNo standalone section found; codes.ord is mentioned inside CHAINS.
COMPLEXITYMissingNo standalone section was found in the published CLAN manual.
CORELEXSectionPresent in CLAN.html.
CHAINSSectionExplicit coding-tier semantics in manual.
CHIPSectionPresent in CLAN.html.
COOCCURMissingNo standalone section was found in the published CLAN manual.
DISTSectionPresent in CLAN.html.
FREQPOSSectionPresent in CLAN.html.
GEMLISTSectionPresent in CLAN.html.
KEYMAPSectionExplicit coding-tier semantics in manual; non-$ strings ignored.
MAXWDSectionPresent in CLAN.html.
MODREPSectionPresent in CLAN.html.
PHONFREQMissingNo standalone section was found in the published CLAN manual.
RELYSectionExplicit %cod/%spa coding-tier semantics and +c1 code-prefix comparison.
SCRIPTSectionPresent in CLAN.html.
TIMEDURSectionPresent in CLAN.html.
TRNFIXMissingNo standalone section was found in the published CLAN manual.
UNIQMissingNo standalone section was found in the published CLAN manual.
WDLENSectionPresent in CLAN.html.
WDSIZEMissingNo standalone section was found in the published CLAN manual.

Transform Commands

CommandManual statusNotes
CHSTRINGSectionPresent in CLAN.html.
COMBTIERSectionManual intent is narrow: combining duplicate %com lines.
COMPOUNDMissingNo standalone section was found in the published CLAN manual.
DATACLEANMissingNo standalone section was found in the published CLAN manual.
DATESSectionPresent in CLAN.html.
DELIMSectionPresent in CLAN.html.
FIXITSectionPresent in CLAN.html.
FIXBULLETSSectionPresent in CLAN.html.
FLOSectionPresent in CLAN.html.
GEMSectionPresent in CLAN.html.
INDENTSectionManual confirms text/layout purpose.
LINESSectionManual confirms text/layout purpose.
LONGTIERSectionManual confirms text/layout purpose.
LOWCASESectionPresent in CLAN.html.
MAKEMODMissingNo standalone section was found in the published CLAN manual.
ORTMissingNo standalone section was found in the published CLAN manual.
POSTMORTEMIndirectMentioned only through the mor *.cha pipeline in the published CLAN manual.
QUOTESSectionPresent in CLAN.html.
REPEATSectionPresent in CLAN.html.
RETRACESectionPresent in CLAN.html.
ROLESMissingNo standalone section was found in the published CLAN manual.
TIERORDERSectionPresent in CLAN.html.
TRIMSectionPresent in CLAN.html.

Format Converters

CommandManual statusNotes
CHAT2TEXTSectionPresent in CLAN.html.
CHAT2ELANSectionPresent in CLAN.html.
CHAT2PRAATSectionPresent in CLAN.html.
CHAT2SRTSectionPresent in CLAN.html.
ELAN2CHATSectionPresent in CLAN.html.
LAB2CHATSectionPresent in CLAN.html.
LENA2CHATSectionPresent in CLAN.html.
LIPP2CHATSectionPresent in CLAN.html.
PLAY2CHATMissingNo standalone section was found in the published CLAN manual.
PRAAT2CHATSectionPresent in CLAN.html.
RTF2CHATMissingNo standalone section was found in the published CLAN manual.
SALT2CHATMissingNo standalone section was found in the published CLAN manual.
SRT2CHATSectionPresent in CLAN.html.
TEXT2CHATMissingNo standalone section was found in the published CLAN manual.

Priorities

The next documentation passes should focus on:

  1. commands with strong manual semantics that should constrain implementation behavior, especially CHAINS, KEYMAP, and RELY
  2. commands whose current docs over-generalize legacy intent, especially COMBTIER
  3. commands with no standalone CLAN.html section, where the book needs to say what evidence is being used instead of pretending the manual is explicit

Command Status Matrix

Status: Current Last updated: 2026-05-12 04:26 EDT

Status of all CLAN commands in the Rust reimplementation.

Coverage: 70/70 CLAN binaries (100%). Every CLAN binary in OSX-CLAN/src/unix/bin/ has a corresponding module. The 6 NLP commands (MOR, POST, MEGRASP, etc.) are deliberately not implemented but produce clear error messages. Additionally, 7 implemented commands are outside that local binary inventory: COMPLEXITY, CORELEX, WDSIZE, LAB2CHAT, RTF2CHAT, ROLES, and TRIM.

CategoryCountGolden Tests
Validation1—
Analysis3453
Transforms2330
Converters1515
Not implemented6—
Total7998

Validation Commands (1)

CommandStatusNotes
CHECKImplementedFull flag support (+cN, +eN, +gN, +u), 161 error numbers mapped

Analysis Commands (34)

CommandStatusGolden TestsParity
CHAINSImplementedYesVerified
COMPLEXITYImplementedYesNew (auto-detects UD/legacy)
CORELEXImplementedYesNew command
CHIPImplementedYes100%
CODESImplementedYesVerified
COMBOImplementedYesVerified
COOCCURImplementedYesVerified
DISTImplementedYes100%
DSSImplementedYesVerified
EVALImplementedYesVerified
EVAL-DImplemented—Variant of EVAL (DementiaBank norms)
FLUCALCImplementedYesVerified
FREQImplementedYesVerified
FREQPOSImplementedYesVerified
GEMLISTImplementedYesVerified
IPSYNImplementedYesVerified
KEYMAPImplementedYesVerified
KIDEVALImplementedYesVerified
KWALImplementedYesVerified
MAXWDImplementedYes100%
MODREPImplementedYesVerified
MLTImplementedYes100%
MLUImplementedYes100%
MORTABLEImplementedYesVerified
PHONFREQImplementedYesVerified
RELYImplementedYesVerified
SCRIPTImplementedYesVerified
SUGARImplementedYesVerified
TIMEDURImplementedYes100%
TRNFIXImplementedYesVerified
UNIQImplementedYes99% (1 accepted)
VOCDImplementedYes100%
WDLENImplementedYes100%
WDSIZEImplementedYesNew (mor stem lengths)

Transform Commands (23)

CommandStatusGolden Tests
CHSTRINGImplementedYes
COMBTIERImplementedYes
COMPOUNDImplementedYes
DATACLEANImplementedYes
DATESImplementedYes
DELIMImplementedYes (4 accepted divergences)
FIXITImplementedYes
FIXBULLETSImplementedYes
FLOImplementedYes
GEMImplementedYes (2 tests: all gems + filtered)
INDENTImplementedYes (CLAN binary has infinite-loop bug, Rust-only)
LINESImplementedYes
LONGTIERImplementedYes
LOWCASEImplementedYes
MAKEMODImplementedYes
ORTImplementedYes
POSTMORTEMImplementedYes
QUOTESImplementedYes
REPEATImplementedYes
RETRACEImplementedYes
ROLESImplementedYes
TIERORDERImplementedYes
TRIMImplementedYes (2 tests: exclude-mor + exclude-all)

Format Converters (15)

CommandStatusNotes
CHAT2TEXTImplementedPlain text export
CHAT2ELANImplementedReverse of ELAN2CHAT
CHAT2PRAATImplementedPraat TextGrid export (bidirectional in praat2chat module)
CHAT2SRTImplementedSRT subtitle export
CHAT2VTTImplementedWebVTT subtitle export (Rust-side extension of the SRT converter; not in CLAN)
ELAN2CHATImplementedELAN XML import
LAB2CHATImplementedLAB format import
LENA2CHATImplementedLENA ITS import
LIPP2CHATImplementedLIPP format import
PLAY2CHATImplementedPLAY format import
PRAAT2CHATImplementedPraat TextGrid import
RTF2CHATImplementedRich Text import
SALT2CHATImplementedSALT format import
SRT2CHATImplementedSubtitle import
TEXT2CHATImplementedPlain text import

Deliberately Not Implemented (6)

These commands depend on the legacy CLAN MOR data model (trie-based lexicons, HMM/Brill taggers, MaxEnt parsers) which is incompatible with the UD-style morphological representation used in the current CHAT grammar. Use batchalign’s neural pipeline instead.

CommandPurposeRationale
MOR / MOR_PMorphological analysis~11K lines C, trie lexicon + 5 rule engines, legacy format
POSTPOS disambiguationRequires ^-separated ambiguity format not in grammar
MEGRASPDependency parsingRequires trained MaxEnt model weights
POSTLISTPOST database listingOperates on proprietary binary format
POSTMODRULESPOST rule modificationOperates on proprietary binary format
POSTTRAINPOST model trainingOperates on proprietary binary format

Compatibility-Alias Subcommands

These CLAN binaries have their own clap subcommand variant — exposed under the same name as the legacy CLAN binary — but are categorized as compatibility aliases in crates/chatter/chatter-cli/src/cli/args/clan_commands.rs (search for CompatibilityAlias). The dedicated subcommand exists so legacy CLAN scripts that invoke the binary by name continue to work.

Five subcommand names are bucketed as CompatibilityAlias: check, fixit, indent, longtier, and gemfreq. Only gemfreq is documented in detail here because its dispatch target differs from its name; the other four (check, fixit, indent, longtier) are listed in the main category tables above and dispatch to their namesakes through the standard pipeline.

SubcommandDispatches toNotes
gemfreqfreq (with --gem required)See GEMFREQ. Implemented via Gemfreq enum variant + clap ArgGroup::required(true) on --gem; routes to AnalysisCommandName::Freq.

Dependent Tier Semantics Matrix

Status: Current Last updated: 2026-05-11 23:50 EDT

Classification of the 31 DependentTier variants (crates/core/talkbank-model/src/model/dependent_tier/types.rs) by structural complexity and how commands access them.

Fully Structured (10 tiers)

These tiers have dedicated AST types with typed items. Commands should iterate typed fields, never split_whitespace() on serialized text.

TierAST TypeTyped ItemsUsed By
%morMorTierVec<Mor> → MorWord { pos, lemma, features }EVAL, KIDEVAL, MLU, VOCD, DSS, IPSYN, SUGAR, FREQ, etc.
%graGraTierVec<GrammaticalRelation> → { head, dependent, relation }EVAL, KIDEVAL, SUGAR, IPSYN
%phoPhoTierVec<PhoItem> → PhoWord { phones }PHONFREQ
%modMod(PhoTier)Same structure as %phoMODREP, MAKEMOD
%sinSinTierSinGroupGestures(not directly analyzed)
%actActTierBulletContent (text with optional timing)(situational)
%codCodTierBulletContent → CodSemanticElement via cod.rsCODES, CHAINS, KEYMAP, RELY
%xmodsylModsyl(SylTier)Vec<NonEmptyString> — syllabified target IPA (Phon)(see Phon Tiers)
%xphosylPhosyl(SylTier)Vec<NonEmptyString> — syllabified actual IPA (Phon)(see Phon Tiers)
%xphoalnPhoaln(PhoalnTier)Vec<WordAlignment> — segmental target↔actual mappings (Phon)(see Phon Tiers)

Bullet-Bearing Text (7 tiers)

These tiers contain free text with optional timing bullets. The text content is not further parsed into typed items. Commands access via BulletContent.text.

TierAST TypeContentUsed By
%addAddTierBulletContent (addressee annotations)(rare)
%comComTierBulletContent (comments)(metadata)
%expExpTierBulletContent (explanations)(metadata)
%gpxGpxTierBulletContent (gestural/proxemic)(situational)
%intIntTierBulletContent (intonation)(situational)
%sitSitTierBulletContent (situational notes)(situational)
%spaSpaTierBulletContent (speech act)(pragmatic analysis)

Simple Text (10 tiers)

These tiers contain plain text without timing bullets. They have minimal structure.

TierContentNotes
%altAlternative transcription
%cohCohesion annotation
%defDefinitions
%engEnglish gloss
%errError coding
%facFacial expression
%floFluent outputGenerated by FLO transform
%glsGloss
%ortOrthographic representation
%parParalinguistic

Special (4 tiers)

TierAST TypeNotes
%timTimTierTiming offset (numeric, not text)
%worWorTierWord-level alignment tier (generated from main tier)
UserDefinedUserDefinedTierArbitrary %xyz tiers not in the standard set
UnsupportedUnsupportedTierParser catch-all for unrecognized tier prefixes

Command Access Patterns

Correct: Typed access

// EVAL, KIDEVAL — iterate typed MorTier items
if let Some(mor_tier) = mor::extract_mor_tier(utterance) {
    for item in mor_tier.items.iter() {
        mor::classify_mor_item(item, &mut counts);
    }
}

Correct: Generic fallback for untyped tiers

// RELY, KEYMAP — split_whitespace() on tiers without typed representation
codes.extend(
    dependent_tier_content_text(dep)
        .split_whitespace()
        .filter(|t| !t.is_empty() && *t != ".")
        .map(str::to_owned),
);

Incorrect: Serializing typed tier then re-tokenizing

// DO NOT: serialize %mor then split_whitespace()
let text = dependent_tier_content_text(dep);
for token in text.split_whitespace() { ... }
// Instead: iterate MorTier.items directly

Transform Taxonomy

Status: Current Classification of the 23 transform commands by operation level.

Text/Layout Transforms (5)

These transforms operate on serialized CHAT text, not the AST. They handle formatting concerns that have no structural representation in the data model.

CommandOperationWhy Text-Level
LONGTIERRemove continuation line wrapping (\n\t → single line)Line wrapping is a display concern
LINESAdd/remove line number prefixesLine numbers have no AST representation
INDENTAlign CA overlap markers by columnColumn alignment is visual, not structural
LOWCASELowercase main tier wordsOperates on AST (Word.text), but simple enough to be text-like
CHSTRINGFind/replace strings using a changes fileText substitution by design

Structured/AST Transforms (15)

These transforms modify the parsed AST and re-serialize. They use typed model fields, not string manipulation.

CommandOperationKey AST Types
QUOTESExtract quoted text to separate utterancesUtterance, Postcode
ORTOrthographic conversion via dictionaryWord.text
RETRACECopy main tier to %ret dependent tierMainTier → DependentTier
COMBTIERMerge duplicate dependent tiersDependentTier list
POSTMORTEMPattern rules on %mor itemsMorTier.items (partially; %mor rewrite pending)
FLOGenerate %flo tier (simplified fluent output)MainTier.content → DependentTier::Flo
FIXBULLETSRepair timing bullet orderingBullet.timing (start_ms, end_ms)
TIERORDERReorder dependent tiers to canonical orderUtterance.dependent_tiers
TRIMRemove selected dependent tiersUtterance.dependent_tiers.retain()
MAKEMODGenerate %mod tier from pronunciation lexiconWord → DependentTier::Mod
COMPOUNDNormalize compound word formatting (dash → plus)Word.text compound markers
REPEATMark utterances with revisions as [+ rep]Utterance retrace annotations
ROLESRename speaker codesSpeakerCode in headers + utterances
DATESCompute participant ages from @Birth/@DateHeader::ID age field
DELIMAdd missing terminatorsMainTier.terminator

Mixed/Hybrid Transforms (3)

These transforms use a combination of AST and text-level operations.

CommandOperationNotes
DATACLEANFix CHAT formatting errors (brackets, tabs, ellipsis)Text-level by design: fixes sub-token formatting that the parser accepts as-is
FIXITNormalize via parse-serialize roundtripAST transform is a no-op; normalization comes from the serializer’s canonical formatting
GEMExtract utterances within @Bg/@Eg gem boundariesAST-based: filters ChatFile.lines by gem header state

Pipeline Patterns

Standard AST pipeline (most transforms)

read file → parse → validate → transform(AST) → serialize → write

Handled by framework::run_transform().

Text-level pipeline (DATACLEAN, LINES)

read file → parse → validate → serialize → transform(text) → write

Uses custom run functions that operate on the serialized string.

File-argument pipeline (COMPOUND, DATES)

copy to temp → run with file path → read output

Some CLAN binaries don’t support stdin; these use file arguments.

Glossary

Status: Current Last updated: 2026-05-12 04:22 EDT

CHAT Format Terms

CHAT (Codes for the Human Analysis of Transcripts)
The transcription format used by TalkBank. Files have .cha extension.
Main tier
The primary transcription line beginning with *SPK: (e.g., *CHI:). Contains the spoken text.
Dependent tier
Lines beginning with % that provide annotations for the preceding main tier (e.g., %mor: for morphology, %gra: for grammar).
Speaker code
Short identifier for a participant — up to seven characters from A-Z, 0-9, _, -, '; three uppercase letters is the convention (e.g., CHI = child, MOT = mother, FAT = father, INV = investigator).
%mor tier
Morphological analysis tier containing POS tags and morpheme breakdowns.
%gra tier
Grammatical relations tier containing dependency structure.
Gem
A marked segment of transcript bounded by @Bg (begin gem) and @Eg (end gem) headers. See Filtering gems.
Terminator
Utterance-ending punctuation (. period, ? question, ! exclamation) that appears at the end of main tier lines.

Analysis Terms

MLU (Mean Length of Utterance)
Average number of morphemes (or words) per utterance. The primary measure of grammatical development in child language.
TTR (Type-Token Ratio)
Ratio of unique words (types) to total words (tokens). A basic measure of lexical diversity.
NDW (Number of Different Words)
Count of unique word types in a sample.
D statistic
Vocabulary diversity measure computed by VOCD. More stable than TTR because it accounts for sample size.
DSS (Developmental Sentence Scoring)
Clinical scoring system for grammatical complexity (Lee & Canter).
IPSyn (Index of Productive Syntax)
Syntactic complexity score based on the presence of specific grammatical structures.
SLD (Stuttering-Like Disfluencies)
Disfluency types characteristic of stuttering: prolongations, blocks, part-word repetitions.
TD (Typical Disfluencies)
Normal disfluency types: phrase repetitions, revisions, filled pauses.

Technical Terms

AST (Abstract Syntax Tree)
The typed, structured representation of a CHAT file produced by the parser. All analysis operates on the AST, not raw text.
Countable word
A word with genuine lexical content, as determined by is_countable_word(). Excludes fillers, fragments, untranscribed markers, and omissions.
NormalizedWord
Canonical lowercased word form used as frequency map key. Implements Borrow<str> for zero-allocation lookups.
Golden test
A test that compares output against the original CLAN C binary to verify parity.
CUTT (CLAN Utility Template Toolkit)
The 17,926-line C framework underlying original CLAN. Replaced by the Rust framework in src/framework/.
Framework
The shared infrastructure (src/framework/) that handles filtering, command lifecycle, and output formatting for all commands.

CLAN Command Divergences & Improvements

Status: Current Last updated: 2026-05-12 04:24 EDT

This document tracks every divergence between the original CLAN commands and the Rust chatter analyze reimplementation. It covers CLI interface, output format, counting semantics, error handling, and intentional improvements.

Audience: Researchers who have existing CLAN pipelines and need to know what will change, and developers extending these commands.

Backward compatibility: Where CLAN’s behavior is well-defined and pipeline-critical, we aim to provide a --clan-compat mode in the future. Until then, this document serves as the authoritative migration reference.


Table of Contents


Philosophical Divergence

The most fundamental difference between CLAN and chatter analyze is not in any individual command, but in the underlying approach to analysis.

CLAN: String Matching Without a Data Model

CLAN was written in the late 1970s before the CHAT format had a formal specification. It operates directly on raw text strings, using character-level prefix matching to classify content:

// CLAN's approach to excluding non-lexical items (freq.c, mlu.c, etc.)
if (word[0] == '0') continue;     // skip omitted words
if (word[0] == '&') continue;     // skip fillers/nonwords
if (word[0] == '+') continue;     // skip terminators
if (word[0] == '-') continue;     // unclear intent
if (word[0] == '#') continue;     // skip pauses
if (strcmp(word, "xxx") == 0) continue;  // skip unintelligible
if (strcmp(word, "yyy") == 0) continue;  // skip phonetic coding
if (strcmp(word, "www") == 0) continue;  // skip untranscribed

This approach is duplicated across commands and conflates string representation with semantic meaning. For example, &-um (a filler) and &~gaga (a nonword) and &+fr (a phonological fragment) are all lumped together because they share the & prefix, even though they have distinct linguistic meanings.

Our Approach: Semantic Classification via AST

Our parser produces a typed AST where each of these categories is a distinct type. Analysis commands operate on semantic types, not string prefixes:

CLAN text patternSemantic intentOur AST representationAnalysis filtering
word[0] == '#'Skip pausesPause (separate AST node)Tree walk never visits
word[0] == '+'Skip terminatorsTerminator (separate AST level)Tree walk never visits
&=laughsSkip eventsEvent (separate AST node)Tree walk never visits
word == "xxx"Skip unintelligibleWord { untranscribed: Unintelligible }is_countable_word() returns false
word == "yyy"Skip phonetic codingWord { untranscribed: Phonetic }is_countable_word() returns false
word == "www"Skip untranscribedWord { untranscribed: Untranscribed }is_countable_word() returns false
word[0] == '0'Skip omitted wordsWord { category: Omission }is_countable_word() returns false
word[0] == '&'Skip fillers/nonwordsWord { category: Filler|Nonword|Fragment }is_countable_word() returns false
word[0] == '-'(unclear)Not a meaningful CHAT categoryNot applicable

Key insight: Pauses, events, actions, and terminators are already separate AST node types that our tree walk never visits. We only need explicit filtering on Word nodes that carry semantic annotations. The is_countable_word() function in framework/word_filter.rs encodes this logic once, and all analysis commands share it.

What This Means for Compatibility

The results should be equivalent: words that CLAN excludes via string prefix matching, we exclude via semantic type checking. But the mechanism is fundamentally different:

  • CLAN can be tricked by unusual formatting (e.g., a real word that happens to start with & due to a transcription error) — our AST only marks words as fillers if the parser recognized the &- prefix and classified them as such during parsing.
  • CLAN’s word[0] == '-' exclusion has unclear intent and may exclude legitimate content — we omit this because there is no corresponding semantic category in CHAT.
  • CA (Conversation Analysis) omissions ((word)) are countable in our system (they represent uncertain but present speech), matching the linguistic intent even though CLAN may exclude them via the 0 prefix check.

General Framework Divergences

CLI Syntax

Both legacy CLAN +flag/-flag and modern --flag syntax are accepted. The CLI auto-translates via clan_args::rewrite_clan_args().

AspectCLANchatter clan
Invocationfreq file.chachatter clan freq file.cha
Speaker include+t*CHI--speaker CHI
Speaker exclude-t*INV--exclude-speaker INV
Tier include+t%mor--tier mor
Tier exclude-t%gra--exclude-tier gra
Word include+s"word"--include-word "word"
Word exclude-s"word"--exclude-word "word"
Gem include+g"Story"--gem "Story"
Gem exclude-g"Warmup"--exclude-gem "Warmup"
ID filter+t@ID="..."--id-filter "..."
Utterance range+z25-125--range 25-125
Include retracings+r6--include-retracings
Case-sensitive+k--case-sensitive
Display mode+dN--display-mode N
Output to file+fEXT--output-ext EXT
Context window+wN / -wN--context-after N / --context-before N
Merge across files+uDefault behavior (always on)
Output formatText only--format text|json|csv|clan
Multiple filesfreq *.cha (glob)chatter clan freq dir/ (recursive)

Output Envelope

CLAN wraps output in a verbose envelope (command echo, timestamp, filter summary). chatter clan uses render_clan() for CLAN-compatible output format and render_text() for clean output. The envelope is omitted in both modes — the information is available via shell history and date.

Error Handling

BehaviorCLANchatter clan
Unparseable filePrints error, continuesLogs warning (via tracing), continues
No .cha files foundVaries by commandPrints error, exits with code 1
Missing %mor tierSkips silentlyReports 0 utterances (MLU), skips (FREQ --mor)

Multi-File Aggregation

BehaviorCLANchatter clan
DefaultPer-file outputMerged across files
MergedRequires +u flagDefault behavior

File Output

CLAN’s +f flag writes output to auto-named files (e.g., sample.frq.cex). chatter analyze always writes to stdout. Use shell redirection:

# CLAN:
freq +f sample.cha              # -> sample.frq.cex

# chatter:
chatter analyze freq sample.cha > sample.frq.txt
chatter analyze freq sample.cha --format json > sample.frq.json

Analysis Commands

Clinically Critical Commands

FREQ — Word Frequency

  • AST-based is_countable_word() replaces string-prefix matching
  • NormalizedWord lowercases and strips compound markers for grouping
  • Deterministic sort (count descending, then alphabetical); CLAN’s order varies
  • JSON and CSV output; --format clan for character-level CLAN compatibility
  • Golden test parity: verified

MLU — Mean Length of Utterance

  • Population SD (/ n), not sample (/ n-1). Verified against CLAN output.
  • Brown’s morpheme rules: Only 7 suffix strings count: PL, PAST, Past, POSS, PASTP, Pastp, PRESP. Each adds +1 to the stem count. Fusional features (&PRES, &INF) do NOT count.
  • When no %mor tier exists and not in --words-only mode, reports 0 utterances (matching CLAN).
  • Golden test parity: 100%

MLT — Mean Length of Turn

  • Population SD (/ n), matching CLAN.
  • SD basis: Computed over per-utterance word counts, not per-turn totals.
  • Turn boundaries detected when a different speaker produces the next utterance.
  • Golden test parity: 100%

DSS — Developmental Sentence Scoring

  • Built-in rules are a simplified subset; supply full .scr file for clinical scoring.
  • Sentence-point assignment uses heuristic (subject + verb POS) rather than full syntax.
  • Up to 50 utterances per speaker scored (configurable via max_utterances).
  • Golden test parity: verified

EVAL — Language Sample Evaluation

  • AST-based word/morpheme identification and typed POS categories.
  • Error counts ([*]) extracted from parsed AST annotations, not text patterns.
  • JSON and CSV output (CLAN produces text only).
  • Golden test parity: verified

KIDEVAL — Child Language Evaluation

  • Same AST-based approach as EVAL with age-normed scoring.
  • Shares the semantic POS classification and error extraction infrastructure.
  • Golden test parity: verified

IPSYN — Index of Productive Syntax

  • Parses %mor tier structure for syntactic pattern matching.
  • The built-in scoring covers a representative subset of the ~56 grammatical categories in the full English IPSyn rule set (see crates/clan/clan-core/src/commands/ipsyn.rs). Full coverage requires supplying an external rule file.
  • Golden test parity: verified for the built-in subset

VOCD — Vocabulary Diversity (D Statistic)

  • Bootstrap sampling of TTR across sample sizes 35-50, least-squares D-curve fitting.
  • Fusional feature stripping: &PRES, &INF etc. stripped from lemmas in %mor echo output.
  • Echoes %mor lemma lines after speaker header for insufficient-token warnings.
  • D values may differ slightly due to random sampling (stochastic algorithm).
  • Golden test parity: 100%

FLUCALC — Fluency Calculation

  • Counts disfluency types (repetitions, revisions, fillers) from main tier annotations.
  • Operates on parsed retrace groups and filler categories, not text patterns.
  • Golden test parity: verified

SUGAR — Grammatical Analysis

  • SUGAR scoring from %mor tier POS categories.
  • AST-based morpheme and word classification.
  • Golden test parity: verified

Other Analysis Commands

CHAINS — Clause Chain Analysis

  • Walks parsed %gra tier dependency structure instead of text-based bracket matching.
  • Clause boundaries identified from dependency relations, not punctuation heuristics.
  • Golden test parity: verified

CHIP — Child/Parent Interaction Profile

  • 36-measure matrix format matching CLAN exactly (ADU/CHI/ASR/CSR columns).
  • Echo: Main tier + %mor only (not %gra tiers), matching CLAN.
  • Interaction scoring (imitation, expansion, reduction) computed from %mor tier alignment.
  • Golden test parity: 100%

CODES — Code Frequency

  • Counts codes from %cod tier annotations using AST structure.
  • Golden test parity: verified
  • Operates on parsed AST content rather than raw text pattern matching.
  • Boolean AND/OR/NOT composition on word-level matches.
  • Golden test parity: verified

COOCCUR — Word Co-occurrence

  • Bigram counting from countable words per utterance.
  • Uses NormalizedWord for consistent key normalization.
  • Golden test parity: verified

DIST — Word Distribution Across Turns

  • Every utterance is its own turn (no speaker-continuity grouping), matching CLAN.
  • Per-speaker distribution of words across turn positions.
  • Golden test parity: 100%

FREQPOS — Positional Frequency

  • Word frequency by utterance position (initial, final, other, one-word).
  • AST-based word identification and position tracking.
  • Golden test parity: verified

GEMLIST — Gem Segments

  • Lists @Bg/@Eg gem boundaries from file headers.
  • Golden test parity: verified

KEYMAP — Contingency Tables

  • Reads coded data from %cod tier, builds contingency matrix.
  • Golden test parity: verified

MAXWD — Longest Words

  • Reports all occurrences with line numbers, matching CLAN.
  • Golden test parity: 100%

MODREP — Model/Replica Comparison

  • Compares %mod (model) and %pho (replica) tiers phonologically.
  • AST-based phonological comparison rather than string distance.
  • Golden test parity: verified

MORTABLE — Morphology Tables

  • Tabulates POS categories and morphological features from %mor tier.
  • Golden test parity: verified

PHONFREQ — Phonological Frequency

  • Frequency counts from %pho tier phonological transcription.
  • Golden test parity: verified

RELY — Inter-rater Agreement

  • Cohen’s kappa for inter-rater reliability between two transcription files.
  • Golden test parity: verified

SCRIPT — Template Comparison

  • Compares transcript against template script, computes accuracy metrics.
  • Golden test parity: verified

TIMEDUR — Time Duration

  • Computes duration from media timestamp bullets.
  • Interaction matrix header includes leading space, matching CLAN exactly.
  • Golden test parity: 100%

TRNFIX — Tier Comparison

  • Compares two dependent tiers side-by-side.
  • Uses ∅ for length mismatches between compared tiers.
  • Golden test parity: verified

UNIQ — Repeated Utterances

  • Includes %mor/%gra dependent tiers in counts, matching CLAN.
  • Splits multi-line headers for counting, matching CLAN.
  • 1 accepted divergence: Unicode sort order for U+230A (LEFT FLOOR) — C-locale strcoll() vs Rust byte-order. Single line position swap, identical content and counts.
  • Golden test parity: 99%

WDLEN — Word Length Distribution

  • 6-section format: characters (1), words (2), morphemes (3), per-utterance (4), per-word morphemes (5), per-word with POS (6).
  • Brown’s morpheme rules: Section 5 = stem + Brown’s suffix (no POS). Section 6 = POS + stem + Brown’s suffix.
  • Clitic handling: Section 5 merges main+clitics as one word. Section 6 counts POS only for main word.
  • Apostrophe stripping: Characters counted after removing apostrophes, matching CLAN.
  • Reverse speaker order: CLAN’s linked-list prepend pattern replicated.
  • XML footer: </Table></Worksheet></Workbook> appended to match CLAN output.
  • Golden test parity: 100%

Transform Commands

All transforms use the AST pipeline: parse → transform → serialize → write.

CHSTRING — String Replacement

  • Operates on parsed AST word nodes, not raw text. Structural integrity preserved.
  • Does not support CLAN’s regex-based patterns in changes files.

COMBTIER — Combine Tiers

  • Merges multiple dependent tiers of the same type via AST manipulation.

COMPOUND — Compound Normalization

  • Normalizes compound word formatting (dash to plus) in the AST.

DATACLEAN — Format Cleanup

  • Fixes common CHAT formatting errors (spacing, brackets) in serialized text.
  • Operates on serialized output rather than AST (custom run function).

DATES — Age Computation

  • Computes participant ages from @Birth and @Date headers.
  • Uses Rust chrono date arithmetic rather than C-style manual calculation.

DELIM — Add Terminators

  • Sets the Terminator field on the AST, not scanning line endings.
  • 4 accepted divergences: CLAN writes empty file when no changes needed; we always write the full file.

FIXBULLETS — Timing Repair

  • Enforces monotonic timing bullet ordering via AST timestamp fields.

FLO — Fluent Output

  • Walks AST nodes (retrace groups, replaced words, events, pauses) instead of regex-stripping.
  • Uses shared is_countable_word() for filtering.

GEM — Gem Extraction

  • Extracts utterances within @Bg/@Eg boundaries.
  • Also available as a filter on analysis commands.

LINES — Line Numbers

  • Operates on serialized text (custom run function).

LOWCASE — Lowercase

  • Lowercases word content in AST nodes, preserving annotations.

MAKEMOD — Model Tier

  • Generates %mod tier from pronunciation lexicon lookup.

ORT — Orthographic Conversion

  • Dictionary-based orthographic conversion via AST word replacement.

POSTMORTEM — Mor Post-processing

  • Pattern-matching rules applied to %mor tier AST structure.
  • Rule application is structural (MorWord fields) rather than text-based.

QUOTES — Extract Quotes

  • Operates on serialized text (custom run function).

REPEAT — Mark Repetitions

  • Marks utterances containing revisions with [+ rep] postcode.
  • Detection via parsed retrace group annotations.

RETRACE — Retrace Tier

  • Generates retrace annotation tier from AST structure.

TIERORDER — Reorder Tiers

  • Reorders dependent tiers by priority (standard CHAT ordering).
  • Operates on DependentTier vector in the AST.

Format Converters

All converters build/consume ChatFile AST structures, not raw text.

CHAT2ELAN — CHAT to ELAN

  • Produces EAF 3.0 XML with time-aligned annotation tiers.
  • CLAN equivalent: separate chat2elan binary.

CHAT2SRT — CHAT to Subtitles

  • Strips CHAT annotations by walking AST content variants.
  • Supports both SRT and WebVTT output formats (CLAN: SRT only).

ELAN2CHAT — ELAN to CHAT

  • ELAN tier IDs mapped to 3-character uppercased speaker codes.
  • Simple string-based XML parsing (no quick-xml dependency).

LAB2CHAT — LAB to CHAT

  • Timing label files from speech research converted to timed CHAT utterances.

LENA2CHAT — LENA to CHAT

  • LENA device XML (.its) import with segment timing.
  • Maps LENA speaker categories to CHAT participant codes.

LIPP2CHAT — LIPP to CHAT

  • LIPP phonetic profile import with %pho tier generation.

PLAY2CHAT — PLAY to CHAT

  • PLAY annotation format import.

PRAAT2CHAT — Praat TextGrid

  • Bidirectional: TextGrid → CHAT and CHAT → TextGrid in one module.
  • Supports both long and short TextGrid formats.
  • CLAN has separate commands for each direction.

RTF2CHAT — Rich Text to CHAT

  • Rich Text Format import with formatting strip.

SALT2CHAT — SALT to CHAT

  • SALT transcription format import with speaker/utterance mapping.
  • Handles SALT-specific conventions (utterance boundaries, codes).

SRT2CHAT — Subtitles to CHAT

  • SRT subtitle import with timing bullet generation.
  • Handles subtitle numbering, multi-line entries, and timestamp parsing.

TEXT2CHAT — Plain Text to CHAT

  • Plain text import with sentence splitting heuristics.
  • Generates minimal CHAT headers (@Begin, @Participants, @End).

Key Discoveries

These findings were established during parity verification (golden tests comparing against CLAN C binaries):

  1. Brown’s Morpheme Rules: CLAN MLU/WDLEN count only 7 suffix strings as bound morphemes: PL, PAST, Past, POSS, PASTP, Pastp, PRESP.

  2. Population SD: Both MLU and MLT use population SD (/ n), not sample (/ n-1).

  3. MLT SD basis: Computed over per-utterance word counts, not per-turn totals.

  4. DIST turns: Every utterance is its own turn (no speaker-continuity grouping).

  5. WDLEN morphemes: Section 5 = stem + Brown’s suffix (no POS). Section 6 = POS + stem + Brown’s suffix. Clitic pairs = one word in section 5, POS counted only for main word in section 6.

  6. WDLEN characters: CLAN strips apostrophes before counting character length.

  7. Speaker ordering: CLAN outputs speakers in reverse encounter order (linked-list prepend pattern from the C implementation).

  8. Fusional features: &PRES, &INF etc. are stored as part of the lemma string in our model (for roundtrip fidelity). Strip with split('&') when needed.

  9. CHIP echo: CLAN echoes main tiers + %mor only, not %gra tiers.


Parity Status

95% (113/118) — 5 accepted divergences across 2 commands.

CommandDivergencesStatus
DELIM4Accepted: CLAN writes empty file when no changes needed
UNIQ1Accepted: Unicode sort order edge case (U+230A)

All other commands: 100% parity with CLAN C binary output in golden tests.


Last Updated: 2026-03-05 Based on CLAN manual (January 27, 2026 edition) and golden test verification

Contributors and operations: reference

Look up syntax, contracts, and factual details.

Platform Support Matrix

Status: Current Last updated: 2026-06-01 00:50 PDT

This document answers two separate questions for each public-facing surface in talkbank-tools:

  1. Is this a supported public surface? See also release-contract.md.
  2. What platform claim is accurate today? That includes CI coverage, release artifacts, and important caveats.

Quick surface summary

SurfaceRelease/support statusAccurate platform claim today
chatter CLIPublic stableRelease binaries for Linux x86_64, macOS arm64/x86_64, and Windows x86_64
Public Rust core crates (talkbank-model, talkbank-parser, talkbank-transform, clan-core)Public previewSource-first cross-platform surface; Linux is the only full PR test gate today
tree-sitter-talkbank grammarPublic previewReusable cross-platform grammar package; grammar CI currently runs on Ubuntu
chatter-lspPublic previewBundled in the same core release archives as chatter for Linux x86_64, macOS arm64/x86_64, and Windows x86_64
VS Code extensionPublic previewGitHub Releases publish platform-specific VSIX bundles (macOS arm64/x64, Linux x64/arm64, Windows x64)
batchalign CLI / local server / dashboardPublic previewWheels for macOS arm64/x86_64, Linux x86_64/aarch64, and Windows x86_64
Chatter Desktop (apps/chatter/chatter-gui/)ExperimentalIn-repo validation GUI only; no supported release distribution
Batchalign Desktop (apps/batchalign/batchalign-gui/)ExperimentalIn-repo Batchalign GUI shell only; no supported release distribution

Support level definitions

  • Tier A: Full PR CI gate for that surface on the named platform.
  • Tier B: Public release artifact or explicit smoke coverage exists, but not a full PR test gate.
  • Tier C: Experimental/in-repo only. No supported end-user guarantee.

chatter CLI

PlatformTierCI / artifact evidenceNotes
Linux x86_64APR CI runs tests and chatter smoke; release archives builtStrongest current support story
macOS arm64BCross-platform smoke in PR CI; release archive builtNative smoke coverage exists, but not a full test matrix
macOS x86_64BRelease archive builtBuild coverage only
Windows x86_64BCross-platform smoke in PR CI; release archive builtSmoke coverage exists, but not a full test matrix

Public Rust core crates

These are the documented source-first Rust dependency surfaces today: talkbank-model, talkbank-parser, talkbank-transform, and clan-core.

PlatformTierCI / artifact evidenceNotes
Linux x86_64APR CI runs crate tests directlyCanonical source-level gate
macOS arm64BCompiled transitively in core release buildsNo crate-level macOS test job yet
macOS x86_64BCompiled transitively in core release buildsNo crate-level Intel macOS test job yet
Windows x86_64BCompiled transitively in core release buildsNo crate-level Windows test job yet

These crates are supported as source surfaces, not as separate prebuilt downloads or published crates.io installs today.

tree-sitter-talkbank grammar

AspectTierCurrent claim
Grammar source + generated artifactsA on Linux CI, B elsewhereGrammar generation and tree-sitter test run in Ubuntu CI
Standalone grammar packagesBPublic preview reusable package line (npm, crates.io, and PyPI metadata)
Downstream bindingsB for Rust, C for othersRust is the primary maintained binding; other bindings in bindings/ are still scaffold-level

Use cross-platform language here carefully: the grammar itself is intended to be portable, but the CI proof today is Ubuntu-only and the binding story is not uniform across languages.

chatter-lsp

PlatformTierCI / artifact evidenceNotes
Linux x86_64BBundled in core release archives; VSIX packaging smoke stages the binaryNo standalone PR test job for chatter-lsp on Linux
macOS arm64BBundled in core release archivesShips via release archives and VSIX packaging
macOS x86_64BBundled in core release archivesShips via release archives and VSIX packaging
Windows x86_64BBundled in core release archivesShips via release archives and VSIX packaging

chatter-lsp is public preview: usable and distributed, but not yet described as a fully frozen integration contract.

VS Code extension

PlatformTierCI / artifact evidenceNotes
Linux x64 VSIXBPR CI compiles/tests extension and smokes Linux VSIX packagingStrongest packaging coverage today
Linux arm64 VSIXBRelease workflow builds VSIXRelease-built only
macOS arm64 VSIXBRelease workflow builds VSIXRelease-built only
macOS x64 VSIXBRelease workflow builds VSIXRelease-built only
Windows x64 VSIXBRelease workflow builds VSIXRelease-built only

For now, user-facing distribution should be described as GitHub Releases VSIX-only, not Marketplace-first.

batchalign CLI / local server / dashboard

PlatformTierCI / artifact evidenceNotes
Linux x86_64AWheel smoke + PR CI on UbuntuPrimary Batchalign CI platform
Linux aarch64BRelease wheel builtBuild coverage only
macOS arm64BWheel smoke + release wheelStronger than build-only, but not a full PR suite
macOS x86_64BRelease wheel builtBuild coverage only
Windows x86_64BWheel smoke + release wheelCLI install/help path is covered; server/worker mode still has limitations
Dashboard UI (browser)BBuilt in CI; desktop/web frontend sharedSupported as part of the preview Batchalign surface, but the API is not frozen for third parties

Important Windows caveat: server/worker lifecycle code still has Unix-specific gaps, so avoid describing Windows as feature-identical to Linux/macOS for every Batchalign deployment mode.

Desktop surfaces

The repo contains two different desktop apps. They are easy to confuse, so docs should always name them explicitly:

  • Chatter Desktop = apps/chatter/chatter-gui/ = experimental native GUI for CHAT validation-only workflows.
  • Batchalign Desktop = apps/batchalign/batchalign-gui/ = experimental native shell around the Batchalign processing/dashboard UI.

Chatter Desktop (apps/chatter/chatter-gui/)

PlatformTierCurrent claim
macOS / Windows / LinuxCExperimental in-repo app only; not part of the supported public release contract

Batchalign Desktop (apps/batchalign/batchalign-gui/)

PlatformTierCurrent claim
macOS / WindowsCExperimental artifact workflow exists, but this is not a supported end-user release line
LinuxCNo supported public desktop distribution

How to phrase support publicly

  • Say “chatter is the stable public CHAT-first CLI”.
  • Say “batchalign is the public preview audio/ML surface”.
  • Say “the Rust crates are public preview and currently source-first via git/path dependencies”.
  • Say “the VS Code extension is public preview and currently distributed as GitHub Releases VSIX bundles”.
  • Say “tree-sitter-talkbank is public preview”, not “fully stable across all bindings”.
  • Say “Chatter Desktop” or “Batchalign Desktop” by name.
  • Do not describe either desktop app as a generally supported end-user release surface today.

Release Contract

Status: Current Last updated: 2026-06-01 00:50 PDT

This document defines which surfaces in the merged talkbank-tools repository are part of the public release contract. A version number in a manifest, generated artifact, or runtime banner does not by itself mean the surface is public or stability-guaranteed.

Tier definitions

TierMeaning
Public stablePublicly documented surface with an explicit compatibility promise. For the current pre-1.0 TalkBank core, semver expectations apply, but breaking changes may still happen in minor releases and must be called out clearly.
Public previewPublicly installable or documented surface that we expect users to try, but we are not yet promising minor-version compatibility.
ExperimentalPresent in the repo, may build, but not part of the supported public release contract.
InternalImplementation surface only. Names, versions, wire formats, and file layout may change without notice.

Current surface classification

SurfaceTierCanonical version sourceNotes
chatter CLIPublic stableCargo.toml workspace.package.versionUser-facing CLI contract for CHAT validation, normalization, conversion, and CLAN-compatible commands.
Public TalkBank Rust crates: talkbank-model, talkbank-parser, talkbank-transform, clan-corePublic previewCargo.toml workspace.package.versionDocumented source-first Rust dependency surfaces. Today consumers use git/path dependencies: the release workflow is binary-only, there is no dedicated crates.io publish workflow yet, and the current manifests still rely on unpublished helper crates such as talkbank-derive and talkbank-re2c-parser.
tree-sitter-talkbank standalone grammar packagesPublic previewgrammar/Cargo.toml package.versionOne grammar release line mirrored into npm and PyPI metadata. The grammar is public and reusable, but CST/API shape may still evolve before stabilization.
batchalign Python package, bundled CLI, and user-visible Batchalign version bannersPublic previewpyproject.toml [project].versionPublicly installable and documented on the 0.1.x preview line. Internal Cargo manifests mirror this version so the CLI/server/runtime surface presents one product release line, but the compatibility promise remains preview-tier.
batchalign serve local server and dashboard UIPublic previewTracks the batchalign product release line in pyproject.tomlEnd-user surface is supported, but the REST/WebSocket/API contract is not yet frozen for third-party integrations.
chatter-lsp language serverPublic previewCargo.toml workspace.package.versionFunctional and documented, but protocol/configuration details may still evolve.
VS Code extensionPublic previewapps/vscode-extension/package.json versionFirst public release channel is GitHub Releases VSIX-only. Marketplace publishing is intentionally deferred while binary discovery and release ops are still being hardened.
Desktop shells in apps/chatter/chatter-gui/ and apps/batchalign/batchalign-gui/ExperimentalTheir local package.json / Cargo.toml manifestsIn-repo experiments only; not part of the supported release contract.
Internal implementation surfaces: crates/batchalign-* Rust crates, batchalign-pyo3, send2clan-sys, apps/batchalign/cli-web-statuspage/, generated worker/OpenAPI artifactsInternalLocal manifests and generated filesBuild/runtime internals only. The Batchalign Rust/PyO3 crates are explicitly unpublished (publish = false), so matching the public product version does not make them supported public Rust APIs. External consumers should not depend on them directly.

Conservative promises

  • Only the TalkBank core CLI is currently treated as a stable public surface.
  • The four listed TalkBank Rust crates are documented and semver-checked, but they remain public preview until the repo has a real crates.io publication path rather than only source-level git/path consumption.
  • tree-sitter-talkbank is releasable and public, but it remains preview-tier until the standalone grammar API is explicitly frozen.
  • batchalign is now a major top-level product in this merged repository, but it is still classified as public preview at the repo-contract level on a deliberately pre-1.0 release line. Its package version does not imply that every Batchalign-adjacent sub-surface is stable.
  • Preview, experimental, and internal surfaces may change independently even when their version numbers happen to match a public surface. For Batchalign, matching Cargo-manifest versions are a packaging-coherence choice, not a crates.io publication promise.
  • Preview and experimental surfaces should remain below 1.0.0 until this document and book/src/operations/versioning.md are updated to grant a stronger promise.

What this document does not promise

  • Matching version numbers across Rust, Python, VS Code, and desktop surfaces.
  • Stable third-party integration against the Batchalign REST/WebSocket/API layer.
  • A first-release crates.io publication promise for the TalkBank Rust library crates.
  • Stability for repo-internal crates, generated schemas, or FFI support code.
  • book/src/operations/versioning.md defines the canonical version source for each surface.
  • book/src/operations/platform-support.md defines platform tiers separately from stability tiers.
  • Promoting any surface to Public stable requires updating this document, book/src/operations/versioning.md, and the corresponding CI/release automation together.

License

BSD-3-Clause across all release surfaces.

Versioning Policy

Status: Current Last updated: 2026-06-01 00:50 PDT

This repository ships multiple product surfaces with different audiences and different release mechanics. A version field is only authoritative for the surface it is assigned to. If several files contain versions, the canonical source below wins for public communication and release policy.

Canonical version sources

Surface / release lineRelease tierCanonical sourceCurrent valueNotes
TalkBank core Rust release line (chatter, preview TalkBank crates, preview chatter-lsp)Mixed: stable CLI + preview libraries/LSPCargo.toml workspace.package.version0.2.0Shared version for the main Rust workspace. Matching the workspace version does not imply that every Rust crate already has a crates.io publication path.
Standalone tree-sitter-talkbank grammar packages (crates.io, npm, PyPI)Public previewgrammar/Cargo.toml package.version0.2.0grammar/package.json and grammar/pyproject.toml mirror this value for the same grammar release line.
batchalign public product release line (Python package, bundled CLI, local server, dashboard UI)Public previewpyproject.toml [project].version0.1.0Authoritative version for PyPI/wheel installs and the user-visible Batchalign product surface.
Batchalign runtime banner metadataInternalbatchalign/version0.1.0Runtime metadata only; intentionally mirrors the public Batchalign preview version, but it is not a release-contract source.
Batchalign Rust/PyO3 build crates (crates/batchalign-*, pyo3/)InternalTheir local Cargo.toml manifests, kept in lockstep with pyproject.toml0.1.0Internal build metadata only. These crates are unpublished (publish = false) and are not independent public Rust APIs.
VS Code extensionPublic previewapps/vscode-extension/package.json version0.1.0Independent Marketplace release line.
Experimental desktop shellsExperimentalapps/chatter/chatter-gui/package.json, apps/chatter/chatter-gui/src-tauri/Cargo.toml, apps/batchalign/batchalign-gui/package.json, apps/batchalign/batchalign-gui/src-tauri/Cargo.toml0.1.0Experimental only; not part of the public release contract.

Rules

  1. One public surface, one canonical version source. Public docs, release notes, and support answers should cite the canonical source for that surface, not whichever version field happens to be easiest to find.

  2. Preview and experimental surfaces stay below 1.0.0 until explicitly promoted. A 1.0.0 tag is a stability signal, not just a bigger number. Do not ship it for preview or experimental surfaces unless book/src/operations/release-contract.md, this file, and the release workflow/docs are all updated together to describe the stronger promise.

  3. TalkBank core Rust surfaces share one version. Cargo.toml workspace.package.version is the source of truth for chatter, the preview TalkBank library crates, and preview chatter-lsp.

  4. The standalone grammar ships as one cross-ecosystem release line. grammar/Cargo.toml is the canonical version source for tree-sitter-talkbank; the npm and PyPI metadata must mirror it in the same patch so crates.io, npm, and PyPI do not drift.

  5. batchalign public versioning is Python-package-first. pyproject.toml defines the public Batchalign release line. The Batchalign Cargo manifests mirror that version for packaging and operator-facing coherence, but the public contract still comes from the Python package surface, not from unpublished Rust crates.

  6. batchalign/version is not authoritative for release policy. It is runtime metadata only. Do not use it to infer what the public Batchalign version or compatibility promise is.

  7. Matching versions do not widen the contract. The Batchalign Cargo manifests intentionally mirror the public batchalign release line, but publish = false keeps those crates internal-only. Equal version numbers do not create a crates.io support promise or a public Rust semver contract.

  8. CI semver checks are a Rust guardrail, not a blanket publication or crates.io promise. cargo-semver-checks runs in CI, but only the surfaces marked public/stable in book/src/operations/release-contract.md are currently covered by the repo’s strongest external compatibility promise.

  9. The TalkBank library crates are source-first until publication work exists. The current release workflow ships chatter and preview chatter-lsp binaries, while library consumers still use git/path dependencies. Do not describe workspace.package.version as a crates.io-ready release line for talkbank-model, talkbank-parser, talkbank-transform, or clan-core until a dedicated publish workflow and dependency-ready manifests exist.

  10. The VS Code extension and desktop apps version independently. Their versions do not need to match the Rust workspace, the grammar line, or the Batchalign Python package.

  11. If a surface changes tier or version authority, update policy docs together. book/src/operations/release-contract.md and book/src/operations/versioning.md should change in the same patch.

  12. When Batchalign ships a new public product version, mirror it into the Batchalign Cargo manifests in the same patch. Update pyproject.toml first, then the crates/batchalign-*/Cargo.toml, pyo3/Cargo.toml, and any user-visible runtime metadata that intentionally echoes the product release line.

Pre-1.0 Policy

Every currently releasable surface in this repo is still pre-1.0.

  • Patch (0.x.y): bug fixes, packaging metadata corrections, documentation-only release metadata updates, and other changes that should not require users to rethink integrations.
  • Minor (0.y.0): any user-visible feature addition, behavior shift, or breaking change. For pre-1.0 surfaces, the minor bump is the compatibility signal that would be a major bump after stabilization.
  • Promotion to 1.0.0: requires an explicit release-contract decision for that surface plus coordinated updates to its workflow/docs. Do not infer readiness from accumulated features alone.

For the current TalkBank core stable line, breaking changes may still occur in minor versions and must be called out clearly.

After a surface is explicitly promoted to stable at 1.0.0 or later:

  • Patch: bug fixes only
  • Minor: backwards-compatible additions
  • Major: breaking changes (with deprecation period)

Surface-aware version bumps

  1. Identify which public surface is changing.
  2. Update that surface’s canonical version source first.
  3. Update any secondary packaging metadata that must match for build/distribution (for example, grammar npm/PyPI mirrors or Batchalign internal manifests).
  4. Re-run the relevant checks for that surface (for example, Rust semver checks for public Rust APIs, or preview release smoke tests for Batchalign/VS Code).
  5. If the change also promotes or demotes a surface, update book/src/operations/release-contract.md in the same patch.

Previous page locations

These URLs are retained so existing links remain usable after the Diátaxis reorganization. Batchalign pages that described the former runtime point to their current replacement or to a fixed historical revision. Start with the Batchalign reference index for current contracts.

Supporting-tool pages remain available here when they were linked from earlier Batchalign navigation. Their product-specific documentation is separate from the current Batchalign runtime contract.

Install Batchalign

Status: Current Last updated: 2026-09-07

Install Batchalign with the command for your operating system in the repository README, then continue with the Batchalign Quick Start.

Quickstart

Status: Current Last updated: 2026-09-06

Start with the Batchalign Quick Start for transcription, forced alignment, morphotagging, and the other supported processing commands.

Migrate an existing Batchalign workflow

Start with Update commands and integrations. It maps the current CLI and Python API without assuming the retired Rust server architecture.

For current behavior, use the CLI reference, Python API, and pipeline explanation.

Earlier migration studies and model experiments are historical reference material. They describe particular earlier implementations and are not current command instructions.

Developer Architecture Migration (batchalign2 -> batchalign3)

The current reference for this topic is Batchalign source map. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

BA2 Compare Migration

The current reference for this topic is Compare a transcript with a reference. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

BA2 Architecture Reference

The BA2 implementation baseline. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Algorithms, Language, and Alignment Migration

A comparison of earlier BA2 and Rust-server algorithms. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Why Replace Python CHAT Handling with Rust

The rationale for an earlier ownership migration. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Persistent State and Behavioral Changes

The current reference for this topic is How result caching works. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Batchalign2 CLI Reference (Baseline)

The BA2 command-line baseline. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Debugging and Tracing Migration

The current reference for this topic is Diagnose a pipeline failure. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Monitor a batch

For command-line processing, use the terminal progress display and final summary. See Read processing progress.

The desktop application has its own batch view backed by the Python HTTP service. Ordinary CLI commands do not open a browser dashboard, and the current service does not provide the retired Rust server’s /dashboard UI. For API clients, job status and events are available through the service’s documented API.

Measure a processing run

The current CLI does not register bench or benchmark. For throughput, time an ordinary processing command on representative inputs and record peak RSS, input counts, backend settings, and cache state. See Performance.

To assess transcription accuracy, first transcribe, then compare against checked reference transcripts.

Evaluate analysis results

The current public CLI does not register eval l2-morphotag. Earlier examples of that command refer to a retired evaluation harness.

For transcription comparison, use compare. For morphology, inspect the generated %mor and %gra tiers against checked examples from your language and workflow. Historical evaluation data is not a guarantee of accuracy for the current backend or corpus.

Check an installation

The current CLI does not register a doctor command. To diagnose an installation:

batchalign version
batchalign --help
batchalign cache path

Then run one small input with the command you need. This checks backend loading and processing as well as command discovery. See Troubleshooting for next steps and the information to include in a bug report.

Python–Rust Boundary

The runtime boundary is now the in-process PyO3 engine and typed backend calls.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Dispatch and Execution

The former dispatch description used the retired Rust server and worker pool.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Command Flowcharts

The former flag-routing charts described commands and server dispatch paths that are no longer registered.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Progress Reporting

Current CLI progress uses Rust events and Python callbacks rather than the former server FileStatus stream.

See Read processing progress for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Replacements in the batchalign Pipeline

The current reference for this topic is Morphosyntax processing contract. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Preprocessing and Postprocessing for Model Inference

The former claim that all linguistic processing lived in Rust was incorrect: Stanza token processing and rendering also live in Python.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Number Expansion in ASR Post-Processing

The current reference for this topic is From recognition output to CHAT. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Language Routing

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

CHAT Parsing (Rust)

The former server/PyO3 ownership map did not match the in-process engine.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Dashboard Architecture

The old browser dashboard and Rust server were replaced by the current desktop/API architecture.

See How the HTTP service runs pipelines for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Batchalign Workers

The current CLI shares backends inside its process instead of managing the former worker-process pool.

See Understanding processing speed and memory use for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Cantonese and CJK — Architecture

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

Audio-Task Cache

The current LMDB cache covers text and audio backend results, not only audio tasks.

See How result caching works for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Validation Cache

Status: Current Last updated: 2026-05-01 17:07 EDT

The CHAT-core validation cache, used by chatter validate and the LSP server. Distinct from the result cache used by Batchalign for text and audio backend results: this cache stores parse + validate results keyed by file path + options.

crates/talkbank-transform/src/unified_cache/.

Architecture

flowchart TD
    req["Validation request\n(path + options)"]
    key["Cache key\n(path + check_alignment flag)"]
    db["SQLite WAL\n~/.cache/talkbank-chat/\ntalkbank-cache.db"]
    hit["Cache hit\n→ return stored result"]
    miss["Cache miss\n→ parse + validate + store"]

    req --> key --> db
    db -->|found + version match| hit
    db -->|not found or stale| miss
    miss --> db

Configuration

ConfigValueWhy
BackendSQLite via sqlxConcurrent reads (WAL), atomic writes, zero-config
Pool size16 connectionsMatches validation worker count
mmap256 MBFast random access for 95k+ entries
InvalidationVersion field + 30-day TTLSchema changes auto-invalidate; stale entries pruned
BridgeEmbedded single-threaded tokio runtimeSync workers call rt.block_on() for async SQLite

Schema

file_cache table:

ColumnRole
file_pathPrimary key — absolute resolved path
check_alignmentWhether alignment validation was requested
parser_kindParser backend (tree-sitter or re2c)
resultSerialized validation outcome
cached_atInsertion timestamp
versionSchema/code version — mismatch invalidates the entry

Database location

PlatformPath
macOS~/Library/Caches/talkbank-chat/talkbank-cache.db
Linux~/.cache/talkbank-chat/talkbank-cache.db
Windows%LocalAppData%\talkbank-chat\talkbank-cache.db

Invalidation

  • Schema changes — bump the version field; old entries become unreachable.
  • Time-based — entries older than 30 days are pruned.
  • Manual — pass --force to bypass cache lookups for a particular validation run.

Per project policy, do not delete the cache directory without explicit request — see the cache-policy section of talkbank-tools/CLAUDE.md.

See also

  • Audio-task cache — Batchalign’s per-utterance cache for FA / UTR ASR / media conversion.

Cache Override Guide

The current CLI does not expose –override-media-cache. Python callers choose a CacheSpec policy.

See Manage the result cache for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Validation

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

Errors — Batchalign Runtime

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

Errors at the Python ↔ Rust Boundary

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

Command Contracts: Input Preconditions and Output Guarantees

The former validity-level table referred to the retired command orchestrators; current preconditions belong to each typed runner.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

NLP Pipeline Decision Architecture

Existing decision types alone do not establish that all current runners emit the former review tiers and CLI controls.

See Review generated transcripts and timings for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Morphotag Reconciliation Invariants

The former MorOutcome and worker reconciliation description does not identify the current typed injection path.

See Morphosyntax processing contract for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Type-Driven Design

The former examples mixed current core types with removed server and worker-protocol types.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Typed Path Provenance

The old client/server path newtypes and remote media mappings are not the local CLI path contract.

See How the HTTP service runs pipelines for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Stanza Capability Registry

The current Stanza backend resolves language configurations directly; the former worker capability exchange is obsolete.

See Morphosyntax processing contract for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Stanza Defect Mitigation Map

The former patch-point table cited deleted worker/inference modules; current tokenizer and renderer ownership is documented here.

See Morphosyntax processing contract for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Observability Architecture

The old SQLite/Temporal job observability model is not implemented by the current CLI.

See Read processing progress for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Incremental Processing

Current reuse comes from typed backend result keys; the old server diff/orchestrator description is not a current CLI contract.

See How result caching works for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Server Model Loading and Caching

The former worker profiles and warmup configuration do not configure the current backends.

See Understanding processing speed and memory use for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Dynamic Programming

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

Test Server and Worker Lifecycle

Current product tests do not use the former shared Rust server fixture lifecycle.

See Test a Batchalign change for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

How align Throughput Works

Worker pre-scaling and server admission heuristics are not the current CLI controls.

See Understanding processing speed and memory use for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Proportional FA Estimation

The previous runtime inventory and source paths on this page predate the current Batchalign engine. See the current reference.

The previous inventory is available at a fixed revision for interpreting older implementations.

Overlap Encoding: &* and +< Internals

The current reference for this topic is Forced-alignment pipeline. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Algorithm Visualizations

The former dashboard visualization endpoints are not part of the current service.

See Review generated transcripts and timings for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Processing Provenance System

The former bracketed ba3 format and server-owned injection sites do not match current stamping.

See Processing provenance comments for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Time Transparency: A Cross-Cutting UX Principle

The UX goal of visible progress remains useful, but the worker progress_v2 mechanism in the former page is obsolete.

See Read processing progress for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Worker Failure Classification and Retry Architecture

The retired worker JSON protocol and server retry classifier do not describe in-process backend failures.

See Diagnose a pipeline failure for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Whisper Usage in Batchalign

The current reference for this topic is Transcribe a recording. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Rev.AI Language Quality Strategy

A proposed Rev.AI quality-assessment strategy. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Multilingual Support

The current reference for this topic is Language selection. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Language-Specific Processing Overview

The current reference for this topic is Language selection. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Language Code Resolution

The current reference for this topic is Language selection. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

L2 and Language Switching

The current reference for this topic is Language selection. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Retokenization — Overview

The current reference for this topic is Morphosyntax processing contract. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Morphotag Retokenization

The old –keeptokens flag and worker-side module paths are obsolete; current modes use –retokenize/–no-retokenize.

See Morphosyntax processing contract for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Stanza Limitations — Observed Defects with Version Pinning

Version-specific model observations and former mitigation claims. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Chinese/Cantonese Word Segmentation

The current reference for this topic is Morphosyntax processing contract. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Japanese Morphosyntax Pipeline

The current reference for this topic is Japanese processing reference. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Hebrew Morphosyntax

The current reference for this topic is Hebrew processing reference. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Utterance Segmentation

The current reference for this topic is Segment a transcript into utterances. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

%wor Tier Specification

The current reference for this topic is Forced-alignment pipeline. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Overlapping Speech in CHAT

The current reference for this topic is Forced-alignment pipeline. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Benchmarks

The former benchmark command is not registered in the current CLI.

See Measure a processing run for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

L2 Morphotag: Per-Word Code-Switching Analysis

Per-word code-switching design and its earlier evaluation. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

L2 Morphotag: Current Status

An earlier implementation-status and accuracy report. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Transcriber $POS Hints

The current reference for this topic is Morphosyntax processing contract. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Literature Review: Code-Switching Morphosyntactic Analysis

Literature considered for the earlier code-switching design. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

NLP Engine Text Input Expectations

The current reference for this topic is Morphosyntax processing contract. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Cross-Repository Compatibility Contract

The core declares versioned Git dependencies on TalkBank model, parser, and transform crates in crates/batchalign/batchalign-core/Cargo.toml. These are not the former sibling-checkout path dependencies.

See Batchalign source map for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

BA3 Cutover Landing Status

The former migration landing checklist is no longer a source map for the current implementation.

See Batchalign source map for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Deterministic Simulation Testing with turmoil

The former network-simulation harness is not a current Batchalign test recipe.

See Test a Batchalign change for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Regression Fixtures

The former harness paths have changed; use the current product tests and representative CLI fixtures.

See Test a Batchalign change for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Investigation Probe Harnesses

The former probe runner commands and server setup are not current test entrypoints.

See Test a Batchalign change for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Model Downloads and Caching (Developer Reference)

The worker-loader inventory and audio-only SQLite cache claims were obsolete.

See How result caching works for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

API Stability

The previous assertion that no Python API exists was false. The package exposes pipelines, recipes, inputs, and backend interfaces.

See Python API reference for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

align — Developer Reference

The current reference for this topic is Forced-alignment pipeline. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

transcribe — Developer Reference

The current reference for this topic is Transcribe a recording. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

morphotag — Developer Reference

The current reference for this topic is Morphosyntax processing contract. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

utseg — Developer Reference

The current reference for this topic is Segment a transcript into utterances. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

translate — Developer Reference

The current reference for this topic is Add translation tiers. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

coref — Developer Reference

The current reference for this topic is Coreference API. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

compare — Developer Reference

The current reference for this topic is Compare a transcript with a reference. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

benchmark — Developer Reference

The former benchmark orchestrator is not a current command entrypoint.

See Measure a processing run for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Rust Contributor Onboarding

Current contributor commands go through the repository just recipes and Bazel targets.

See Build Batchalign from source for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Rust Core (batchalign_core)

The extension is batchalign._core and its runtime lives in batchalign-engine, not the retired batchalign-pyo3 layout.

See Batchalign source map for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Workflow Contributor Guide

ReleasedCommand and Rust command-family registration no longer own the CLI surface.

See Add a processing command for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Decision Provenance

The former blanket review-tier emission contract is not exposed by the current CLI.

See Review generated transcripts and timings for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Terminator Architecture

The current reference for this topic is CHAT handling by the pipeline. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Overlap-Aware Alignment Improvements

The old –utr-strategy/–utr-fuzzy CLI controls are not registered; the UTR implementation has its own current strategy selection.

See Forced-alignment pipeline for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Worker Protocol V2

Backend calls cross the in-process typed engine boundary; the old worker_v2 transport is not the current interface.

See Add an inference backend for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Host Facts Pipeline

The former host-facts/bootstrap coordinator is not part of this CLI execution path.

See Understanding processing speed and memory use for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Rust CLI and Server

The current CLI and HTTP service are Python entrypoints around the Rust pipeline.

See How a processing command runs for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

HTTP Request Body Limits

The former Rust route-body configuration is not the current FastAPI service contract; inspect its OpenAPI schema and route implementation.

See How the HTTP service runs pipelines for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Plugin Architecture (Removed)

The current extension point is the backend interface and recipe composition, not the former worker capability registry.

See Add an inference backend for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Non-English Language Workarounds

The current reference for this topic is Language selection. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

%gra Structural Safeguards

The former guarantee cited a different generator; the current renderer and typed alignment checks are documented here.

See Morphosyntax processing contract for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Rust→Python IPC Type Sync

Protocol schemas are generated from the core proto types through Bazel, not the removed batchalign-types worker_v2 tree.

See Add an inference backend for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Debugging Infrastructure

The current CLI does not promise the former always-on server failure dumps.

See Diagnose a pipeline failure for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

CHAT Validation Failures

The current reference for this topic is CHAT handling by the pipeline. The earlier page mixed language/format background with implementation paths, flags, or behavior from a previous runtime.

The earlier detailed notes remain available for interpreting older output and experiments. They are not a claim that the same algorithms, model results, or CLI options ship today.

Apple MPS Workarounds

The blanket MPS prohibition is obsolete: applicable commands now have an explicit –allow-mps option.

See Understanding processing speed and memory use for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Arena Allocators (Evaluation — Not Used)

An earlier allocator evaluation. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Maturin Build and PyO3 Dependency Surface

The old extension-module name, crate layout, and editable-install instructions were obsolete.

See Batchalign source map for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Tauri + React Dashboard

The old cli-web-statuspage/dashboard-desktop paths and Rust server setup have been replaced.

See Run the desktop application from source for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Memory Safety: Preventing Kernel OOM Crashes

The current engine bounds queues and active files; it does not enforce the former host-wide worker-startup memory leases.

See Understanding processing speed and memory use for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Long-Term Reliability Program

The former scorecard mixed proposed fleet procedures with claims about shipped tests.

See Test a Batchalign change for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Models Training Runtime ADR

An earlier model-training/runtime decision. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Lenient Parsing

An earlier parser-policy decision. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Trait-Based Dispatch

An earlier algorithm-dispatch decision. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Release Readiness State Machine

An earlier release-state proposal. This is a historical record, not a current runtime contract.

Read the archived study at its fixed repository revision. For current documentation and the other studies, see Historical studies and decisions.

Job State Machine

The former persistent Rust job state machine does not describe the FastAPI in-memory registry.

See How the HTTP service runs pipelines for the current documentation.

The previous description is retained in repository history for interpreting older builds. It is not a configuration or behavior reference for the current release.

Batchalign: explanation

Understand the reasons and mechanisms behind the behavior.

How a processing command runs

The current CLI is a Python Typer application. It constructs backend objects and a recipe, then runs a Rust Pipeline through the in-process PyO3 extension. The pipeline owns file scheduling and task execution. There is no worker-process protocol on this path.

flowchart LR
    CLI[Python CLI] --> Inputs[Discover input paths]
    Inputs --> Pipeline[Rust Pipeline]
    Pipeline --> Runner[Typed task runner]
    Runner --> Queue[Bounded backend queue]
    Queue --> Cache{Result cached?}
    Cache -->|yes| Result[Typed task output]
    Cache -->|no| Backend[Python or native backend]
    Backend --> Result
    Result --> Apply[Rust result application]
    Apply --> Outcome[File outcome]
    Outcome --> Write[CLI writes output]

File and task scheduling

--parallel becomes Pipeline(workers=N), default 8. The callback-driven CLI path admits a bounded window of files, converts inputs within that window, and writes outcomes as files finish. Rust task runners prepare typed requests, dispatch backend work, and apply the returned result to pipeline state.

A registered backend has one bounded queue and one batcher, even when it serves several task kinds. The batcher checks LMDB, gathers misses according to BatchPolicy, calls the backend on a blocking thread, and returns results to waiting runners. Different backends can overlap. A single backend batcher awaits its current call before calling it again.

Morphotag has an additional per-file window of 128 pending utterances. Its Stanza backend defaults to batches of at most 128 requests and a 100 ms collection window. Stanza partitions a received batch by language configuration before model inference. See Performance for memory implications.

Pipeline composition

Recipes in python/batchalign/recipes.py define task order. For example, morphotag is one morphology task; align composes optional timing recovery and forced alignment. Transcription composes ASR, an optional utterance segmenter, and an optional speaker stage. The CLI chooses the backends supplied to recipes. It does not automatically append alignment or morphology to transcription.

Ownership

ConcernImplementation
CLI arguments, input discovery, output pathspython/batchalign/cli/
Recipes and backend constructorspython/batchalign/recipes.py, backends/
File admission, outcomes, cancellationcrates/batchalign/batchalign-engine/src/pipeline.rs
Routing and batchingEngine engine.rs, batcher.rs
Result cacheEngine cache.rs
Typed requests and task runnerscrates/batchalign/batchalign-core/src/proto/, taskrunners/
CHAT model, parsing, transformationsTalkBank Rust crates used by the core

The GUI runs through the separate HTTP service, which constructs pipelines using the same recipe layer. It is not an implicit CLI daemon.

Understanding processing speed and memory use

Processing time depends on the input, backend, hardware, and cache state. A first run may download models; a later CLI invocation still has to start Python and initialize the backends it needs. Saved results can avoid inference, including for text tasks such as morphosyntax. See Caching.

How work overlaps

The CLI runs a Rust pipeline inside the Python process. It admits a limited number of input files and sends their task requests to shared backend queues. --parallel N controls file admission; the default is 8.

Each registered backend has one batcher. It checks the result cache, collects misses up to the backend’s batch size or batching deadline, then calls that backend. That batcher awaits the call before starting another call. Different backends can overlap work; --parallel does not create N model processes.

For morphosyntax, the current defaults are:

ControlValueMeaning
Active files8CLI --parallel default
Pending utterances per active file128Rust morphotag dispatch window
Requests per Stanza backend batchUp to 128Backend batch maximum
Batch collection windowUp to 100 ms after the first missAllows requests to accumulate
Resident Stanza pipeline configurations in the process-wide LRU2Keyed by language set and retokenization mode

Stanza groups a received batch by language configuration. A mixed-language batch may produce several smaller model calls. The CLI orders morphotag files by language header, then by descending file size within a group, to improve locality. One sufficiently large file can fill a batch without additional file concurrency. A multilingual pipeline configuration can contain models for several languages; the two-entry LRU is not a limit of two individual model objects.

What the bounds do—and do not—mean

The engine bounds active file work and backend queue lengths, and morphotag bounds pending utterance futures. These limits prevent the scheduler from submitting the entire corpus for inference at once. They are not an RSS budget. The CLI still discovers input paths up front, and active files retain transcript state. Long utterances, decoded audio, model weights, inference tensors, and allocator retention can dominate memory.

Increasing file concurrency can improve overlap or batch filling, but can also increase RSS without speeding up inference. Increasing utterance batch size makes each model call larger. These are separate tuning decisions.

Device selection

Device behavior belongs to the selected backend. align, transcribe, and utseg expose --force-cpu and an explicit --allow-mps option for applicable local models. morphotag does not expose those flags; its Stanza configuration uses the library’s device selection. There is no universal GPU speedup factor.

Measure your workload

Compare the same inputs, outputs, backend settings, package version, and cache policy. Record model-download time separately from ordinary startup, and report whether results were cached. Use utterances per second as well as files per second when transcript lengths vary. Measure peak RSS alongside elapsed time; a larger batch can trade substantial memory for a small throughput improvement.

For a practical file-concurrency comparison, see Tune file concurrency. The current CLI has no bench command. The GUI’s HTTP service is a separate entrypoint; ordinary CLI processing does not automatically connect to it or keep a background model service alive between invocations.

How result caching works

Batchalign saves backend results so repeated processing can reuse completed computations. This includes text analysis: morphosyntax, utterance segmentation, and translation use the same result cache as audio inference.

Results and models are separate

The result cache stores individual task outputs in an LMDB database. Model files downloaded by Stanza, Hugging Face, or other libraries live in those libraries’ own download locations. Clearing result entries does not remove model files. Loaded models also occupy memory for the lifetime of their Python objects; a result-cache hit and a model already loaded in memory are different kinds of reuse.

The engine checks for a cached result before sending a request to a backend. A hit avoids that inference request. Input discovery, CHAT parsing, preprocessing, output assembly, and file writing still happen. Some backends initialize before cache lookup, so even a fully cached run can have startup costs.

What is reused

Pipeline workDefault behavior
Morphosyntax (morphotag)Cache backend results per utterance
Utterance segmentation (utseg)Cache backend results
Translation (translate) and AI editing (ai)Cache backend results
ASR (transcribe), timing recovery, forced alignment (align)Cache backend results
Speaker inference (diarize or transcription with diarization)Cache backend results
Media conversion (convert)The default conversion recipe bypasses the result cache
CHAT parsing, tier assembly, output writingPerformed by the pipeline; not saved as finished output files in this cache

The cache is shared across pipeline instances and processes at the same path. LMDB allows concurrent readers and serializes write transactions.

When a result stops matching

A cache key combines the compiled build identity, task, backend name, and the input fields selected by that task’s CacheKey implementation. For example, morphosyntax includes text, tokens, language, and retokenization mode. Routing identifiers such as source file ID and utterance number do not participate, so identical content in different files can share an entry.

Changing a key component causes a miss. The build identity normally comes from the compiled Git SHA, with the Rust package version as a fallback. A package upgrade can therefore leave older entries on disk while no longer reading them.

This does not fingerprint arbitrary downloaded model files or every remote provider update. If a provider or model changes without changing the backend identity or input, use a refresh policy in the Python API or clear the result cache before processing again.

Storage and limits

Run batchalign cache path to see the location for your installation. Defaults:

PlatformResult-cache directory
macOS~/Library/Caches/batchalign/cache.lmdb/
Linux$XDG_CACHE_HOME/batchalign/cache.lmdb/, or ~/.cache/batchalign/cache.lmdb/
Windows%LOCALAPPDATA%\batchalign\cache.lmdb\

The directory contains data.mdb and lock.mdb. The current engine sets a 16 GiB maximum LMDB map size. This is a virtual address-space limit, not a 16 GiB resident-memory allocation or an automatic eviction policy. Cache writes are best effort; a full database can prevent new results from being saved.

For commands to inspect or clear entries, see Manage the result cache. For custom paths and read/write policies, see Python API.

Implementation: crates/batchalign/batchalign-engine/src/cache.rs, batcher.rs, the core proto/ input types, and python/batchalign/recipes.py.

The HTTP service and the CLI

Ordinary batchalign morphotag, align, and transcribe commands create and run a pipeline in their own process. They do not start or discover an HTTP daemon, and they do not accept a global --server option.

The desktop application uses a separate Python FastAPI service, packaged as a PyApp sidecar. That service exposes recipes and job progress to the GUI. It can also be started explicitly with batchalign daemon for API development or integration. This is distinct from CLI file concurrency.

The service keeps its job registry in memory. It does not provide the retired Rust server’s worker registry, server.yaml fleet configuration, Temporal job persistence, or automatic CLI routing. Do not rely on jobs surviving a service restart.

For instructions, see Run the HTTP service. For local CLI throughput, see Tune file concurrency.

How the HTTP service runs pipelines

The desktop GUI uses the FastAPI application in python/batchalign/api.py. Recipe discovery exposes available task compositions and backend descriptors; requests materialize typed inputs and create a pipeline through the recipe API. Job status, events, results, and cancellation are exposed through /jobs/{job_id} and its associated routes.

The default job registry is in memory. A service restart loses registry state, and separate Uvicorn processes do not share it. This is why the daemon defaults to one HTTP worker. The service is packaged with Python and the wheel in a PyApp sidecar for the Tauri application.

The desktop starts the sidecar on a loopback port and discovers the bound port from its DAEMON_PORT announcement. The ordinary CLI invokes pipelines directly and does not use this handshake or service lifecycle.

Backend batching and caching follow the same engine contracts as the local pipeline. The API’s job registry is not the LMDB result cache, and persistent result entries do not make jobs restartable.

For API integration, inspect the running service’s /docs and /capabilities. See Run the HTTP service for startup instructions.

Read processing progress

The CLI tracks each selected file through task stages and prints a final summary of completed, failed, and skipped work. Multiple active files can be at different stages. The terminal renderer is selected automatically; use global --plain for line-oriented output or --ansi to request live rendering.

batchalign --plain morphotag corpus/ -o tagged/
batchalign -v align corpus/ -o aligned/

Rust progress events reach Python callbacks and the terminal task model. Backend calls may report finer progress, but a batched call cannot always attribute a sub-step to each file. A quiet progress counter alone does not establish that inference is stuck. Startup can also include downloads and model initialization.

File completion and output writing happen as outcomes arrive on the CLI’s callback path. Text commands do not submit the entire corpus as one model call. See Performance for scheduling and Troubleshooting if a run is not advancing.

From recognition output to CHAT

The transcription runner sends prepared audio, a language hint, and task options to the selected ASR backend. The returned segments and words are converted to Rust ASR structures for cleanup and CHAT assembly.

The core asr/ modules handle compound merging, timed-word extraction, splitting multiword recognizer tokens, numeric forms, orthographic cleanup, and construction of CHAT utterances and headers. Some cleanup is language-specific. Backends also normalize their own vendor outputs before returning the typed ASR result.

This means a backend token and a final CHAT word are not interchangeable. A single recognizer token can be split, a numeric token can expand into several words, and punctuation can be represented as a CHAT terminator rather than word text. Timing must remain attached to the transformed content through these steps.

The recipe can run an utterance-segmentation backend after ASR and a speaker backend afterward. It does not automatically append forced alignment or morphology. Generated CHAT includes the unchecked-ASR comment, and successful pipeline outputs receive task provenance.

Source: core taskrunners/asr.rs, asr/prepare.rs, asr/cleanup.rs, asr/expand.rs, asr/chat/, and python/batchalign/recipes.py.

Number expansion in ASR output

The Rust ASR pipeline recognizes numeric expressions and expands them before building CHAT. All ASR backends use this path. It does not invoke Python num2words or depend on morphosyntactic analysis.

Recognition and rendering

SourceResponsibility
asr/data/number_rules.jsonOrdered regular expressions, named captures, language selection, and punctuation protection
asr/num2text/mod.rsCompile rules once, recognize expressions, dispatch captures, and distinguish unsupported expressions from ordinary text
asr/num2text/eng.rsEnglish ordinal and decade rendering
asr/num2text/nld.rsDutch ordinal rendering
asr/num2text/por.rsPortuguese ordinal rendering with gender and plural agreement
asr/num2text/table.rs and asr/data/num2lang.jsonExisting cardinal lookup tables for 46 languages and integer decomposition
asr/num2text/cjk.rs and asr/num2chinese.rsExisting CJK conversion and script selection
asr/num2text/common.rsShared cardinal, currency, numeric-group, and hyphenated-compound handlers; percentage vocabulary
asr/prepare.rs and asr/expand.rsTokenization, protected abbreviation spans, and timing propagation

Language rules are tried in their JSON array order, then common rules. The first match selects the handler; unsupported rendering does not fall through to a less specific interpretation. Patterns match entire tokens and provide named captures such as value, gender, or symbol. Languages without a special renderer use their cardinal table when available.

Rules marked protect_punctuation also participate in ASR tokenization. For example, the Portuguese ordinal in 54.ª. consumes its abbreviation period, but leaves the final period as a sentence terminator. Protection requires a complete expression boundary; it does not match prefixes of longer words.

Supported forms and limits

  • Cardinal tables retain their existing exact entries and decomposition behavior. This refactor does not add grammatical context to cardinal generation.
  • English ordinals and decades retain the existing Rust implementation.
  • Dutch ordinals accept e, de, ste, and superscript ᵉ after digits for values 1–1000: 29e becomes negenentwintigste, 3de becomes derde, and 117e becomes honderdzeventiende. The numeric value determines the written suffix. Larger values remain unsupported.
  • Portuguese ordinals accept digits followed by º or ª, an optional abbreviation period, and optional plural s: 54ª, 54.ª, 54ºs. Values 1–1000 are supported. For example, 54ª becomes quinquagésima quarta; 54.º becomes quinquagésimo quarto.
  • Degree °, plain letters a/o, and arbitrary embedded digits are not interpreted as Portuguese ordinal markers.
  • Currency expansion retains the previous currency vocabulary, including English currency labels for non-English languages. Percentages retain their separate preparation-stage handling.

The Portuguese forms follow the ordinal composition described in Ciberdúvidas. Expansion reflects the recognizer’s written interpretation; it cannot establish which words the speaker actually said.

Dutch forms follow Taaladvies’s number spelling rules.

Results and timing

Recognition and rendering distinguish ordinary text, a successful expansion, and a recognized but unsupported expression. Unsupported expressions retain their source text and produce a debug diagnostic. The normal CHAT validation still applies and can reject remaining digits with E220.

An expansion can contain several words. The ASR pipeline retains the original token’s overall time span and apportions internal boundaries by character count; these are approximate boundaries for subsequent forced alignment to refine. It retains speaker information outside the language renderer.

The regression suite checks frozen outputs from the previous converter across all table languages and CJK/fallback paths, every applicable cardinal-table entry, and Dutch and Portuguese ordinal recognition, CHAT legality, punctuation, and timings. For surrounding stages, see ASR processing.

Why one word can have several morphology units

A written contraction can represent several grammatical units. Stanza may represent it as a multi-word token, while CHAT represents morphological components within the analysis of a main-tier word. These counts must not be confused.

The current backend reconciles token boundaries and renders structured units; the Rust runner aligns the resulting %mor/%gra tiers to the CHAT main tier. See Morphosyntax processing for ownership and failure handling.

Retokenization is a separate choice about main-tier word boundaries. Use --retokenize only when you want that mode, and review its effect on the transcript.

VS Code: explanation

Understand the reasons and mechanisms behind the behavior.

Review Mode

Status: Current Last updated: 2026-03-30 13:40 EDT

Review Mode helps you evaluate and correct alignment quality in CHAT files produced by batchalign align. When align is uncertain about a timing decision, it marks the utterance with a %xrev tier. Review Mode lets you step through these flagged utterances, listen to the audio, and rate or correct the timing — all without leaving VS Code.

When to use it

After running batchalign align --bullet-repair:

  • Every utterance where align made a repair decision gets a %xalign tier documenting what happened (e.g., “boundary averaged 155ms overlap”)
  • Low-confidence utterances also get a %xrev: [?] tier — these are the ones that need your attention

Review Mode navigates directly to the [?] utterances, plays their audio, and lets you rate them with a single keystroke.

What you’re rating

Each flagged utterance has a timing bullet (e.g., •12095_12335•) that should correspond to where the words are in the audio. Your job is to listen and answer: does this bullet point to the right place?

RatingKeyWhen to use
Good1Audio matches the text at this timestamp
Early2Bullet plays audio before the words start
Late3Bullet plays audio after the words start
Wrong4Hearing completely different content
Skip5Can’t tell, or don’t want to rate this one

What the tiers mean

%xalign is machine-generated metadata. It records what the alignment pipeline did and why. Never edit this tier — it’s the algorithm’s log.

%xalign:	boundary_averaged overlap=155ms machine=12095_12335 adjacent=UEL:3

%xrev is your review. It starts as [?] (unreviewed). You change it to your rating. You can also add notes after the rating.

%xrev:	[?]           ← before review
%xrev:	[ok]          ← after review (sounds right)
%xrev:	[early] about 200ms before speech starts  ← with a note

Visual indicators

Even without activating Review Mode, %xrev lines are color-coded in the editor:

  • Yellow left border — [?] unreviewed (needs attention)
  • Green left border — [ok], [corrected], or [stamped] (confirmed)
  • Red left border — [wrong], [early], or [late] (problem found)

These decorations appear on any CHAT file with %xrev tiers, automatically.

Coder Mode Overview

Status: Current Last updated: 2026-03-30 13:40 EDT

Coder Mode is a structured annotation workflow for coding CHAT transcripts with a predefined coding scheme. It is the VS Code equivalent of CLAN’s coder mode (implemented in ced_codes.cpp), adapted to work with VS Code’s QuickPick interface and the LSP-backed document model.

What Is Coding?

In language research, “coding” means annotating each utterance in a transcript with one or more labels from a controlled vocabulary. These labels (codes) capture information that is not directly present in the transcript text – for example, the pragmatic function of an utterance (request, demand, acknowledgment), the type of error a speaker made, or the conversational strategy being employed.

Codes are stored on dependent tiers, one per utterance:

*CHI:	I want that one .
%cod:	$PRA:request
*MOT:	which one ?
%cod:	$PRA:question

The coding scheme is defined in a .cut file – a tab-indented hierarchy of valid codes. Different research projects use different coding schemes depending on what they are studying.

Why Use Coder Mode?

Without Coder Mode, coding a transcript is tedious and error-prone: the coder must remember the valid codes, type them correctly on dependent tiers, and manually navigate to the next uncoded utterance. Coder Mode automates all of this:

  • Code selection from a picker – no memorization or typos. The hierarchical QuickPick shows all valid codes from the .cut file.
  • Automatic navigation – after inserting a code, the cursor advances to the next uncoded utterance. No manual searching.
  • Progress tracking – the picker shows how many utterances remain uncoded.
  • Consistent results – every code comes from the controlled vocabulary, ensuring inter-rater reliability.

CLAN Equivalent

In CLAN, coder mode is activated from the Commands Window and uses ced_codes.cpp to drive a custom modal editor. The coder navigates with keyboard shortcuts (ESC-e to toggle modes, ESC-c to move between code levels, CTRL-T to advance speakers) and selects codes from a hierarchical menu.

The VS Code implementation replaces this with standard VS Code UI patterns:

CLANVS Code
ESC-e toggle editing modeCommand Palette: Start/Stop Coder Mode
ESC-c code level navigationHierarchical QuickPick with indentation
CTRL-T next speakerCmd+Enter next uncoded utterance
Custom modal menuVS Code QuickPick with fuzzy search

Available Standard Code Files

TalkBank distributes several standard .cut code files in the clan-info/lib/coder/ directory:

FileTarget TierPurpose
codes-basic.cut%spaBasic speech act coding (positive/negative, questions/responses) for mother-child interaction
codes1.cut%spaExtended speech act coding with question types (NV/VE), answers, comments, acknowledgments
codeserr.cut%errError coding for lexical errors (incomplete, uncertain, accented, added, lost, etc.)
codeshar.cut%spaConversational strategy coding (NIA categories: acknowledgment, adaptation, clarification, etc.)

These files serve as examples and starting points. Most research projects create custom .cut files tailored to their specific coding scheme. See Codes Files for the file format and how to create your own.

ADR-001: LSP over Embedded Parser

Status: Accepted Last updated: 2026-04-16 22:00 EDT

Context

The extension needs to parse and validate CHAT text on every keystroke, resolve alignment metadata for hover / highlights / graph features, and run CLAN analyses on demand. Two plausible shapes:

  1. Embedded parser. Ship a WASM or npm build of tree-sitter-talkbank plus a TypeScript port of talkbank-model / talkbank-validation so the extension can parse CHAT in-process.
  2. Language server. Ship a Rust binary (chatter-lsp) that hosts the entire parser + model + validation + CLAN analysis stack and talks to the extension over LSP stdio.

Decision

Use a language server. The extension is a thin presentation/workflow layer; chatter-lsp owns every piece of CHAT domain knowledge.

Concretely:

  • The LSP binary is built from the same Rust workspace that produces chatter (the CLI) and is used by batchalign. Embedding would fork the codebase into two parallel implementations.
  • CHAT parsing goes through tree-sitter-talkbank → talkbank-parser → talkbank-model::ChatFile, with incremental reparse driven by tree-sitter edits.
  • All alignment, validation, and CLAN commands run in the server.
  • Custom features that don’t fit standard LSP (e.g. dependency graph DOT, alignment sidecar, speaker extraction) expose themselves through twelve talkbank/* custom executeCommand endpoints — see reference/rpc-contracts.md.

Consequences

Positive.

  • One parser implementation, one validation implementation, one CLAN implementation. No TS/Rust drift.
  • The same Rust model that powers the VS Code extension also powers chatter validate, batchalign, the Tauri desktop app. Bug fixes land in one place.
  • Rich incremental parsing via tree-sitter without porting the C grammar to JavaScript.
  • Memory-intensive structures (validation caches, tree-sitter trees) live in the server’s address space, not VS Code’s — and die with the server process when the extension unloads.

Negative.

  • The VSIX must ship a platform-specific binary for each target OS (macOS ARM + Intel, Linux x86 + ARM, Windows x86). See ADR-004.
  • First-time activation fails on platforms without a prebuilt binary unless the user sets talkbank.lsp.binaryPath to a local cargo install location.
  • LSP transport adds a JSON-RPC roundtrip to every feature request. Latency measured in tens of microseconds — insignificant vs. the human-scale interactions it supports.
  • Protocol design discipline required: every new feature either fits a standard LSP request or gets a typed custom talkbank/* endpoint. No shared in-memory state across the TS↔Rust boundary.

Alternatives considered

Embed tree-sitter-talkbank only, validate in TS. Rejected: the validation codebase is 3,500+ lines of Rust with deep coupling to the parser’s CST shape and the typed Mor / Gra / Pho / Sin / Wor / ChatFile models. Porting to TypeScript would create a permanent maintenance tax.

Embed a WASM build of the entire Rust stack. Rejected at the time for toolchain maturity and memory-footprint reasons; may be worth revisiting when wasm-bindgen + async LSP-over-WASM mature (the MCP / skills ecosystem is pushing adjacent patterns). If revisited, it would be a transport swap, not a re-architecture — the server code would stay the same.

Skip a server and ask the CHAT tree-sitter grammar to do enough syntax highlighting alone. Rejected: the extension’s flagship features (cross-tier alignment, dependency graph, CLAN analyses, validation diagnostics) all need the full typed model, not just CST node ranges.

Source anchors

  • Extension-side LSP activation: src/activation/lsp.ts.
  • Server binary target: crates/chatter-lsp/src/bin/chatter-lsp.rs.
  • Server dispatch entry: crates/chatter-lsp/src/backend/mod.rs.
  • Custom RPC registration: crates/chatter-lsp/src/backend/execute_commands.rs.

ADR-002: Effect-based command runtime

Status: Accepted Last updated: 2026-04-16 22:01 EDT

Context

VS Code’s command API — vscode.commands.registerCommand(id, handler) — takes any callback. Handlers that fail at runtime either throw (surfacing as an unhandled rejection) or silently return undefined. Features that compose multiple commands (e.g. “filter by speaker, then run analysis on the filtered virtual document”) end up stringing try/catch blocks together.

The LSP client’s RPC methods are async and fallible; each executeCommand call can fail three distinct ways (transport, wrong response shape, server error). Commands that call multiple RPCs need to classify and recover from each kind without collapsing them into generic unknown catches.

Decision

Wire every command through the Effect runtime via registerEffectCommand(context, id, effect) in src/effectCommandRuntime.ts. Handlers return Effect.Effect<void, TypedError, RuntimeServices> instead of Promise<void>.

Typed error families at the boundaries:

  • ExecuteCommandStructuredError — LSP RPC calls. Subtypes: ExecuteCommandRequestError (transport), ExecuteCommandResponseError (shape), ExecuteCommandServerError (server-side), plus StructuredPayloadDecodeError from effectBoundary.ts.
  • StructuredPayloadDecodeError — any unknown payload crossing a typed boundary (webview messages, LSP responses).
  • Per-feature error types: CoderCommandError, TranscriptionError, etc.

The runtime catches, classifies, logs, and user-visibly reports every failure category. No try/catch in handlers; no unhandledPromiseRejection surprises.

Consequences

Positive.

  • Every handler is a pure value until the runtime runs it. The same effect can be run in tests, composed into another effect, or retried without re-registering.
  • Error classification is compositional. A command that calls analyze → then filterDocument → then opens a panel carries the union of all three error families in its type, and the runtime branches on _tag rather than instanceof chains.
  • Webview message decoders share the same boundary: a StructuredPayloadDecodeError from any panel flows through the same reporter as an RPC response-shape mismatch.
  • Tests use Effect.runPromiseExit to assert on the outcome value without catching exceptions.

Negative.

  • Extra ceremony for trivial commands. “Show an info message” is one line with vscode.window.showInformationMessage(...) but three lines when wrapped as Effect.sync(() => ...).
  • Effect is a dependency. Developers unfamiliar with it need to learn Effect.flatMap, Effect.catchTag, and the tagged-error pattern from effect/Data.
  • The runtime file itself (effectCommandRuntime.ts) is another layer between registerCommand and the handler, with its own test surface.

Alternatives considered

Direct registerCommand everywhere, async/await with try/catch. Rejected: the 41 command handlers would replicate boilerplate for error classification, logging, and user notification. A single bug in the boilerplate (forgotten catch, swallowed error class) would hide across every command.

Promise-based custom wrapper without Effect. Rejected: most of the value here is the typed-error algebra. A Promise wrapper gives the boilerplate benefit but loses the compositional-error typing. Effect already has the abstraction; writing a half-version in-house would be its own maintenance burden.

Tagged unions + a custom runtime. Rejected as a smaller version of “reinvent Effect.” The effect library is stable, well-documented, and tree-shakes cleanly.

Source anchors

  • Runtime: src/effectCommandRuntime.ts.
  • Typed-error helpers: src/effectBoundary.ts.
  • LSP error family: src/lsp/executeCommandErrors.ts.
  • Panel decoders: src/webviewMessageContracts.ts + src/webviewContracts/*.
  • Example handler: any file in src/activation/commands/.

ADR-003: Webview panels over TreeView

Status: Accepted Last updated: 2026-05-11 20:25 EDT

Context

The extension surfaces five rich auxiliary views — analysis results, dependency graph, media playback, waveform, KidEval assessment — and two simpler views (ID editor, picture viewer). VS Code offers three mechanisms:

  1. TreeView (vscode.window.createTreeView) — native side-bar tree with VS Code’s built-in styling. Tight integration with context-menu contributions; rigid rendering (items with icons and labels).
  2. Custom editor (vscode.window.registerCustomEditorProvider) — replaces the text editor with a fully custom UI. Suitable for non-text formats.
  3. Webview panel (vscode.window.createWebviewPanel) — an iframe-like HTML surface inside VS Code, with a PostMessage-based JSON bridge to the extension host.

Decision

Use webview panels for all interactive auxiliary views. One singleton panel per view type (mediaPanel, graphPanel, etc.), createOrShow pattern for reuse, disposal hooks to tear down resources when the panel closes.

Native TreeView is used only for the Validation Explorer (file- system-keyed tree of validation results) where the native chrome is the right affordance.

Webview message contracts are typed per panel — see webview-contracts.md. Each panel’s message union is an Effect Schema-decoded discriminated union; panel controllers translate decode failures into a PanelErrorMessage sent back to the webview.

Consequences

Positive.

  • Full control over rendering: Graphviz DOT (rendered with @hpcc-js/wasm), HTML5 <audio>/<video>, canvas-based waveforms, CSV tables with sortable columns — none of which fit TreeView.
  • Panels can carry state the extension host doesn’t need to know about (e.g. the current waveform scroll position lives in the webview’s JS).
  • The same HTML/JS assets can render in a future non-VS-Code host (planned for the Tauri desktop app) with only the PostMessage transport swapped.

Negative.

  • Webview content-security-policy handling, asset-URI resolution, and the vscode-webview-ui-toolkit dependency all become the extension’s problem.
  • Every panel carries its own test surface and lifecycle bugs (e.g. the 2026-04-16 fix for a status-bar item never disposed when the review panel closed).
  • Webview → extension messages cross a serialization boundary. The effectBoundary.ts decoders make the boundary typed, but every new message shape needs an Effect Schema plus a decoder test.

Alternatives considered

TreeView + custom hover card. Rejected for the dependency graph and waveform specifically: neither renders in TreeView’s item slots. The media panel could live as a TreeView of segments but would lose inline playback controls.

Custom editor. Rejected: a .cha file is first and foremost a text document. Users want to edit it in the normal text editor with full VS Code affordances (LSP diagnostics, search, git), not in a replacement editor. The auxiliary views augment the text editor rather than replace it.

Multiple windows or an external Electron app. Rejected: all the workflows coupled to transcription, walking, coder mode — require in-editor interaction. Splitting them into a separate app would break the cursor-position contract (e.g. “play media at cursor”).

Source anchors

  • Panel controllers: src/<panel>Panel.ts — one TypeScript controller per panel type (analysisPanel, coderPanel, graphPanel, idEditorPanel, kidevalPanel, mediaPanel, picturePanel, waveformPanel).
  • Shared lifecycle: src/panelHost.ts, src/panelAssets.ts, src/panelLifecycle.ts.
  • Webview HTML/JS: src/webview/<panel>Panel.{html,js} — one HTML/JS pair per webview-driven panel (the simpler controllers that drive native VS Code dialogs instead of a webview do not need one).
  • Message contracts: src/webviewMessageContracts.ts (historical umbrella)
    • src/webviewContracts/<panel>PanelContract.ts (preferred home for new panels).
  • Validation explorer (only TreeView): src/activation/validation.ts.

ADR-004: Bundled LSP binary in the VSIX

Status: Accepted Last updated: 2026-04-29 10:16 EDT

Context

Per ADR-001, the extension needs a chatter-lsp binary to function. Two distribution shapes:

  1. Zero-install. Ship one generic VSIX; at activation time, either find the binary on PATH or run cargo install chatter-lsp. No binary ships in the VSIX.
  2. Bundled. Build per-platform VSIXes (five targets: macOS ARM, macOS Intel, Linux x86, Linux ARM, Windows x86), each carrying its own prebuilt chatter-lsp. Distribute those VSIXes from GitHub Releases for the first public release.

Decision

Bundle per platform. The extension’s release workflow builds five platform-specific VSIXes and publishes them to a GitHub Release. The extension’s activate path locates the bundled binary via vscode.ExtensionContext.extensionUri before falling back to PATH and finally to the talkbank.lsp.binaryPath setting.

The activation fallback order:

  1. talkbank.lsp.binaryPath setting (user-provided absolute path).
  2. Bundled binary at <extensionPath>/server/chatter-lsp (.exe on Windows).
  3. chatter-lsp on PATH (for users who cargo installed).

If all three fail, activation surfaces a “TalkBank LSP binary not found” message with an “Open Settings” action pointing at the path setting.

Consequences

Positive.

  • First-time install works with zero configuration on any supported platform. Non-programmer users (linguists, corpus builders) never need a Rust toolchain.
  • The extension ships the exact LSP version it was tested against. No version-skew between the client protocol and a separately- installed server.
  • CI pipeline enforces platform coverage: a release that fails to build for one target can’t ship that platform’s VSIX.
  • GitHub Releases keeps the first public release path simple: one workflow, one release page, no Marketplace credentials or staged rollout policy yet.

Negative.

  • Each VSIX is 10–15 MB (vs. < 1 MB for an extension without a binary). Download time perceptible on slow connections.
  • The release workflow has a 5-target matrix to maintain. A new target (Linux ARM32, FreeBSD, …) is a real change rather than a no-op.
  • GitHub Releases users must choose the correct asset manually because there is no Marketplace auto-targeting for the first release.
  • Platform-specific signing (Apple notarization, Windows SmartScreen) is the extension’s responsibility. Currently signed on macOS; Windows relies on publisher reputation rather than code-signing certificate.
  • The talkbank.lsp.binaryPath escape hatch has to keep working — developers testing a local LSP build must be able to override the bundled binary without uninstalling the VSIX.

Alternatives considered

Single generic VSIX + cargo install on first run. Rejected: users without Rust installed fail at activation; error message pointing them at cargo install is a non-starter for the primary audience (linguists).

Download binary at first activation. Rejected: the download would need signature verification (otherwise an attacker MITMing the CDN owns the user’s editor), a separate release channel, and ongoing availability guarantees. GitHub Releases plus bundled VSIXes keeps the first public release smaller in scope and avoids an extra distribution service in the activation path.

Marketplace-first rollout. Deferred: the first public release is GitHub Releases VSIX-only so release automation, install docs, and operator expectations stay aligned while the extension remains preview-tier.

Ship source, build with postinstall. Rejected: VS Code extensions historically do not run post-install scripts; doing so would require the user to have a Rust toolchain already.

Source anchors

  • Activation binary resolution: src/activation/lsp.ts (LSP_BINARY_PATH_SETTING = 'talkbank.lsp.binaryPath').
  • Release workflow: .github/workflows/vscode-release.yml.
  • Per-platform build script: scripts/build-vsix-<target>.sh.
  • Package bundling: apps/vscode-extension/package.json vsce:prepublish scripts.
  • Setting declaration: apps/vscode-extension/package.json contributes.configuration → talkbank.lsp.binaryPath.

CLAN: explanation

Understand the reasons and mechanisms behind the behavior.

Design Philosophy

Status: Current Last updated: 2026-05-12 08:37 EDT

Semantic AST processing

The original CLAN processes CHAT files as text, using string-prefix checks like starts_with('&') or == "xxx" to identify word types. The Rust reimplementation works on a parsed AST where every word has typed fields (Word.category, Word.untranscribed()) and where replacements live as a distinct UtteranceContent::ReplacedWord variant wrapping the original Word. This eliminates entire classes of bugs where string patterns match unintended content.

Two-layer architecture

framework/     Shared infrastructure (runner, filters, output, word utilities)
commands/      Individual command implementations
transforms/    File-modifying commands
converters/    Format conversion commands

The framework handles file discovery, CHAT parsing, speaker/tier/word/gem/range filtering, and output formatting. Commands implement only their analysis logic.

Typed results

Every command defines a result struct implementing CommandOutput. This struct is the single source of truth — render_text(), render_clan(), render_csv(), and JSON output (via the Serialize bound and to_json_value() / render(OutputFormat::Json)) all derive from the same data. No ad-hoc string building that could drift between formats.

Stateless commands

Commands hold only configuration (Config). All mutable state lives in a separate State type that is built up utterance-by-utterance via process_utterance(), optionally settled per file in end_file(), and consumed at the end of the run by finalize(state) -> Output. This makes the data flow explicit and testable.

Parity by default

Output from render_clan() must match legacy CLAN output exactly, warts and all. Improvements go in render_text(). Golden tests compare against actual CLAN binary output to enforce this.

Framework

Status: Current Last updated: 2026-05-12 08:35 EDT

The framework (crates/clan/clan-core/src/framework/) provides shared infrastructure for all commands.

Runner

The command runner (AnalysisRunner in framework/runner.rs) handles the lifecycle:

  1. Parse — read and parse the CHAT file via talkbank-transform
  2. Filter — apply speaker, tier, word, gem, range, and ID filters
  3. Dispatch — call process_utterance() for each matching utterance
  4. Per-file end — call end_file() after all utterances in a file
  5. Finalize — after all files are processed, call finalize(state) -> Output
  6. Render — format the result as text, JSON, CSV, or CLAN-compatible output via Output::render(format)
flowchart TD
    input["Input files"]
    runner["AnalysisRunner::run()"]
    parse["parse_file_and_validate()\n→ ChatFile"]
    filter["FilterConfig\n(speaker, tier, word,\ngem, range)"]
    process["cmd.process_utterance()\n→ update State"]
    endfile["cmd.end_file()"]
    finalize["cmd.finalize(state)\n→ Output"]
    render["Output::render(format)\n→ Text / JSON / CSV"]

    input --> runner
    runner -->|per file| parse --> filter
    filter -->|matching utts| process
    process -->|loop| filter
    filter -->|file done| endfile
    endfile -->|all files done| finalize --> render

Traits

AnalysisCommand

Per crates/clan/clan-core/src/framework/command.rs:52:

trait AnalysisCommand {
    type Config;
    type State: Default;
    type Output: CommandOutput;

    fn process_utterance(
        &self,
        utterance: &Utterance,
        file_context: &FileContext<'_>,
        state: &mut Self::State,
    );

    /// Called after each file. Default impl does nothing.
    fn end_file(&self, _file_context: &FileContext<'_>, _state: &mut Self::State) {}

    /// Called once after all files. Produces the typed output.
    fn finalize(&self, state: Self::State) -> Self::Output;
}
classDiagram
    class AnalysisCommand {
        <<trait>>
        +process_utterance(utt, ctx, state)
        +end_file(ctx, state)
        +finalize(state) Output
    }
    class Config {
        command-specific options
    }
    class State {
        +Default
        accumulated results
    }
    class Output {
        +Serialize
        +CommandOutput
        +render(format) String
    }

    AnalysisCommand --> Config : holds
    AnalysisCommand --> State : mutates
    AnalysisCommand --> Output : produces

TransformCommand

Transform commands receive a mutable ChatFile and modify it in place.

CommandOutput

Per crates/clan/clan-core/src/framework/output.rs:48:

trait CommandOutput: Serialize + std::fmt::Debug {
    /// Dispatch render by format.
    fn render(&self, format: OutputFormat) -> String { ... }

    /// JSON serialization (uses the `Serialize` bound).
    fn to_json_value(&self) -> serde_json::Value { ... }

    /// Required: clean text rendering.
    fn render_text(&self) -> String;

    /// Default: falls back to `render_text()` for commands not yet
    /// CLAN-matched.
    fn render_clan(&self) -> String { self.render_text() }

    /// Default: empty string.
    fn render_csv(&self) -> String { String::new() }
}

JSON output is produced by render(OutputFormat::Json), which uses serde_json::to_string_pretty(self) via the trait’s Serialize bound, not a custom render_json() method.

Word utilities

  • countable_words() — iterate words that should be counted (skips untranscribed, fillers, etc.)
  • is_countable_word() — predicate for individual words
  • NormalizedWord — wrapper with Borrow<str> for zero-allocation frequency map lookups

Filter system

Filters are configured from CLI flags and applied by the runner before dispatching to commands. Commands never check filters directly.

Why We Diverge

Status: Current

The parity goal

The primary goal is exact output parity with legacy CLAN wherever possible. We match CLAN’s output format, counting rules, and even its quirks (like reverse speaker ordering from C linked-list prepend patterns). Every implemented command has golden tests that compare our output character-by-character against the original CLAN C binary.

Current parity: 95% (113/118 golden snapshots match exactly). The 5 accepted divergences are in DELIM (4 cosmetic) and UNIQ (1 Unicode sort order difference).

When we accept divergences

We diverge from CLAN only when one of these conditions holds:

  1. CLAN has a bug that produces incorrect results — we fix the bug and document it. Examples: INDENT’s infinite loop on certain overlap patterns, FREQ double-counting words in retrace groups.

  2. The difference is cosmetic and doesn’t affect analysis results — different whitespace, column alignment, or header formatting that no downstream tool depends on.

  3. Matching would require unreasonable complexity for zero user benefit — e.g., replicating CLAN’s 80-character line wrapping in output, or matching exact floating-point rounding from C’s printf("%.3f").

  4. We deliberately improve on CLAN’s behavior — structured output formats (JSON, CSV), recursive directory traversal, rich error diagnostics with file locations. These are extensions, not replacements of existing behavior.

What we never change

  • Counting semantics: If CLAN counts a word, we count it. If CLAN uses population SD (/n), we use population SD. Counting rules are discovered empirically through golden test comparison and documented in Framework-Level Divergences.

  • Speaker ordering: CLAN outputs speakers in reverse encounter order due to its C linked-list prepend pattern. We replicate this, even though it’s a data structure artifact, because users and scripts depend on it.

  • Algorithm behavior: For stochastic commands like VOCD, we use the same random sampling algorithm (D_optimum curve fitting on 100 random samples at each token count from 35-50).

The AST advantage

The most fundamental architectural difference: CLAN processes CHAT files as flat text using string pattern matching and character scanning. The Rust reimplementation parses files into a typed AST (Abstract Syntax Tree) and operates on structured data.

This means:

  • Word classification uses typed fields (word.category, word.untranscribed()) instead of string prefix checks (starts_with('&'), == "xxx")
  • Annotations are structured objects with typed fields, not regex-extracted substrings
  • Transform commands modify AST nodes, then serialize — guaranteeing structural validity
  • Filters operate on typed speaker codes, tier names, and word categories

This eliminates entire classes of bugs where CLAN’s string patterns match unintended content (e.g., a word starting with & in a speaker name being misclassified as a special form). The tradeoff is occasional whitespace or formatting differences in edge cases, which we track and document.

Documentation policy

Every divergence — intentional or discovered — is documented in three places:

  1. Module doc comments: Each command’s //! header includes a “Differences from CLAN” section
  2. The book: Per-Command Divergences provides a centralized reference
  3. Golden test snapshots: The @clan vs @rust snapshot pairs make divergences visible in code review

Framework-Level Divergences

Status: Current Last updated: 2026-05-12 21:21 EDT

These differences apply across all commands, not to specific ones.

CLI syntax

AspectLegacy CLANRust CLAN
Flag style+flag / -flag--flag (both styles accepted)
Invocationfreq file.chachatter clan freq file.cha
DirectoryManual file listing or *.chaRecursive directory traversal
Output formatText onlyText, JSON, CSV
Error outputSimple messages to stderrRich diagnostics with file location and context

Both flag styles are accepted — +t*CHI and --speaker CHI are equivalent. The automatic rewriting happens in clan_args::rewrite_clan_args() before argument parsing.

Output formatting

  • File headers: Our headers include the full file path; CLAN uses basename only in some commands
  • Speaker ordering: CLAN outputs speakers in reverse encounter order (C linked-list prepend pattern). We replicate this behavior for parity.
  • Encoding: UTF-8 only. CLAN supports legacy encodings via CP2UTF.
  • Line wrapping: Our text output does not wrap long lines. CLAN wraps at ~80 characters in some commands.

Counting semantics

These counting rules were discovered during golden test parity verification and apply across multiple commands:

RuleCommands affectedDetails
Population SDMLU, MLTUses / n, not / n-1 (sample SD)
Brown’s morphemesMLU, WDLENOnly 7 suffix strings count: PL, PAST, Past, POSS, PASTP, Pastp, PRESP
Fusional featuresVOCD, MLU&PRES, &INF etc. are part of the lemma; strip with split('&')
Apostrophe strippingWDLENCharacters counted after removing apostrophes
Turn = utteranceDISTEvery utterance is its own turn (no speaker-continuity grouping)

AST-based processing

The most fundamental divergence: CLAN processes CHAT files as text using string pattern matching. The Rust reimplementation parses files into a typed AST and operates on structured data. This means:

  • Word classification uses typed fields (word.category, word.untranscribed()) instead of string prefixes (starts_with('&'))
  • Annotations are structured objects, not regex-extracted substrings
  • Transform commands modify AST nodes, then serialize — guaranteeing structural validity

This eliminates classes of bugs where string patterns match unintended content, but occasionally produces different whitespace or formatting in edge cases.

Per-Command Divergences

Status: Current This page documents every known divergence between the Rust chatter clan commands and the original CLAN C binaries. Divergences are verified through golden tests that compare output character-by-character.

Overall parity: 95% (113/118) – 5 accepted divergences across 2 commands.

Parity Summary

StatusCommands
100% parityFREQ, MLU, MLT, VOCD, CHIP, DIST, MAXWD, TIMEDUR, WDLEN
VerifiedDSS, EVAL, KIDEVAL, IPSYN, FLUCALC, SUGAR, CHAINS, CODES, COMBO, COOCCUR, FREQPOS, GEMLIST, KEYMAP, MODREP, MORTABLE, PHONFREQ, RELY, SCRIPT, TRNFIX, CHSTRING, FLO, POSTMORTEM
Accepted divergencesDELIM (4), UNIQ (1)

“Verified” means golden tests pass but character-level parity has not been exhaustively confirmed across all edge cases.


Analysis Commands

FREQ – Word Frequency

  • AST-based is_countable_word() replaces string-prefix matching
  • The CLAN manual explicitly says FREQ ignores xxx, www, and items beginning with 0, &, +, -, or # by default; clan-core preserves that intent through typed word classes rather than raw prefix checks.
  • NormalizedWord lowercases and strips compound markers for grouping
  • Deterministic sort (count descending, then alphabetical); CLAN’s order varies
  • JSON and CSV output; --format clan for character-level CLAN compatibility
  • Parity: 100%

MLU – Mean Length of Utterance

  • Population SD (/ n), not sample (/ n-1). Verified against CLAN output.
  • Brown’s morpheme rules: Only 7 suffix strings count: PL, PAST, Past, POSS, PASTP, Pastp, PRESP. Each adds +1 to the stem count. Fusional features (&PRES, &INF) do NOT count.
  • When no %mor tier exists and not in --words-only mode, reports 0 utterances (matching CLAN).
  • Parity: 100%

MLT – Mean Length of Turn

  • Population SD (/ n), matching CLAN.
  • SD basis: Computed over per-utterance word counts, not per-turn totals.
  • Turn boundaries detected when a different speaker produces the next utterance.
  • Parity: 100%

DSS – Developmental Sentence Scoring

  • Built-in rules are a simplified subset; supply full .scr file for clinical scoring.
  • Sentence-point assignment uses heuristic (subject + verb POS) rather than full syntax.
  • Up to 50 utterances per speaker scored (configurable via max_utterances).
  • Parity: Verified

EVAL – Language Sample Evaluation

  • AST-based word/morpheme identification and typed POS categories.
  • Error counts ([*]) extracted from parsed AST annotations, not text patterns.
  • Parity: Verified

KIDEVAL – Child Language Evaluation

  • Same AST-based approach as EVAL with combined metrics.
  • VOCD uses simplified TTR-based D estimate in the combined report.
  • Parity: Verified

IPSYN – Index of Productive Syntax

  • Parses %mor tier structure for syntactic pattern matching.
  • Built-in rule set is a simplified subset; supply rules file for full coverage.
  • Parity: Verified

VOCD – Vocabulary Diversity (D Statistic)

  • Bootstrap sampling of TTR across sample sizes 35-50, least-squares D-curve fitting.
  • Fusional feature stripping: &PRES, &INF etc. stripped from lemmas in %mor echo output.
  • D values may differ slightly due to random sampling (stochastic algorithm).
  • Parity: 100% (within expected stochastic variation)

FLUCALC – Fluency Calculation

  • Counts disfluency types from main tier annotations.
  • Some categories detected via text pattern matching rather than full AST traversal.
  • Parity: Verified

SUGAR – Grammatical Analysis

  • SUGAR scoring from %mor tier POS categories.
  • %mor post-clitics count as structured morphology, so clitic-bearing chunks contribute to morpheme totals and verb detection.
  • Minimum utterance threshold is configurable (CLAN uses fixed value).
  • Parity: Verified

CHAINS – Clause Chain Analysis

  • CHAINS consumes a clan-local semantic %cod item layer, so selectors like <w4> scope codes instead of being treated as codes themselves.
  • Uses sample SD (N-1), not population SD.
  • Parity: Verified

CHIP – Child/Parent Interaction Profile

  • 36-measure matrix format matching CLAN exactly (ADU/CHI/ASR/CSR columns).
  • Echo: Main tier + %mor only (not %gra tiers), matching CLAN.
  • Parity: 100%

CODES – Code Frequency

  • Codes extracted from parsed %cod tier, not raw text.
  • %cod is interpreted through a clan-local semantic item layer derived from the AST. Optional selectors like <w4> or <w4-5> scope the next code item instead of being counted as codes themselves.
  • Parity: Verified

COMBO – Boolean Search

  • AST-based content matching rather than raw text pattern matching.
  • Operator syntax: + for AND, , for OR (CLAN uses ^ and |).
  • Parity: Verified

COOCCUR – Word Co-occurrence

  • Bigram counting from countable words per utterance.
  • Parity: Verified

DIST – Word Distribution

  • Every utterance is its own turn (no speaker-continuity grouping), matching CLAN.
  • Parity: 100%

FREQPOS – Positional Frequency

  • Word frequency by utterance position (initial, final, other, one-word).
  • Parity: Verified

GEMLIST – Gem Segments

  • Lists @Bg/@Eg gem boundaries from file headers.
  • Parity: Verified

KEYMAP – Contingency Tables

  • Reads coded data from %cod tier, builds contingency matrix.
  • %cod selector tokens are treated as item scope rather than keyword/following-code values; KEYMAP consumes semantic %cod items.
  • Parity: Verified

MAXWD – Longest Words

  • Reports all occurrences with line numbers, matching CLAN.
  • Parity: 100%

MODREP – Model/Replica Comparison

  • Compares %mod and %pho tiers phonologically via AST.
  • Parity: Verified

MORTABLE – Morphology Tables

  • Tabulates POS categories from %mor tier using script files.
  • POS extraction reads typed %mor items directly instead of reparsing serialized %mor payload text.
  • Parity: Verified

KWAL – Keyword and Line

  • The CLAN manual does document KWAL, including cluster-based search semantics.
  • Scope reduction: The legacy manual describes richer cluster/tier selection and %mor/%gra combined searches than the current implementation exposes.
  • Parity: Verified

PHONFREQ – Phonological Frequency

  • Frequency counts from %pho tier.
  • Parity: Verified

RELY – Inter-rater Agreement

  • Cohen’s kappa for inter-rater reliability.
  • %cod comparison uses semantic code items from the AST-derived layer rather than whitespace tokens from flattened tier text.
  • Parity: Verified

SCRIPT – Template Comparison

  • Word matching uses frequency maps rather than positional alignment.
  • Parity: Verified

TIMEDUR – Time Duration

  • Interaction matrix header includes leading space, matching CLAN exactly.
  • Parity: 100%

TRNFIX – Tier Comparison

  • Uses ∅ for length mismatches between compared tiers.
  • %trn is treated as a structural alias of %mor; %grt is treated as a structural alias of %gra.
  • %mor/%gra comparison preserves typed token boundaries directly from the AST instead of comparing whitespace-split serialized payloads.
  • Parity: Verified

UNIQ – Repeated Utterances

  • Includes %mor/%gra dependent tiers in counts, matching CLAN.
  • Splits multi-line headers for counting, matching CLAN.
  • 1 accepted divergence: Unicode sort order for U+230A (LEFT FLOOR) – C-locale strcoll() vs Rust byte-order. Single line position swap, identical content and counts.
  • Parity: 99%

WDLEN – Word Length Distribution

  • 6-section format matching CLAN exactly.
  • Brown’s morpheme rules: Section 5 = stem + Brown’s suffix (no POS). Section 6 = POS + stem + Brown’s suffix.
  • Clitic handling: Section 5 merges main+clitics as one word. Section 6 counts POS only for main word.
  • Apostrophe stripping: Characters counted after removing apostrophes.
  • Reverse speaker order: CLAN’s linked-list prepend pattern replicated.
  • XML footer: </Table></Worksheet></Workbook> appended.
  • Parity: 100%

Transform Commands

All transforms use the AST pipeline: parse -> transform -> serialize -> write.

DELIM – Add Terminators

  • 4 accepted divergences: CLAN writes empty file when no changes needed; we always write the full file.
  • Parity: 4 accepted divergences

CHSTRING – String Replacement

  • Does not support CLAN’s regex-based patterns in changes files.
  • Parity: Verified

FLO – Fluent Output

  • Walks AST nodes instead of regex-stripping annotation markers.
  • Parity: Verified

POSTMORTEM – Mor Post-processing

  • Typed %mor rewrites are intentionally rejected until an AST-native rewrite path exists. POSTMORTEM errors explicitly when a rule would modify parsed %mor.
  • Current status: user-defined text tiers remain supported rewrite targets; typed %mor rewrite is intentionally unsupported until implemented through the AST.

Other transforms

  • COMBTIER preserves bullet/text tier variants such as %cod and %com instead of degrading them to user-defined tiers.
  • FIXBULLETS supports global offsets and tier-scoped repair across parsed main-tier, %wor, and bullet-content-tier bullets.
  • Bullet-bearing @Comment headers are parsed structurally, so FIXBULLETS can offset those header bullets through the AST as well.
  • Scope reduction remains: old-format bullet conversion, @Media insertion, multi-bullet merge, and +l remain unsupported.
  • Scope reduction: TIERORDER currently uses a built-in tier ordering, while the CLAN manual documents tierorder.cut as a user-controlled ordering source.
  • TRIM follows the CLAN manual intent by removing selected dependent tiers instead of extracting utterance ranges or gem segments. COMBTIER, COMPOUND, DATACLEAN, DATES, FIXBULLETS, LINES, LOWCASE, MAKEMOD, ORT, QUOTES, REPEAT, RETRACE, TIERORDER – all operate on AST rather than raw text, except for the intentionally text-level formatting transforms (DATACLEAN, LINES) and layout transforms (INDENT, LONGTIER) discussed in the dependent-tier semantics audit.

Key Discoveries

These findings were established during parity verification (golden tests comparing against CLAN C binaries):

  1. Brown’s Morpheme Rules: CLAN MLU/WDLEN count only 7 suffix strings as bound morphemes.
  2. Population SD: Both MLU and MLT use population SD (/ n), not sample (/ n-1).
  3. MLT SD basis: Computed over per-utterance word counts, not per-turn totals.
  4. DIST turns: Every utterance is its own turn (no speaker-continuity grouping).
  5. Speaker ordering: CLAN outputs speakers in reverse encounter order (linked-list prepend).
  6. Fusional features: &PRES, &INF etc. are part of the lemma string; strip with split('&').
  7. CHIP echo: CLAN echoes main tiers + %mor only, not %gra tiers.
  8. WDLEN characters: CLAN strips apostrophes before counting character length.

CHECK vs chatter validate

Status: Current Last updated: 2026-05-12 21:18 EDT

The chatter CLI has two validation tools with different purposes:

chatter clan checkchatter validate
PurposeCLAN CHECK compatibilityModern validation workflow
AudienceUsers migrating from CLANDay-to-day development
Output formatCLAN-style *** File "path": line N.Rich diagnostics with context, spans, suggestions
Error codesCHECK numbers (1–161)Typed codes (E###, W###)
FlagsCLAN +flag syntaxModern --flag syntax
CachingNoYes (SQLite-backed)
Directory supportSingle fileRecursive with parallelism
JSON outputVia --format jsonVia --format json
Fix suggestionsNoYes (some errors)
Exit code0 = clean, 1 = errors0 = clean, 1 = errors

Same Validation Engine

Both tools run the same underlying parse_and_validate_streaming pipeline from talkbank-transform. They catch the same errors — the difference is how errors are presented.

                    ┌──────────────────────────────┐
                    │  talkbank-transform           │
                    │  parse_and_validate_streaming  │
                    └──────────┬───────────────────┘
                               │
              ┌────────────────┼────────────────┐
              ▼                                 ▼
   chatter clan check                  chatter validate
   (CLAN-compatible output)            (modern diagnostics)

When to Use Which

Use chatter clan check when:

  • Migrating from CLAN: You have scripts that parse CHECK output format
  • Error filtering: You need CHECK’s +eN/-eN to filter by error number
  • Specific CLAN checks: You need +g2 (Target_Child), +g5 (unused speakers)
  • Comparing with colleagues who use original CLAN CHECK

Use chatter validate when:

  • Day-to-day work: Richer error messages with context and suggestions
  • Batch validation: Directory-wide validation with caching and parallelism (-j flag)
  • CI/CD pipelines: JSON output (--format json) for machine parsing
  • Performance: SQLite cache avoids re-validating unchanged files
  • Watch mode: chatter watch provides continuous validation on save

Output Comparison

The same error looks different in each tool. For a file declaring MOT in @Participants: but with no matching @ID: line:

chatter clan check sample.cha:

*** File "sample.cha": line 4.
@Participants:	CHI Target_Child, MOT Mother
Speaker 'MOT' declared in @Participants but has no matching @ID header(60)

chatter validate sample.cha:

× error[E522]: Speaker 'MOT' declared in @Participants but has no matching
│ @ID header (line 4, column 1, bytes 29..73)
 ╭─[sample.cha:4:1]
 4 │ @Participants:  CHI Target_Child, MOT Mother
   · ──────────────────────┬─────────────────────
   ·                       ╰── here
 ╰────
help: Add @ID header: @ID:	<lang>|<corpus>|MOT|<age>|<sex>|<group>|<ses>|
      Mother|<edu>|<custom>|

Same underlying check (E522 on the typed side ↔ CHECK number 60 on the legacy side); different rendering.

Additional Checks in CHECK

chatter clan check supports a few checks not available in chatter validate:

CheckFlagDescription
Target_Child+g2 / --check-targetVerifies CHI participant has Target_Child role
Unused speakers+g5 / --check-unusedReports speakers in @Participants but never used
UD features+u / --check-udValidates Universal Dependencies features on %mor

These are CHECK-specific because they are CLAN research conventions rather than CHAT format requirements. chatter validate checks format correctness; CHECK additionally checks research conventions.

Additional Features in chatter validate

FeatureDescription
CachingSQLite cache skips unchanged files
Parallelism-j N for multi-core directory validation
Watch modechatter watch for continuous validation
Fix suggestionsSome errors include actionable help: suggestions
Roundtrip test--roundtrip serializes and re-parses to verify fidelity
Quiet mode--quiet suppresses success output (exit code only)
Max errors--max-errors N stops after N errors

Error Number Mapping

The mapping table from typed ErrorCode to CHECK number 1–161 lives in crates/clan/clan-core/src/commands/check/error_map.rs (check_error_number). The match arms there are the live source of truth — count them directly rather than caching a stale number in this doc. Variants without a CHECK equivalent fall through to 0; those errors render as [<code>] in CHECK output instead of (<n>).

Use chatter clan check +e to see all 161 CHECK error messages (the canonical strings from CLAN’s check_mess()).

Deliberately Not Implemented

Status: Current

NLP Commands

These commands depend on the legacy CLAN MOR data model – trie-based lexicons, HMM/Brill taggers, and MaxEnt parsers – which is incompatible with the UD-style morphological representation used in the current CHAT grammar and data model. Attempting to invoke them produces a clear error message explaining why.

For morphological analysis, POS tagging, and dependency parsing, use batchalign’s neural pipeline, which supports more languages with higher accuracy.

CommandPurposeWhy not ported
MOR / MOR_PMorphological analysis (add %mor tiers)~11,000 lines of C with trie lexicon and 5 rule engines; output format uses legacy CLAN morphology conventions incompatible with UD-style %mor
POSTPOS disambiguation (disambiguate %mor)Reads ambiguous %mor tiers with ^ separators – a format not supported by the current CHAT grammar
MEGRASPGrammatical relation parsing (add %gra tiers)Requires trained MaxEnt model weights for the legacy CLAN %mor format
POSTLISTList POST database entriesOperates on POST’s proprietary binary database format
POSTMODRULESModify POST rulesOperates on POST’s proprietary binary database format
POSTTRAINTrain POST databaseProduces POST’s proprietary binary database format

CHECK

CommandStatus
CHECKReimplemented as chatter clan check with full flag support

CHECK is fully ported. chatter validate provides additional spec-driven validation with rich diagnostics beyond what legacy CHECK offered.

Subsumed by existing commands

These commands’ functionality is available through other implemented commands:

CommandAvailable via
GEM (standalone)freq --gem, gemlist
GEMFREQfreq --gem

Truly obsolete

These commands address problems that no longer exist in modern environments:

CommandReason
DOS2UNIXModern systems handle line endings natively
CP2UTFUTF-8 is universal; legacy codepage files are rare
FIXMP3Audio repair, not a CHAT analysis tool
SILENCE_P / PID_PAudio processing, out of scope for transcript analysis

Low-priority / specialized

CommandReason for low priority
GPS / PP / TEMPLATEExtremely specialized, no known active users

Parity Improvement Plan

Status: Current Current: 95% (113/118) — 5 accepted divergences across 2 commands.

Progress

Started at 74% (88/118). Fixed 25 divergences across 10 commands:

CommandDivergences FixedKey Changes
MLU4 → 0Brown’s morpheme counting, population SD
MLT4 → 0Per-utterance SD, population SD
DIST2 → 0Every-utterance turn counting
MAXWD1 → 0All occurrences with line numbers
TIMEDUR4 → 0Interaction matrix header leading space
VOCD4 → 0Echo %mor lemmas (stripped fusional), insufficient tokens
WDLEN4 → 06-section format, Brown’s morphemes, POS-inclusive section 6, apostrophe stripping, reversed speaker order, XML footer
UNIQ3 → 1Include %mor/%gra tiers, split multi-line headers
CHIP4 → 036-measure CLAN matrix format (main + %mor echo, no %gra)

Remaining Divergences (5) — All Accepted

DELIM — 4 divergences (all test files)

CLAN writes an empty .cex file when no changes are needed (all utterances already have terminators). Our transform always writes the full file. This is by design — the transform pipeline always writes output, and empty-file detection would require change tracking that adds complexity without user benefit.

UNIQ — 1 divergence (overlaps only)

Unicode sort order edge case: ⌊ (U+230A, LEFT FLOOR) sorts differently between CLAN’s C-locale strcoll() and Rust’s BTreeMap byte-order comparison. Single line position swap, identical content and counts. Not worth adding locale-dependent sorting.

Key Discoveries

  1. Brown’s Morpheme Rules: CLAN MLU/WDLEN count only 7 suffix strings as bound morphemes: PL, PAST, Past, POSS, PASTP, Pastp, PRESP.

  2. Population SD: Both MLU and MLT use population SD (/ n), not sample (/ n-1).

  3. MLT SD basis: Computed over per-utterance word counts, not per-turn totals.

  4. DIST turns: Every utterance is its own turn (no speaker-continuity grouping).

  5. WDLEN morphemes: Section 5 = stem + Brown’s suffix (no POS). Section 6 = POS + stem + Brown’s suffix. Clitic pairs = one word in section 5, POS counted only for main word in section 6.

  6. WDLEN characters: CLAN strips apostrophes before counting character length.

  7. Speaker ordering: CLAN outputs speakers in reverse encounter order (linked-list prepend pattern from the C implementation).

  8. Fusional features: &PRES, &INF etc. are stored as part of the lemma string in our model (for roundtrip fidelity). Strip with split('&') when needed.

  9. CHIP echo: CLAN echoes main tiers + %mor only, not %gra tiers.

Contributors and operations: explanation

Understand the reasons and mechanisms behind the behavior.

Architecture Overview

Status: Current Last updated: 2026-05-11 08:29 EDT

The talkbank-tools repository holds the entire TalkBank toolchain: the CHAT specification, tree-sitter grammar, parsing/model/validation/transform crates, the chatter CLI, the LSP server, the VS Code extension, the desktop app, and the Batchalign runtime/application layer. Everything lives in one repository under one root Cargo workspace.

Data Flow

Specification is the source of truth. Code is generated downstream from it.

spec/           Source of truth (CHAT specification)
    ↓
grammar.js      Tree-sitter grammar (in grammar/)
    ↓
parser.c        Generated C parser (never hand-edited)
    ↓
Rust crates     Parser → Model → Validation → Transform
    ↓
Applications    chatter CLI, LSP server, VS Code, desktop app, batchalign

Two layers

The repository contains two architectural layers:

CHAT core (talkbank-* crates). Parsing, data model, validation, transform, CLAN analysis, CLI, LSP. Compiles and runs on a fresh machine with no model downloads, no network, no Python. Everything that “is CHAT” lives here. See the crate-boundary policy in talkbank-tools/CLAUDE.md for what goes where.

Batchalign runtime (batchalign-* crates + batchalign/ Python). ML applications layered on top of the CHAT core: ASR, forced alignment, morphosyntax, utterance segmentation, translation, coreference, audio analysis. Rust owns all CHAT semantics; Python is a stateless ML inference host. The standalone batchalign3 repo was decommissioned in 2026-04; its source folded in here as sibling crates.

Crate Dependency Graph

flowchart TD
    derive["talkbank-derive\nProc macros"]
    model["talkbank-model\nData model, validation, alignment, errors"]
    parser["talkbank-parser\nCanonical parser (tree-sitter)"]
    re2c["talkbank-parser-re2c\nAlternate parser (equivalence oracle)"]
    transform["talkbank-transform\nPipelines, CHAT↔JSON, caching"]
    clan["clan-core\nCLAN analysis commands"]
    cli["chatter-cli (chatter)\nCLI: validate, normalize, convert"]
    lsp["chatter-lsp\nLanguage Server Protocol"]
    s2c["send2clan-sys\nFFI to CLAN app"]
    desktop["chatter-gui\nDesktop validation app (Tauri)"]
    tests["talkbank-parser-tests\nEquivalence tests"]

    batchalign_types["batchalign-types\nWire types (V2 protocol)"]
    batchalign["batchalign\nbatchalign: server, runner, dispatch, workers, CLI binary"]
    batchalign_pyo3["batchalign-pyo3\nPython↔Rust worker runtime (.so, cdylib+rlib)"]

    derive --> model
    model --> parser & re2c
    parser --> transform
    transform --> clan & cli & lsp & desktop
    clan --> cli & lsp
    s2c --> cli & desktop
    parser --> tests
    re2c --> tests

    model --> batchalign
    parser --> batchalign
    transform --> batchalign & batchalign_pyo3
    batchalign_types --> batchalign & batchalign_pyo3

Repository Layout

talkbank-tools/
├── grammar/                Tree-sitter grammar
├── spec/                   CHAT specification (source of truth)
│   ├── constructs/         Valid CHAT examples + expected parse trees
│   ├── errors/             Invalid CHAT examples + expected error codes
│   ├── symbols/            Shared symbol registry (JSON)
│   ├── tools/              Core spec generators
│   └── runtime-tools/      Runtime-aware spec bootstrap/validation tools
├── crates/                 All Rust crates (CHAT core + batchalign)
├── corpus/                 Reference corpus
├── schema/                 JSON Schema (auto-generated)
├── apps/vscode-extension/                 VS Code extension
├── apps/chatter/chatter-gui/   Desktop validation app (Tauri v2, React)
├── apps/batchalign/dashboard-desktop/ Batchalign dashboard Tauri shell (experimental)
├── batchalign/             Python worker code (ML inference only)
├── apps/batchalign/cli-web-statuspage/               React dashboard (served by batchalign server)
├── book/                   This documentation
└── fuzz/                   Fuzz testing targets (separate Cargo workspace)

Cargo Workspaces

Two separate workspaces:

  1. Root workspace (Cargo.toml) — all Rust crates for parsing, model, transform, batchalign runtime, plus apps/chatter/chatter-gui/src-tauri.
  2. Spec workspace (crates/spec/talkbank-spec-testgen/Cargo.toml) — spec/tools for core generation, spec/runtime-tools for runtime-aware spec tooling.

Use the relevant manifest path when working in the spec workspace: crates/spec/talkbank-spec-testgen/Cargo.toml for generators, crates/spec/talkbank-spec-testrun/Cargo.toml for bootstrap/mining/runtime validation.

For per-topic detail (sections being consolidated; see SUMMARY for the authoritative current list):

  • Spec System, Grammar, Parser Backends — how CHAT becomes typed AST.
  • CHAT model — the AST itself, content traversal, wide-struct rule.
  • Alignment — tier alignment, DP, forced alignment.
  • Runtime — server, dispatch, concurrency, model loading, per-command data flow, caching.
  • Python–Rust boundary — how Python workers fit in (batchalign only; the CHAT core has no Python).
  • Language and multilingual — Cantonese / CJK, language routing, Stanza.
  • Errors and validation — per-app error systems, validation gates G0–G10.
  • Memory and Ownership, Type-Driven Design (lands during M11 errors-and-validation work).
  • XML Emitter — projection.

For per-crate summaries see Crate Reference.

Spec System

Status: Current Last updated: 2026-03-24 01:32 EDT

Specifications in spec/ are the authoritative source of truth for the CHAT format. They drive grammar artifact generation, validation/error docs, and targeted test generation.

Historical note: This system was originally shaped during a dual-parser era. The chumsky-based direct parser was removed in March 2026. Today the canonical parser is tree-sitter (talkbank-parser); a second implementation, talkbank-parser-re2c, exists as a specification oracle and high-throughput batch parser. Fragment specs remain valuable, but synthetic tree-sitter wrapper behavior is audit-only legacy unless a page or test explicitly says otherwise.

Spec Types

Construct Specs (resources/spec/constructs/)

Each construct spec defines a valid CHAT pattern with its expected parse tree:

# example_name

Description of what this example tests.

## Input

\```mor_dependent_tier
%mor:	VERB|eat .
\```

## Expected CST

\```cst
(mor_dependent_tier
  (mor_tier_prefix)
  ...)
\```

## Metadata

- **Level**: tier
- **Category**: tiers

The Input code fence label (e.g., mor_dependent_tier, utterance) selects which template wraps the fragment into a full CHAT file for parsing.

That is an explicit grammar/test templating mechanism. It is useful, but it does not by itself define honest isolated-fragment semantics for the direct parser.

Error Specs (resources/spec/errors/)

Each error spec defines an invalid CHAT pattern with expected error codes:

# Error E301

## Metadata

- Code: E301
- Name: missing_participants
- Severity: Error
- Layer: parser

## Examples

### missing_participants_1

\```chat
@UTF8
@Begin
*CHI: hello .
@End
\```

Key metadata fields:

  • Layer: parser — error caught during parsing (returns Err)
  • Layer: validation — error caught after successful parse
  • Status: not_implemented — generates #[ignore] tests

Symbol Registry (resources/spec/symbols/)

symbol_registry.json defines character sets used by both the grammar and Rust crates. Running node resources/spec/symbols/validate_symbol_registry.js && node scripts/generate-symbol-sets.js && node resources/spec/symbols/generate_rust_symbol_sets.js generates:

  • JavaScript constants for grammar.js
  • Rust constants for model validation

Test Generation

Running just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs currently executes three generators:

1. Tree-sitter Corpus Tests

gen_tree_sitter_tests reads construct specs and error specs, then:

  • Wraps each Input in a template to create a full CHAT file
  • Parses with tree-sitter and checks for error nodes
  • Writes Expected CST to grammar/test/corpus/

For error specs, it captures the actual parse (with ERROR nodes) as the expected tree.

2. Rust Tests

gen_rust_tests generates Rust test functions:

  • Construct specs become parse-and-compare tests
  • Parser-layer error specs become parser.parse_chat_file() tests expecting Err
  • Validation-layer error specs become parse-then-validate tests

Output: crates/talkbank-parser-tests/tests/generated/

The generated suites are useful as grammar/audit support and regression coverage, but they are not the sole authority for parser semantics.

3. Error Documentation

gen_error_docs generates markdown pages for each error code at book/src/operations/errors/.

Workflow After Spec Changes

cd talkbank-tools
just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs     # Regenerate the affected spec-driven artifacts
bazel build //... && bazel test //...       # Run pre-merge verification gates

Never hand-edit generated artifacts — always regenerate from specs.

Post-Bootstrap Doctrine

  • spec/tools remains the generator/validator for grammar corpus tests, error docs, and shared symbol artifacts.
  • talkbank-parser-tests owns parser equivalence and roundtrip contracts.
  • Isolated grammar additions should usually need two things: one grammar corpus example and one full-file fixture. They should not require the old bootstrap ritual unless generated artifacts really changed.

Grammar

Status: Current Last updated: 2026-03-24 00:01 EDT

The CHAT grammar is defined in grammar/grammar.js using the tree-sitter parser generator. It produces a GLR parser that handles the full CHAT format with error recovery.

Design Principles

Explicit Whitespace

Unlike most tree-sitter grammars, CHAT does not use extras for whitespace. All whitespace is grammar-visible because CHAT’s structure is whitespace-sensitive:

  • Tab separates tier prefix from content
  • Newline ends tiers
  • Line continuation uses tab-at-start-of-line
  • Space separates words and annotations

Two-Level Structure

The grammar has two structural levels:

  1. Document level — headers, utterances, @Begin/@End
  2. Tier level — main tier content, dependent tier content (each with distinct rules)

Opaque Lemmas

In the %mor tier rules, lemmas are parsed as opaque Unicode strings. The grammar does not attempt to decompose lemma content — that happens in the model layer. This follows the “parse, don’t validate” principle.

Key Grammar Rules

Document Structure

document → utf8_header, begin_header, lines..., end_header
line → header | utterance
utterance → main_tier, dependent_tiers...

Main Tier

main_tier → star, speaker, colon, tab, tier_body
tier_body → contents, utterance_end
contents → content_item, (whitespace, content_item)...

MOR Tier (UD-style)

mor_contents → mor_content, (whitespace, mor_content)..., terminator
mor_content → mor_word, mor_post_clitic*
mor_word → mor_pos, pipe, mor_lemma, mor_feature*
mor_post_clitic → tilde, mor_word
mor_feature → hyphen, mor_feature_value

POS tags are simple identifiers (no subcategories). Lemmas are opaque strings. Features are hyphen-separated values that may contain = for Key=Value pairs and , for multi-value features.

Grammar Change Workflow

parser.c is generated from grammar.js — never edit it directly.

After any change to grammar.js:

  1. cd grammar && tree-sitter generate
  2. tree-sitter test (160 tests)
  3. cargo test -p talkbank-parser
  4. cargo nextest run -p talkbank-parser-tests (reference corpus equivalence, per-file)
  5. Verify the 78-file reference corpus passes at 100%

Conflict Resolution

The grammar uses tree-sitter’s precedence and conflict mechanisms to handle ambiguities:

  • Word tokens use prec(5) to win over separators
  • Inline bullets use prec(10) for their delimiters
  • CA (conversation analysis) symbols use prec(3) for colon disambiguation

Generated Artifacts

Running tree-sitter generate produces:

  • src/parser.c — the C parser
  • src/node-types.json — node type metadata

The Rust crate talkbank-parser references node-types.json to generate node_types.rs (a generated constants file).

Parsing

Status: Current Last updated: 2026-03-24 01:32 EDT

The parsing pipeline converts CHAT text into a typed ChatFile AST. The default and canonical parser is the tree-sitter parser (talkbank-parser). A second implementation, talkbank-parser-re2c, exists alongside it as a specification oracle and high-throughput batch parser; it produces the same ChatFile model and is opt-in via chatter validate --parser re2c. The LSP and all production paths use the tree-sitter parser.

Tree-Sitter Parser

The talkbank-parser crate wraps the tree-sitter C parser and converts its concrete syntax tree (CST) into the ChatFile model.

Full-file parsing is the canonical entry point. TreeSitterParser also provides fragment methods (parse_word_fragment(), parse_main_tier_fragment(), parse_chat_file_fragment(), etc.) for parsing isolated CHAT fragments directly.

CST → AST Pipeline

flowchart LR
    chat["CHAT text\n(.cha file)"]
    grammar["tree-sitter grammar\n(grammar.js → parser.c)"]
    cst["Concrete Syntax Tree\n(all whitespace preserved)"]
    walker["TreeSitterParser\n(CST traversal)"]
    ast["ChatFile AST\n(semantic model)"]

    chat --> grammar --> cst --> walker --> ast
Source text
    ↓ tree-sitter parse
Concrete Syntax Tree (CST) — green tree with all tokens
    ↓ tree_parsing (Rust)
ChatFile AST — typed model with validation-ready data

The CST preserves every character of the source (whitespace, punctuation, comments). The Rust tree-parsing modules walk the CST and extract semantic information into the typed model.

Error Recovery

Tree-sitter’s GLR algorithm provides automatic error recovery. When the parser encounters unexpected input, it:

  1. Inserts ERROR nodes in the CST
  2. Continues parsing the rest of the file
  3. Reports parse errors via the ErrorSink trait

This means the parser always produces a result — even for malformed files, it extracts as much structure as possible.

ParseOutcome

Individual parse functions return ParseOutcome<T>:

  • ParseOutcome::parsed(value) — successfully parsed
  • ParseOutcome::rejected() — could not parse this node (error already reported)

This allows the parser to skip individual malformed elements while continuing to parse the rest of the file.

Parser Equivalence

The 78-file reference corpus is the primary correctness guarantee:

cargo nextest run -p talkbank-parser-tests -E 'test(parser_equivalence)'

Each .cha file is its own test — nextest runs them in parallel and reports individual failures.

TreeSitterParser API

TreeSitterParser is the sole API handle for parsing. Callers create one instance and pass &TreeSitterParser to all parsing call sites. There is no trait abstraction — TreeSitterParser is a concrete type in the talkbank-parser crate.

use talkbank_parser::TreeSitterParser;

let parser = TreeSitterParser::new()?;

// Full-file parsing (methods on TreeSitterParser)
let chat_file = parser.parse_chat_file(&source, &errors)?;
let chat_file = parser.parse_chat_file_streaming(&source, callback)?;

// Fragment parsing (methods on TreeSitterParser)
let word = parser.parse_word_fragment(word_text, &errors);
let main_tier = parser.parse_main_tier_fragment(tier_text, &errors);

AST Structure

The resulting ChatFile AST has a recursive content structure:

flowchart TD
    cf["ChatFile"]
    hdr["Headers\n@Languages, @Participants,\n@ID, @Options"]
    utts["Utterances[]"]
    mt["MainTier\nspeaker + content"]
    dt["DependentTiers[]\n%mor, %gra, %pho, %sin, %wor"]
    uc["UtteranceContent\n24 variants"]
    leaf["Leaves\nWord | ReplacedWord | Separator"]
    group["Groups\nGroup | AnnotatedGroup |\nRetrace | PhoGroup | SinGroup | Quotation"]

    cf --> hdr & utts
    utts --> mt & dt
    mt --> uc
    uc --> leaf & group
    group -->|recurse| uc

Parser String Handling

The tree-sitter parser constructs owned model types (e.g., MorWord, GrammaticalRelation) directly from CST text. String-heavy types like PosCategory and MorStem use Arc<str> interning to avoid redundant allocations for repeated values. Short strings in model newtypes use SmolStr for inline storage up to 23 bytes.

CHAT Data Model

Status: Current Last updated: 2026-05-01 15:57 EDT

The talkbank-model crate defines the typed AST for CHAT files. Every other crate — parser, transform, CLAN, CLI, LSP, and the entire batchalign runtime — depends on it. This page describes the model itself, the three-level content hierarchy, the content-walker primitives, and the extract → infer → inject pattern that all NLP tasks follow.

ChatFile

The root type is ChatFile, representing a complete CHAT transcript:

pub struct ChatFile {
    pub lines: Vec<Line>,
    pub participants: IndexMap<SpeakerCode, Participant>,
    pub languages: LanguageCodes,
    pub options: ChatOptionFlags,
}

Each Line is either a Header or an Utterance. The full ownership tree:

flowchart TD
    chatfile["ChatFile\n(chat_file/core.rs)"]
    chatfile --> lines["lines: Vec&lt;Line&gt;"]
    chatfile --> participants["participants:\nIndexMap&lt;SpeakerCode, Participant&gt;"]
    chatfile --> languages["languages: LanguageCodes"]
    chatfile --> options["options: ChatOptionFlags"]

    lines --> header_line["Line::Header (Header)"]
    lines --> utt_line["Line::Utterance (Utterance)"]

    utt_line --> preceding["preceding_headers:\nSmallVec&lt;Header&gt;"]
    utt_line --> main["main: MainTier"]
    utt_line --> deptiers["dependent_tiers:\nVec&lt;DependentTier&gt;"]
    utt_line --> health["parse_health: ParseHealthState"]

    main --> speaker["speaker: SpeakerCode"]
    main --> tiercontent["content: TierContent"]
    tiercontent --> linkers["linkers: Vec&lt;Linker&gt;"]
    tiercontent --> uttcontent["utterance_content:\nVec&lt;UtteranceContent&gt;\n(24 variants)"]
    tiercontent --> terminator["terminator: Option&lt;Terminator&gt;"]
    tiercontent --> bullet["bullet: Option&lt;Bullet&gt;"]

The DependentTier enum has 25 variants: structured linguistic (Mor/Gra/Pho/Mod/Sin/Act/Cod/Wor), with-inline-bullets (Add/Com/Exp/Gpx/Int/Sit/Spa), text-only (Alt/Coh/Def/Eng/Err/Fac/Flo/Gls/Ort/Par/Tim), Phon-project (Modsyl/Phosyl/Phoaln), and UserDefined / Unsupported.

Three-Level Content Hierarchy

CHAT main-tier content is a tree with three nesting levels. Every content traversal must understand all three.

ChatFile
└── Line::Utterance
    └── MainTier
        └── TierContent
            ├── content: Vec<UtteranceContent>     ← Level 1
            │   ├── Word(Box<Word>)
            │   │   └── content: Vec<WordContent>  ← Level 3
            │   ├── OverlapPoint(OverlapPoint)
            │   ├── Group(Group)
            │   │   └── BracketedContent
            │   │       └── Vec<BracketedItem>     ← Level 2
            │   ├── PhoGroup, SinGroup, Quotation
            │   │   └── (same BracketedContent)
            │   ├── Retrace(Box<Retrace>)
            │   ├── Pause, Event, Separator, ...
            │   └── AnnotatedWord, AnnotatedGroup, ...
            ├── bullet: Option<Bullet>
            ├── linkers: Linkers
            └── terminator: Terminator

Level 1 — UtteranceContent (24 variants)

What you iterate when walking utterance.main.content.content.0:

CategoryVariants
WordsWord, AnnotatedWord, ReplacedWord
GroupsGroup, AnnotatedGroup, PhoGroup, SinGroup, Quotation
CA markersOverlapPoint, Separator
EventsEvent, AnnotatedEvent, OtherSpokenEvent
ActionsAnnotatedAction
TimingInternalBullet
Scope markersLongFeatureBegin/End, NonvocalBegin/End/Simple, UnderlineBegin/End
OtherFreecode, Pause

Critical rule: every match on UtteranceContent must explicitly list all 24 variants. No _ => catch-alls. Project policy: silent data loss when new variants are added is unacceptable.

Level 2 — BracketedItem (22 variants)

Content inside groups (<...>, ‹...›, 〔...〕, "..."). Accessed via group.content.content.0 (the double .content.content.0 is not a typo — Group.content is BracketedContent, which has .content: BracketedItems, which has .0: Vec<BracketedItem>).

BracketedItem mirrors UtteranceContent closely. Retrace content (<word word> [/], word [//]) is a dedicated Retrace variant at both levels, not hidden inside AnnotatedGroup. Groups can nest arbitrarily deep.

Level 3 — WordContent (11 variants)

Content inside a single word token, accessed via word.content:

VariantExample
Textplain text segment
Shortening(lo) omitted sound
OverlapPointbutt⌈er⌉ — overlap inside a word
CAElement↑ ↓ prosody markers
CADelimiter° ∆ paired delimiters
StressMarkerˈ ˌ
Lengthening:
SyllablePause^
CompoundMarker+ in ice+cream
UnderlineBegin/Endscope delimiters

Key insight: overlap markers can appear at all three levels — as standalone UtteranceContent::OverlapPoint (space-separated: ⌈ word ⌉), as BracketedItem::OverlapPoint (inside groups), or as WordContent::OverlapPoint (intra-word: butt⌈er⌉). Any traversal looking for overlap markers must check all three levels.

Annotated Wrappers and Replaced Words

Annotated<T>

Adds scoped annotations ([/], [* m], [= explanation], etc.) to any annotatable inner type:

pub struct Annotated<T> {
    pub inner: T,
    pub annotations: Vec<ContentAnnotation>,
    pub span: Span,
}

At Level 1: AnnotatedWord(Box<Annotated<Word>>), AnnotatedGroup(Annotated<Group>), AnnotatedEvent(Annotated<Event>), AnnotatedAction(Annotated<Action>). Same variants exist at Level 2.

ReplacedWord

Represents word [: replacement] — a surface form with a replacement:

pub struct ReplacedWord {
    pub word: Word,
    pub replacement: Replacement,
}
pub struct Replacement {
    pub words: Vec<Word>,
}

Convention when extracting words for NLP: use replacement words if non-empty, else the surface form (Wor and Mor domains both follow this, each with its own counts_for_tier filter).

Tier Domains

Different NLP tasks need different views of the same content. The TierDomain enum controls which words count for each tier and how groups are traversed:

DomainUsed bySkipsCounts separators?
Mor%mor / %gra generationRetrace groupsYes — , „ ‡ carry mor items (cm|cm, end|end, beg|beg)
Wor%wor generation, FANothingNo
Pho%pho alignmentPhoGroupNo
Sin%sin alignmentSinGroupNo

The content walker takes Option<TierDomain>: Some(domain) for domain-aware gating, None to recurse everything unconditionally.

Content Walkers

talkbank-model exports closure-based walkers. Two layers:

  • walk_content — generic, visits all content items (custom traversals).
  • walk_words / walk_words_mut — filtered to words / replaced words / separators, with domain-aware gating. The primary primitive.
use talkbank_model::alignment::helpers::{
    walk_words, walk_words_mut,
    WordItem, WordItemMut,
    TierDomain,
};

walk_words(content, Some(TierDomain::Wor), &mut |leaf| {
    match leaf {
        WordItem::Word(word) => { /* ... */ }
        WordItem::ReplacedWord(replaced) => { /* ... */ }
        WordItem::Separator(sep) => { /* ... */ }
    }
});
flowchart TD
    input["&[UtteranceContent]\n+ domain: Option&lt;TierDomain&gt;"]
    dispatch["Match variant\n(24 UtteranceContent variants)"]
    word["Word → emit WordItem::Word"]
    rw["ReplacedWord → emit WordItem::ReplacedWord"]
    sep["Separator → emit WordItem::Separator"]
    group["Group / AnnotatedGroup /\nPhoGroup / SinGroup / Quotation"]
    gate{"Domain\ngating"}
    skip["Skip\n(atomic unit)"]
    recurse["Recurse into\ngroup.content"]

    input --> dispatch
    dispatch --> word & rw & sep & group
    group --> gate
    gate -->|"Mor: skip retraces"| skip
    gate -->|"Pho/Sin: skip groups"| skip
    gate -->|"None: recurse all"| recurse
    recurse -->|back| dispatch

What walk_words does NOT visit

Only words and separators. Not OverlapPoint (any level), not CAElement within words, not events / pauses / actions, not internal bullets. For these, write a custom traversal — see talkbank-model/validation/utterance/overlap.rs for the reference pattern.

walk_overlap_points — overlap marker iterator

walk_overlap_points(content, &mut |visit| {
    // visit.point.kind, visit.point.index, visit.word_position
});

Visits every OverlapPoint at all three content levels with its word-position context. Used by the alignment pipeline (onset estimation) and the validator (pairing checks). For region-level analysis (pairing ⌈ with ⌉ by index), use extract_overlap_info() which builds OverlapRegion structs. For whole-file analysis, analyze_file_overlaps() matches top regions (⌈) with bottom regions (⌊) across utterances with 1:N support (used by E347 and chatter debug overlap-audit).

Extract → Infer → Inject Pattern

Every NLP task follows the same pattern:

┌─────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Parse  │────▶│ Extract  │────▶│  Infer   │────▶│  Inject  │
│  CHAT   │     │  Words   │     │ (Python) │     │ Results  │
└─────────┘     └──────────┘     └──────────┘     └──────────┘
   Rust            Rust              IPC             Rust
  1. Parse — parse_lenient() or parse_strict() → ChatFile AST.
  2. Extract — walk_words collects words / payloads.
  3. Infer — structured payload to Python worker via IPC; raw ML output back.
  4. Inject — walk_words_mut writes results back into the AST.
TaskExtract moduleInject moduleDomain
Morphosyntaxmorphosyntax.rs::collect_payloadsmorphosyntax.rs::injectMor
Utterance segmentationutseg.rs::collect_payloadsutseg.rs::applyWor
Translationtranslate.rs::collect_payloadstranslate.rs::inject_translationn/a (text)
Coreferencecoref.rs::collect_payloadscoref.rs::inject_corefWor
Forced alignmentfa/extraction.rs::collect_fa_wordsfa/injection.rsNone (all words)

Critical invariant: the extract and inject traversals must visit words in the same order. The walker guarantees this — same domain argument, same traversal order.

Validation

Beyond what the grammar enforces, validate_with_alignment() checks semantic constraints:

  • %mor alignment: number of MOR items matches alignable main-tier words.
  • %gra structure: sequential indices, ROOT checks, circular dependency.
  • Header consistency: @ID codes match @Participants.
  • Speaker references: all *SPEAKER: codes declared.

Five parallel alignment flows are computed against the main tier:

flowchart TD
    main["MainTier content"]
    walker["walk_words()\ncount alignable words"]

    subgraph "5 Parallel Alignment Flows"
        mor["%mor\ncustom logic\n(clitic handling)"]
        pho["%pho\npositional_align()\n(skip PhoGroup)"]
        sin["%sin\npositional_align()\n(skip SinGroup)"]
        wor["%wor\npositional_align()\n(LCS diff format)"]
        gra["%gra\nalign to %mor chunks\n(not main tier)"]
    end

    main --> walker
    walker --> mor & pho & sin & wor
    mor --> gra

For the alignment algorithms themselves, see Alignment.

Common Pitfalls

  1. “Consecutive” means in-order traversal, not adjacent array indices. When CHAT tools speak of “consecutive” or “sequential” items on the main tier, this always means document order via recursive traversal — accounting for groups (<...>), retrace groups (<...> [/]), quotations ("..."), and all other bracketed structures. Never check adjacency in the flat Vec<UtteranceContent> — use walk_words or equivalent in-order traversal.
  2. Missing intra-word content. Overlap markers, CA elements, and other markers can appear inside Word.content. Checking only UtteranceContent::OverlapPoint misses WordContent::OverlapPoint (e.g., butt⌈er⌉, a⌈nd).
  3. Missing annotated variants. UtteranceContent::AnnotatedWord and AnnotatedGroup wrap inner types in Annotated<T> and are easy to forget.
  4. BracketedContent access. Group.content → BracketedContent, with .content: BracketedItems, with .0: Vec<BracketedItem>.
  5. Separator counter sync (Mor domain). Tag-marker separators (, „ ‡) count as NLP words because they have %mor items. Any code counting words in the Mor domain must count these separators too.

Serialization

  • CHAT — WriteChat trait writes any model type back to CHAT format.
  • JSON — all model types implement Serialize/Deserialize. Format per the JSON Schema.
  • JSON Schema — derived via JsonSchema. Run cargo test --test generate_schema to regenerate schemas/chat-file/chat-file.schema.json.

Memory and Interning

String-heavy types (PosCategory, MorStem, MorFeature) use Arc<str> with a global interner — significant memory savings on large corpora where the same POS tags and lemmas appear thousands of times.

Collections that are typically small use SmallVec for inline storage:

  • SmallVec<[MorFeature; 4]> — features per word (usually 0–4).
  • SmallVec<[MorWord; 2]> — post-clitics (usually 0–1).

Transform Pipeline

Status: Current Last updated: 2026-03-24 00:01 EDT

The talkbank-transform crate provides high-level pipelines that compose parsing, validation, and serialization into reusable workflows.

Core Pipelines

Parse + Validate

The most common pipeline: parse a CHAT file and validate it.

use talkbank_transform::parse_and_validate;

let result = parse_and_validate(source, &parser, &error_collector);

This:

  1. Parses the source text into a ChatFile AST
  2. Runs validation (alignment checks, header consistency, etc.)
  3. Collects all errors and warnings into the ErrorSink

CHAT → JSON

Convert a CHAT file to its JSON representation:

use talkbank_transform::chat_to_json;

let json = chat_to_json(source, &parser)?;

The JSON follows the schema at schemas/chat-file/chat-file.schema.json.

JSON → CHAT

Convert JSON back to CHAT format:

use talkbank_transform::json_to_chat;

let chat = json_to_chat(json_str)?;

The roundtrip is lossless for well-formed input.

CHAT → CHAT (Normalize)

Parse and reserialize to normalize formatting:

use talkbank_transform::normalize;

let normalized = normalize(source, &parser)?;

Validation + Roundtrip Cache Lifecycle

The following diagram shows the full validation and roundtrip pipeline, including the cache layer:

flowchart TD
    file["CHAT file"]
    cache{"Cache\nhit?"}
    parse["Parse\n(tree-sitter → AST)"]
    validate["Validate\n(per-file → per-utterance →\nmain tier → dependent tiers)"]
    rt{"Roundtrip\nflag?"}
    ser1["Serialize → CHAT text"]
    reparse["Reparse CHAT text"]
    ser2["Serialize again"]
    cmp{"Two\nserializations\nmatch?"}
    store["Store in cache\n(SQLite)"]
    pass["Pass"]
    fail["Fail"]
    cached["Return cached result"]

    file --> cache
    cache -->|miss| parse --> validate --> rt
    cache -->|hit| cached
    rt -->|yes| ser1 --> reparse --> ser2 --> cmp
    rt -->|no| store --> pass
    cmp -->|yes| store
    cmp -->|no| fail

Streaming Parse

For large files or interactive use, the transform crate supports streaming parse where utterances are processed incrementally rather than loading the entire AST into memory.

Caching

The transform layer integrates with a file-system cache. Validation results are keyed by content hash, so unchanged files skip re-validation. Cache location is platform-specific: ~/Library/Caches/talkbank-chat/ (macOS), ~/.cache/talkbank-chat/ (Linux), %LocalAppData%\talkbank-chat\ (Windows).

Use --force to bypass the cache for specific paths.

Error Collection

Pipelines use the ErrorSink trait for error reporting. Callers can provide:

  • A collecting sink (gathers all diagnostics for batch output)
  • A printing sink (writes diagnostics to stderr in real-time)
  • A custom sink (for LSP diagnostics, JSON output, etc.)

XML Emitter

Status: Current Last updated: 2026-05-01 09:47 EDT

Purpose

crates/talkbank-transform/src/xml/ serialises a ChatFile<S> into TalkBank-XML matching the output of the legacy Java Chatter tool. It is the Rust-side replacement for Java’s CHAT → XML projection.

Scope:

  • Legacy / rare-use facility. The TalkBank project no longer publishes XML for download; CHAT is the canonical distribution format. The XML emitter exists to support rare legacy consumers that still need the XML projection — it is not a primary interchange path. New integrations should consume CHAT directly.
  • Emission only. XML ingest (XML → CHAT) is explicitly out of scope. The only historical consumer that ever needed XML → CHAT was Phon (via its PhonTalk plug-in, which invoked Java Chatter for the round-trip); Phon has since pivoted to reading CHAT directly. The other XML readers are all either dormant or migrated:
    • NLTK’s CHILDESCorpusReader is unmaintained and was always read-only.
    • langcog/childes-db has had no commits since September 2022.
    • TalkBankDB and the current TalkBank analysis stack read CHAT directly, not XML.
  • Phonetic tiers are permanently unsupported. %pho, %mod, %phosyl, %modsyl, %phoaln report XmlWriteError::PhoneticTierUnsupported. Phon has pivoted to CHAT-only interchange; no downstream consumer reads the rich <pg>/<pw>/<ph>/<cmph>/<ss> XML. Files carrying these tiers still parse, validate, and round-trip through CHAT unchanged — only the XML projection is declined.
  • Parity oracle. The goldens in resources/corpus/reference-xml/ (produced by Java Chatter against the reference CHAT corpus) are the parity target. All paired goldens pass structurally — full parity across every reference .cha file Java Chatter can emit. A small number of reference fixtures have no golden because Java can’t emit them at all: some use UD POS tags (propn) Java’s tree walker doesn’t recognize, and others declare @Media with a linkage type that the E544 validator catches before emission has a chance to run. Intentional divergences, not Rust gaps.

Module layout

The emitter is split across six submodules under xml/. Each file contributes an impl XmlEmitter { … } block plus any free helpers it owns; state lives on the single XmlEmitter struct defined in writer.rs.

flowchart TD
    entry["write_chat_xml<br/>(writer.rs)"]
    emitter["XmlEmitter struct<br/>owns quick-xml Writer<br/>+ next_utterance_id"]
    root["root.rs<br/>document / participants /<br/>body / utterance orchestration<br/>+ metadata helpers"]
    word["word.rs<br/>&lt;w&gt; / &lt;t&gt; / &lt;tagMarker&gt; /<br/>&lt;pause&gt; / &lt;g&gt; wrappers /<br/>word-internal markers /<br/>scoped annotations"]
    mor["mor.rs<br/>&lt;mor&gt; / &lt;mw&gt; / &lt;gra&gt; /<br/>UtteranceTiers collector /<br/>%mor feature serialization"]
    wor["wor.rs<br/>&lt;media&gt; / &lt;wor&gt; /<br/>&lt;internal-media&gt; /<br/>ms → seconds formatting"]
    deptier["deptier.rs<br/>&lt;a type=…&gt; side tiers<br/>(%act / %com / %exp /<br/>%gpx / %sit / %xLABEL)"]
    error["error.rs<br/>XmlWriteError variants"]

    entry --> emitter
    emitter --> root
    emitter --> word
    emitter --> mor
    emitter --> wor
    emitter --> deptier
    root -->|"terminator,<br/>separator"| word
    root -->|"collect_utterance_tiers,<br/>UtteranceTiers"| mor
    root -->|"&lt;media&gt;,<br/>&lt;wor&gt;"| wor
    root -->|"side tiers"| deptier
    word -->|"&lt;mor&gt; subtree<br/>inside &lt;w&gt;"| mor
    word -->|"&lt;mor&gt; subtree<br/>inside &lt;tagMarker&gt;"| mor
    wor -->|"%wor terminator<br/>label"| word
    error -.->|"errors"| entry
    error -.->|"errors"| root
    error -.->|"errors"| word
    error -.->|"errors"| mor
    error -.->|"errors"| wor
    error -.->|"errors"| deptier
FileRole
writer.rsXmlEmitter struct, namespace/version constants, write_chat_xml entry point, minimal-document unit test, escape_text helper
root.rsDocument / participants / body / utterance orchestration; root-element metadata helpers (corpus lookup, date/age/sex formatting, @Options flags, @Types projection, per-speaker extras)
word.rsAll word-level element shapes; word-internal marker walking; scoped-annotation dispatch; event / action emission
mor.rs%mor / %gra emission including post-clitic <mor-post>; UtteranceTiers aggregator
wor.rs%wor tier emission plus utterance-level <media>; format_seconds ms → seconds
deptier.rsText-content “side tiers” that render as <a type=…>text</a> (%act, %com, %exp, %gpx, %sit, %xLABEL)
error.rsXmlWriteError thiserror enum

Top-level data flow

sequenceDiagram
    participant Caller
    participant write_chat_xml as write_chat_xml<br/>(writer.rs)
    participant XmlEmitter as XmlEmitter
    participant emit_document as emit_document<br/>(root.rs)
    participant emit_body as emit_body<br/>(root.rs)
    participant emit_utterance as emit_utterance<br/>(root.rs)

    Caller->>write_chat_xml: ChatFile&lt;S&gt;
    write_chat_xml->>XmlEmitter: new()
    write_chat_xml->>emit_document: emit_document(file)
    emit_document->>emit_document: emit &lt;?xml?&gt; + &lt;CHAT&gt; attrs
    emit_document->>emit_document: emit_participants(file)
    emit_document->>emit_body: emit_body(file)
    loop each Line
        alt Line::Header
            emit_body->>emit_body: emit_header_if_body(header)
        else Line::Utterance
            emit_body->>emit_utterance: emit_utterance(utterance)
        end
    end
    write_chat_xml->>XmlEmitter: finish() → String
    XmlEmitter-->>Caller: Ok(String)

Utterance emission in detail

emit_utterance is the most complex orchestrator: it walks the main tier in parallel with two cursors into the dependent tiers.

flowchart TD
    start([emit_utterance])
    preHdr[emit pre-begin<br/>headers]
    collect["collect_utterance_tiers<br/>→ UtteranceTiers {<br/>mor, gra, wor, sin, side_tiers }"]
    openU["&lt;u who=… uID=…&gt;"]
    linkers["emit_linker × N<br/>(utterance.main.content.linkers)"]
    walk{"walk<br/>utterance.main.content.content"}
    term{"terminator<br/>present?"}
    emitTerm["emit_terminator<br/>(word.rs)"]
    missing["&lt;t type='missing<br/>CA terminator'/&gt;"]
    media{"main bullet<br/>present?"}
    emitMedia["emit_utterance_media<br/>(wor.rs)"]
    wor{"%wor tier<br/>present?"}
    emitWor["emit_wor<br/>(wor.rs)"]
    side{"side tiers<br/>non-empty?"}
    emitSide["emit_side_tiers<br/>(deptier.rs)"]
    closeU["&lt;/u&gt;"]
    done([return])

    start --> preHdr --> collect --> openU --> linkers --> walk
    walk -->|"Word / AnnotatedWord /<br/>ReplacedWord / AnnotatedGroup /<br/>Separator / Pause / Retrace /<br/>Event / AnnotatedAction /<br/>OverlapPoint"| walk
    walk --> term
    term -->|yes| emitTerm
    term -->|no| missing
    emitTerm --> media
    missing --> media
    media -->|yes| emitMedia
    media -->|no| wor
    emitMedia --> wor
    wor -->|yes| emitWor
    wor -->|no| side
    emitWor --> side
    side -->|yes| emitSide
    side -->|no| closeU
    emitSide --> closeU
    closeU --> done

The TierCursors invariant

Walking the main tier requires tracking three independent cursors into the %mor / %gra / %sin tiers. This separation is the single most important correctness invariant in the emitter; a merged cursor silently drifts on any utterance containing a clitic chain, an untranscribed placeholder, or a sign-language item.

A TierCursors helper in mor.rs owns the three cursors and provides mor_index() / gra_chunk() / sin_index() accessors plus consume_mor(post_clitics_len) / consume_sin() / advance_bulk(mor, gra) advance methods. Every content-arm in emit_utterance runs a fixed template: look up partners at the current cursor positions, emit, call consume_*. The advance math has exactly one home.

CursorIndexes intoAdvances by
mormor_tier.items (one Mor per main-tier word)1 per alignable word
gragra.relations (1-based <gra index=…/>)1 + post_clitics.len() per Mor
sinsin_tier.items (one SinItem per sin-countable word)1 per sin-countable word

A Mor item like pron|what-Int-S1~aux|be-Fin-Ind-Pres-S3 is one entry in mor_tier.items but contributes two %gra edges — one for the main <mw>, one for each <mor-post><mw/></mor-post>. So mor and gra cursors advance at different rates.

%sin uses a separate counting predicate than %mor. The model’s counts_for_tier(word, TierDomain) function encodes the differences:

  • TierDomain::Mor excludes nonwords (&~), fillers (&-), phonological fragments (&+), and untranscribed placeholders (xxx, yyy, www).
  • TierDomain::Sin includes everything that was phonologically or gesturally produced — fragments and untranscribed do participate. A gesture can accompany an unintelligible vocalisation.

Because the predicates diverge, the sin cursor advances on its own schedule. For *CHI: mommy xxx . %sin: g:point 0 . the xxx word consumes a %sin item but not a %mor item.

Four main-tier content variants delegate cursor arithmetic through their emitters: emit_replaced_word and emit_annotated_group return (mor_used, gra_used) tuples consumed via cursors.advance_bulk(mor_used, gra_used); emit_word and emit_annotated_word call cursors.consume_mor(post_count) inline.

Why cursor-based, not AlignmentSet-based?

talkbank-model’s AlignmentSet (Utterance.alignments) holds pre-computed MorAlignmentPair / SinAlignmentPair / etc. — the same main-word-index ↔ target-tier-index mapping the emitter computes on-the-fly. Why not use it directly?

The XML emitter accepts ChatFile<S: ValidationState> for any S. When called on a ChatFile<NotValidated>, compute_alignments has never run and Utterance.alignments is None. Rather than force callers to validate first, or risk panics on unvalidated input, the emitter recomputes what it needs via the cursor walk.

The cursor walk is equivalent to the model’s alignment output for every reference-corpus input; it only diverges on malformed files that the model’s alignment would also flag. The cursors stay as local emitter state, and the alignment module stays a separate, optional layer.

%sin → <sg><w><sw/></w></sg> emission

When a %sin tier is present and the current word counts for TierDomain::Sin, the emitter wraps the <w> element (and its nested <mor> subtree if any) in a <sg> (sign group) with a <sw> (sign word) sibling:

<sg><w>what<mor>...</mor></w><sw>0</sw></sg>

SinItem::Token(text) renders as <sw>text</sw>; SinItem::SinGroup(…) joins its gesture tokens with spaces. The emission is the entirety of XmlEmitter::emit_sin_word; everything else is just the <sg>…</sg> wrap in emit_utterance’s Word arm.

@Media linkage and timing evidence (E544)

Validation fires E544 before XML emission when an unqualified @Media header (status-less) claims linkage but the transcript carries no timing evidence (no main-tier bullets, no positional %wor sidecar). This is a validator-level rule (lives in crates/talkbank-model/src/model/file/chat_file/validate.rs check_media_linkage_has_timing), not an emitter rule — it runs during ChatFile::validate and blocks downstream emission on validation-gated entry points. See resources/spec/errors/E544_media_linkage_without_timing.md.

The emitter itself doesn’t care about bullet presence; the former Java-Chatter emitter imposed this check as a parser-level semantic failure, and Rust implements it in the validator instead.

Post-clitic emission

flowchart LR
    mor["&lt;mor type='mor'&gt;"]
    mw["&lt;mw&gt;…&lt;/mw&gt;<br/>(main MorWord)"]
    gra["&lt;gra type='gra'<br/>index=N head=… relation=…/&gt;"]
    post["&lt;mor-post&gt;"]
    pmw["&lt;mw&gt;…&lt;/mw&gt;<br/>(post-clitic MorWord)"]
    pgra["&lt;gra type='gra'<br/>index=N+1 head=… relation=…/&gt;"]
    postEnd["&lt;/mor-post&gt;"]
    endMor["&lt;/mor&gt;"]

    mor --> mw --> gra --> post --> pmw --> pgra --> postEnd --> endMor

Each post-clitic gets its own <mor-post> wrapper containing one <mw> plus the next <gra> index. Multiple post-clitics emit sequentially.

Emitter / parser / model boundary

The emitter generally defers to the Rust model’s canonical predicates rather than inventing output-side rules. Four cases are exceptions where the emitter bridges a parser-vs-Java semantic disagreement at the output boundary. All four are legitimate divergences, not regressions: the Rust model is correct, Java Chatter is frozen at a pre-evolution CHAT snapshot, and the emitter’s bridges are the right place to reconcile the output shape.

CA intonation contour terminators

Rust parses ⇗, ↗, →, ↘, ⇘ at the end of an utterance as Terminator::CaRisingToHigh etc. Java Chatter classifies them as separators followed by an implicit “missing CA terminator”. The emitter splits a pitch-contour terminator into two sibling elements:

<s type="rising to high"/>
<t type="missing CA terminator"/>

See ca_terminator_separator_label in word.rs. If the Rust parser ever migrates to classify these as separators, the emitter’s bridge becomes dead and should be removed.

CAOmission as whole-word shortening

(parens) (a fully-parenthesised word) parses to WordCategory::CAOmission. Java emits <w><shortening>parens</shortening></w> — a <shortening> wrapper around the word body with no type="omission" attribute. The 0word syntax (true omission) gets <w type="omission">word</w> with no shortening wrapper.

The emitter branches on CAOmission and opens a <shortening> wrapper around emit_word_contents. word_category_attr returns None for CAOmission so no type="omission" attribute is emitted.

Leading overlap-point hoisting

Rust parses ⌈°overlapping+soft⌉° as a single word whose WordContent vector starts with a TopOverlapBegin marker. Java Chatter keeps the leading ⌈ as a top-level sibling of <w> but keeps the trailing ⌉ inside. The emitter hoists the prefix of leading WordContent::OverlapPoint items out before opening <w>, and emit_word_contents skips them during the content walk.

xxx / yyy / www case-sensitivity

The model’s word.untranscribed() helper is case-insensitive — it treats XXX and xxx identically as “unintelligible” to protect downstream Stanza/MOR pipelines from spurious uppercase entries. The XML schema’s untranscribed attribute, however, attaches only to the strictly lowercase placeholders. The emitter uses a local untranscribed_attribute_for_xml helper that does the case-sensitive check at the output boundary.

Both behaviours are deliberate and stay: the model’s case-insensitive helper is a Stanza/MOR correctness fix, and the emitter’s case-sensitive gate matches the XML schema contract.

Reserving element boundaries: single state holder

XmlEmitter owns a quick_xml::Writer<Vec<u8>> and a running next_utterance_id: u32 counter. Every emission helper writes through that single writer so indentation, escaping, and the document-order contract are centrally enforced.

Every BytesText emission routes through escape_text (in writer.rs) which uses quick_xml::escape::partial_escape to escape only <, >, &. Apostrophes and double quotes pass through literally, matching Java Chatter’s output and avoiding entity-decode issues that would otherwise split text at &apos; boundaries during structural comparison.

Testing

Two complementary test surfaces:

  1. Unit tests in xml/writer.rs (minimal document smoke) and xml/wor.rs (format_seconds fractional padding) exercise internal helpers directly.

  2. Golden-XML parity harness at crates/talkbank-parser-tests/tests/xml_golden.rs. Runs one parametrised test per file in resources/corpus/reference-xml/**/*.xml, parses both emitted and golden XML via quick-xml, and diffs event streams with whitespace and attribute-order normalisation. Comparator lives in crates/talkbank-parser-tests/tests/xml_support/mod.rs.

The harness diagnostic surfaces the first divergence as actual: … vs expected: …. To debug further, temporary dump helpers (write the emitted XML to /tmp/emitted.xml and side-by-side diff against the golden) are the quickest path; add them as #[ignore]d tests in crates/talkbank-parser-tests/tests/xml_dump.rs when needed and delete after the divergence is resolved.

  • docs/reference-xml-coverage-gaps.md — why some reference .cha files have no paired XML golden. The permanent exclusions are UD-POS files Java can’t walk + @Media-without-timing files E544 blocks at validation.
  • docs/rust-vs-java-chatter-regressions.md — audit of the Rust-vs-Java parser-level differences that the emitter bridges.
  • docs/talkbank-xml-consumers-2026-04.md — downstream XML consumers and the Phon-pivot context.
  • resources/spec/errors/E544_media_linkage_without_timing.md — the @Media bullet-existence validator that runs before emission.

Staged features

The emitter reports XmlWriteError::FeatureNotImplemented for CHAT constructs that have a known XML shape but haven’t been wired in yet. With all paired reference-XML goldens passing, any new staged feature that lands will be triggered by a file added to the reference corpus that exercises it. When that happens:

  1. Run cargo nextest run -p talkbank-parser-tests --test xml_golden and read the failure message.
  2. Find the Java Chatter output for the construct in the paired golden.
  3. Add a match arm in the appropriate submodule (word.rs::emit_scoped_annotation, deptier.rs::emit_side_tier, word.rs::ca_delimiter_label, etc.) with a short comment explaining the mapping.
  4. If the construct changes %mor / %gra cursor accounting, update emit_utterance in root.rs, not individual callers.

Permanently-unsupported tiers (%pho, %mod, %phosyl, %modsyl, %phoaln) use XmlWriteError::PhoneticTierUnsupported and are not staged for future work — Phon’s pivot to CHAT-only interchange removed the downstream need.

Errors — talkbank-tools (CHAT core)

Status: Current Last updated: 2026-05-01 17:07 EDT

The error infrastructure used across all CHAT-core crates (talkbank-model, talkbank-parser, talkbank-transform, clan-core, chatter-cli, chatter-lsp). Defined in the errors module of talkbank-model.

For the Batchalign-runtime side (ML / IPC / network / ASR / worker crashes), see batchalign-errors. For the Python ↔ Rust boundary, see python-rust-errors. For the diagnostic UX standard that applies workspace-wide, see error-diagnostics-ux.

Core Types

ParseError

Every diagnostic is a ParseError:

pub struct ParseError {
    pub code: ErrorCode,
    pub severity: Severity,
    pub location: SourceLocation,
    pub context: ErrorContext,
    pub message: String,
}

ErrorCode

Error codes follow a structured numbering scheme:

RangeCategory
E1xxEncoding
E2xxFile structure
E3xxHeaders
E4xxMain tier
E5xxWords and content
E6xxSpeakers
E7xxDependent tiers
W1xx–WxxxWarnings (same categories)
flowchart LR
    subgraph "Parser layer\n(parser.parse_chat_file())"
        E1["E1xx\nEncoding\n(BOM, charset)"]
        E2["E2xx\nFile structure\n(Begin/End, line format)"]
        E3["E3xx\nHeaders\n(format, required fields,\nparticipant resolution)"]
        E4["E4xx\nMain tier\n(speaker, content,\nterminator, postcodes)"]
        E5["E5xx\nWords and content\n(word syntax, events,\noverlap markers)"]
    end

    subgraph "Validation layer\n(validate_with_alignment)"
        E6["E6xx\nSpeakers\n(undeclared, unused,\nID consistency)"]
        E7["E7xx\nDependent tiers\n(alignment, GRA indices,\norphaned tiers)"]
    end

    W["Wxxx\nWarnings\n(same categories,\nnon-fatal)"]

    E1 ~~~ E2 ~~~ E3 ~~~ E4 ~~~ E5
    E6 ~~~ E7

The full error code reference is generated in book/src/operations/errors/ at the repository root.

Severity

  • Error — must be fixed; indicates invalid CHAT.
  • Warning — should be fixed; indicates questionable but parseable CHAT.

SourceLocation and Span

Byte offsets into the source text:

#![allow(unused)]
fn main() {
pub struct SourceLocation { pub start: usize, pub end: usize }
pub struct Span { pub start: usize, pub end: usize }
}

ErrorContext

Carries the source fragment around the error location:

pub struct ErrorContext {
    pub source_fragment: String,
    pub byte_range: Range<usize>,
    pub node_kind: String,
}

ErrorSink Trait

The central abstraction for error reporting:

flowchart LR
    val["Validator / Parser"]
    pe["ParseError\ncode + severity +\nlocation + message"]
    sink["ErrorSink trait\n.report()"]
    vec["ErrorCollector\ncollect to Vec"]
    chan["ChannelErrorSink\ncrossbeam channel\n(feature = channels)"]
    asyncchan["AsyncChannelErrorSink\ntokio mpsc"]
    cfg["ConfigurableErrorSink\nseverity gating"]
    null["NullErrorSink\nno-op"]

    val --> pe --> sink
    sink --> vec & chan & asyncchan & cfg & null
pub trait ErrorSink {
    fn report(&self, error: ParseError);
}

All parsing and validation functions accept &impl ErrorSink rather than returning errors directly. This allows:

  • Collecting all errors (for batch processing).
  • Printing errors in real-time (for interactive use).
  • Filtering by severity or code.
  • Counting errors without storing them.

The trait uses &self (not &mut self) so it can be shared across threads. Implementations typically use interior mutability (Mutex<Vec<ParseError>>).

ErrorCollector is the in-memory collector in errors/collectors.rs. The stored-diagnostics role is explicit in both code and docs.

Module layout in talkbank-model:

  • errors/error_sink.rs — trait and lightweight forwarding sinks.
  • errors/collectors.rs — in-memory collectors and counters.
  • errors/async_channel_sink.rs — Tokio-channel streaming.
  • errors/configurable_sink.rs, errors/offset_adjusting_sink.rs, errors/tee_sink.rs — adapters.

ChannelErrorSink is opt-in behind the channels feature so the default talkbank-model dependency does not pull in crossbeam just to own the core error trait and in-memory collectors.

Two Error Layers

Errors are detected at two layers. This distinction matters for spec testing.

  1. Parser layer — structural errors caught during parser.parse_chat_file(). These prevent the file from being fully parsed (missing @Begin, invalid syntax). Parser-layer specs test that parser.parse_chat_file() returns Err.

  2. Validation layer — semantic errors caught by validate_with_alignment() after a successful parse. The file parsed correctly but violates constraints (%mor alignment mismatch, undeclared speakers). Validation-layer specs test that validation reports specific error codes.

Adding a New Error Code

  1. Add the variant to ErrorCode in crates/talkbank-model/src/errors/codes/error_code.rs with a #[code("Exxx")] attribute.
  2. Create a spec file in resources/spec/errors/Exxx-description.md following the existing template.
  3. Construct ParseError::new(ErrorCode::YourVariant, ...) at the detection site in the parser or validator.
  4. Run just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs to regenerate test fixtures from specs.
  5. Run bazel build //... && bazel test //... to confirm the gate passes.

Crate Reference

Status: Current Last updated: 2026-03-24 01:32 EDT

Summary of every crate in the talkbank-tools workspace.

Core Crates

talkbank-model

The typed data model for CHAT files. Defines ChatFile, Utterance, DependentTier, MorTier, GraTier, and all other AST types. Includes validation logic, the WriteChat trait for CHAT serialization, serde support for JSON, and JsonSchema derivations. Also owns error types (ParseError, ErrorSink trait, Span, SourceLocation), diagnostic infrastructure, and ParseValidateOptions. Provides a closure-based content walker (walk_words / walk_words_mut) that centralizes recursive traversal of UtteranceContent and BracketedItem with domain-aware group gating.

talkbank-derive

Procedural macros for the model crate (SemanticEq, SemanticDiff, SpanShift, ValidationTagged, and the error_code_enum macro).

talkbank-parser

The sole parser. Wraps the tree-sitter C parser and converts the concrete syntax tree (CST) into ChatFile model types. Provides error recovery via tree-sitter’s GLR algorithm. Used by the LSP, CLI, and batchalign.

talkbank-transform

High-level pipelines: parse+validate, CHAT-to-JSON, JSON-to-CHAT, normalization. Integrates the validation cache, JSON schema validation, and parallel directory validation.

Application Crates

clan-core

CLAN analysis commands (FREQ, MLU, etc.), transforms (FLO, etc.), and format converters. Each command implements the CommandOutput trait with typed results. The crate also owns higher-level library integration seams such as UtteranceRange, DiscoveredChatFiles, service::AnalysisCommandName, service::AnalysisService / service::AnalysisRequest, and the service::AnalysisOptions / service::AnalysisRequestBuilder layer so other crates can execute CLAN analyses without reimplementing directory walking, range parsing, raw command-name parsing, JSON output policy, or command-default selection.

chatter-cli

The chatter CLI binary: validate, normalize, to-json, CLAN command dispatch, and corpus management.

chatter-lsp

Language Server Protocol server with tree-sitter incremental parsing, real-time diagnostics, and semantic highlighting.

send2clan-sys

FFI bindings for sending files to the CLAN application (macOS Apple Events, Windows WM_APP).

chatter-gui

Desktop validation app (Tauri v2, React). Mandates TUI parity with the CLI.

Test Crates

talkbank-parser-tests

Parser tests. Runs the parser on each file in the 78-file reference corpus and validates results (each .cha file is its own #[test] via rstest, nextest-compatible). Also runs spec-generated tests, roundtrip tests, and property tests.

Repository Architecture and Boundaries

Status: Current Last updated: 2026-05-01 05:30 EDT

Top-level layout

spec/                     canonical syntax and error spec source
spec/tools/               deterministic generators + validators (separate Cargo workspace)
grammar/                  tree-sitter grammar source + generated parser artifacts
crates/                   all Rust crates (root Cargo workspace)
  talkbank-model/         data model, validation, alignment, errors, parser API trait
  talkbank-derive/        proc macros (SemanticEq, SpanShift, ValidationTagged, error_code_enum)
  talkbank-parser/        canonical parser (tree-sitter)
  talkbank-parser-re2c/   alternate parser (specification oracle, opt-in batch parser)
  talkbank-parser-tests/  parser equivalence and roundtrip tests
  talkbank-transform/     pipelines, CHAT↔JSON, caching, parallel validation
  clan-core/          CLAN analysis commands and format converters
  chatter-cli/           the `chatter` CLI binary
  chatter-lsp/           LSP server
  send2clan-sys/          C FFI to the legacy CLAN app
  batchalign/             Batchalign runtime: CLI, axum server, dispatch, FA, morphosyntax, Rev.AI client
  batchalign-types/       Batchalign shared domain + worker IPC types
  batchalign-pyo3/        PyO3 bridge — builds the `batchalign_core` Python extension
batchalign/               Batchalign Python worker code (ML inference hosting only)
apps/                     desktop apps (Tauri v2 + React): chatter-gui, dashboard-desktop
apps/batchalign/cli-web-statuspage/                 React dashboard for the Batchalign server
apps/vscode-extension/                   VS Code extension (TypeScript)
corpus/                   reference corpus (must pass 100%)
schema/                   JSON Schema for ChatFile AST
tests/                    workspace-level integration tests and fixtures
fuzz/                     fuzz targets (separate Cargo workspace)
book/                     mdBook documentation source
docs/                     release-contract policy documents and the auto-generated error catalog

Architectural principles

  1. Clear boundaries between specification, generation, runtime logic, and documentation.
  2. Generated artifacts and hand-authored code are kept separate with hard guardrails — parser.c, node-types.json, generated tests and error-doc artifacts are never edited by hand.
  3. Each crate has a single clear responsibility.
  4. Entry-point docs guide new contributors to authoritative references quickly.

Canonical ownership rules

  • spec/ owns the language intent and accepted examples — what CHAT means.
  • grammar/ owns tokenization and CST shape only, not semantic validation policy.
  • talkbank-model owns semantic validity, serialization invariants, error types, and parser API contracts.
  • talkbank-transform owns pipelines and JSON schema validation.
  • batchalign (the crate) owns the Batchalign-specific runtime, server, and ML-pipeline orchestration; it consumes the talkbank-* crates above.

Dependency direction rules

  1. spec does not depend on runtime crates.
  2. grammar is consumed by parser crates, not vice versa.
  3. talkbank-model is dependency-minimal and stable; all other talkbank-* crates depend on it.
  4. The batchalign crate consumes the talkbank-* core crates (model, parser, transform, etc.) but never reimplements CHAT primitives.
  5. CLI / LSP / desktop apps depend on stable internal APIs, never directly on unstable internals of other crates.
  6. Generator tools may read specs and grammar metadata but do not become runtime dependencies.

Acceptance criteria

  • Every top-level directory has a clear purpose statement.
  • No crate depends on internal modules outside declared boundaries.
  • No generated artifact is edited manually.
  • New contributors can identify authoritative docs in less than five minutes.

Grammar System and Token Governance

Status: Current Last updated: 2026-03-24 00:01 EDT

Current Reality

grammar/grammar.js encodes substantial implicit language knowledge directly in regex exclusions, reserved symbol lists, and leniency decisions. Example areas:

  • word segment forbidden start/rest classes,
  • CA delimiter/element symbol groups,
  • event segment exclusions,
  • hand-maintained coupling between comments and token rules.

This is currently powerful but fragile.

Primary Failure Modes

  1. New symbolic token added in one place but not in exclusion sets.
  2. Parser behavior changes silently due to regex class edits.
  3. Generated node types drift from assumptions in spec tooling.
  4. Lenient parsing choices become undocumented policy.

Canonical Target Design

Implement a generated symbol registry as the single source of token constraints.

Proposed Registry Artifacts

  • resources/spec/symbols/symbol_registry.yaml (human-authored intent):
    • symbol string
    • category (delimiter, continuation, overlap, punctuation, etc.)
    • contexts where reserved/allowed
    • parse role and precedence notes
  • Generated outputs:
    • grammar/src/generated_symbol_sets.js
    • crates/talkbank-model/src/generated/symbol_sets.rs
    • crates/spec/talkbank-spec-testgen/src/generated/symbol_sets.rs
    • docs: Symbol Registry

Grammar Refactor Requirements

  1. Replace large manual regex strings with generated character classes.
  2. Keep final grammar readable by preserving semantic names in generated constants.
  3. Distinguish clearly between:
  • syntax permissiveness,
  • semantic validation restrictions.
  1. Add comments only for design rationale, not for duplicating manual references.

Node Type Drift Controls

  • Enforce regeneration and consistency checks:
    • grammar source change must regenerate parser and node types,
    • node type constants consumed by spec/tools and parser code must compile,
    • CI fails if generated files differ from committed state.

Leniency Policy

Explicitly classify every lenient parse behavior:

  • Parse-lenient + validate-strict.
  • Parse-lenient + validate-warning.
  • Parse-strict (hard fail).

Document this matrix in the Leniency Policy.

Grammar Test Strategy

  1. Keep corpus tests generated from spec/constructs.
  2. Add targeted hand-authored edge tests for symbol boundary interactions.
  3. Add mutation-style tests for forbidden-character regressions.
  4. Add parser equivalence tests for tokenizer-sensitive cases.

Acceptance Criteria

  • No manual reserved-symbol duplication in grammar.js.
  • Symbol registry is generated to all required consumers.
  • Grammar modifications cannot land with stale generated artifacts.
  • Every special token category has explicit policy documentation.

Parser, Model, and API Contracts

Status: Current Last updated: 2026-05-01 05:35 EDT

Single-handle parser API

talkbank-parser provides TreeSitterParser as the canonical API handle for all parsing — full-file and fragment methods live directly on the struct. Callers create one instance and pass &TreeSitterParser everywhere. The alternate talkbank-parser-re2c is opt-in (specification oracle and high-throughput batch parsing) and produces the same ChatFile model.

Contract for Batchalign

The Batchalign runtime (the batchalign crate) consumes these guarantees from the talkbank-* core crates:

  • parsing produces a typed ChatFile or an explicit parse-status signal
  • parse-health taint is visible to alignment consumers
  • alignment helpers operate on semantic model types, not raw text hacks
  • recovery never fabricates valid-looking placeholder semantics for malformed input

The parser/model boundary stays honest enough for downstream workflows — align, compare, benchmark, morphotagging — to make their own validity decisions.

Canonical Contract Model

Public Contract Layers

  1. Parse API Contract:
  • stable function signatures,
  • deterministic parse result envelope,
  • clear partial-success semantics.
  1. Semantic Model Contract:
  • stable core model fields,
  • explicit unstable/internal fields policy.
  1. Diagnostic Contract:
  • stable error code IDs and severity semantics,
  • best-effort message text compatibility.
  1. Serialization Contract:
  • deterministic output constraints,
  • normalized formatting policy.

Required Types

  • ParseOutcome<T>
    • value: T | omitted-by-status
    • diagnostics: Vec<Diagnostic>
    • status: Success | Partial | Failed
  • Diagnostic
    • code, severity, category, message, location, context, suggestion

Parser Role

  • talkbank-parser: the sole parser, used by CLI/LSP/API/batchalign. TreeSitterParser is the only API handle — callers create one and pass &TreeSitterParser everywhere.
  • Tree-sitter GLR provides error recovery; the Rust traversal code converts CST to typed model.
  • Full-file methods: parser.parse_chat_file(), parser.parse_chat_file_streaming().
  • Fragment methods: parser.parse_word_fragment(), parser.parse_main_tier_fragment(), etc.

Invariants

  1. Parsing with offset must shift all spans consistently.
  2. Parse-level and validation-level diagnostics must remain distinguishable.
  3. Serialization should preserve semantic equivalence and documented formatting rules.
  4. Roundtrip behavior must be testable per parser implementation.
  5. Parser functions that accept ErrorSink should not return Option<T> for fallible parse state.

API Versioning Policy (Pre-Release but Strict)

  • Even before 1.0, publish internal CONTRACT_LEVELS.md:
    • Stable-for-integrators
    • Stable-internal
    • Experimental
  • Mark every public function/type by contract level.

Acceptance Criteria

  • Single canonical parse outcome envelope exposed for integrators.
  • Parser implementations conform to shared contract tests.
  • Contract-level annotations exist for all public API surfaces.
  • Documentation for parse/validate/serialize lifecycle is centralized and current.

Recovery Contract: No Fabricated Semantic Values

The parser contract must forbid sentinel semantic values during error recovery.

Disallowed recovery behavior:

  • returning arbitrary enum variants as fallback for unknown/missing nodes,
  • returning empty strings as stand-ins for required fields,
  • constructing fake words/chunks like "missing", "error", or other placeholders.

Required recovery behavior:

  1. Emit structured diagnostic with precise span and expected node kind.
  2. Return an explicit parse-status signal (Partial/Failed) through ParseOutcome.
  3. Omit invalid semantic node OR store it in explicit recovery metadata, never as a valid semantic value.

Current enforcement:

  • CI guardrail script tracks and blocks introduction of new ErrorSink + Option signatures.
  • See scripts/check-errorsink-option-signatures.sh and scripts/errorsink_option_allowlist.txt.

Rationale:

  • fabricated semantic values create secondary, misleading diagnostics against synthetic data,
  • downstream tools cannot distinguish real user content from parser-generated placeholders,
  • equivalence and regression tests become noisy and non-actionable.

For batchalign, this is especially important because alignment workflows must be able to tell the difference between:

  • a malformed input that should taint or block alignment
  • a recoverable input where raw text can be preserved
  • a clean input that should proceed through the align/compare pipeline

String Storage Policy

The model uses three string storage strategies:

  • Arc<str> interning (interned_newtype!): For high-frequency repeated values (POS tags, stems, speaker codes). Global interner avoids redundant allocations.
  • SmolStr (string_newtype!): For short strings (median 10–15 chars) that benefit from inline storage. O(1) clone, no heap allocation for strings ≤23 bytes.
  • String: Only for utility types outside the core model (e.g., semantic_diff/).

Parser Backends

Status: Current Last updated: 2026-05-01 05:19 EDT

TalkBank has two CHAT parser implementations. Both implement the ChatParser trait and produce identical ChatFile model types.

TreeSitterParser (default)

  • Crate: talkbank-parser
  • Technology: tree-sitter GLR parser
  • Grammar: grammar/grammar.js → generated C parser
  • Strengths: Incremental reparsing (LSP), robust error recovery (GLR), CST-level diagnostics
  • Weaknesses: Slower on batch workloads, !Send + !Sync (one parser per thread)

Used by the LSP, the default CLI, and all production validation.

Re2cParser

  • Crate: talkbank-parser-re2c
  • Technology: re2c DFA lexer + chumsky parser combinators
  • Grammar: Translated from grammar.js rules → re2c conditions + chumsky combinators
  • Strengths: 4-8x faster, Send + Sync, zero constructor cost, specification oracle
  • Weaknesses: No incremental reparsing, Box::leak memory strategy

Used for batch validation, parser parity testing, and performance benchmarking.

CLI Usage

# Default: tree-sitter
chatter validate corpus/

# Use re2c for faster batch validation
chatter validate --parser re2c corpus/

# Roundtrip with re2c
chatter validate --parser re2c --roundtrip corpus/

The --parser flag accepts tree-sitter (default) or re2c. Cache entries are parser-specific — switching parsers does not invalidate the other’s cache.

Parity Status

Both parsers produce SemanticEq-identical output on the 87-file reference corpus (100% match). On the ~100k-file wild corpus, parity is ~98.7%.

Error Detection

MetricValue
Specs tested140
Both detect error140/140 (100%)
Same error code79/140 (56.4%)
Different code, both detect61/140 (43.6%)
Re2c silent (misses error)0

The 61 code mismatches come from architectural differences, not bugs. Both parsers report actionable diagnostics for all 140 testable error specs.

Performance

BenchmarkTreeSitterRe2cSpeedup
Small file (13 lines)44 µs9.6 µs4.6x
Medium file (dependent tiers)69 µs9.4 µs7.3x
Large file (complex)7,734 µs970 µs8.0x
Batch (35 files)21.7 ms3.0 ms7.2x

Run benchmarks: cargo bench -p talkbank-parser-re2c --bench parse_comparison

When to Use Which

Use CaseRecommended ParserWhy
LSP / editor integrationtree-sitterIncremental reparsing
Batch validation (>100 files)re2c4-8x faster
CI validationEitherBoth correct; re2c saves CI time
Error diagnostics (user-facing)tree-sitterMore specific E3xx codes
Parser parity testingBothRe2c is the specification oracle
Profiling / benchmarkingre2cDFA lexer gives a performance floor

Shared Model Infrastructure

Both parsers convert to the same talkbank_model::ChatFile type and share post-hoc promotion logic:

  • TierContent::extract_terminal_bullet() — trailing InternalBullet → utterance bullet
  • TierContent::resolve_ca_terminator() — trailing CA arrow → terminator
  • parse_bullet_node_timestamps() — structured bullet CST → (start_ms, end_ms)

Detailed Parity Report

See crates/talkbank-parser-re2c/docs/parity-report.md for the full gap analysis, divergence categories, and remaining work items.

Parser Leniency Policy

Status: Current Last updated: 2026-05-01 05:19 EDT

This document is the single source of truth for how the tree-sitter grammar, Rust validation layer, and CLI tooling divide responsibility for enforcing the CHAT specification. It consolidates decisions scattered across grammar.js comments, analysis documents, and code.

Scope: Documentation only. This document does not implement new validation rules — it records what exists, what is intentionally absent, and proposes a roadmap for closing gaps.


Philosophy: Parse, Don’t Validate

The tree-sitter grammar intentionally accepts a superset of valid CHAT. The rationale:

  1. Maximise parse coverage — Real-world .cha files contain legacy patterns, whitespace variations, and edge cases. A grammar that rejects them produces no AST and therefore no diagnostics. Accepting them gives the validation layer something to work with.

  2. Separate syntax from semantics — The grammar captures structure (headers, utterances, tiers, annotations). The Rust validation layer enforces semantic rules (required headers, participant declarations, alignment counts).

  3. Enable configurable strictness — Different consumers need different policies. A roundtrip pipeline can be strict; an editor providing live diagnostics should be lenient. Validation profiles (see Validation Profile Infrastructure) make this possible.

Three-Tier Classification

Every intentional leniency decision falls into one of three tiers:

TierLabelMeaning
AParse-lenient + validate-strictGrammar accepts it; validation rejects it as an error
BParse-lenient + validate-warningGrammar accepts it; validation emits a warning
CParse-lenient onlyGrammar accepts it; no validation needed — the construct is genuinely optional or the broad acceptance is by design

This classification was proposed in an earlier grammar governance analysis and is formalised here.


Leniency Matrix

Master table of every documented leniency decision in the grammar. The Status column indicates whether downstream validation compensates for the grammar’s permissiveness.

#Grammar ConstructSpec RequirementGrammar BehaviorTierValidationError CodeStatus
1@UTF8 headerRequired, must be first lineOptional (not enforced)AValidatedE503OK
2@Begin headerRequiredOptional (grammar.js ~L104)AValidatedE504OK
3@End headerRequiredOptional (grammar.js ~L106)AValidatedE502OK
4Pre-first-utterance header orderNo enforced order (matches CLAN CHECK)choice(), any order (grammar.js ~L122–135)CN/A (by design)—OK
5Headers after utterancesAllowed (e.g. @Bg, @Eg, @G, @Comment)Interleaved freelyCN/A (by design)—OK
6Content type context restrictionsUnified across contextsUnified base_content_item (grammar.js ~L731–738)CN/A (by design); specific semantic rules (E371, E372) exist separately—OK
7Terminator presenceRequired (except CA mode)Optional (grammar.js ~L691–692)AValidatedE305OK
8Bare shortening as wordCA mode onlyAccepted anywhereAValidatedE2xxOK
9Trailing whitespace in annotationsNot specifiedOptional trailing space (grammar.js ~L957, 966, 975, 1004, 1013)CN/A—OK
10MOR segment UnicodeVery permissive (broad language support)Exclusion-based regex (grammar.js ~L1909–1915)CN/A (by design)—OK
11MOR fusional suffixes with hyphensALNUM + IPA onlyAllows hyphens (grammar.js ~L1942–1945)CN/A (by design)—OK
12MOR nested translationsNo nested structuresAllows () and [] nesting (grammar.js ~L1954–1966)CN/A (by design)—OK
13Linkers / language codesTruly optionalOptionalCN/A—OK
14Word annotationsTruly optionalOptionalCN/A—OK
15Media bulletTruly optionalOptionalCN/A—OK
16Group whitespace (leading/trailing)No whitespace inside < >Optional (grammar.js ~L1097, 1099)CN/A—OK
17Long feature label charactersLimited character set/[A-Za-z0-9@%_-]+/ (grammar.js ~L1327)CN/A—OK
18Catch-all headers ($.anything)Structured content for some headers/[^\r\n]+/ for ~19 header typesCN/A (content is opaque)—OK
19Header gap whitespaceSingle space/tabrepeat1(choice(space, tab)) (grammar.js ~L467, 477, 489)CN/A—OK
20@Types header whitespaceNo spaces around commasOptional whitespace around commas (grammar.js ~L584–592)CN/A—OK

Permissiveness Regression Decisions

During development, several validation rules were tightened and then relaxed after they produced false positives against the reference corpus. These decisions are documented in the permissiveness regression log (archived). Each is summarised here with its rationale.

Decision 1: [*] bare annotation — E214 disabled

  • Previous behaviour: E214 emitted when [*] appeared without an explicit error code (empty ContentAnnotation::Error).
  • Current behaviour: Bare [*] is accepted without error.
  • Implementation: Removed validation branch in talkbank-model/src/model/annotation/annotated.rs.
  • Rationale: Reference files (errormarkers.cha, compound.cha) use bare [*] as valid CHAT.
  • Revisit: If coded error annotations become required, do it behind an explicit strict profile.

Decision 2: @t without @s:<lang> — E248 disabled

  • Previous behaviour: E248 emitted for @t markers without an explicit language marker.
  • Current behaviour: @t accepted without requiring @s:<lang>.
  • Implementation: Removed checks in talkbank-model/src/validation/word/structure.rs.
  • Rationale: Reference file formmarkers.cha contains a@t and is expected to be valid.
  • Revisit: Scope to explicit strict validation mode if desired.

Decision 3: Undeclared inline language codes — E254 removed

  • Previous behaviour: Inline @s:... markers with language codes not declared in @Languages emitted E254.
  • Current behaviour: No E254 emitted; error code removed from codebase.
  • Implementation: Removed checks in talkbank-model/src/validation/word/language/resolve.rs.
  • Rationale: Reference file lang-marker.cha exercises undeclared codes.
  • Revisit: Team decision needed — strict declaration enforcement vs permissive inline experimentation.

Decision 4: Mixed-language digit legality — permissive-any rule

  • Previous behaviour: Digits had to be legal in all applicable languages for mixed/ambiguous markers.
  • Current behaviour: Digits accepted if legal in at least one applicable language.
  • Implementation: Changed from is_valid_in_all() to any() in talkbank-model/src/validation/word/language/digits.rs.
  • Rationale: Prevents false positives in mixed-language reference examples.
  • Revisit: Confirm spec intent for mixed/ambiguous validation semantics.

Decision 5: @Bg nesting — same-label only

  • Previous behaviour: Any nested @Bg while another gem scope was open emitted E529.
  • Current behaviour: E529 only fires when nesting the same label (or same unlabeled scope key). Different labels may nest hierarchically.
  • Implementation: Changed from any_scope_open to same_scope_open in talkbank-model/src/validation/header/structure.rs.
  • Rationale: Avoids false positives on hierarchical markup patterns (e.g., HSLLD corpus).
  • Revisit: Decide whether nesting policy should be global or per-label.

Decision 6: Temporal bullets in CA mode — skipped

  • Previous behaviour: E701/E704 temporal checks ran even for CA-mode files.
  • Current behaviour: Temporal constraints are skipped when file is in CA mode.
  • Implementation: validate_temporal_constraints() early-returns when ca_mode is true (talkbank-model/src/validation/temporal.rs).
  • Rationale: CA reference files include patterns that triggered false monotonicity/self-overlap diagnostics.
  • Revisit: Implement CA-specific temporal policy rather than global skip.

Decision 7: Pipeline severity threshold — errors only

  • Previous behaviour: Any validation diagnostic (including warnings) caused PipelineError::Validation.
  • Current behaviour: Pipeline returns failure only if at least one diagnostic has Severity::Error.
  • Implementation: talkbank-transform/src/pipeline/parse.rs.
  • Rationale: Warnings should not block parse/transform/export pipelines.
  • Revisit: Keep as default; add explicit --strict flag/profile if needed.

Decision 8: Spacing warnings W210/W211 — disabled

  • Previous behaviour: Style-level spacing warnings around terminators and overlap markers.
  • Current behaviour: Checks removed from core main-tier validation path.
  • Implementation: check_spacing_warnings() invocation removed from talkbank-model/src/model/content/main_tier.rs.
  • Rationale: Generated unexpected diagnostics on files treated as valid in reference workflow.
  • Revisit: Reintroduce as optional lint profile, not core validator hard path.

Validation Gap Roadmap

Concrete items where the grammar is lenient but no validation compensates. Each proposes a new error code and priority.

Priority 1: @UTF8 Presence (E503) — DONE

  • Grammar: @UTF8 is optional.
  • Spec: Required, must be the first line.
  • Implemented: E503 (MissingUTF8Header) added to check_headers() in talkbank-model/src/validation/header/structure.rs.
  • Severity: Error.
  • Note: All 340 reference corpus files contain @UTF8 — zero roundtrip impact.

Priority 2: Pre-First-Utterance Header Order (proposed E534) — Not a Gap

  • Grammar: choice() accepts headers in any order between @Begin and the first utterance.
  • Assessment: CLAN CHECK does not enforce any ordering for post-@Begin headers — it validates presence and format only. Our grammar’s flexible ordering matches CHECK’s behavior.
  • Status: Reclassified from Tier B (GAP) to Tier C (by design).

Priority 3: Content Type Context Validation — Not a Gap

  • Grammar: Unified base_content_item accepts any content type in any context.
  • Assessment: The unified rule is correct by design. Nested groups are legal CHAT (e.g., <the <dag> [: dog]> [= something]). The two specific semantic restrictions that do exist (no pauses in pho groups — E371; no nested quotations — E372) are already validated.
  • Status: Reclassified from Tier A (PARTIAL) to Tier C (by design).

Validation Profile Infrastructure

What Exists

ValidationConfig (talkbank-model/src/errors/config.rs)

Builder-pattern configuration for per-error-code severity overrides.

let config = ValidationConfig::new()
    .downgrade(ErrorCode::IllegalUntranscribed, Severity::Warning)
    .disable(ErrorCode::InvalidOverlapIndex)
    .upgrade(ErrorCode::UnknownAnnotation, Severity::Error);

API:

  • new() — empty config, all codes use original severity
  • downgrade(code, severity) — lower severity (chainable)
  • disable(code) — suppress entirely (chainable)
  • upgrade(code, severity) — raise severity (chainable)
  • set_severity(code, Option<Severity>) — set or disable (chainable)
  • effective_severity(code, original) -> Option<Severity> — query
  • is_disabled(code) -> bool — check

Pre-built profiles:

  • lenient() — Downgrades IllegalUntranscribed and InvalidOverlapIndex to Severity::Warning. Designed for legacy corpora gradual migration.
  • strict() — Placeholder. Currently returns Self::new() (no overrides).

ConfigurableErrorSink (talkbank-model/src/errors/configurable_sink.rs)

Wrapper that intercepts errors and applies ValidationConfig before forwarding to an inner ErrorSink.

let inner = ErrorCollector::new();
let sink = ConfigurableErrorSink::new(&inner, config);
// Pass `sink` to parser/validator — disabled errors are filtered,
// severity overrides are applied.

Runner-Level Flags (talkbank-transform, chatter-cli)

FlagEffect
--skip-alignmentSkip tier alignment validation
--roundtripTest serialization idempotency after validation
--forceClear cache for path and revalidate
--max-errors NStop after N errors

What Is Missing

GapDescriptionEffort
strict() profile not implementedReturns empty config; should upgrade all warning codes to errorsSmall
No --profile CLI flagUsers cannot select strict / lenient / lint from the command lineMedium
ConfigurableErrorSink not wired into validation pipelineInfrastructure exists but is not used by chatter validateMedium
No lint-style profileSpacing/style warnings (W210, W211) have no homeSmall (once profiles are wired)
No profile serializationCannot load profiles from TOML/JSON config filesMedium
No corpus-specific profilesE.g., HSLLD-specific rulesFuture

Proposed Profiles

From the permissiveness regression log:

ProfilePurposeBehaviour
reference-compatibleCurrent permissive baselineDefault — matches current validation behaviour
strict-chatFull spec enforcementRe-enable selected tightenings (E214, E248, E254, etc.)
lint-styleSpacing/style warnings onlyEnable W210, W211; do not fail pipeline

The roundtrip gate should be pinned to an agreed profile to prevent future ambiguity about what “pass” means.


Silent Recovery Points (NLP Pipelines)

An earlier Python-Rust boundary audit identified several places where batchalign-core silently massages data without diagnostics. These are related to leniency because they represent permissive acceptance without transparency.

PipelineRecovery MechanismDiagnostics?
Stanza morphosyntaxretokenize.rs DP alignment; Word::new_unchecked fallbackNo
Whisper/Wave2Vec FAforced_alignment.rs DP “best fit”No
Google TranslateImported verbatim into %xtraNo filtering
Stanza segmentationSilent abort on assignment mismatchNo

Key infrastructure gap: ParseHealth exists in talkbank-model (per-utterance tier cleanliness flags with taint(), is_clean(), can_align_main_to_mor() methods). It is used by the tree-sitter and direct parsers during parsing. However, batchalign-core does not read, write, or propagate ParseHealth during any mutation (morphosyntax injection, FA injection, retokenisation). The infrastructure exists in the model layer but is not connected to the pipeline layer.


Cross-References

SourceWhat It Contains
Grammar governance analysis (archived)Proposed this document; leniency matrix concept; three-tier classification
Permissiveness regression log (archived)8 permissiveness regression decisions with rationale
Python-Rust boundary audit (archived)Silent recovery points; ParseHealth gap; NLP pipeline audit
grammar/grammar.jsInline comments on each leniency decision (line references in matrix above)
talkbank-model/src/errors/config.rsValidationConfig API
talkbank-model/src/errors/configurable_sink.rsConfigurableErrorSink adapter
talkbank-model/src/validation/header/structure.rsHeader validation: E501, E502, E503, E504–E533
talkbank-model/src/validation/temporal.rsTemporal constraint checks (E701, E704); CA-mode skip
talkbank-model/src/model/content/main_tier.rsWhere W210/W211 were removed

Last updated: 2026-02-18

Error Diagnostics UX Standard

Status: Current Last updated: 2026-05-01 17:07 EDT

Workspace-wide standard for diagnostic shape, severity, recovery behavior, span correctness, and integrator output formats. Applies both to the CHAT-core error system and to the Batchalign runtime errors.

Objective

Make diagnostics precise, explainable, and actionable for both developers and non-technical editors, while keeping machine readability for downstream tools.

Open concerns

  • Message quality across the error catalog is not yet governed by one central style standard. Different error codes were authored at different times and converge unevenly on the message-quality guidance below.

Canonical Diagnostic Schema

Diagnostic {
  code: String,
  severity: Error | Warning | Info,
  category: Parse | Validation | Alignment | Header | Tier | Internal,
  location: SourceLocation,
  context: ErrorContext,
  message: String,
  suggestion: Option<String>,
  related: Vec<RelatedLocation>
}

Message Quality Standard

Each diagnostic must answer:

  1. What failed.
  2. Where it failed.
  3. Why it likely failed.
  4. What to do next.

Avoid internal jargon unless accompanied by user-facing explanation.

Severity Policy

  • Error — blocks parse/validation outcome.
  • Warning — content is usable but has quality/compliance concerns.
  • Info — optional guidance and migration hints.

Severity must not be overloaded for tooling convenience.

Recovery Policy: Diagnostic-First, Not Sentinel-First

When parser recovery is required:

  • do not invent semantic fallback values to keep type construction convenient,
  • do not use empty strings or arbitrary enum defaults as recovered content.

Instead:

  1. Report a diagnostic with expected/actual node context.
  2. Preserve span information for tooling and UI.
  3. Propagate partial/failure status explicitly.

Any synthetic placeholders that are unavoidable for internal plumbing must be:

  • non-semantic (not exposed as real model content),
  • marked internal-only,
  • excluded from user-facing diagnostics and serialization.

Sentinel vs error-variant rule

If an unexpected condition changes semantic trust in parsed content:

  • Represent that explicitly as an error-bearing state (enum variant, parse-taint flag, or explicit outcome type).
  • Never represent it as None or a default payload that can be mistaken for valid content.

This applies both to parser outputs and to runtime metadata consumed during validation.

Diagnostic construction

Use shared constructors/helpers for common diagnostics to reduce drift:

  • span-only diagnostics (code + severity + span + message),
  • source-backed diagnostics (code + severity + span + source + offending + message).

Benefits: consistent location/context population, fewer ad-hoc ParseError::new(...) call shapes, simpler migration to richer miette rendering.

Error Code Governance

  • Central registry under talkbank-model (errors module).
  • One authoritative description and example per code.
  • Deprecated codes remain mapped with explicit migration notes.
  • CI check forbids duplicate code definitions or orphaned docs.

Span and Location Correctness

  • All diagnostics use consistent line/column and byte-offset definitions.
  • Golden tests cover:
    • single-byte and multi-byte UTF-8 content,
    • embedded content offsets,
    • continuation lines and tabs.

Integrator Output Formats

  • Human-readable CLI diagnostics.
  • Machine-readable JSON diagnostics.
  • LSP diagnostic mapping.

All formats share the same underlying diagnostic schema.

Acceptance Criteria

  • Every emitted diagnostic includes code, severity, location, and suggestion policy.
  • Error code documentation and runtime definitions are synchronized automatically.
  • Span correctness is covered by dedicated tests.
  • CLI and JSON outputs are contract-tested for schema compliance.

Wide Struct Audit

Status: Current Last updated: 2026-05-01 15:57 EDT

A workspace-wide audit rule for struct shape. Applies to all crates in talkbank-tools (parsing, model, validation, transform, CLI, LSP, batchalign runtime).

A struct with many fields is not automatically wrong. The smell is:

  • many unrelated concerns packed into one value
  • several related booleans that act like implicit policy enums
  • repeated field-name prefixes that point to missing sub-structs
  • parallel vectors or stringly runtime fields
  • runtime code reaching into many unrelated fields of the same value

The repo therefore treats 10 or more named fields as an audit threshold, not as an automatic ban.

Categories

Wide structs fall into four categories.

1. Boundary shim — may stay wide

CLI, JSON, or clap boundary types. Acceptable if they are converted into typed policies or sub-structs before entering core runtime code.

Examples: GlobalOpts, AlignArgs, TranscribeArgs, BenchmarkArgs.

2. Transport or schema record — may stay wide

DB rows, HTTP response shapes, JSON schema mirrors. Acceptable as long as they don’t become the internal runtime shape.

Examples: JobRow, NewJobRecord, JobSubmission, JobInfo, HealthResponse, WordJsonSchema, DbMetadata, CoverageReport.

3. Real aggregate — may stay wide

Domain values whose fields all answer one coherent question and whose callers consume the whole rather than spelunking through unrelated subsets.

Examples: FileStatus, AttemptRecord, plus metric records like SpeakerEval, SpeakerKideval, SpeakerComplexity, SpeakerFluency (report records, not runtime coordination).

4. Refactor target — must be split

Mix of policy and state, multiple responsibilities, or callers needing to know the whole subsystem to use a subset of fields.

Design Rules

  1. Treat 10 or more named fields as an audit trigger.
  2. Treat 3 or more related boolean fields as a smell even below that threshold.
  3. Boundary and transport records may stay wide when they mirror a real external shape.
  4. Runtime coordination structs prefer named sub-structs over flat bags.
  5. Replace parallel vectors with per-item records where possible.
  6. If a wide struct stays wide, it must be recorded explicitly in the audit test with a reason and a field-count cap.

Refactor Examples

ValidateDirectoryOptions (chatter-cli) — was a flat bag

Used to be a flat bag of format, cache, traversal, roundtrip, parser, audit, and TUI flags. Now grouped by concern:

  • ValidationRules
  • ValidationExecution
  • ValidationTraversalMode
  • ValidationPresentation

Shape this audit wants for policy-rich CLI boundaries: one small top-level struct with explicit sub-objects and enums rather than a dozen flat fields.

ParseHealth (talkbank-model) — was a ten-boolean state vector

Now stores taint as a compact tier bitset keyed by ParseHealthTier, the shape this audit expects for fixed domain sets.

flowchart LR
    tier["ParseHealthTier"] --> set["Tier health set"]
    set --> checks["Alignment safety checks"]

Job (batchalign) — interior runtime, grouped by concern

crates/batchalign/src/store/job.rs now decomposes into:

  • JobIdentity
  • JobDispatchConfig
  • JobSourceContext
  • JobFilesystemConfig
  • JobExecutionState
  • JobScheduleState
  • JobRuntimeControl
flowchart LR
    boundary["Submission or DB row"] --> job["Job"]
    job --> identity["JobIdentity"]
    job --> dispatch["JobDispatchConfig"]
    job --> source["JobSourceContext"]
    job --> filesystem["JobFilesystemConfig"]
    job --> execution["JobExecutionState"]
    job --> schedule["JobScheduleState"]
    job --> runtime["JobRuntimeControl"]

The matching runner-facing projection (RunnerJobIdentity, RunnerDispatchConfig, RunnerFilesystemConfig) is the worker-facing view.

AppState (batchalign) — service-root aggregate

crates/batchalign/src/state.rs keeps server route state shallow under named sub-aggregates:

flowchart LR
    state["AppState"] --> control["AppControlPlane"]
    state --> workers["WorkerSubsystem"]
    state --> environment["AppEnvironment"]
    state --> build["AppBuildInfo"]

That is the expected shape for owned root state: one small root, named sub-aggregates for real seams, no caches or runner-only data threaded through every handler.

Open Hotspots

Dashboard and TUI state bags

Real state owners that still want grouping by concern (selection vs. progress vs. render flags vs. status):

  • src/test_dashboard/app.rs AppState
  • crates/chatter-cli/src/ui/validation_tui/state.rs TuiState

Backend (chatter-lsp)

crates/chatter-lsp/src/backend/state.rs is a service-root aggregate. Defensible, but still wants grouping such as document caches, parse caches, validation state, language services.

Metric structs

SpeakerEval/SpeakerKideval are acceptable as report records. If output renderers keep needing subsets (lexical metrics, morphosyntax metrics, error counts, derived scores), those records should eventually nest along those lines.

Audit Guardrail

The Rust audit test at crates/chatter-cli/tests/wide_struct_audit.rs classifies the current wide structs and fails when a new one appears without an explicit review entry. The corresponding xtask (cargo xtask lint-wide-structs) enforces field caps on reviewed structs.

Spec Tooling and Generation Pipeline

Status: Current Last updated: 2026-05-01 05:30 EDT

Objective

Make spec/ the reliable language-contract source while keeping generation deterministic, maintainable, and appropriately scoped.

The goal is to separate:

  • grammar artifact generation
  • validation/error-doc generation
  • parser semantic testing (fragment and full-file)

Anything that still looks like bootstrap-era synthetic fragment orchestration is now audit-only unless a doc says it remains operational.

Open structural concerns

  • spec/tools still carries bootstrap-era Rust parser/model dependencies that create circular or awkward workflow coupling.
  • Contributor workflows still over-assume that just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs is the right reaction to every parser-related change.

Current Generation Pipeline

spec constructs/errors
  -> spec validators
  -> generated grammar corpus tests
  -> generated rust parser/validation tests
  -> generated error docs
  -> coverage dashboards and quality reports

That pipeline is still useful, but it is too broad to remain the single mental model for parser testing.

Desired Post-Bootstrap Split

grammar specs/templates
  -> generated tree-sitter corpus tests

error specs
  -> generated validation/parser error tests
  -> generated error docs

fragment semantic fixtures and invariants
  -> fragment-level parser tests

reference corpus / curated full files
  -> parser parity tests

Structural Reorganization for spec/tools

  • Split into explicit internal modules:
    • input (markdown/spec parsing)
    • ir (normalized internal representation)
    • emit (grammar tests, rust tests, docs)
    • validate (schema and semantic checks)
    • sync (grammar node-types and symbol-registry checks)
  • Narrow its mission back to spec-driven artifact generation and validation, rather than letting it remain a bootstrap-era staging ground for parser semantics.

Legacy vs Active

Keep these active:

  • grammar corpus generation
  • error doc generation
  • symbol registry sync/validation
  • affected regeneration when a spec or grammar input truly changed

Treat these as legacy audit paths:

  • synthetic tree-sitter fragment wrappers
  • bootstrap-era parser equivalence rituals

Determinism Requirements

  1. Stable ordering of generated outputs.
  2. Stable formatting of generated code/docs.
  3. Re-runs without source changes produce no diffs.

Drift Prevention Controls

  • Node type compatibility check:
    • spec/tools must compile and run against current generated node constants.
  • Registry compatibility check:
    • all symbol categories used in specs and grammar must be known in registry.
  • Generation integration check:
    • full generation pass with clean tree must produce zero diff.
  • Boundary check:
    • generated grammar/docs flows should not silently become the sole authority for fragment parsing semantics.

Authoring Experience

  • Provide strict but simple spec templates for constructs and errors.
  • Add spec lint command for immediate feedback:
    • missing fields,
    • invalid tags,
    • malformed examples,
    • unknown error codes.
  • Document when just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs is actually needed and when a small direct test is the right answer instead.

Versioning and Metadata

Each spec file should include:

  • ownership,
  • status (draft, accepted, deprecated),
  • parser/validation scope,
  • linked tests and generated outputs.

Acceptance Criteria

  • spec/tools is green and deterministic.
  • Every generation target has explicit provenance from source specs.
  • Drift between node types, specs, and generators is blocked in CI.
  • Spec contributors have a documented and automated happy path.
  • Small grammar changes no longer force a giant regeneration ritual by default.
  • Fragment parsing semantics are tested outside the generation pipeline.

Symbol Registry Architecture

Status: Current Last updated: 2026-03-24 00:01 EDT

Purpose

resources/spec/symbols/symbol_registry.json is the canonical source of token/symbol classes used by CHAT grammar tokenization policy.

Scope

The registry currently governs:

  • CA delimiter symbols,
  • CA element symbols,
  • word segment forbidden symbol classes,
  • event segment forbidden symbol classes.

Governance Rules

  1. Symbol changes must be made only in resources/spec/symbols/symbol_registry.json.
  2. Registry must pass validation:
    • node resources/spec/symbols/validate_symbol_registry.js
  3. Grammar symbol sets must be regenerated after any registry change:
    • node scripts/generate-symbol-sets.js
  4. Generated files are read-only and must not be edited manually.

Determinism Requirements

  • Every category list in the registry must be lexicographically sorted.
  • Duplicate symbols are forbidden.
  • ca_delimiter_symbols and ca_element_symbols must be disjoint.

These constraints keep generated outputs stable and review diffs minimal.

Consuming Outputs

Generated symbol constants are emitted to:

  • grammar/src/generated_symbol_sets.js

grammar/grammar.js imports from this generated module to avoid manual duplication of critical symbol policy.

Change Workflow

  1. Edit registry JSON.
  2. Run registry validation.
  3. Regenerate symbol sets.
  4. Run grammar generation/tests.
  5. Run parser equivalence tests.
  6. Commit source + generated outputs together.

Auditability

Registry and generated outputs are covered by just spec gen-tree-sitter-tests && just spec gen-rust-tests && just spec gen-error-docs && git diff --exit-code and CI checks, so drift between source policy and consumed grammar constants is merge-blocking.

Bullet Validation

Status: Current Last updated: 2026-05-01 05:19 EDT

Media bullets are timestamps embedded in CHAT utterances that link transcript text to audio/video. They appear as •start_end• at the end of a main tier line (e.g., *CHI: hello . •1000_2000•). Validating that these timestamps are internally consistent is one of the more subtle parts of CHAT validation, because the “obvious” rules turn out to be wrong for multi-party conversation.

This chapter documents what CLAN CHECK does, where its implementation falls short of its own intent, and how chatter validate interprets and improves on that intent.

The three temporal checks

There are three distinct temporal constraints that can be checked on bullet timestamps. They differ in scope, severity, and whether they should run by default.

E701: Same-speaker start-time monotonicity (CLAN Error 83)

Rule: For each speaker, their utterances’ start times must be non-decreasing. If speaker CHI has utterance A starting at 10,000ms and utterance B (later in document order) starting at 8,000ms, that is an error — CHI’s timeline has gone backward.

Scope: Per-speaker. Cross-speaker non-monotonicity is allowed (see Why cross-speaker non-monotonicity is not an error).

Severity: Error.

E704: Same-speaker self-overlap (CLAN Error 133)

Rule: For each speaker, the current utterance’s start time must not be more than 500ms before the same speaker’s previous utterance’s end time. In other words, a speaker cannot overlap with themselves by more than 500ms.

Scope: Per-speaker. The 500ms tolerance accounts for annotation rounding and minor timing imprecision at boundaries.

Severity: Error.

E729: Cross-speaker overlap (CLAN Error 84)

Rule: The current utterance’s start time must not be before the previous utterance’s (any speaker) end time. This checks for any temporal overlap between adjacent utterances, regardless of speaker.

Scope: Global (cross-speaker). Only fires with CLAN’s +c0 flag.

Severity: Warning. Not part of default validation.

This check is part of CLAN’s “strict timeline contiguity” mode, which requires that every utterance’s start time equals the previous utterance’s end time — no gaps (Error 85) and no overlaps (Error 84). It is designed for a very specific use case: verifying that audio has been exhaustively and non-redundantly segmented. In normal conversational transcripts, cross-speaker overlap is ubiquitous, so this check would be absurd as a default.

What CLAN CHECK does

CLAN CHECK implements bullet validation in the function check_checkBulletsConsist() in check.cpp. Understanding its implementation is essential because it has several accidental behaviors that affect the error counts users see.

The snapshot-and-compare pattern

The function uses a global pair (check_SNDBeg, check_SNDEnd) to hold the “current” bullet timing, and saves the previous values into local variables (tBegTime, tEndTime) at the start of each call. The comparison flow is:

1. Save previous: tBegTime = check_SNDBeg, tEndTime = check_SNDEnd
2. Parse new bullet into check_SNDBeg, check_SNDEnd
3. Check error 83: check_SNDBeg < tBegTime?           (cross-speaker comparison)
4. Check error 133: speaker's last END - check_SNDBeg > 500?  (same-speaker)
5. If +c0 mode: check error 84 (overlap) and error 85 (gap)
6. Update speaker's last END time via check_setLastTime()

The early-return shadowing bug

The critical implementation detail is that error 83 fires via return(83) at step 3. This causes the function to exit immediately, skipping steps 4 through 6. Two consequences follow:

  1. Error 83 shadows error 133. An utterance that triggers error 83 (global non-monotonicity) can never also trigger error 133 (same-speaker overlap) in the same call, even if both conditions are true. This is not intentional — it is an artifact of C-style early-return control flow.

  2. Speaker state goes stale. Step 6 (check_setLastTime) updates the speaker’s per-speaker tracking in the SPLIST linked list. When error 83 fires, this update is skipped. All subsequent error-133 checks for that speaker compare against a stale endTime value, causing cascading state corruption that suppresses legitimate error 133 reports.

Error 83 is global, not per-speaker

CLAN fires error 83 by comparing the current utterance’s start time against the previous utterance’s start time, regardless of speaker. In a multi-party conversation:

*PIL: something . •100000_102000•
*UEL: response .  •99500_101000•      ← Error 83: 99500 < 100000

This fires error 83 because UEL’s start time (99,500ms) is before PIL’s start time (100,000ms). But this is just two people talking at the same time — normal conversational overlap. The [>] and [<] markers in CHAT explicitly annotate this as intentional simultaneous speech.

In files with many speakers (the Koine/bre corpus has 7-9 speakers per file, including children talking over each other), this fires on a huge fraction of utterances. CLAN’s accidental shadowing partially masks the problem by suppressing downstream error-133 reports when error 83 fires.

Why cross-speaker non-monotonicity is not an error

Consider a classroom recording with a teacher (PIL) and seven children. The teacher asks a question, and three children answer simultaneously:

*PIL: qué es esto ?        •50000_52000•
*UEL: un coche .           •51200_52500•   ← started during PIL's question
*MAR: coches .             •51000_51800•   ← started even earlier
*REN: es un coche grande . •51500_53000•   ← started between UEL and MAR

In document order, the start times are: 50000, 51200, 51000, 51500. This is non-monotonic (51000 < 51200), but there is nothing wrong with this data. The children are simply talking at the same time. No amount of reordering the utterances in the file would make all start times monotonically increasing while preserving the speaker-turn structure.

Cross-speaker non-monotonicity is an inherent property of multi-party conversation, not a data error. Flagging it as an error produces thousands of false positives on any corpus with overlapping speech.

When IS non-monotonic start time an error?

Same-speaker non-monotonicity IS an error. If CHI speaks at 10,000ms, then later in the file CHI speaks again at 8,000ms, CHI’s timeline has gone backward. This almost certainly indicates a transcription or alignment mistake.

The test is simple: within the same speaker’s utterance sequence, start times must be non-decreasing. This is what chatter validate checks for E701.

How chatter validate implements bullet validation

E701: Per-speaker monotonicity (not global)

chatter validate tracks each speaker’s last start time in a HashMap. E701 only fires when the same speaker’s start time goes backward. Cross-speaker non-monotonicity is silently accepted.

This is an intentional semantic divergence from CLAN CHECK, which fires error 83 globally. We believe CLAN’s global check reflects the implementation (comparing against a single global tBegTime) rather than the intent (detecting disordered timestamps). The per-speaker version matches the intent without drowning users in false positives from normal conversational overlap.

E704: Per-speaker overlap with 500ms tolerance

chatter validate tracks each speaker’s last end time in a HashMap. E704 fires when the overlap exceeds 500ms (same threshold as CLAN Error 133).

Unlike CLAN, E704 runs independently of E701. An utterance can trigger both errors if it is both non-monotonic (E701) and self-overlapping (E704). CLAN’s early-return pattern prevents error 133 from firing when error 83 fires, which is a bug, not a feature.

Speaker state is always updated regardless of whether errors fire. This avoids the cascading state corruption that CLAN’s implementation suffers from.

E729: Not in default validation

E729 (CLAN Error 84, cross-speaker overlap) is implemented but not called during default validation. It exists for future use in a strict-bullet mode equivalent to CLAN’s +c0 flag.

Untranscribed utterances are skipped

Utterances containing only untranscribed markers (www, xxx, yyy) are skipped for E704 checks. These utterances often carry broad segment bullets (covering a long span of background speech) that would create false self-overlap reports. This matches CLAN CHECK’s behavior, where untranscribed tiers do not contribute to timing comparisons.

CA mode disables all temporal checks

When the file header includes @Options: CA, all temporal validation is skipped. Conversation Analysis mode intentionally relaxes timing constraints because CA transcription conventions use overlapping and non-sequential timing as part of the analytic notation.

Comparison: CLAN CHECK vs chatter validate

The following table summarizes the behavioral differences:

┌────────────────────────────┬──────────────┬─────────────────┐
│ Behavior                   │ CLAN CHECK   │ chatter validate│
├────────────────────────────┼──────────────┼─────────────────┤
│ Error 83 / E701 scope      │ Global       │ Per-speaker     │
│ Error 133 / E704 scope     │ Per-speaker  │ Per-speaker     │
│ Error 84 / E729 default    │ Off (+c0)    │ Off             │
│ 83 shadows 133             │ Yes (bug)    │ No              │
│ 83 corrupts speaker state  │ Yes (bug)    │ No              │
│ E701 + E704 independent    │ No           │ Yes             │
│ Speaker state always fresh │ No           │ Yes             │
│ Untranscribed skipped      │ Implicit     │ Explicit        │
│ CA mode bypass             │ Yes          │ Yes             │
│ 500ms tolerance (E704)     │ Yes          │ Yes             │
└───��────────────────────────┴──────────────┴─────────��───────┘

Expected count differences

On multi-party files with overlapping speech:

  • E701 count will be lower than CLAN’s error 83 count. CLAN fires error 83 on cross-speaker non-monotonicity; we don’t. The difference represents legitimate conversational overlap that we intentionally do not flag.

  • E704 count will be higher than CLAN’s error 133 count. CLAN’s early-return shadowing prevents error 133 from firing when error 83 fires, and the stale speaker state causes further suppression. Our correctly maintained per-speaker tracking reports all genuine self-overlaps.

On single-speaker files or files with minimal overlap, the counts should be very close or identical.

Implementation details

The implementation lives in crates/talkbank-model/src/validation/temporal.rs.

Data flow

flowchart TD
    A["collect_bullets(file)\n(temporal.rs:101)"] -->|"Vec&lt;BulletInfo&gt;"| B
    B["validate_global_timeline()\n(temporal.rs:169)"] -->|"Per-speaker HashMap"| C["E701 errors"]
    A -->|"Vec&lt;BulletInfo&gt;"| D
    D["validate_speaker_timelines()\n(temporal.rs:212)"] -->|"Per-speaker HashMap"| E["E704 errors"]

BulletInfo

Each utterance with a bullet produces a BulletInfo containing:

  • utterance_idx — 0-based index in the file
  • speaker — the speaker code (e.g., "CHI", "PIL")
  • bullet — the Bullet struct with start_ms and end_ms
  • has_timeable_content — whether the utterance contains transcribed words (used to skip untranscribed-only turns for E704)

Only main speaker tiers are collected. Dependent tiers (%mor, %gra, etc.) are excluded.

Per-speaker tracking

Both E701 and E704 use HashMap<&str, ...> keyed by speaker code:

  • E701: stores (utterance_idx, start_ms) — the speaker’s most recent start time
  • E704: stores (utterance_idx, end_ms) �� the speaker’s most recent end time

State is always updated after processing each bullet, regardless of whether an error was reported. This ensures clean tracking for subsequent comparisons.

CLAN source reference

For readers who want to trace the CLAN implementation:

  • Function: check_checkBulletsConsist() in OSX-CLAN/src/clan/check.cpp, lines 3849-3967
  • Error 83: lines 3883-3890 (early return(83))
  • Error 133: lines 3892-3895 (only reached if error 83 did not fire)
  • Speaker state update: line 3909 (check_setLastTime) — only reached if no error fired
  • Per-speaker tracking: SPLIST linked list, lookup via check_getLatTime() / check_setLastTime()
  • +c0 mode: checkBullets flag, set via +c0 command-line option (line 5920), guards errors 84/85 at lines 3897 and 3953
  • Call site: check_ParseWords() line 4801, guarded by utterance->speaker[0] == '*' (main tiers only)

CA Terminator Resolution

Status: Current Last updated: 2026-05-05 12:23 EDT

How CA markers are split between separators and linkers in the parser/model.

Current rule

The parser/model no longer promotes CA markers into utterance terminators.

The supported split is:

  1. Standard utterance terminators remain the CHAT terminators such as . ? ! +... +/. and related final punctuation tokens.
  2. CA intonation arrows (⇗ ↗ → ↘ ⇘) stay Separator content items.
  3. CA TCU markers (≈ ≋) stay Separator content items.
  4. CA TCU linker forms (+≈ +≋) stay Linker items.

This means a trailing →, ≈, or ≋ remains in main-tier content rather than being retyped as Terminator.

Parser/model consequences

  1. Tree-sitter grammar keeps arrows and ≈/≋ on the separator path.
  2. The tree parser converts those nodes directly into Separator variants.
  3. The re2c parser classifies ≈/≋ as separators and +≈/+≋ as linkers.
  4. The old post-hoc resolve_ca_terminator() promotion pass was removed.
  5. Terminator::try_from_chat_str() intentionally rejects CA arrows, ≈, ≋, +≈, and +≋.

Data Model

The active surface split is:

KindCHAT tokens
Terminator. ? ! +... +/. +//. +/? +!? +"/. +". +//? +..? +.
Separator⇗ ↗ → ↘ ⇘ ≈ ≋ plus the other CA/content separators
Linker+≈ +≋ plus the other utterance linkers

Legacy CA-only Terminator variants still exist in the type for backward compatibility with older serialized data, but new parser/classifier code does not construct them from CHAT text.

Regression coverage

The regression surface for this split is:

  • ca_symbols_are_not_chat_terminators in talkbank-model
  • trailing_ca_arrow_stays_separator in talkbank-parser
  • trailing_ca_no_break_stays_separator in talkbank-parser
  • trailing_ca_technical_break_stays_separator in talkbank-parser

Alignment

Status: Current Last updated: 2026-05-01 15:57 EDT

Alignment in the toolchain operates at three distinct layers. They use different data structures because they solve different problems: Layer 1 is structural (counting AST nodes), Layer 2 is positional (token / character indices), Layer 3 is edit-distance (Hirschberg DP).

LayerWherePurpose
Tier alignmenttalkbank-model::alignment1:1 mapping between main tier and dependent tiers (%mor, %pho, %wor, %sin, %gra)
NLP alignmenttalkbank-transform, batchalignMapping NLP model output (Stanza tokens, FA timings) back to CHAT AST words
Sequence alignmenttalkbank-transform::dp_alignEdit-distance alignment for WER and transcript comparison

Layer 1 — Tier Alignment

Validates that dependent tiers have the correct number and arrangement of items relative to the main tier. Lives in crates/talkbank-model/src/alignment/.

TierDomain

#![allow(unused)]
fn main() {
enum TierDomain { Mor, Pho, Sin, Wor }
}

The same utterance produces different counts per domain:

RuleMorPhoSinWor
Skip retrace groupsYesNoNoNo
Count pausesNoYesNoNo
PhoGroupRecurseAtomic (1)Skip (0)Recurse
SinGroupRecurseSkip (0)Atomic (1)Recurse
Include fragments (&+)NoYesYesNo
Include nonwords (&~)NoYesYesNo
Include fillers (&-)NoYesYesYes
Include untranscribedNoYesYesNo
Include tag-marker separatorsYesNoNoNo
ReplacedWord aligns toReplacementOriginalOriginalOriginal

For the underlying word filter (counts_for_tier, should_skip_group), the content walker, and the ChatFile model itself, see CHAT Data Model. The walker plus the domain table together govern every tier-alignment count.

Retrace handling — alignment-critical

Retraces are the most alignment-critical content type. A Retrace node wraps content the speaker said then corrected.

  • Mor: skip entirely (count 0). The retrace was a false start; only the correction carries morphological analysis.
  • Pho, Sin: recurse — words were physically produced and have phonological / gestural data.
  • Wor: recurse — retrace ancestry does not change %wor membership.

Critical invariant: the parser must emit UtteranceContent::Retrace for all retrace patterns, including single-word retraces with replacements (word [: repl] [* err] [//]). If a retrace is accidentally emitted as a bare ReplacedWord, it counts for %mor alignment, causing false E705 errors. Enforced by tests/retrace_replaced_word_regression.rs. Full data model + parsing pipeline + CHAT examples in Retraces and Repetitions.

AlignmentPair

#![allow(unused)]
fn main() {
struct AlignmentPair {
    source_index: Option<usize>,
    target_index: Option<usize>,
}
}

Universal index-pair primitive. Some/Some = matched. One None = insertion / deletion placeholder for mismatch diagnostics. is_complete() — both indices Some. is_placeholder() — unmatched.

Per-domain results

TypeFunctionSource → Target
MorAlignmentalign_main_to_mor()Main → %mor items
PhoAlignmentalign_main_to_pho()Main → %pho tokens
SinAlignmentalign_main_to_sin()Main → %sin tokens
WorAlignmentalign_main_to_wor()Main → %wor tokens
GraAlignmentalign_mor_to_gra()%mor chunks → %gra relations

%gra aligns to %mor chunks, not items. Clitics create additional chunks (pro|it~v|be&PRES = 2 chunks: pre-clitic + main).

Trait abstractions

TraitPurposeImplementors
IndexPairsource()/target() on any pair typeAlignmentPair, GraAlignmentPair
TierAlignmentResultpairs()/errors()/push_*() accumulatorAll 5 alignment result types
AlignableTierWhat a tier provides for generic alignmentPhoTier, SinTier, WorTier
TierCountablecount_tier_positions() / collect_tier_items() methods[UtteranceContent]

The generic positional_align() function uses AlignableTier to eliminate duplication: align_main_to_{pho,sin,wor}() are thin wrappers around it. %mor doesn’t use it (additional terminator validation logic). %gra doesn’t use it (source is MorTier, not MainTier). WorTier overrides mismatch_format() to Diff (LCS) since both sides are word sequences; the others use Positional.

%wor is not validated

%wor is a timing-annotation tier. There is no downstream positional indexing into %wor, and validate_alignments() does not check %wor word count against the main tier. Old corpus files may have xxx, fragments, or nonwords in %wor (pre-2026-04 behavior) without producing false errors.

Phon tier-to-tier alignment

A second class of alignment that operates between dependent tiers:

SourceTargetCode
%modsyl%modE725
%phosyl%phoE726
%phoaln%modE727
%phoaln%phoE728

Derived-view alignments: %modsyl is a syllabified reannotation of %mod, %phosyl of %pho, %phoaln aligns both. Word counts must match between source and target. Computed in compute_alignments() after the main-tier alignments. build_tier_to_tier_alignment() constructs index pairs and emits build_count_mismatch_error() when counts disagree. %phoaln checks against both %mod and %pho, potentially emitting E727 and E728 simultaneously.

Known data issue: Phon XML source data has orthography↔IPA word count discrepancies in ~4% of files (518 / 12,340). Expected in child phonology data. The PhonTalk converter handles this inconsistently — %mod/%pho are truncated to match orthography via OneToOne, but %xmodsyl/%xphosyl/%xphoaln are written from raw IPATranscript, exposing the full IPA word count. Result: E725–E728 mismatches.

Parse-health gating

Alignment diagnostics honor ParseHealth metadata. If a dependent tier’s domain is parse-tainted, mismatch errors for that domain pair are suppressed. Main-tier taint blocks all main→dependent alignments. Dependent-tier taint blocks only that tier. Phon tier-to-tier checks have their own gates (can_align_modsyl_to_mod, can_align_phosyl_to_pho, can_align_phoaln).

Layer 2 — NLP Alignment

Maps external model outputs (Stanza tokens, FA word timings) back to CHAT AST positions. All algorithms here are deterministic — no DP at runtime.

Word extraction

extract_words() (in crates/talkbank-transform/src/extract.rs) uses the content walker to pull words from the AST in domain-specific order. Returns Vec<ExtractedWord> with text, word_index, is_separator, special_form. Tag-marker separators (, „ ‡) are included as NLP words in Mor domain because they have %mor items (cm|cm, end|end, beg|beg).

Retokenize mapping

crates/talkbank-transform/src/retokenize/ maps original CHAT words to Stanza token indices after Stanza may have split or merged words.

struct WordTokenMapping {
    inner: Vec<SmallVec<[usize; 4]>>,  // word_idx → [token_idx...]
}

Dense Vec indexed by word position (O(1) lookup, no hashing). SmallVec<[usize; 4]> keeps 1–2 token mappings inline. Two-stage algorithm: deterministic span-join first, length-aware monotonic fallback when text diverges.

Tokenizer realignment

crates/talkbank-transform/src/tokenizer_realign.rs maps Stanza’s re-tokenized output back to original CHAT words using character-position arrays. O(n) algorithm: concatenate both sides, build per-char owner arrays, walk in parallel, apply language-specific MWT patches (French, Italian, Portuguese, Dutch). Output: PatchedToken::Plain(String) for clean 1-to-1, PatchedToken::Hint(String, bool) for MWT expansions.

FA response alignment

crates/batchalign/src/chat_ops/fa/alignment.rs maps forced-alignment timing responses back to extracted words.

  • Indexed path: 1:1 by position, no remapping.
  • Token-level path: deterministic token→word stitching when FA returns sub-word tokens; unmatched words remain untimed (no DP).

FA injection

crates/batchalign/src/chat_ops/fa/injection.rs walks utterance content with walk_words_mut() in Wor domain, applying timing bullets to each word in traversal order.

FA postprocess

crates/batchalign/src/chat_ops/fa/postprocess.rs:

  • enforce_monotonicity() — strips timing from regression violations (E362).
  • strip_e704_same_speaker_overlaps() — removes conflicting same-speaker timing.
  • Proportional boundary estimation for untimed utterances. See the Proportional FA Estimation page for the algorithm.

Layer 3 — Sequence Alignment

Hirschberg divide-and-conquer edit-distance alignment. crates/talkbank-transform/src/dp_align/. Linear space O(mn), with a SMALL_CUTOFF = 2048 threshold for the full-table fast path.

OperationCost
Match0
Substitution2
Insertion / Deletion1

Optimizations:

  • Prefix/suffix stripping before the DP core, in O(n). For the primary use case (WER / transcript comparison, 80–95% accuracy), reduces effective problem size 10–100×.
  • Generic Alignable trait — both String (word-level) and char variants share one implementation; monomorphization eliminates duplication with zero overhead.
  • Flat table for small problems — align_small() uses a flat Vec instead of Vec<Vec<...>>, reducing allocation count.
  • Scratch buffer reuse — row_costs() reuses two Vec<usize> buffers across DP rows via std::mem::swap.
enum AlignResult {
    Match { key, payload_idx, reference_idx },
    ExtraPayload { key, payload_idx },
    ExtraReference { key, reference_idx },
}

enum MatchMode { Exact, CaseInsensitive }

Call sites

CallerPurpose
crates/batchalign/src/benchmark.rsWER computation (hypothesis vs reference)
crates/batchalign/src/compare.rsTranscript comparison, %xsrep / %xsmor annotation
batchalign_core.dp_alignPyO3 bridge for Python callers

DP policy

Runtime DP is restricted to intrinsic uses (WER, CTC, DTW) and architecturally unavoidable cases. A policy test (test_dp_allowlist.py) fails CI if new runtime DP callsites appear outside the allowlist. Full inventory and necessity assessment in the Dynamic Programming page.

Overlap Marker Iteration

CA overlap markers (⌈⌉⌊⌋) appear at three content levels — UtteranceContent (top-level), BracketedItem (inside groups), and WordContent (intra-word, butt⌈er⌉). Two APIs in talkbank-model/src/alignment/helpers/overlap.rs:

walk_overlap_points — low-level

Visits every OverlapPoint in document order with word-position context. Analogous to walk_words but for overlap markers:

walk_overlap_points(&utterance.main.content.content.0, &mut |visit| {
    // visit.point: &OverlapPoint (kind + optional index)
    // visit.word_position: usize (alignable words seen so far)
});

extract_overlap_info — region-based

Pairs markers by (kind, index) into OverlapRegion structs. Each region represents a matched ⌈…⌉ or ⌊…⌋ pair. Index-aware: ⌈2...⌉2 forms a separate region from ⌈...⌉. Mismatched indices leave markers unpaired. Onset-only ⌈ (without ⌉) is a legitimate CA convention — region has end_at_word = None, is_well_paired() = false, but top_onset_fraction() still works.

Cross-utterance — analyze_file_overlaps

For whole-file analysis, in overlap_groups.rs. 1:N matching: one top region from speaker A can match multiple bottom regions from speakers B, C, etc. Used by E347, the FA pipeline (CA marker-aware UTR pass-2 search windows), and chatter debug overlap-audit.

Overlap validation

CodeLevelCheck
E347Cross-utteranceOrphaned tops/bottoms with 1:N matching (warning)
E348UtteranceUnpaired markers within a single utterance (warning)
E373UtteranceInvalid overlap index values (must be 2–9)
E704Cross-utteranceSame speaker encoding both top and bottom (error)

chatter debug overlap-audit <path> reports per-file statistics (groups, bottoms, orphans, temporal consistency) in TSV format. Use --database <path.jsonl> for a persistent JSON-lines database.

Design Principles

  1. No string hacking. All alignment operates on typed AST structures (Word, MorTier, AlignmentPair), never on serialized CHAT text.
  2. Domain-aware from the start. TierDomain gates traversal at the walker level. Downstream code never re-implements retrace / group skipping logic.
  3. Deterministic over approximate. Runtime alignment (FA injection, retokenize, tokenizer realign) uses deterministic algorithms. DP is reserved for intrinsically approximate problems (WER, CTC FA).
  4. Dense indexed structures. WordTokenMapping uses Vec<SmallVec> instead of HashMap. AlignmentPair uses Option<usize> rather than cloned data.
  5. Exhaustive matching. Every match on UtteranceContent (24 variants) or BracketedItem (22 variants) lists all variants explicitly. New variants are a compile error, not a silent bug.
  6. Walker as shared primitive. walk_words() removed ~330 lines of duplicated traversal boilerplate across 7 call sites.

Downstream Consumers

ConsumerCrateUsage
Validationtalkbank-modelCross-tier checks (E714/E715, E725–E728), overlap (E347/E348/E373/E704)
LSP hoverchatter-lspShow aligned tier items for word under cursor
Word extractiontalkbank-transform / batchalignNLP-ready words from utterances
FA injectionbatchalignInsert timing bullets into AST
Overlap windowingbatchalignCA marker-aware UTR pass-2 search windows
Overlap auditchatter-clichatter debug overlap-audit
%wor generationtalkbank-modelBuild %wor tier from main tier
CLAN commandsclan-coreDSS, EVAL, KIDEVAL via typed %mor access

Memory and Ownership

Status: Current Last updated: 2026-03-24 01:32 EDT

This chapter documents the memory management and ownership patterns used across the TalkBank Rust crates. Understanding these decisions helps contributors make consistent choices when adding new code.

String Representation Strategy

CHAT corpora contain massive repetition — the same speaker codes, language codes, POS tags, and high-frequency words appear millions of times across files. The codebase uses three string types, chosen by expected cardinality and duplication:

flowchart LR
    raw["Raw input (&amp;str)"]
    smol["SmolStr\n(inline ≤23 bytes)"]
    arc["Arc&lt;str&gt;\n(interned, deduplicated)"]
    string["String\n(owned, unique)"]

    raw -->|short, low repetition| smol
    raw -->|high repetition domain value| arc
    raw -->|ephemeral/unique| string
TypeWhen to useExamples
SmolStrShort tokens, low duplicationPostcode text, tier content, event labels
Arc<str> (interned)High-cardinality domain symbolsSpeaker codes, language codes, POS tags, stems
StringEphemeral or unique valuesError messages, temporary formatting

String Interning

Location: talkbank-model/src/model/intern.rs

Five global process-local interners, each a DashMap<Arc<str>, Arc<str>> behind OnceLock<StringInterner>:

InternerPre-seeded valuesTypical savings
speaker_interner()30+ codes (CHI, MOT, FAT, …)High — 3-letter codes repeat per utterance
language_interner()45+ ISO 639-3 codesModerate — per-file
pos_interner()60+ POS tags + UD relationsVery high — every %mor word
stem_interner()200+ frequent English stemsHigh — function words dominate
participant_interner()14 roles (Target_Child, …)Low — per-file

How it works:

  • Fast path: get() on DashMap, O(1) Arc::clone if found
  • Slow path: insert() new Arc if miss, deduplicates on future access
  • Thread-safe: DashMap uses shard-level locks, no global contention
  • After initialization, reads are lock-free

Memory impact: 50–200 MB savings on large corpora (5–20% reduction). Arc::clone is O(1) atomic increment vs String::clone O(n) copy.

Newtype Macros

Two macros generate domain-typed string wrappers:

  • string_newtype! — wraps SmolStr. Used for generic CHAT text.
  • interned_newtype! — wraps Arc<str> with automatic interning. Used for domain symbols.
// SmolStr-backed: no interning, inline small strings
string_newtype!(PostcodeText);

// Arc<str>-backed: interned via global interner
interned_newtype!(SpeakerCode, speaker_interner);

Ownership Model

ChatFile Lifecycle

flowchart TD
    src["Source text (&amp;str)"]
    cst["tree-sitter CST\n(Tree, borrowed nodes)"]
    model["ChatFile\n(owned AST)"]
    cache["SQLite cache\n(validation result)"]
    lsp["LSP server\n(per-document state)"]
    json["JSON output\n(serde serialization)"]
    cli["CLI output\n(CHAT text)"]

    src -->|tree-sitter parse| cst
    cst -->|CST-to-model conversion| model
    model -->|validate + hash| cache
    model -->|held in backend| lsp
    model -->|to_json()| json
    model -->|to_chat_string()| cli
  • Parsing: tree-sitter Tree owns the CST. Node<'a> values borrow from Tree — zero-copy traversal. The CST-to-model conversion copies data into owned ChatFile fields (SmolStr, Arc<str>). The Tree is dropped after conversion.
  • Validation: ChatFile is borrowed (&self) during validation. Errors are streamed to an ErrorSink — no accumulation required.
  • LSP: Each open document holds an owned ChatFile in the backend. Re-parsed on every edit via tree-sitter incremental parsing.
  • CLI batch: Each file is independently parsed → validated → reported → dropped. No cross-file state except the shared cache.

Arc Usage

Arc appears in three distinct roles:

RoleTypeWhy
String interningArc<str> in model typesO(1) clone for high-repetition domain values
Worker poolArc<WorkerGroup> in batchalignRAII CheckedOutWorker::drop() needs group reference to return worker
Cache backendArc<dyn CacheBackend> in batchalignShared across async request handlers

No Rc (single-threaded sharing not needed). No Cow<str> (SmolStr covers the inline-small-string use case more naturally).

Interior Mutability

PatternWhereWhat it protects
RefCell<Parser> inside TreeSitterParsertalkbank-parserTree-sitter Parser needs &mut self but isn’t Sync. Callers create a TreeSitterParser and pass &TreeSitterParser everywhere.
DashMap<Arc<str>, Arc<str>>String internersConcurrent interning during parallel parsing. Shard-level locks.
OnceLock<StringInterner>5 global internersLazy init, lock-free after first access
LazyLock<Regex>All regex patterns workspace-wideCompile-once, no per-call overhead
std::sync::Mutex<VecDeque>batchalign worker idle queueHeld < 10 μs for push/pop only
tokio::sync::Mutex<HashMap>batchalign job storeShort reads/writes, never held across .await
SemaphoreWorker availability (batchalign)Async signaling without holding locks during dispatch

Rule: std::sync::Mutex for data accessed from sync code or held briefly. tokio::sync::Mutex only when the lock must be held across .await points (which we avoid when possible). DashMap when many threads read concurrently.

Collection Choices

CollectionWhereWhy not HashMap/Vec
BTreeMapAll test/snapshot JSON outputDeterministic key ordering for reviewable diffs
IndexMapParticipants, per-speaker resultsPreserves encounter order (CHAT spec requires @Participants order)
SmallVec<[T; N]>Headers (N=2), tiers (N=3), features (N=4), token mappings (N=4)Inline storage for common sizes; avoids heap for typical cases
VecDequeWorker idle queue (batchalign)FIFO fair scheduling
Dense Vec indexed by positionRetokenize word-to-token mappingO(1) lookup, no hashing overhead, cache-friendly

No LinkedList, BinaryHeap, or custom allocators.

Tree-Sitter Memory Model

Tree-sitter parsing is zero-copy for CST traversal:

// Node<'a> borrows from Tree — no allocation per node
fn process_node<'a>(node: Node<'a>, source: &str) -> ParseResult<...> {
    for i in 0..node.child_count() {
        let child: Node<'a> = node.child(i).unwrap(); // Stack-only, no heap
        let text: &str = child.utf8_text(source.as_bytes())?; // Borrows source
        // ... convert to owned model types ...
    }
}

The tree-sitter parser consumes &str, produces a CST, and the Rust traversal code constructs owned model types from CST nodes.

SQLite Memory-Mapped I/O

The validation cache uses SQLite with memory-mapped I/O for fast random access:

SqliteConnectOptions::new()
    .journal_mode(SqliteJournalMode::Wal)       // Concurrent reads during writes
    .pragma("cache_size", "-8000")               // 8 MB page cache
    .pragma("mmap_size", "268435456")            // 256 MB memory-mapped region
    .synchronous(SqliteSynchronous::Normal)      // Balanced durability

This configuration handles 95,000+ cached entries efficiently. The cache is never deleted (use --force to refresh specific paths).

Manual Drop Implementations

Three types have custom Drop for resource cleanup:

TypeCleanup actionWhy
AuditReporterJoins audit writer thread and flushes outputAudit mode owns file IO in a dedicated writer thread
CheckedOutWorkerReturns worker to idle queue + releases semaphore permitRAII pool resource management
WorkerHandleSends SIGTERM/SIGKILL to child processProcess must be terminated when handle drops

All drops are acyclic — no ordering dependencies between them.

Allocation Optimization Patterns

Rather than using an arena allocator (bumpalo was evaluated and removed — the data lifetimes don’t fit the “allocate many, free all at once” pattern), the codebase uses targeted optimizations:

PatternWhereSavings
Scratch buffer reuse (clear + swap)DP alignment row costs~50% fewer allocations in inner loop
Flat table (vec![...; rows * cols])DP small-problem fallback1 allocation vs rows+1
Dense Vec instead of HashMapRetokenize word mappingO(1) lookup, no hash overhead
SmallVec inline storageThroughoutAvoids heap for 1–4 element collections
SmolStr inline stringsAll short CHAT tokensNo heap allocation for ≤23 byte strings

See also: the batchalign book’s Arena Allocators page for the full evaluation of where arenas do and don’t help.

Algorithms and Data Structures

Status: Current Last updated: 2026-03-24 01:32 EDT

This chapter documents the key algorithms and data structure decisions across the TalkBank Rust crates.

CHAT AST Representation

The CHAT model is a tree of owned enums. The two central types are:

  • UtteranceContent — 24 variants covering all main-tier content
  • BracketedItem — 22 variants for content inside groups/brackets
flowchart TD
    file["ChatFile"]
    header["Headers\n(@Languages, @Participants, ...)"]
    utt["Utterance"]
    mc["MainContent\nVec&lt;UtteranceContent&gt;"]
    dt["DependentTiers\n(%mor, %pho, %gra, ...)"]

    file --> header
    file --> utt
    utt --> mc
    utt --> dt

    mc --> word["Word / AnnotatedWord / ReplacedWord"]
    mc --> group["Group / PhoGroup / SinGroup / Quotation"]
    mc --> marker["Pause / Separator / OverlapPoint / ..."]
    group --> bi["BracketedContent\nVec&lt;BracketedItem&gt;"]
    bi --> word2["Word / ReplacedWord / Separator"]
    bi --> nested["Nested groups"]

Memory layout: Large variants (e.g., AnnotatedWord with scoped annotations) are Boxed to keep the enum’s stack size bounded.

Content Walker

Location: talkbank-model/src/alignment/helpers/walk/

Closure-based recursive traversal centralizing the walk over all 24+22 variants:

pub fn for_each_leaf<'a>(
    content: &'a [UtteranceContent],
    domain: Option<AlignmentDomain>,
    f: &mut impl FnMut(ContentLeaf<'a>),
)

Domain-aware gating:

  • Some(Mor) — skips retrace groups (retrace words aren’t morphologically analyzed)
  • Some(Pho | Sin) — skips PhoGroup/SinGroup (treated as atomic by those tiers)
  • None — recurses everything unconditionally

Both immutable (for_each_leaf) and mutable (for_each_leaf_mut) versions exist. Used by talkbank-model (%wor generation) and batchalign (word extraction, FA injection, postprocessing).

Parsing Strategies

Tree-Sitter (Canonical Parser)

flowchart LR
    src["Source .cha text"]
    ts["tree-sitter C parser\n(generated from grammar.js)"]
    cst["CST (Tree)"]
    conv["Recursive descent\nover CST nodes"]
    model["ChatFile (owned AST)"]
    errors["ErrorSink\n(diagnostics)"]

    src --> ts --> cst --> conv --> model
    conv --> errors
  • Grammar defined in grammar/grammar.js (source of truth)
  • parser.c is generated — never edit directly
  • CST-to-model conversion: recursive dispatch on node kind, skip WHITESPACES, report unrecognized nodes via ErrorSink
  • Strict + catch-all pattern: Known header values get named grammar rules (syntax highlighting); unknown values hit a catch-all (flagged by validator)

Fragment Parsing

TreeSitterParser provides fragment methods for parsing individual CHAT fragments (a word, a tier line) directly. Methods like parser.parse_word_fragment(), parser.parse_main_tier_fragment(), etc. are used when synthesizing CHAT from non-CHAT sources (ASR output, UD annotations).

Historical note: A Chumsky-based direct parser previously provided combinator-based fragment parsing. It was removed in March 2026; tree-sitter is now the sole parser.

Sequence Alignment

Hirschberg (Linear-Space Edit Distance)

Location: batchalign/src/dp_align.rs

PropertyValue
AlgorithmHirschberg divide-and-conquer
TimeO(mn)
SpaceO(min(m,n)) — linear
Cost modelMatch=0, Substitution=2, Gap=1

Optimizations:

  1. Prefix/suffix stripping — O(n) pre-pass removes identical leading/trailing elements. For WER (80–95% accuracy), reduces effective DP size by 10–100x.
  2. Small-problem cutoff (2048 elements) — switches to full flat DP table to avoid recursion overhead.
  3. Scratch buffer reuse — clear() + swap() avoids allocation per DP row.
  4. Generic Alignable trait — monomorphized for both String (word-level) and char (character-level) without code duplication.

Entry points: align() for word sequences, align_chars() for character-level. Match modes: Exact (byte equality), CaseInsensitive (ASCII case folding).

Tier Alignment (1:1 Positional)

Location: talkbank-model/src/alignment/traits.rs

Generic positional_align() pairs main-tier words with dependent-tier items by position (O(n)). Traits: AlignableTier, TierAlignmentResult, AlignableContent.

  • %pho, %sin, %wor — use generic positional alignment
  • %mor, %gra — domain-specific custom implementations
  • Mismatch diagnostics via similar crate (Patience diff algorithm, O(n log n))

Caching

The CHAT-core validation cache is documented separately in Validation Cache. The Batchalign audio-task cache (FA / UTR ASR / media conversion) is in Audio-Task Cache.

CLAN Analysis Algorithms

VOCD (Vocabulary Diversity)

Location: clan-core/src/commands/vocd.rs

Bootstrap sampling + least-squares curve fitting:

  1. For each of 3 trials: for each sample size N in [35..50]: draw 100 random samples of N tokens → compute mean TTR
  2. Fit empirical (N, TTR) curve to theoretical TTR(N) = (D/N)[√(1+2N/D) − 1] via gradient descent (step 0.001)
  3. Average D across trials

DSS (Developmental Sentence Scoring)

Location: clan-core/src/commands/dss.rs

Rule-based pattern matching on %mor POS tags. 10-category scoring (copula, conjunction, personal pronouns, etc.), 1–3 points per category per utterance, averaged across up to 50 utterances per speaker.

IPSyn (Index of Productive Syntax)

Location: clan-core/src/commands/ipsyn.rs

Syntactic structure coverage scoring: 56 rules across 4 categories (N, V, Q, S). Each rule earns max 2 points (discovery + variety). Total range: 0–112.

Text Processing

Regex Compilation

All regex patterns use LazyLock<Regex> from std::sync — compiled once at first use, lock-free thereafter. Never call Regex::new() inside functions or loops.

WER Word Conforming

Location: batchalign/src/wer_conform.rs

Priority-ordered 10-step rule chain for word normalization before WER comparison:

  1. Compound splitting (O(1) hash lookup, 3,584 pairs)
  2. Abbreviation letter expansion
  3. Contraction expansion ('s → is, etc.)
  4. Filler normalization (um, uhm, eh → um)
  5. Hyphen splitting
  6. Special word expansion (gonna → going to)
  7. Name replacement (6,700 names → "name")
  8. Acronym expansion
  9. Underscore splitting
  10. Passthrough (lowercased)

Deterministic Output

  • BTreeMap for all test/snapshot JSON (lexicographic key ordering)
  • IndexMap for participant/speaker ordering (preserves encounter order per spec)
  • Frequency results collected into BTreeMap<NormalizedWord, Count>

Pipeline Dependency Resolution (batchalign)

Location: batchalign/src/pipeline/plan.rs

DAG-based greedy execution of 16 pipeline stages. Each stage declares dependencies; enabled stages execute when all deps complete. Cycle/deadlock detection if no progress is made in a pass. No external topological sort library — the stage count is small enough for inline scheduling.

Concurrency

Status: Current Last updated: 2026-05-01 17:07 EDT

Shared concurrency infrastructure across the workspace: Tokio runtime configuration, the Semaphore + RAII pool pattern, channel patterns, lock ordering, and the workspace-wide mutex policy. For batchalign-specific worker-pool sizing, memory budgets, and model loading, see Batchalign Workers.

Two Threading Worlds

The workspace splits cleanly into two concurrency styles:

flowchart TD
    subgraph "talkbank-tools (CHAT core)"
        cli["chatter CLI"]
        workers["std::thread workers\n(crossbeam channels)"]
        cache["CachePool\n(embedded tokio rt)"]
        lsp["chatter-lsp\n(tokio multi-thread)"]
        cli --> workers
        workers --> cache
    end

    subgraph "Batchalign runtime"
        server["axum HTTP server\n(tokio multi-thread)"]
        pool["WorkerPool\n(Semaphore + RAII checkout)"]
        python["Python worker processes\n(stdio JSON-lines IPC)"]
        ws["WebSocket\n(broadcast channel)"]
        server --> pool --> python
        server --> ws
    end
  • CHAT core — CPU-bound validation with std::thread + crossbeam channels.
  • Batchalign runtime — I/O-bound server with the tokio async runtime.

Tokio Runtime Configuration

ComponentRuntimeWhy
batchalign serverMulti-thread (default)Concurrent HTTP + WebSocket + job tasks
chatter-lspMulti-threadConcurrent LSP requests from editor
Validation CachePoolcurrent_thread (embedded)Bridge for sync workers; minimal overhead

Validation Parallelism (CHAT core)

chatter-cli/src/commands/validate_parallel/{runtime.rs,audit.rs}.

flowchart LR
    disc["File discovery\n(walkdir)"]
    queue["crossbeam bounded channel\n(capacity = num_files)"]
    w1["Worker thread 1"]
    w2["Worker thread 2"]
    wn["Worker thread N"]
    events["crossbeam unbounded channel\n(ValidationEvent stream)"]
    ui["UI thread\n(progress bars / TUI)"]

    disc --> queue
    queue --> w1 & w2 & wn
    w1 & w2 & wn --> events --> ui
  • N worker threads via std::thread::spawn() (default num_cpus::get()).
  • Workers pull paths from a bounded channel, parse + validate, emit events.
  • UI thread receives events for progress display.
  • No Rayon — crossbeam channels give fine-grained cancellation control.

Audit mode follows the same worker model, but its output path is intentionally separate from the renderer/event loop used by standard validation. Workers send completed file results to a dedicated audit writer thread via a bounded channel; the writer thread owns both JSONL file IO and summary statistics, so workers do not contend on a shared Mutex<BufWriter<File>>.

Cancellation

First Ctrl+C  → send to cancel channel → workers check every 1-10 files
Second Ctrl+C → std::process::exit(130) (immediate, no cleanup)

Workers poll cancel_rx.try_recv() between files. Atomic counter tracks Ctrl+C presses.

Cache Bridge — sync workers, async SQLite

Validation workers are std::thread, but the validation cache uses async sqlx. CachePool embeds a single-threaded tokio runtime to bridge:

pub struct CachePool {
    pool: SqlitePool,              // Async pool
    rt: tokio::runtime::Runtime,   // Single-threaded, embedded
}

impl CachePool {
    fn get(&self, path: &str) -> Option<CacheEntry> {
        self.rt.block_on(async { /* sqlx query */ })
    }
}

Each sync worker calls rt.block_on(). The embedded runtime is lightweight (single-threaded, no background tasks).

The Semaphore + RAII Pool Pattern

The canonical pool primitive used by the Batchalign worker pool. Pattern:

flowchart TD
    req["Job dispatch request"]
    sem["Semaphore\n(permits = idle workers)"]
    queue["Mutex&lt;VecDeque&gt;\n(idle queue)"]
    guard["CheckedOutWorker\n(RAII guard)"]
    work["Process files\n(10–300 seconds)"]
    ret["Drop guard\n→ return to queue\n→ release permit"]

    req -->|acquire permit| sem
    sem -->|pop worker| queue
    queue --> guard --> work --> ret
    ret -->|push worker| queue
    ret -->|add permit| sem

Key design point: No lock is held during the 10–300 second dispatch. The semaphore signals worker availability; the mutex protects only the fast push/pop (< 10 μs).

Checkout flow:

  1. Try non-blocking semaphore acquire.
  2. If none available → try spawning new worker (CAS on AtomicUsize total).
  3. If at capacity → async wait for permit.
  4. Pop from idle queue, wrap in CheckedOutWorker.

Return flow (Drop impl):

  • Push worker back to idle queue.
  • Add semaphore permit (wakes next waiter).
  • If worker died (take()), decrement total instead.

Channels

ChannelTypePurposeCapacity
Job events → WebSocket clientstokio::sync::broadcastFan-out to all connected browsers4096
Error streamingmpscAsync error sink for validationUnbounded
Queue work signalNotifyWake dispatcher when work arrivesn/a

Broadcast pattern. Lagged clients receive RecvError::Lagged(n) and skip messages — no backpressure on the server. Clients that disconnect break the receive loop cleanly.

CancellationToken (tokio_util::sync)

Used at three levels:

  • Per-job — checked before/after semaphore acquire and between files.
  • Worker pool — cancels background health-check tasks.
  • Server — wired to SIGINT/SIGTERM for graceful shutdown.
if job.cancel_token.is_cancelled() {
    return Ok(());
}

select! Patterns

LocationBranchesPurpose
Server shutdownctrl_c / SIGTERMWhichever signal fires first triggers shutdown
WebSocket handlerbroadcast.recv() / socket.recv()Forward events OR handle client messages
Health check loopcancel.cancelled() / interval.tick()Stop OR check worker health
Queue dispatchernotify.notified() / sleepWork arrived OR timeout

IPC — stdio JSON-lines

Protocol between the Rust server and Python workers:

sequenceDiagram
    participant Server as Rust Server
    participant Worker as Python Worker

    Server->>Worker: spawn: python -m batchalign.worker --command morphotag --lang eng
    Worker->>Server: {"ready": true, "pid": 1234, "transport": "stdio"}

    loop Request/Response
        Server->>Worker: {"op": "batch_infer", "task": "morphosyntax", ...}\n
        Worker->>Server: {"result": [...], "status": "ok"}\n
    end

    Server->>Worker: {"op": "shutdown"}\n
    Worker->>Server: {"status": "ok"}\n
  • One JSON object per line (newline-delimited).
  • No framing bytes — newline = message boundary.
  • Worker startup loads models, prints ready message.
  • Operations: process, infer, batch_infer, health, capabilities, shutdown.

For the wire-protocol contract details, see Python–Rust Boundary.

Database Concurrency

SQLite WAL configuration:

SettingValueWhy
Journal modeWALReaders don’t block writers
SynchronousNormalBalanced durability vs speed
Busy timeout5000 msAuto-retry on SQLITE_BUSY
Max connections16Matches worker thread count
mmap size256 MBFast random access for 95k+ entries

Lock Ordering

No formal documented hierarchy, but the observed invariants are:

  1. Worker pool — semaphore acquired first (async) → then idle queue mutex (sync, < 10 μs). Never reversed.
  2. Job store — HashMap lock acquired for short reads/writes only. Never nested with worker pool locks.
  3. DashMap (string interner, media cache) — single-level, no nested locks.
  4. All tokio::sync::Mutex guards are dropped before .await points.

Deadlock prevention: No cyclic lock dependencies. Longest lock hold is the job semaphore (intentional backpressure, not a mutex).

Signal Handling

Server (Batchalign)

tokio::select! {
    () = signal::ctrl_c() => info!("SIGINT, shutting down"),
    () = sigterm_future   => info!("SIGTERM, shutting down"),
}

CLI (CHAT-core)

Double Ctrl+C pattern — first press: graceful cancel via crossbeam channel; second press: std::process::exit(130) (immediate).

Desktop App (Tauri)

apps/chatter/chatter-gui/src-tauri/src/commands.rs uses ArcSwapOption (from arc-swap) for lock-free storage of the cancel sender:

pub struct ValidationState {
    cancel_tx: ArcSwapOption<Sender<()>>,
}
  • validate() atomically stores the cancel sender via .store(Some(...)).
  • cancel_validation() atomically takes it via .swap(None).
  • Zero contention: no mutex, no lock, no blocking.

Event forwarding uses the same crossbeam channel pattern as the TUI: validate_directory_streaming() returns a Receiver<ValidationEvent>, and a dedicated thread forwards events to the Tauri frontend via app.emit().

Mutex Policy

Avoid Mutex wherever possible. Use lock-free alternatives:

NeedUseNot
Atomic swap of optional valueArcSwapOptionMutex<Option<T>>
Concurrent mapDashMapMutex<HashMap>
Lock-free counterAtomicUsize / AtomicBoolMutex<usize>
Lazy initializationOnceLock / LazyLockMutex<Option<T>>
Work distributioncrossbeam_channelMutex<VecDeque>
Async event fan-outtokio::sync::broadcastshared vec behind mutex
Async availability gatetokio::sync::Semaphoremutex-guarded counter
One-shot signalcrossbeam_channel / tokio::sync::oneshotmutex-guarded bool

When Mutex is acceptable: sub-microsecond critical sections (push/pop on a VecDeque, single HashMap lookup) that never cross an .await point. Document the justification in a code comment.

Current Mutex inventory

LocationTypeFieldJustification
talkbank-model::ErrorCollectorparking_lot::Mutexerrors: Mutex<Option<Vec<ParseError>>>Parser error collection; held for push() only (~1 μs)
batchalign::WorkerGroupstd::sync::Mutexidle: VecDeque<WorkerHandle>push/pop (~1 μs), never across .await
batchalign::WorkerPoolstd::sync::Mutexgroups: HashMap<WorkerKey, Arc<WorkerGroup>>lookup (~1 μs), never across .await
batchalign::WorkerGrouptokio::sync::Mutexbootstrap: Mutex<()>Serializes worker spawning (held across .await; tokio::sync required)
batchalign::JobRegistrytokio::sync::Mutexjobs: HashMap<JobId, Job>State transitions held across .await

No RwLock usage anywhere. No std::sync::Mutex held across .await points.

Design Rationale

DecisionWhy
crossbeam channels, not RayonFine-grained cancellation polling; simpler progress UI integration
Embedded single-threaded tokio in CachePoolSync workers can’t be tokio tasks; lightweight bridge
RAII CheckedOutWorker + SemaphoreEliminates 10–300 s mutex holds; permits track availability, not workers
broadcast for WebSocketFan-out; lagged clients skip gracefully
DashMap for string interningShard-level locks; concurrent safe interning without global contention
WAL + 5 s busy timeoutMultiple concurrent readers + writers; auto-retry prevents failures
std::sync::Mutex for worker queueHeld < 10 μs; no need for async-aware lock
ArcSwapOption for desktop cancel senderLock-free atomic swap; no contention

Performance Optimizations

Status: Current Last updated: 2026-05-01 17:07 EDT

Performance-critical patterns shipped in talkbank-tools and the batchalign runtime. Each entry describes what runs in production and where, not the alternatives that were rejected.

Word::cleaned_text() — OnceLock<SmolStr> Cache

Word carries a lazy cache (CachedStr) wrapping OnceLock<SmolStr>. The first call to cleaned_text() iterates the word’s content (concatenating Text and Shortening variants) and stores the result; subsequent calls return &str from the cache.

Most CHAT words fit in SmolStr’s inline buffer (≤ 23 bytes), so the common case has zero heap allocation. Without the cache, cleaned_text() was the single largest allocation hotspot — called 2–4× per word during validation and alignment, across millions of words.

The CachedStr newtype:

  • Has a custom PartialEq that always returns true, so the cache field never affects equality checks or test assertions.
  • Carries serde(skip), schemars(skip), semantic_eq(skip), span_shift(skip) attributes — excluded from serialization, JSON schema, semantic equality, and span-shift derive macros.

Return type is &str (not String); callers that need an owned copy add .to_string() explicitly.

crates/talkbank-model/src/model/content/word/types.rs.

ValidationContext — Arc<SharedValidationData> Split

ValidationContext is split into:

  • Seven file-level-constant fields wrapped in Arc<SharedValidationData>: participant_ids, default_language, declared_languages, ca_mode, enable_quotation_validation, bullets_mode, config.
  • Five per-tier mutable overlay fields kept inline: tier_language, field_span, field_text, field_label, field_error_code.

Cloning a ValidationContext now copies the Arc pointer (8 bytes) + 5 small overlay fields, instead of deep-cloning a HashSet, two Vecs, and a ValidationConfig. The struct is cloned 3+ times per utterance (main / word / dependent tiers) and again per word.

Builder methods use Arc::make_mut() for shared fields (copy-on-write if the Arc is shared, no-op if unique). The builder API is unchanged; the Arc only matters once construction is complete.

crates/talkbank-model/src/validation/context.rs plus the validators that read shared fields (context.shared.participant_ids, not context.participant_ids).

LSP DashMap<Url, Arc<ChatFile>>

LSP request handlers (hover, completion, symbols, folding, inlay hints, formatting — ~10 handlers) get Arc::clone() of the ChatFile rather than holding a DashMap shard guard. The shard lock is released immediately after the pointer copy.

The mutation path (validation orchestrator) needs an owned ChatFile for alignment computation. It extracts the old file via ChatFile::clone(entry.value()) — a deep clone — but this only happens on file change (debounced), not on every request. After mutation, the new ChatFile is wrapped in Arc::new() and inserted atomically.

crates/chatter-lsp/src/backend/state.rs, requests.rs, validation_orchestrator.rs.

Reusable TreeSitterParser

TreeSitterParser is expensive to create (~1 ms for tree-sitter init). Callers create one instance and pass &TreeSitterParser through every operation. For batch workflows, parse_and_validate_with_parser() accepts &TreeSitterParser to avoid per-call construction overhead.

SQLite Pragmas (Validation Cache)

The validation cache opens connections with:

synchronous = NORMAL
cache_size  = -8000  (8 MB)
busy_timeout = 5000
mmap_size   = 268435456  (256 MB)

Combined ~2–3× write-throughput improvement with no durability risk — WAL mode is already crash-safe.

Per-Thread SQLite Connections

CachePool uses thread_local::ThreadLocal<CacheConnection> for its SQLite handles. Each worker thread lazily opens its own connection. WAL mode handles concurrency natively — concurrent readers, serialized writers with busy_timeout retry. No Mutex anywhere; cache hits are contention-free.

Pass/Fail-Only Cache Schema

The validation cache stores a single boolean per file (valid / invalid). The legacy schema’s errors table (10+ columns per error including miette-rendered diagnostics) and corpora table are gone. Cache writes no longer serialize ParseError payloads; DB size on a 95k-file corpus dropped from ~100 MB to ~2 MB.

Release Profile

Both Rust workspaces (talkbank-tools, tb) compile release binaries with:

[profile.release]
lto = true              # Fat LTO — whole-program optimization
codegen-units = 1       # Best cross-crate inlining
strip = true            # Strip symbols from deployed binaries

Net ~15% throughput improvement for CLI validation. Deploy scripts use --release so these settings apply to fleet binaries automatically; dev builds are unaffected.

LSP LineIndex

offset_to_position is O(log n) via a pre-computed line-offset index and binary search, replacing the original O(n) scan. This eliminates O(n²) behavior for semantic tokens on large files.

LSP did_save Skip

LSP skips re-validation on save if the document hasn’t changed since the last debounced did_change validation.

Audit-Mode Parallelization

run_audit_mode() uses crossbeam worker threads with a bounded work-stealing channel, matching the regular validation path. Workers report completed file results to a dedicated audit-writer thread, which writes JSONL results to disk immediately and owns the summary counters. No worker contends on a shared Mutex<BufWriter<File>>.

Roundtrip — Parse-Only

run_roundtrip() re-parses the serialized output with ParseValidateOptions::default() (parse only, no validation). Roundtrip checks serialization fidelity, not content validity — the caller already ensures validation passed before invoking roundtrip.

Prefix/Suffix Stripping in DP Alignment

The Hirschberg align() strips matching prefixes and suffixes in O(n) before entering the O(mn) DP core. For WER and transcript comparison (typically 80–95% accuracy), this reduces effective DP problem size 10–100×. Only the differing middle portion enters recursion. See Dynamic Programming.

Lazy Worker Bootstrap (Batchalign)

The Batchalign daemon binds the TCP port immediately, with /health available, but no Python process runs until the first job arrives. The daemon starts in < 1 second and uses zero memory at idle. Memory guards only fire when actual work is requested. See Batchalign Workers — Memory Check Flow.