Skip to content

Governed Chat Command Language

2026-03-16 — Design spec for slash commands, constraint bar UI, and state model


Problem

The governed chat dashboard has a hardcoded constraint UI: 5 phoneme toggle buttons, 2 norm sliders (AoA, concreteness), and number inputs for WCM/syllables. The governor engine supports 9 constraint types, 35+ norms, and mechanisms (gates, boosts, projections) that have no UI surface at all.

Adding widgets for every constraint type doesn't scale and creates a cluttered interface. We need a composable, extensible interface that lets users build arbitrary constraint sets — from simple single-phoneme exclusion to complex multi-constraint clinical profiles.

Solution

A slash command language in the chat input. Commands modify the active constraint set without counting as chat turns. The constraint state is visible as a pinned chip bar above the chat. Commands and chips are two views of the same state — dismissing a chip is equivalent to typing /remove.

Design principles

  • Commands are the source of truth. The command vocabulary defines the full constraint surface. UI widgets are a curated subset.
  • Accumulate, don't replace. Each command adds to or modifies the active set. Constraints persist until explicitly removed or cleared.
  • Be forgiving on input, strict on confirmation. Accept ASCII r and normalize to IPA /ɹ/. Show exactly what was applied in the confirmation.
  • Profiles dissolve. Loading a profile populates the constraint store. From that point, the store is the truth — no "editing profile X" state.

Command Taxonomy

Approach

Domain verbs: each constraint type IS the command name. One universal undo verb (/remove). Minimal meta commands (/clear, /show, /help).

Phonological commands

Command Syntax Engine constraint Example
/exclude /exclude <phoneme>... Exclude /exclude ɹ s
/exclude-clusters /exclude-clusters <phoneme>... ExcludeInClusters /exclude-clusters s
/include /include <phoneme>... [N%] Include (boost) / Coverage (projection) /include k 20%
/boost /boost minpair <t> <c> [strength] MinPairBoost /boost minpair s ʃ
/boost /boost maxopp <t> <c> [strength] MaxOppositionBoost /boost maxopp s m

Psycholinguistic commands (named norms)

Each named norm has a default direction (the clinically typical bound). Direction is always optional — when omitted, the default applies. Can be overridden explicitly.

Command Default direction Norm key Example
/aoa max aoa_kuperman /aoa 5 or /aoa max 5
/concreteness min concreteness /concreteness 3.5 or /concreteness min 3.5
/valence min valence /valence 6 or /valence min 6
/arousal max arousal /arousal 3 or /arousal max 3
/imageability min imageability /imageability 5 or /imageability min 5
/familiarity min familiarity /familiarity 4 or /familiarity min 4
/frequency min log_frequency /frequency 3 or /frequency min 3

All named norm commands compile to Bound(norm=<key>, min|max=<value>). When a Bound constraint is added, a NormCovered gate is automatically composed alongside it at compile time to prevent tokens lacking norm data from slipping through. NormCovered is implicit — it never appears in the store, is not visible as a chip, and cannot be /removed. When multiple bounds share norms, compile-time merges their coverage sets into one NormCovered gate. Removing a bound removes its norm from the coverage set automatically.

Generic norm command

For power users — any norm key in the lookup is valid:

/bound <norm_key> <min|max> <number>

Example: /bound dominance min 5 or /bound socialness min 4

Structural commands

Command Syntax Engine constraint Example
/complexity /complexity wcm max <N> Complexity(max_wcm=N) /complexity wcm max 8
/complexity /complexity syllables max <N> Complexity(max_syllables=N) /complexity syllables max 2
/complexity /complexity shapes <SHAPE>... Complexity(allowed_shapes=[...]) /complexity shapes CV CVC
/msh /msh <1-5> MSHStage(max_stage=N) /msh 3

Meta commands

Command Syntax What it does
/remove /remove <command> [args] Remove a specific constraint
/clear /clear Remove all constraints
/show /show Print active constraints in chat (for sharing, copy-paste, or accessibility)
/help /help [command] List commands or show syntax for one

Boost defaults

/boost minpair s ʃ uses a default strength of 2.0 (matching the engine default). Valid range: 0.5–5.0. Fractional values accepted. Higher values bias more aggressively toward target pairs.

