Initial Commit
This commit is contained in:
231
openspec/changes/enterprise-dmx/proposal.md
Normal file
231
openspec/changes/enterprise-dmx/proposal.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# OpenSpec Change: enterprise-dmx
|
||||
|
||||
- **Change ID**: enterprise-dmx
|
||||
- **Package**: `@suaveplan/dmx`
|
||||
- **Category**: universal
|
||||
- **Target path**: `universal/media/dmx`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/dmx.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/dmx` on the genesis branch from scratch at enterprise quality. The package is a DMX512 lighting control engine: universe management (512 channels per universe), fixture definition and addressing, scene/chase/cue building, fade interpolation with injectable clock, transport-agnostic output (serial, WebSocket, UDP/Art-Net), and a chase sequencer with BPM clock sync. Pure TypeScript with no platform hard deps — transport is an injectable adapter.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. The main-branch package had 99.28% coverage.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `DmxError`; `DMX.*` codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injectable clock |
|
||||
| `@suaveplan/events` (`workspace:^`) | Event bus for universe-update notifications |
|
||||
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API
|
||||
|
||||
### Core types
|
||||
|
||||
```ts
|
||||
class DmxError extends SuaveplanError
|
||||
|
||||
const DMX_UNIVERSE_SIZE = 512
|
||||
const DMX_MIN_VALUE = 0
|
||||
const DMX_MAX_VALUE = 255
|
||||
|
||||
type DmxChannel = number // 1–512
|
||||
type DmxValue = number // 0–255
|
||||
type DmxUniverse = Uint8Array // length 512
|
||||
|
||||
interface FixtureDefinition {
|
||||
name: string
|
||||
channelCount: number
|
||||
channels: FixtureChannel[]
|
||||
}
|
||||
|
||||
interface FixtureChannel {
|
||||
offset: number // 0-based offset within fixture
|
||||
name: string
|
||||
defaultValue?: DmxValue
|
||||
}
|
||||
|
||||
interface FixtureInstance {
|
||||
id: string
|
||||
definition: FixtureDefinition
|
||||
startChannel: DmxChannel
|
||||
universeId: string
|
||||
}
|
||||
|
||||
interface Scene {
|
||||
id: string
|
||||
values: Map<string, DmxValue> // fixtureId:channelName → value
|
||||
}
|
||||
|
||||
interface FadeOptions {
|
||||
durationMs: number
|
||||
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
|
||||
}
|
||||
|
||||
interface DmxTransport {
|
||||
send(universe: DmxUniverse, universeId: string): Promise<void>
|
||||
connect?(): Promise<void>
|
||||
disconnect?(): Promise<void>
|
||||
}
|
||||
|
||||
interface DmxEngineOptions {
|
||||
transport: DmxTransport
|
||||
clock?: Clock
|
||||
logger?: LoggerLike
|
||||
refreshRateHz?: number // default: 40 (25ms frame)
|
||||
}
|
||||
|
||||
class DmxEngine {
|
||||
constructor(options: DmxEngineOptions)
|
||||
addUniverse(id: string): void
|
||||
removeUniverse(id: string): void
|
||||
addFixture(instance: FixtureInstance): void
|
||||
removeFixture(id: string): void
|
||||
setChannel(universeId: string, channel: DmxChannel, value: DmxValue): void
|
||||
getChannel(universeId: string, channel: DmxChannel): DmxValue
|
||||
getUniverse(id: string): Readonly<DmxUniverse>
|
||||
applyScene(scene: Scene, fade?: FadeOptions): Promise<void>
|
||||
blackout(universeId?: string): void
|
||||
start(): void
|
||||
stop(): void
|
||||
}
|
||||
|
||||
function createDmxEngine(options: DmxEngineOptions): DmxEngine
|
||||
function buildScene(id: string, values: Record<string, DmxValue>): Scene
|
||||
function interpolate(from: DmxValue, to: DmxValue, t: number): DmxValue
|
||||
```
|
||||
|
||||
### Chase sequencer
|
||||
|
||||
```ts
|
||||
interface CueStep {
|
||||
scene: Scene
|
||||
holdMs: number
|
||||
fade?: FadeOptions
|
||||
}
|
||||
|
||||
class ChaseSequencer {
|
||||
constructor(engine: DmxEngine, clock: Clock)
|
||||
addStep(step: CueStep): void
|
||||
start(looping?: boolean): void
|
||||
stop(): void
|
||||
jumpToStep(index: number): void
|
||||
readonly currentStep: number
|
||||
readonly running: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type DmxErrorCode =
|
||||
| 'DMX_UNIVERSE_NOT_FOUND'
|
||||
| 'DMX_FIXTURE_NOT_FOUND'
|
||||
| 'DMX_CHANNEL_OUT_OF_RANGE'
|
||||
| 'DMX_VALUE_OUT_OF_RANGE'
|
||||
| 'DMX_TRANSPORT_FAILED'
|
||||
| 'DMX_ADDRESS_CONFLICT'
|
||||
|
||||
const DMX_ERROR_METADATA: Readonly<Record<DmxErrorCode, CodeMetadata>>
|
||||
function registerDmxErrorCodes(): void
|
||||
```
|
||||
|
||||
### Zod schemas
|
||||
|
||||
```ts
|
||||
FixtureDefinitionSchema
|
||||
FixtureInstanceSchema
|
||||
SceneSchema
|
||||
FadeOptionsSchema
|
||||
DmxEngineOptionsSchema
|
||||
CueStepSchema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
1. **`DmxTransport` interface** — injectable; `NullTransport` ships for tests (captures send calls).
|
||||
2. **`ChaseSequencer`** — BPM-based step timing; uses injectable `Clock`; emits events via `@suaveplan/events`.
|
||||
3. **Fade interpolation** — four easing functions; frame-rate controlled by `refreshRateHz`.
|
||||
4. **`blackout`** — sets all channels in one or all universes to 0 immediately, bypassing any active fade.
|
||||
5. **Address-conflict detection** — `addFixture` throws `DmxError(DMX_ADDRESS_CONFLICT)` if channel range overlaps an existing fixture in the same universe.
|
||||
6. **OTel instrumentation** — counter `dmx.frame_total`, histogram `dmx.frame_render_ms`, counter `dmx.scene_apply_total`.
|
||||
7. **Property-based tests** — channel value clamping invariant, fade interpolation monotonicity, scene application round-trip.
|
||||
8. **Benchmark suite** — single-universe 512-channel render loop, 8-fixture scene apply.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/media/dmx/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
core/
|
||||
dmx-engine/
|
||||
dmx-engine.ts
|
||||
dmx-engine.md
|
||||
dmx-engine.types.ts
|
||||
dmx-engine.test.ts
|
||||
index.ts
|
||||
chase-sequencer/
|
||||
chase-sequencer.ts
|
||||
chase-sequencer.md
|
||||
chase-sequencer.types.ts
|
||||
chase-sequencer.test.ts
|
||||
index.ts
|
||||
transports/
|
||||
null-transport/
|
||||
null-transport.ts
|
||||
null-transport.md
|
||||
null-transport.test.ts
|
||||
index.ts
|
||||
utils/
|
||||
interpolate.ts
|
||||
interpolate.md
|
||||
interpolate.test.ts
|
||||
scene.ts
|
||||
scene.md
|
||||
scene.test.ts
|
||||
__benchmarks__/
|
||||
dmx.bench.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: `@suaveplan/r3f-dmx` (React layer), any live-event lighting integrations.
|
||||
**Breaking changes**: none — fresh build.
|
||||
**Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events` published to Verdaccio.
|
||||
88
openspec/changes/enterprise-dmx/tasks.md
Normal file
88
openspec/changes/enterprise-dmx/tasks.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Tasks: enterprise-dmx
|
||||
|
||||
**Change ID**: enterprise-dmx
|
||||
**Package**: `@suaveplan/dmx`
|
||||
**Target path**: `universal/media/dmx`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create directory `universal/media/dmx/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
|
||||
- [ ] Write `package.json` at version `0.1.0` with `"type": "module"`, `"sideEffects": false`, full `exports` map, deps: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/events workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`.
|
||||
- [ ] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
|
||||
- [ ] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`.
|
||||
- [ ] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check` scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types + errors
|
||||
|
||||
- [ ] Write `src/types/common.ts` — all interfaces and type aliases from proposal.
|
||||
- [ ] Write `src/types/common.md`.
|
||||
- [ ] Write `src/error-codes.ts` — `DmxErrorCode`, `DMX_ERROR_METADATA`, `registerDmxErrorCodes()`.
|
||||
- [ ] Write Zod schemas: `FixtureDefinitionSchema`, `FixtureInstanceSchema`, `SceneSchema`, `FadeOptionsSchema`, `DmxEngineOptionsSchema`, `CueStepSchema`.
|
||||
- [ ] Write `src/constants/index.ts` — `DMX_UNIVERSE_SIZE = 512`, `DMX_MIN_VALUE = 0`, `DMX_MAX_VALUE = 255`.
|
||||
|
||||
---
|
||||
|
||||
## 3. NullTransport
|
||||
|
||||
- [ ] Implement `NullTransport implements DmxTransport` — captures all `send` calls in a `calls: Array<{universe: Uint8Array; universeId: string}>` array; exposes `reset()`.
|
||||
- [ ] Write `null-transport.test.ts` and `null-transport.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Interpolation utilities
|
||||
|
||||
- [ ] Implement `interpolate(from, to, t): DmxValue` — four easing functions (linear, ease-in, ease-out, ease-in-out). Clamp result to [0, 255].
|
||||
- [ ] Implement `buildScene(id, values): Scene`.
|
||||
- [ ] Write `interpolate.test.ts`, `interpolate.md`, `scene.test.ts`, `scene.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. DmxEngine
|
||||
|
||||
- [ ] Implement `DmxEngine`: universe management, fixture registry with address-conflict detection, `setChannel`/`getChannel`, `getUniverse`, `applyScene` with fade interpolation (uses injectable `Clock` for frame timing), `blackout`, `start`/`stop` render loop.
|
||||
- [ ] Wire OTel: counter `dmx.frame_total`, histogram `dmx.frame_render_ms`, counter `dmx.scene_apply_total`.
|
||||
- [ ] Write `dmx-engine.test.ts` (≤500 lines): universe CRUD, fixture conflict detection, scene apply, blackout, fade end-state, abort on stop.
|
||||
- [ ] Write `dmx-engine.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. ChaseSequencer
|
||||
|
||||
- [ ] Implement `ChaseSequencer` — step list, `start(looping)`, `stop`, `jumpToStep`; uses injectable `Clock`; emits step events via `@suaveplan/events`.
|
||||
- [ ] Write `chase-sequencer.test.ts` (≤500 lines): step progression, loop restart, manual jump, stop mid-sequence.
|
||||
- [ ] Write `chase-sequencer.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Benchmarks
|
||||
|
||||
- [ ] Write `src/__benchmarks__/dmx.bench.ts` — single-universe 512-channel render loop, 8-fixture scene apply.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tests
|
||||
|
||||
- [ ] Write `tests/integration.test.ts` — engine + sequencer end-to-end with NullTransport.
|
||||
- [ ] Write `tests/property.test.ts` (fast-check) — channel value clamping, fade interpolation monotonicity, scene round-trip.
|
||||
|
||||
---
|
||||
|
||||
## 9. Docs + verify + publish
|
||||
|
||||
- [ ] All co-located `.md` files (≥200 words each).
|
||||
- [ ] Write `README.md` at package root.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/dmx` — green.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/dmx` — 100%.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/dmx`.
|
||||
- [ ] Archive to `openspec/archive/2026-04-24-enterprise-dmx/`.
|
||||
Reference in New Issue
Block a user