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
rand 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:
- Autocomplete (default path): Typing triggers a dropdown.
r→/ɹ/ — alveolar approximant.th→/θ/, /ð/. Arrow keys + Enter to select. Handles ASCII-to-IPA normalization. - 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 toPOST /api/profileswith 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:
- Command input in the chat text field (detects
/prefix). - Constraint bar above the chat area.
- 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_onlyrendering: 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_stepsto 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, andMSHStageare not re-exported indiffusion_governors/__init__.py— add them to the public API.HFGovernorProcessormust passstep/total_stepstoGovernorContextto enable warmup scheduling.ProfileStore()inmain.pymust 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)