ADR-004: Type-Safe Event Schemas
Status
Accepted
Context
During LLM Council verification of v4.7.0 and analysis of ADR-002 (MIDI Learn Event Streaming), schema fragility was identified as a medium-severity technical debt issue.
Problem Statement
Event types and pattern types were using free-form strings across three application layers:
| Layer | File | Hard-coded Strings |
|---|---|---|
| Daemon | engine_manager.rs | 19 strings (lines 960-1129) |
| GUI Rust | commands.rs | 8 strings (lines 1445-1799) |
| GUI JavaScript | stores.js | 17 strings (lines 653-851) |
Total: 32 hard-coded strings across 3 layers with no compile-time safety.
Example of Fragility
// Daemon layer
"pattern_type": "long_press" // Typo-prone, no validation
// GUI layer - must match exactly
if event.pattern_type === "long_press" { ... } // Silent failure on mismatch
Risks
- Typos: "long_pres" silently falls through match/switch cases
- Refactoring: Renaming requires finding all 32+ occurrences
- Documentation drift: No single source of truth for valid values
- Testing gaps: String comparisons don't catch compile-time errors
Decision
Implement type-safe event and pattern type enums with serde-based JSON serialization.
Core Types (conductor-core/src/event_types.rs)
use serde::{Deserialize, Serialize};
/// Event types emitted during MIDI Learn capture.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
// MIDI Events
NoteOn,
NoteOff,
Cc,
Encoder,
PitchBend,
Aftertouch,
PolyPressure,
// Gamepad Events (v3.0+)
GamepadButton,
GamepadButtonRelease,
GamepadAxis,
GamepadStick,
GamepadTrigger,
}
/// Pattern types detected by EventProcessor during MIDI Learn.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatternType {
LongPress,
DoubleTap,
Chord,
GamepadChord,
}
Serialization Strategy
Use #[serde(rename_all = "snake_case")] to maintain JSON API compatibility:
| Rust Variant | JSON String |
|---|---|
EventType::NoteOn | "note_on" |
EventType::GamepadButton | "gamepad_button" |
PatternType::LongPress | "long_press" |
PatternType::GamepadChord | "gamepad_chord" |
Cross-Layer Type Safety
Daemon Layer (engine_manager.rs)
use conductor_core::{EventType, PatternType};
// Before: "event_type": "note_on"
// After:
let event_type = EventType::NoteOn;
serde_json::to_value(event_type)? // Serializes to "note_on"
GUI Rust Layer (commands.rs)
use conductor_core::{EventType, PatternType};
// Deserialize from daemon JSON
let event_type: EventType = serde_json::from_value(json_value)?;
GUI JavaScript Layer (stores.js)
// types.ts
export type EventType =
| 'note_on'
| 'note_off'
| 'cc'
| 'encoder'
| 'pitch_bend'
| 'aftertouch'
| 'poly_pressure'
| 'gamepad_button'
| 'gamepad_button_release'
| 'gamepad_axis'
| 'gamepad_stick'
| 'gamepad_trigger';
export type PatternType =
| 'long_press'
| 'double_tap'
| 'chord'
| 'gamepad_chord';
// Type guard for runtime validation
export function isValidEventType(value: string): value is EventType {
const validTypes: EventType[] = [
'note_on', 'note_off', 'cc', 'encoder', 'pitch_bend',
'aftertouch', 'poly_pressure', 'gamepad_button',
'gamepad_button_release', 'gamepad_axis', 'gamepad_stick',
'gamepad_trigger'
];
return validTypes.includes(value as EventType);
}
Benefits
- Compile-time safety: Invalid event types cause compilation errors in Rust
- IDE support: Auto-completion for enum variants
- Exhaustive matching:
matchstatements must cover all variants - Single source of truth: conductor-core defines valid values
- Refactoring safety: Rename refactoring propagates across codebase
- Documentation: Enum variants are self-documenting
Adding New Event Types
When adding a new event type:
- Add variant to
EventTypeorPatternTypeenum inconductor-core/src/event_types.rs - Add corresponding TypeScript type to
conductor-gui/ui/src/lib/types.ts - Update exhaustive match statements in daemon and GUI
- Run tests:
cargo test --workspace && npm test
Implementation
Phase 1: Core Types (v4.9.0) - Complete
- Create
conductor-core/src/event_types.rswith enums - Add TDD tests for serde serialization
- Export from
conductor-core/src/lib.rs
Phase 2: Layer Integration (Future)
- Refactor
engine_manager.rsto use typed enums - Update
commands.rsto use typed enums - Add TypeScript type definitions
- Update
stores.jsto use type guards
Consequences
Positive
- Eliminates string-based schema fragility
- Catches type errors at compile time
- Improves code maintainability
- Enables better IDE tooling
Negative
- Requires coordinated changes across Rust and TypeScript
- TypeScript types must be manually synchronized (no code generation)
- Adds slight complexity for simple string matching cases
Neutral
- JSON wire format remains unchanged (backward compatible)
- No migration needed for existing configs or IPC messages
Related
- ADR-002: MIDI Learn Event Streaming (identified this issue)
- GitHub Issues: #42-#47 (v4.9.0 tracking)
- conductor-core/src/event_types.rs (implementation)