Files
suaveplan/openspec/archive/2026-05-24-enterprise-controller/proposal.md
2026-05-27 23:29:53 +01:00

5.6 KiB

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.