# enterprise-controller **Package**: `@suaveplan/controller` **Status**: PROPOSED **Type**: new-package (migration from `../suaveplan/packages/universal/controller/`) - **Design**: ./design.md - **Spec delta**: ./specs/controller/spec.md ## Why Genesis lacks a universal-tier input controller. The upstream `@suaveplan/controller` package (67 files, ~5000 LoC) provides a unified `ControllerInput` shape across gamepad, keyboard, mouse, touch, plus pattern recognition and input buffering. Games built on `@suaveplan/ecs` and the forthcoming `@suaveplan/game-server` need a deterministic, injectable input layer that does not read wall-clock time directly and can be exercised under a `FakeClock`. The upstream code uses `Date.now()` throughout, mutates module-level state, and lacks structured errors, Zod boundary validation, and OTel instrumentation. This migration re-implements the public surface with genesis primitives (`Clock`/`IdGenerator` injection, `defineCodes`, OTel counter + histogram, Zod schemas) while preserving the `ControllerInput` shape, the `BaseController` lifecycle, the `ControllerMerger` semantics, and the device adapters. ## What changes - ADDED: `BaseController` — abstract base with state, thresholds, deadzone/threshold logic, pollRate gating, lifecycle, event emission, `Clock` injection. - ADDED: `KeyboardController` — DOM keyboard adapter with configurable key→input mapping. - ADDED: `GamepadController` — `navigator.getGamepads()` polling adapter with axis deadzone. - ADDED: `ControllerMerger` — merges N child controllers into a single `ControllerInput` state via max-value reduction with stable iteration order. - ADDED: `InputEventEmitter` — typed event dispatcher with `onInput`/`onStateChange`/`onCombo` and listener disposal. - ADDED: `ControllerError` + `CONTROLLER_CODES` — stable error-code registry via `defineCodes`. - ADDED: OTel `controller_inputs_total` counter (tags: `device`, `input`) and `controller_pattern_match_duration_ms` histogram, no-op without exporter. - ADDED: Zod-validated public input boundaries (options, snapshots, patterns). - ADDED: Sibling `.md` (≥200 words, 6 sections) for every implementation file. ## Out of scope (Priority 2 — Deferred 2026-05-24) Per the migration brief's explicit allowance ("ship the input-device adapters + controller merger as Priority 1 and defer pattern-recognition + input-buffering"), the following upstream modules are deferred to a follow-up change: - `TouchController` — touch event adapter (Priority 2; requires DOM `Touch` polyfills in test). - `MouseController` — 1296-line upstream module requires structural decomposition into ≤300-line genesis modules per CLAUDE.md §3; deferred to Priority 2. - `InputBuffer` — circular buffer for combo sequences (Priority 2; pattern-recognition layer). - `InputStateMachine` — pattern-recognition state machine (Priority 2). - `ModalController` — input-mode router (Priority 2). - Default `defaultPatterns` constant (hadoken / shoryuken / charge-shot / rapid-fire) — moves to Priority 2 with the pattern engine. - `demo/`, `test-utils/`, `e2e/` — non-shipping; demo + integration harnesses are downstream concerns and not part of the published surface. Justification for deferral (≤1 sentence per upstream §0a anti-patterns rule): every deferred module depends on a co-shipped component that is itself deferred or on a structural decomposition that requires its own design review — pattern recognition + input buffering form one cluster (combo grammar + buffer + state machine) that should land atomically, and the mouse rewrite needs its own 1296→multi-file split design. ## Source mapping This change derives from `../suaveplan/packages/universal/controller/`. There is no upstream `enterprise/controller.md` audit document; this section maps to upstream source files instead. | Requirement | Source | Notes | |---|---|---| | ControllerInput shape | `src/types.ts` L1-L66 | verbatim 32-field shape | | BaseController lifecycle | `src/base/base-controller.ts` L1-L386 | re-authored to inject `Clock` instead of `Date.now()`; threshold logic preserved | | KeyboardController | `src/keyboard/keyboard-controller.ts` L1-L226 | re-authored; DOM event listeners + key map | | GamepadController | `src/gamepad/gamepad-controller.ts` L1-L315 | re-authored; `navigator.getGamepads()` polling | | ControllerMerger | `src/controller-merger.ts` L1-L298 | re-authored; max-value reduction preserved | | InputEventEmitter | `src/input/input-event-emitter.ts` L1-L115 | re-authored; typed listener registry | ### Requirements with no source line | Requirement | Justification | |---|---| | `defineCodes` error registry | Genesis convention — upstream threw plain `Error` | | OTel instrumentation | Genesis convention — upstream had no observability | | Zod boundary schemas | Genesis convention — upstream had no runtime boundary validation | | `Clock` injection | Genesis convention — upstream read `Date.now()` directly | ## Verification approach - `src/base/base-controller.test.ts` — lifecycle, threshold, deadzone, poll-rate gating. - `src/keyboard/keyboard-controller.test.ts` — key→input mapping, focus loss cleanup. - `src/gamepad/gamepad-controller.test.ts` — axis deadzone, multi-pad fanout. - `src/controller-merger/controller-merger.test.ts` — max-value reduction, add/remove. - `src/event-emitter/event-emitter.test.ts` — listener add/remove/dispose. - `src/errors/errors.test.ts` — every code in `CONTROLLER_CODES` reachable. - `src/observability/observability.test.ts` — counter + histogram emit under in-memory meter. ### Affected dependents None — additive, new package.