/include k uses a default strength of 2.0. /include k 20% adjusts strength dynamically based on running coverage — the user-facing parameter is the target rate, not the strength.

Command scope

Commands apply in both chat and generate modes. In generate mode, the constraint store state is used for the single generation request. The command input works the same way in both modes.

Each input line is one command. Multi-command input is not supported — type each command separately. This keeps parsing simple and confirmations clear (one system message per command).

Future / backburner

Command Status Notes
/density Subsumed Density engine constraint is reachable via /include <phoneme> N% (coverage target). No separate command needed.
/vocab Backburner VocabOnly — bad generation results, needs research
/theme Future Association graph boost — engine not built yet
/temp, /tokens Future Generation parameter control

Grammar

command     := "/" verb args
verb        := "exclude" | "exclude-clusters" | "include" | "complexity"
             | "msh" | "aoa" | "concreteness" | "valence" | "arousal"
             | "imageability" | "familiarity" | "frequency" | "bound"
             | "boost" | "remove" | "clear" | "show" | "help"

# Phoneme commands
exclude          := /exclude <phoneme>+
exclude-clusters := /exclude-clusters <phoneme>+
include          := /include <phoneme>+ [<number>%]

# Norm commands (named — direction optional, defaults applied)
aoa              := /aoa [max] <number>
concreteness     := /concreteness [min] <number>
valence          := /valence [min] <number>
arousal          := /arousal [max] <number>
imageability     := /imageability [min] <number>
familiarity      := /familiarity [min] <number>
frequency        := /frequency [min] <number>

# Norm command (generic — direction required)
bound            := /bound <norm_key> <min|max> <number>

# Structural
complexity       := /complexity <wcm max N | syllables max N | shapes SHAPE+>
msh              := /msh <1-5>

# Boost
boost            := /boost <minpair|maxopp> <phoneme> <phoneme> [strength]

# Meta
remove           := /remove <verb> [args]
clear            := /clear
show             := /show
help             := /help [verb]

/remove mirrors add syntax. /remove exclude ɹ removes /ɹ/ from the exclusion set without clearing other excluded phonemes. /remove aoa removes the entire AoA bound.


Input: Autocomplete + IPA Keyboard

