96 lines
9.3 KiB
Markdown
96 lines
9.3 KiB
Markdown
# OpenSpec Change: enterprise-ecs
|
|
|
|
- **Change ID**: enterprise-ecs
|
|
- **Package**: `@suaveplan/ecs`
|
|
- **Category**: games (universal-tier)
|
|
- **Target path**: `packages/games/ecs`
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-05-19
|
|
- **Spec delta**: `./specs/ecs/spec.md`
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/ecs.md`
|
|
|
|
This change supersedes the earlier `enterprise-gaming-ecs` proposal (`openspec/changes/enterprise-gaming-ecs/`) which predates the `packages/<plugin>/<pkg>` folder layout and the source-mapping / `Verified by:` discipline. The superseded proposal will be archived under the same date prefix.
|
|
|
|
---
|
|
|
|
## Why
|
|
|
|
`@suaveplan/ecs` is the foundational gameplay runtime — an archetype-based Entity Component System with bitset queries, archetype-graph transitions, system scheduling, and entity-ID recycling. The upstream `../suaveplan/packages/universal/ecs` package (1.0.1, 95.98% coverage) ships the algorithmic core but lacks the genesis enterprise floor: no `SuaveplanError` integration, no Zod-validated component schemas, no OTel instrumentation, no injectable clock, no property-based tests, and event payloads typed as tuples instead of `EventBus`-shaped envelopes.
|
|
|
|
Migrating this package on genesis unblocks the `games`-plugin work (procedural-world systems, fauna behaviour, fruit-machine state) and gives every downstream gameplay package a single, observable, deterministic ECS substrate.
|
|
|
|
## What changes
|
|
|
|
This is a fresh genesis package — no prior version exists on the branch, so every public symbol is ADDED.
|
|
|
|
- ADDED: `createWorld(config?: WorldConfig)` / `class World` — central container managing entities, archetypes, components, queries, and systems with structured errors, OTel spans on `world.spawn` / `world.execute` / `world.snapshot` / `world.restore`, and an `@suaveplan/events` `EventBus` for entity lifecycle events.
|
|
- ADDED: `class BitSet` — fixed-size `Uint32Array`-backed bitset with `set` / `unset` / `has` / `containsAll` / `intersects` / `equals` / `count` / `clone` / `clear` / iterator / `toKey`.
|
|
- ADDED: `class Archetype` — column-oriented (SoA) storage with O(1) swap-remove, mask-keyed identity, and per-component column arrays.
|
|
- ADDED: `class ComponentRegistry` — stable integer IDs for component schemas with name-uniqueness enforcement, `maxComponents` cap, and lookup by id / schema / name.
|
|
- ADDED: `class EntityPool` — entity-ID allocator with recycling and per-ID generation counters for stale-reference detection.
|
|
- ADDED: `class Query` + `defineQuery(include, exclude?)` — compiled bitset queries with cached archetype matching and `invalidate()` for archetype-graph mutations.
|
|
- ADDED: `class SystemScheduler` + `defineSystem(options)` — phase-ordered (`preUpdate` → `update` → `postUpdate` → `render`), priority-sorted system scheduler.
|
|
- ADDED: `defineComponent(name, defaults, schema?)` — Zod-validated component schema factory (genesis addition: accepts an optional `z.ZodType` validator wired through `World.addComponent` / `setComponent` / `restore`).
|
|
- ADDED: `EcsError` extending `SuaveplanError` with stable `ECS.*` codes (`ECS.ENTITY_NOT_ALIVE`, `ECS.COMPONENT_NOT_REGISTERED`, `ECS.COMPONENT_NAME_CONFLICT`, `ECS.COMPONENT_LIMIT_REACHED`, `ECS.COMPONENT_VALIDATION_FAILED`, `ECS.SYSTEM_NAME_CONFLICT`, `ECS.SNAPSHOT_INVALID`).
|
|
- ADDED: `WorldEvents` — `EventBus<WorldEventMap>` payloads as single objects (`{ entity }` / `{ entity, component }`) per the genesis `@suaveplan/events` shape — replaces upstream's `[{ entity }]` tuple payloads.
|
|
- ADDED: Public Zod schemas for `WorldSnapshot`, `ComponentDescriptor`, `QueryDescriptor`, and `WorldConfig`.
|
|
- ADDED: OTel instrumentation via `@suaveplan/opentelemetry` — a counter for entity spawns/despawns, a histogram for `world.execute` duration, and spans on `spawn` / `despawn` / `execute` / `snapshot` / `restore`.
|
|
|
|
## Out of scope
|
|
|
|
- **`@suaveplan/streams` integration / async tick** — upstream `ecs.md` § 3 mentions `AbortSignal` and streams for "network / IO helper". The ECS core is fully synchronous (a tick is a CPU loop, not IO). No async surface exists in upstream source; adding one without a concrete consumer would be speculative. Will be added when the first async-IO system lands.
|
|
- **`@suaveplan/feature-flags` rollout gates** — applies only to "risky new behaviour" per the source. The current public surface is a faithful port of an already-shipped 1.0.1 design; no risky branch needs gating.
|
|
- **`migrate` codemod** — applicable only when the public API changes. Fresh package, no prior consumers on genesis.
|
|
- **Benchmark suite** — covered by an `__benchmarks__` slot in the file layout but the perf-regression gate is wired in a follow-up change once the benchmark harness graduates across the monorepo. Not blocking enterprise sign-off because the package's foundational hot paths (BitSet ops, archetype lookups) are pinned by deterministic unit tests.
|
|
|
|
## Source mapping
|
|
|
|
This change derives from `../suaveplan/enterprise/ecs.md` (116 lines) and the upstream implementation at `../suaveplan/packages/universal/ecs/`.
|
|
|
|
| Requirement | Source lines | Notes |
|
|
|---|---|---|
|
|
| Universal-tier purity (zero browser/Node/React deps) | `ecs.md` L37 | verbatim ("Zero browser / Node / React dependencies — pure TypeScript, runs in Bun, Node, and browser") |
|
|
| Zod schemas for public types | `ecs.md` L38 | verbatim ("Zod schemas for all public types, ... versioned evolution") |
|
|
| Property-based tests (fast-check) | `ecs.md` L39 | verbatim |
|
|
| Deterministic behaviour / injectable clock | `ecs.md` L40 | verbatim ("no `Math.random()` / `Date.now()` at module scope; injectable clock and RNG") |
|
|
| OpenTelemetry instrumentation | `ecs.md` L41 | verbatim ("via `@suaveplan/opentelemetry` for all public entrypoints") |
|
|
| Structured errors with stable codes | `ecs.md` L42 | verbatim ("extending `@suaveplan/error` with stable error codes") |
|
|
| 100% test coverage | `ecs.md` L50 | verbatim cross-cutting (Istanbul, statement + branch) |
|
|
| 100% docs coverage (co-located `.md`) | `ecs.md` L51 | verbatim cross-cutting |
|
|
| Archetype-based ECS with bitset queries | `ecs.md` L12 | one-line summary expanded into the World/Archetype/BitSet/Query requirements via upstream `world.ts`, `archetype.ts`, `bitset.ts`, `query.ts` |
|
|
| Entity pooling with recycling | upstream `entity-pool.ts` L1-79 | implementation-level evidence for §1 purpose ("entity pooling") — surface preserved |
|
|
| System scheduling (phases + priorities) | upstream `system.ts` L29-80 | implementation-level evidence for §1 purpose ("system scheduling") — surface preserved |
|
|
| WorldSnapshot / serialization round-trip | upstream `world.ts` L199-236 | preserved as a stable persistence boundary |
|
|
|
|
### Requirements with no source line
|
|
|
|
| Requirement | Justification |
|
|
|---|---|
|
|
| `EventBus`-shape entity events (`{ entity }` single-object payload) | The genesis `@suaveplan/events` package replaced the upstream `EventEmitter` (which used tuple payloads `[{ entity }]`) with a single-payload `EventBus<EventMap>` shape. This is a forced rewrite, not an additive requirement; payload shape follows the genesis events contract. |
|
|
| 100% coverage threshold (not the upstream 95.98%) | Genesis-wide policy per `CLAUDE.md` Rules §Quality. |
|
|
| `@suaveplan/testing/runner` test-import gate | Genesis-wide CI rule per `CLAUDE.md` rule 11 / `openspec/AGENTS.md` §7. |
|
|
| Explicit-named feature-module barrels (no `export *`) | Genesis-wide policy per `CLAUDE.md` rule 10 / `openspec/AGENTS.md` §6a. |
|
|
|
|
## Verification approach
|
|
|
|
Every requirement in `specs/ecs/spec.md` carries a `**Verified by:**` line at authoring time. Test layout:
|
|
|
|
- `src/bitset/bitset.test.ts` — every `BitSet` operator and the iterator contract.
|
|
- `src/bitset/bitset.spec-conformance.test.ts` — universal-tier purity + iterator type pinning.
|
|
- `src/component-registry/component-registry.test.ts` — registration, ID stability, name-uniqueness, cap enforcement.
|
|
- `src/entity-pool/entity-pool.test.ts` — allocate/free/recycle behaviour and generation counters.
|
|
- `src/archetype/archetype.test.ts` — column storage, swap-remove identity, mask integrity.
|
|
- `src/query/query.test.ts` — include / exclude / cached evaluation / invalidation.
|
|
- `src/system/system.test.ts` — `defineSystem` defaults, phase ordering, priority sort, name conflict.
|
|
- `src/world/world.test.ts` — spawn / despawn / addComponent / removeComponent / getComponent / setComponent / hasComponent / query / addSystem / removeSystem / execute / snapshot / restore / clear.
|
|
- `src/world/world.spec-conformance.test.ts` — OTel emission (real `MeterProvider`), error-code coverage, EventBus payload typing, injectable Clock.
|
|
- `src/schema/schema.test.ts` — `defineComponent` factory with and without Zod validator.
|
|
- `src/errors/errors.test.ts` — every `ECS.*` code is thrown from at least one production path.
|
|
- `tests/property.test.ts` — fast-check: entity count monotonicity, bitset algebra, query consistency vs exhaustive scan, snapshot/restore round-trip.
|
|
- `tests/purity.test.ts` — built dist references no `node:*` / `Buffer` / `process`.
|
|
- `tests/dep-graph.test.ts` — runtime deps limited to the declared allowlist.
|
|
|
|
## Migration impact
|
|
|
|
None — additive only. No genesis package currently depends on `@suaveplan/ecs`. Downstream `games`-plugin consumers (procedural-world, fauna, fruit-machine, dialogue, economy) are not yet on genesis and will adopt the new surface when they migrate.
|