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

5.6 KiB

OpenSpec Change: enterprise-gaming-ecs

  • Change ID: enterprise-gaming-ecs
  • Package: @suaveplan/ecs
  • Category: gaming/engine
  • Target path: gaming/engine/ecs
  • Status: PROPOSED
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/ecs.md

Summary

Deliver @suaveplan/ecs on the genesis branch from scratch at enterprise quality. The package provides an archetype-based Entity Component System with bitset queries — the foundational runtime for all gameplay systems. Entities are integer IDs; components are plain typed structs stored in contiguous typed arrays per archetype; queries filter archetypes by bitset AND/NOT masks for O(1) iteration. Genesis adds structured errors via @suaveplan/error, Zod schemas for all component descriptors, OTel span instrumentation on world tick and query execution, injectable clock and RNG, AbortSignal for async tick cancellation, and property-based tests with fast-check.


Current-state audit (genesis)

Package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Base EcsError; stable ECS.* error codes
@suaveplan/types (workspace:^) Clock, IdGenerator, LoggerLike
@suaveplan/events (workspace:^) Entity lifecycle events (spawn, despawn, component added/removed)
zod (catalog:forms) Component descriptor schemas, query option schemas
@opentelemetry/api (catalog:observability) Spans on world.tick, query.run, world.spawn

Public API surface

Core

// World factory
function createWorld(options?: WorldOptions): World

interface WorldOptions {
  maxEntities?: number      // default 65536
  clock?: Clock
  logger?: LoggerLike
}

interface World {
  spawn(components?: ComponentInit[]): Entity
  despawn(entity: Entity): void
  addComponent<T>(entity: Entity, descriptor: ComponentDescriptor<T>, value: T): void
  removeComponent(entity: Entity, type: ComponentType): void
  hasComponent(entity: Entity, type: ComponentType): boolean
  getComponent<T>(entity: Entity, descriptor: ComponentDescriptor<T>): T | undefined
  query(mask: QueryMask): QueryResult
  tick(dt: number, signal?: AbortSignal): void
  registerSystem(system: System): void
  destroy(): void
}

type Entity = number & { readonly __brand: 'Entity' }

// Component descriptor registry
function defineComponent<T>(name: string, schema: ZodType<T>): ComponentDescriptor<T>

interface ComponentDescriptor<T> {
  readonly type: ComponentType
  readonly name: string
  readonly schema: ZodType<T>
}

type ComponentType = number & { readonly __brand: 'ComponentType' }

// Query API
interface QueryMask {
  all?: ComponentType[]
  none?: ComponentType[]
  any?: ComponentType[]
}

interface QueryResult {
  entities: readonly Entity[]
  size: number
  forEach(cb: (entity: Entity) => void): void
}

// System API
interface System {
  readonly name: string
  update(world: World, dt: number): void
}

// Archetype internals (exposed for advanced consumers)
interface Archetype {
  readonly mask: bigint
  readonly entities: readonly Entity[]
  readonly size: number
}

Error codes

type EcsErrorCode =
  | 'ECS_ENTITY_NOT_FOUND'
  | 'ECS_COMPONENT_NOT_REGISTERED'
  | 'ECS_MAX_ENTITIES_EXCEEDED'
  | 'ECS_INVALID_QUERY'
  | 'ECS_WORLD_DESTROYED'

New additions for genesis

  1. Bitset queries via BigInt masks — archetype mask stored as bigint; AND/NOT/ANY evaluated with bitwise ops for O(1) archetype filtering.
  2. Typed array component storage — Float32Array/Int32Array backing per numeric component type; fallback to generic Map for complex structs.
  3. OTel instrumentation — ecs.tick span with attribute ecs.system_count; ecs.query histogram for query execution time; ecs.entities_alive gauge.
  4. Reactive entity events — @suaveplan/events bus fires ecs:spawn, ecs:despawn, ecs:component:add, ecs:component:remove.
  5. Property-based tests — fast-check: entity count monotonicity, query result consistency with exhaustive scan, archetype mask algebra.
  6. Benchmark suite — __benchmarks__/ecs.bench.ts: 10k entity spawn, 100k query iteration, 60fps tick with 10 systems.

File layout

gaming/engine/ecs/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      common.ts
      common.md
    core/
      world/
        world.ts
        world.md
        world.types.ts
        world.test.ts
        index.ts
      archetype/
        archetype.ts
        archetype.md
        archetype.types.ts
        archetype.test.ts
        index.ts
      component/
        component.ts
        component.md
        component.types.ts
        component.test.ts
        index.ts
      query/
        query.ts
        query.md
        query.types.ts
        query.test.ts
        index.ts
      system/
        system.ts
        system.md
        system.types.ts
        system.test.ts
        index.ts
    utils/
      bitset.ts
      bitset.md
      bitset.test.ts
    __benchmarks__/
      ecs.bench.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Downstream: @suaveplan/game-loop orchestrates world ticks; @suaveplan/r3f-* packages read ECS component state for rendering. No existing genesis package currently depends on it.

Breaking changes from main: none — fresh build.

Prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events published to Verdaccio.