Skip to main content

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:

LayerFileHard-coded Strings
Daemonengine_manager.rs19 strings (lines 960-1129)
GUI Rustcommands.rs8 strings (lines 1445-1799)
GUI JavaScriptstores.js17 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

  1. Typos: "long_pres" silently falls through match/switch cases
  2. Refactoring: Renaming requires finding all 32+ occurrences
  3. Documentation drift: No single source of truth for valid values
  4. 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 VariantJSON 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

  1. Compile-time safety: Invalid event types cause compilation errors in Rust
  2. IDE support: Auto-completion for enum variants
  3. Exhaustive matching: match statements must cover all variants
  4. Single source of truth: conductor-core defines valid values
  5. Refactoring safety: Rename refactoring propagates across codebase
  6. Documentation: Enum variants are self-documenting

Adding New Event Types

When adding a new event type:

  1. Add variant to EventType or PatternType enum in conductor-core/src/event_types.rs
  2. Add corresponding TypeScript type to conductor-gui/ui/src/lib/types.ts
  3. Update exhaustive match statements in daemon and GUI
  4. Run tests: cargo test --workspace && npm test

Implementation

Phase 1: Core Types (v4.9.0) - Complete

  • Create conductor-core/src/event_types.rs with enums
  • Add TDD tests for serde serialization
  • Export from conductor-core/src/lib.rs

Phase 2: Layer Integration (Future)

  • Refactor engine_manager.rs to use typed enums
  • Update commands.rs to use typed enums
  • Add TypeScript type definitions
  • Update stores.js to 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
  • ADR-002: MIDI Learn Event Streaming (identified this issue)
  • GitHub Issues: #42-#47 (v4.9.0 tracking)
  • conductor-core/src/event_types.rs (implementation)