Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# 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`