Phoneme arguments use a hybrid input model:

  1. Autocomplete (default path): Typing triggers a dropdown. r/ɹ/ — alveolar approximant. th/θ/, /ð/. Arrow keys + Enter to select. Handles ASCII-to-IPA normalization.
  2. IPA keyboard toggle: A button opens a compact phoneme grid organized by manner/place (reuses PhonoLex's existing IPA keyboard layout). Click to add phonemes to the command.

The same autocomplete pattern extends to norm names, vocab list names, and syllable shapes.


Constraint Bar

A pinned horizontal bar below the header, always visible above the chat area.

Layout

┌─────────────────────────────────────────────────────────────────┐
│ ACTIVE  [Exclude /ɹ/, /s/ ×] [AoA ≤ 5.0 ×] [Include /k/ ~20% ×]  │
│         ⚠ Additional constraints can sometimes affect coherence │
└─────────────────────────────────────────────────────────────────┘

Behavior

  • Each active constraint renders as a dismissible chip with × button.
  • Clicking × removes that constraint from the store and inserts a system message in chat.
  • Coherence warning appears when 3+ constraints are active: "Additional constraints can sometimes affect coherence."
  • Chips are color-coded by constraint category:
  • Red/warm: phoneme exclusion (gates)
  • Blue: psycholinguistic bounds
  • Green: phoneme inclusion/boost
  • Neutral: structural (complexity, MSH)

Chip labels

Concise, matching the system message text:

Constraint Chip label
Exclude /ɹ/, /s/ Exclude /ɹ/, /s/
AoA ≤ 5.0 AoA ≤ 5.0
Include /k/ ~20% Include /k/ ~20%
WCM ≤ 8 WCM ≤ 8
Motor speech stage ≤ 3 MSH ≤ 3
Minimal pair boost MinPair /s/ ~ /ʃ/

System Messages

Commands produce inline system messages in the chat feed, styled distinctly from user/assistant turns. These are part of the conversation record and are never hidden or collapsed.

Confirmation format

System messages use the same labels as chips (imperative verb, not gerund) for consistency.

Event Prefix Message
Constraint added Exclude /ɹ/, /s/
Include with target Include /k/ ~20% (approximate)
Include bare Include /k/
Norm bound added AoA ≤ 5.0
Constraint removed Removed Exclude /ɹ/
All cleared All constraints cleared
Validation error /s/ and /z/ are both obstruents — maximal opposition requires different classes
Unknown phoneme Unknown phoneme "blorp" — type /help exclude for syntax
Unknown norm Unknown norm "foo" — type /help bound for available norms
Coherence warning Additional constraints can sometimes affect coherence

The coherence warning appears once when the constraint count crosses 3. It does not repeat on every subsequent add.


State Model

ConstraintStore

Single source of truth. React state (or Zustand/similar) holding the active constraint set.

ConstraintStore
  constraints: Constraint[]
  add(constraint): void         ← command parser writes here
  remove(type, args?): void     ← chip dismiss + /remove write here
  clear(): void                 ← /clear writes here
  snapshot(): Constraint[]      ← profile save reads here
  load(constraints): void       ← profile select writes here (bulk add)
  hash(): string                ← cache key for governor rebuild

Constraint identity and granularity

The store holds per-entry constraints, not bundled objects. Each /exclude ɹ adds one entry. /exclude s adds another. The store conceptually holds:

[
  { type: "exclude", phoneme: "ɹ" },
  { type: "exclude", phoneme: "s" },
  { type: "bound", norm: "aoa_kuperman", max: 5.0 },
  { type: "complexity_wcm", max: 8 },
  { type: "complexity_shapes", shapes: ["CV", "CVC"] },
]

When compiled to the engine, entries are merged: all exclude entries become one Exclude(phonemes={"ɹ", "s"}), all complexity_* entries become one Complexity(max_wcm=8, allowed_shapes=["CV", "CVC"]). This merging happens at the API boundary, not in the store.

/remove exclude ɹ removes the single { type: "exclude", phoneme: "ɹ" } entry. The remaining /s/ exclusion stays. /remove complexity wcm removes just the WCM entry while preserving syllable shapes. This is consistent: each command adds one entry, /remove removes one entry.

The constraint bar renders merged chips: Exclude /ɹ/, /s/ as one chip (clicking × on it removes ALL exclude entries), or per-phoneme chips (clicking × on /ɹ/ removes just that one). Implementation should use per-phoneme chips for maximum granularity.

Three consumers, one store

┌─────────────────────────────────────────────┐
│              ConstraintStore                 │
│  (single source of truth — React state)     │
└──────┬──────────────┬──────────────┬────────┘
       │              │              │
 ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────────┐
 │ Constraint │ │  Command    │ │  Governor     │
 │ Bar        │ │  Parser     │ │  Rebuild      │
 │            │ │             │ │               │
 │ × dismiss  │ │ /exclude ɹ  │ │ constraints   │
 │ = remove() │ │ = add()     │ │ → API call    │
 └────────────┘ └─────────────┘ │ → new profile │
                                └───────────────┘

Profile interaction

  • Loading a profile: calls store.load(profile.constraints) — the store is populated, the profile name is discarded.
  • Saving a profile: calls store.snapshot() → sends to POST /api/profiles with a user-provided name.
  • No "active profile" concept. The store is always the truth. A profile is just a named snapshot.

Governor rebuild

On every store change, the frontend sends the current constraint set to the backend. The backend builds (or cache-hits) the governor and uses it for subsequent generation calls. The rebuild is keyed by a hash of the constraint set, not by profile ID.

Debounce: Rapid consecutive commands (e.g., /exclude a then /exclude b typed quickly) are debounced — the governor rebuild fires after 300ms of no store changes. The debounced callback sends store.snapshot() to the backend, which computes the constraint hash for GovernorCache lookup. This prevents expensive rebuilds (especially MinPairBoost's O(N^2) pair search) on every keystroke.

API contract

The generation endpoint shifts from profile-based to inline constraints:

# Before (profile-based)
POST /api/generate
{ "message": "...", "profile_id": "r_aoa5" }

# After (inline constraints)
POST /api/generate
{ "message": "...", "constraints": [...] }

The constraints array contains the same Pydantic-typed constraint objects that the schema already defines, plus new types added in this spec. The backend hashes the constraint array for GovernorCache lookup — identical constraint sets reuse the cached governor.

The existing GovernorCache is replaced — keyed by constraint hash instead of profile.id. The hash is computed from the sorted, merged constraint representation (i.e., the compiled form, not the raw per-entry store), so /exclude s then /exclude ɹ cache-hits the same governor as the reverse order. The profile CRUD endpoints remain for saving/loading named snapshots but are no longer involved in generation.

Both /api/generate and /api/generate-single endpoints are migrated to accept inline constraints instead of profile_id.

Turn history

Each Turn records the constraint snapshot that was active during generation:

Turn {
  ...existing fields...
  constraints: Constraint[]   // replaces profile_id
}

This allows the compliance panel to show what constraints were active for any historical turn, even if the current store has changed since then.


Migration from Current UI

The existing ConstraintPanel (PhonemeToggles, NormSliders, ComplexityBounds, ProfileSelector) is replaced by:

  1. Command input in the chat text field (detects / prefix).
  2. Constraint bar above the chat area.
  3. Profile selector remains as a dropdown that populates the store — but it's a convenience, not the primary interface.

The existing buildConstraints() function and its lossy round-trip (ExcludeInClusters → Exclude) are eliminated. The store holds typed constraint objects that map 1:1 to the engine.

Bug fixes included

  • Lossy round-trip: eliminated — store holds typed constraints, not UI-derived reconstructions.
  • vocab_only rendering: ActiveConstraints no longer falls through to "Unknown."
  • Profile persistence: ProfileStore will be instantiated with a path for JSON persistence.
  • Warmup scheduling: HFGovernorProcessor will pass step/total_steps to GovernorContext.

Engine Work Required

New constraint: Include (B2 roadmap item)

The /include command requires a new constraint type not yet in the engine:

Bare include (flat boost): - New IncludeConstraint(phonemes, strength) in constraints.py - Compiles to LogitBoost — adds +strength to logits of tokens containing target phonemes - Mechanism exists (LogitBoost), just needs a new constraint class

Include with coverage target: - New CoverageConstraint(phonemes, target_rate) in constraints.py - Stateful projection: tracks phoneme hit rate across generated tokens, adjusts boost dynamically - Requires GovernorContext.token_ids to compute running coverage - Coverage is approximate — system message says "~N%" and "(approximate)" - This replaces the existing Density constraint class, which targets a similar goal (phoneme rate) but with a less intuitive API (min/max density floats). Density is deprecated in favor of CoverageConstraint.

Schema additions

New Pydantic models in schemas.py: - IncludeConstraint(type="include", phonemes: list[str], strength: float = 2.0) — flat boost, user controls strength - CoverageConstraint(type="coverage", phonemes: list[str], target_rate: float) — stateful projection, strength is dynamic (separate model from Include because the mechanism is fundamentally different: boost vs projection) - MSHConstraint(type="msh", max_stage: int) - MinPairBoostConstraint(type="boost_minpair", target: str, contrast: str, strength: float = 2.0) - MaxOppositionBoostConstraint(type="boost_maxopp", target: str, contrast: str, strength: float = 2.0)

Updated model: - BoundConstraint gains optional mechanism: Literal["gate", "cdd"] = "gate" field. The existing _to_dg_constraint() must pass this through to Bound(mechanism=...).

New wiring in governor.py._to_dg_constraint() for all new schema types.

Prerequisite fixes

  • MinPairBoost, MaxOppositionBoost, and MSHStage are not re-exported in diffusion_governors/__init__.py — add them to the public API.
  • HFGovernorProcessor must pass step/total_steps to GovernorContext to enable warmup scheduling.
  • ProfileStore() in main.py must be instantiated with a persistence path.

Scope Boundary

In scope: - Command parser + autocomplete + IPA keyboard - Constraint bar with dismissible chips - Inline system messages - ConstraintStore replacing ProfileStore as primary state - Named norm commands for Tier 1+2 norms - Schema + wiring for all existing engine constraint types - Include constraint (engine + schema + command)

Out of scope (future): - /vocab command (bad generation results — needs research) - /theme command (association graph boost — engine not built) - /temp, /tokens (generation params) - Content Catalog integration - Compliance panel redesign (separate effort)