Files
2026-05-27 23:29:53 +01:00

5.6 KiB

control-surface-schema spec delta

ADDED Requirements

Requirement: ControlSurface root document

@suaveplan/control-surface-schema SHALL export a ControlSurface type with: id (uuid v7), name, version (semver matching CURRENT_SCHEMA_VERSION), createdAt, updatedAt, skin, root: ControlNode. Every field is validated by the Zod schema.

Scenario: Empty surface validates

  • WHEN validateSurface({ id, name: "x", version: CURRENT_SCHEMA_VERSION, createdAt: 0, updatedAt: 0, skin: { id: "default" }, root: { type: "group", id: "root", layout: { kind: "absolute" }, transform: { position: { x:0, y:0 } }, children: [] } }) is called
  • THEN it returns { surface } with no error

Scenario: Missing fields rejected

  • WHEN validateSurface({}) is called
  • THEN it returns { error } with code CONTROL_SURFACE_SCHEMA.INVALID_DOCUMENT and Zod issues attached

Verified by: src/validate/validate.test.ts

Requirement: ControlNode discriminated union

ControlNode = Group | Control. Validators distinguish by type field; cross-validation rejects controls inside controls or groups with mixed-type children that don't match the layout kind.

Scenario: Nested groups validate

  • WHEN a surface contains a Group containing two Groups each containing three Controls
  • THEN validation succeeds and walkSurface(surface, visitor) visits 9 nodes in DFS order

Verified by: src/walk/walk.test.ts

Requirement: 18 ControlKind variants

ControlKind SHALL be a closed union of 18 values: 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. The validator rejects unknown kinds.

Scenario: Unknown kind rejected

  • WHEN a Control has kind: "not-a-thing"
  • THEN validateSurface rejects with INVALID_DOCUMENT

Verified by: src/validate/validate.test.ts::"unknown ControlKind"

Requirement: Six Binding variants

Binding SHALL cover pluginParam, transport, midiOut, midiIn, dmx, compound. The compound variant wraps an array of other bindings (no nested compounds — flat-only) so one knob can drive a plugin parameter, a MIDI CC, and a DMX channel simultaneously.

Scenario: pluginParam binding round-trips through validate + JSON

  • WHEN a Control has binding: { kind: "pluginParam", pluginId: "p1", paramPath: "channels/0/volume", curve: "log" }
  • THEN JSON.parse(JSON.stringify(validateSurface(surface).surface)) deep-equals the original

Scenario: Nested compound rejected

  • WHEN a compound binding contains another compound in its bindings array
  • THEN validation rejects with INVALID_DOCUMENT

Verified by: src/validate/validate.test.ts::"binding variants"

Requirement: GroupLayout — 4 variants

GroupLayout SHALL cover grid, absolute, stack, free3d. free3d is the only layout that admits z-axis transforms; the validator rejects nonzero transform.position.z under any other layout.

Scenario: 2D layout rejects z-transform

  • WHEN a Group has layout: { kind: "grid", columns: 4, rows: 4, gap: 8 } and a child with transform: { position: { x:0, y:0, z:1 } }
  • THEN validation rejects with INVALID_DOCUMENT

Verified by: src/validate/validate.test.ts::"layout z-axis discipline"

Requirement: Schema versioning + forward migration

upgradeSurface(unknown) SHALL accept any prior version registered in the migration chain and migrate forward to CURRENT_SCHEMA_VERSION. Unknown versions reject with UNSUPPORTED_VERSION.

Scenario: Upgrade 0.1.0 → 0.2.0 (illustrative)

  • WHEN a v0.1.0 document is passed to upgradeSurface and a registered 0.1.0 → 0.2.0 migration exists
  • THEN the returned document validates against the current schema

Scenario: Unknown version rejects

  • WHEN upgradeSurface({ version: "99.0.0", ... }) is called
  • THEN it throws SchemaError("CONTROL_SURFACE_SCHEMA.UNSUPPORTED_VERSION")

Verified by: src/upgrade/upgrade.test.ts

Requirement: Duplicate-id detection

validateSurface SHALL detect duplicate id values across the entire node tree and reject with DUPLICATE_ID carrying the offending ids on context.ids.

Scenario: Two controls with the same id rejected

  • WHEN a surface has two Control nodes with id: "x"
  • THEN validation rejects with DUPLICATE_ID listing ["x"]

Verified by: src/identity/identity.test.ts

Requirement: Binding-reference cross-check

When a binding references a pluginId that is not present in the surface's optional pluginCatalog (passed alongside), validation SHALL reject with BINDING_REFERENCE_MISSING carrying the missing reference on context.

Scenario: Reference missing plugin

  • WHEN validateSurface(surface, { pluginCatalog: new Set(["p1"]) }) is called and a control references pluginId: "p99"
  • THEN validation rejects with BINDING_REFERENCE_MISSING carrying { missing: "p99" }

Verified by: src/identity/identity.test.ts::"binding ref"

Requirement: walkSurface DFS traversal

walkSurface(surface, visitor) SHALL visit every node depth-first, passing the node + its parent stack (root-first). Mutation during traversal is undefined behaviour (visitor is read-only by contract).

Scenario: DFS order

  • WHEN a surface has root → A → [B, C → [D]] → E
  • THEN visitor is called with node ids in order ["root", "A", "B", "C", "D", "E"]

Verified by: src/walk/walk.test.ts