Files
suaveplan/openspec/archive/2026-05-23-enterprise-control-surface-schema/proposal.md
2026-05-27 23:29:53 +01:00

5.1 KiB

@suaveplan/control-surface-schema — universal control-surface JSON schema

  • Target path: packages/media/audio/control-surface-schema/
  • Tier: universal (zero React/3D/audio coupling — pure data + Zod)
  • Date: 2026-05-23
  • Status: PROPOSED

Why

Every downstream control-surface renderer — 2D React (@suaveplan/react-control-surface), 3D R3F (@suaveplan/r3f-control-surface), VR/XR runtimes, future native ports — needs to read the same surface definition: nested groups of atomic controls bound to plugin parameters / MIDI CCs / DMX channels, with persistable layout and skin information.

If each renderer owns its own format, they diverge. The same surface saved by a 2D editor can't be opened by the 3D editor. Cross-platform replication (the live-DJ-in-gaming-session scenario) becomes impossible.

This package owns the canonical schema. It is the source of truth for surface persistence, the validation layer at every save/load boundary, and the shared vocabulary every renderer + editor + replication layer agrees on.

What changes

  1. NEW package @suaveplan/control-surface-schema at packages/media/audio/control-surface-schema/.
  2. ControlSurface root document type:
    • id: string (uuid), name, version (semver of the schema), createdAt, updatedAt
    • skin: { id: string; vars?: Record<string, string> } — references a skin package + optional CSS-var overrides
    • root: ControlNode — nested group tree
  3. ControlNode discriminated union:
    • Group — { type: "group"; id; label?; layout: GroupLayout; transform: Transform; children: ControlNode[]; collapsible?: boolean }
    • Control — { type: "control"; id; kind: ControlKind; label?; transform; appearance?; binding?; range?; behaviour? }
  4. ControlKind enum — knob, slider-v, slider-h, button-momentary, button-latching, switch-2, switch-3, led, digital-display, meter-vu, meter-peak, meter-spectral, xy-pad, jog-wheel, piano-key, drum-pad, text-label, image.
  5. Transform — { position: { x; y; z? }; rotation?: { x?; y?; z? }; scale?: { x?; y?; z? } }. z and rotation/scale fields are optional so 2D-only surfaces stay clean.
  6. GroupLayout discriminated union — Grid ({ kind: "grid"; columns; rows; gap }), Absolute ({ kind: "absolute" }), Stack ({ kind: "stack"; direction: "row" | "column"; gap }), Free3D ({ kind: "free3d" }).
  7. Binding discriminated union:
    • PluginParameter — { kind: "pluginParam"; pluginId; paramPath: string; curve?: "linear" | "log" | "exp" | { kind: "custom"; points: ReadonlyArray<{ at: number; value: number }> } }
    • TransportControl — { kind: "transport"; field: "play" | "stop" | "tempo" | "position" | "loop" }
    • MidiOut — { kind: "midiOut"; portId; channel; cc: number; range?: [number, number] }
    • MidiIn — { kind: "midiIn"; portId; channel; cc: number; range?: [number, number] } (control reflects external MIDI input)
    • DmxChannel — { kind: "dmx"; universe; channel; range?: [0, 255] }
    • Compound — { kind: "compound"; bindings: ReadonlyArray<Binding> } (one control drives many)
  8. Range — { min: number; max: number; step?: number; default?: number; units?: string }.
  9. Behaviour — { momentaryDelayMs?; latchSticky?; doubleTapAction?: Action; longPressAction?: Action; …}.
  10. Action discriminated union — { kind: "resetToDefault" } | { kind: "setTo"; value: number } | { kind: "toggleGroupCollapse"; targetId } | { kind: "emitEvent"; eventId; payload? }.
  11. Appearance — { skinKey?: string; renderHint?: "auto" | "2d" | "3d"; cssVars?: Record<string, string>; tooltip?: string; icon?: { kind: "url" | "svg" | "skinAsset"; value: string } }.
  12. validateSurface(unknown): { surface: ControlSurface } | { error: SchemaError } — Zod-driven safe parse, returns typed error union.
  13. upgradeSurface(unknown): ControlSurface — accepts schema version N-1 documents and migrates forward via registered upgraders.
  14. walkSurface(surface, visitor): void — DFS traversal helper that yields every node with its parent stack.
  15. Error codes — CONTROL_SURFACE_SCHEMA.{INVALID_DOCUMENT, UNSUPPORTED_VERSION, UPGRADE_FAILED, DUPLICATE_ID, BINDING_REFERENCE_MISSING} via defineCodes.
  16. Schema versioning — bump on every breaking change; upgradeSurface chains migrations.

Impact

  • New universal package. Zero runtime deps except @suaveplan/error, @suaveplan/types, tslib, zod.
  • Unblocks renderers (react-control-surface, r3f-control-surface), editors, persistence layers, network replication.
  • Schema versioning means 0.1.0 → 0.2.0 requires registering an upgrader before consumers can read old documents.

Out of scope

  • Renderers — separate packages, no React/R3F imports here.
  • Skin packages — separate per-device families (control-surface-skin-{native-instruments, mackie, piano, …}).
  • Editor UI — separate (react-control-surface-editor).
  • Replication / persistence transports — separate (network-control-surface).