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 codeCONTROL_SURFACE_SCHEMA.INVALID_DOCUMENTand 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
validateSurfacerejects withINVALID_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
compoundbinding contains anothercompoundin itsbindingsarray - 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 withtransform: { 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
upgradeSurfaceand a registered0.1.0 → 0.2.0migration 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
Controlnodes withid: "x" - THEN validation rejects with
DUPLICATE_IDlisting["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 referencespluginId: "p99" - THEN validation rejects with
BINDING_REFERENCE_MISSINGcarrying{ 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