@forgeax/engine-ecs 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +94 -35
  2. package/dist/__tests__/world-read.unit.test.d.ts +2 -0
  3. package/dist/__tests__/world-read.unit.test.d.ts.map +1 -0
  4. package/dist/commands.d.ts +2 -0
  5. package/dist/commands.d.ts.map +1 -1
  6. package/dist/index.mjs +3471 -4263
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal.d.ts +2 -3
  9. package/dist/internal.d.ts.map +1 -1
  10. package/dist/internal.mjs +3 -333
  11. package/dist/internal.mjs.map +1 -1
  12. package/dist/projection/index.mjs.map +1 -1
  13. package/dist/shared.mjs.map +1 -1
  14. package/dist/world-entity-lifecycle.d.ts +3 -14
  15. package/dist/world-entity-lifecycle.d.ts.map +1 -1
  16. package/dist/world-internal.d.ts +65 -5
  17. package/dist/world-internal.d.ts.map +1 -1
  18. package/dist/world-read.d.ts +16 -0
  19. package/dist/world-read.d.ts.map +1 -0
  20. package/dist/world-read.mjs +8 -0
  21. package/dist/world-read.mjs.map +1 -0
  22. package/dist/world-scheduling.d.ts +0 -4
  23. package/dist/world-scheduling.d.ts.map +1 -1
  24. package/dist/world-storage-primitives.d.ts +26 -0
  25. package/dist/world-storage-primitives.d.ts.map +1 -0
  26. package/dist/world.d.ts +352 -157
  27. package/dist/world.d.ts.map +1 -1
  28. package/package.json +8 -4
  29. package/src/__tests__/command-buffer.test.ts +29 -3
  30. package/src/__tests__/hierarchy.unit.test.ts +3 -3
  31. package/src/__tests__/world-health.contract.test.ts +195 -2
  32. package/src/__tests__/world-read.unit.test.ts +30 -0
  33. package/src/commands.ts +22 -9
  34. package/src/internal.ts +5 -3
  35. package/src/world-entity-lifecycle.ts +23 -252
  36. package/src/world-internal-augmentation.d.ts +11 -0
  37. package/src/world-internal.ts +114 -63
  38. package/src/world-read.ts +38 -0
  39. package/src/world-scheduling.ts +0 -26
  40. package/src/world-storage-primitives.ts +179 -0
  41. package/src/world.ts +2590 -509
  42. package/dist/world-component-access.d.ts +0 -311
  43. package/dist/world-component-access.d.ts.map +0 -1
  44. package/dist/world-component-storage.d.ts +0 -298
  45. package/dist/world-component-storage.d.ts.map +0 -1
  46. package/dist/world-core.d.ts +0 -39
  47. package/dist/world-core.d.ts.map +0 -1
  48. package/src/world-component-access.ts +0 -1769
  49. package/src/world-component-storage.ts +0 -1264
  50. package/src/world-core.ts +0 -74
package/dist/world.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { Handle, Result } from '@forgeax/engine-types';
2
2
  import { type Component, ComponentCatalog, type ComponentSchema, type InputShapeOf, type ShapeOf } from './component';
3
3
  import { type EntityHandle } from './entity-handle';
4
- import type { CommandFailedError, ComponentAlreadyPresentError, ComponentFieldInvalidValueError, ComponentNotDefinedError, ComponentNotPresentError, ComponentNumericValueInvalidError, DerivedRangeOutOfBoundsError, FixedSizeMismatchError, ManagedArrayInvalidValueError, ManagedBufferOutOfBoundsError, ManagedBufferShrinkNotSupportedError, RelationshipDetachMismatchError, RelationshipMirrorComponentNotRegisteredError, RelationshipMirrorFieldTypeMismatchError, RelationshipSelfCycleError, RemoveEssentialComponentError, ScheduleMutationError, ScheduleScopeMismatchError, SharedKernelEligibilityError, SharedKernelFailureError, StaleEntityError, SystemFailedError, SystemSetNotRegisteredError, TimeConfigInvalidError, TimeDeltaInvalidError, UniqueRefDoubleReleaseError, UniqueRefReleasedError, WorldPoisonedError } from './errors';
5
- import { ChangeEpochExhaustedError, RelationshipTargetReadonlyError } from './errors';
4
+ import type { CommandFailedError, ComponentFieldInvalidValueError, ComponentNotDefinedError, ComponentNumericValueInvalidError, DerivedRangeOutOfBoundsError, ManagedArrayInvalidValueError, ManagedBufferShrinkNotSupportedError, RelationshipDetachMismatchError, RelationshipMirrorComponentNotRegisteredError, RelationshipMirrorFieldTypeMismatchError, ScheduleMutationError, ScheduleScopeMismatchError, SharedKernelEligibilityError, SharedKernelFailureError, SystemFailedError, SystemSetNotRegisteredError, TimeConfigInvalidError, TimeDeltaInvalidError, UniqueRefDoubleReleaseError, UniqueRefReleasedError } from './errors';
5
+ import { ChangeEpochExhaustedError, ComponentAlreadyPresentError, ComponentNotPresentError, FixedSizeMismatchError, ManagedBufferOutOfBoundsError, RelationshipSelfCycleError, RelationshipTargetReadonlyError, RemoveEssentialComponentError, StaleEntityError, WorldPoisonedError } from './errors';
6
6
  import { type WorldExecutionState } from './execution/shared-kernel';
7
7
  import type { QueryDescriptor } from './query/query';
8
8
  import { type Query, type QueryCreationError } from './query/query';
@@ -11,7 +11,7 @@ import { type SystemDescriptor, type SystemSet } from './schedule';
11
11
  import type { SharedRefStore } from './shared-ref-store';
12
12
  import { type ChangeTicks } from './storage/change-detection';
13
13
  import { type WorldOptions } from './time';
14
- import { type WorldInternal, worldInternal } from './world-internal';
14
+ import { type WorldRead, worldRead } from './world-read';
15
15
  /**
16
16
  * Union of all EcsError types that World methods can return via Result.
17
17
  * AI users: switch on `.code` for programmatic branching.
@@ -144,8 +144,10 @@ export interface WorldScheduleData {
144
144
  * permanently (it is never pushed back to `freeIndices`). The single liveness
145
145
  * predicate is therefore "handle gen matches AND archetypeId !== -1" -- see
146
146
  * `World.recordIsLive`. A deferred-spawn allocation is "pending" when
147
- * archetypeId === -1 (not yet materialized into an archetype row); no separate
148
- * boolean is needed.
147
+ * archetypeId === -1 (not yet materialized into an archetype row). During a
148
+ * failed append, `archetypeId` is reserved before the first storage write and
149
+ * `archetypeRow` remains `-1`; that marker prevents command abort from
150
+ * reclaiming a reservation that may already have touched table storage.
149
151
  */
150
152
  export interface EntityRecord {
151
153
  generation: number;
@@ -153,37 +155,32 @@ export interface EntityRecord {
153
155
  archetypeRow: number;
154
156
  }
155
157
  /**
156
- * The World owns:
157
- * - the registry of known component schemas;
158
- * - all archetypes (via ArchetypeGraph);
159
- * - the entity index table (records by index slot);
160
- * - the free-list of recyclable entity slots.
158
+ * The World owns the component registry, archetype graph, entity index table,
159
+ * and recyclable free-list.
161
160
  */
162
161
  export declare class World {
163
- /** Package-internal implementation seam; not exported from any entry point. */
164
- readonly [worldInternal]: WorldInternal;
165
- /** The single package-private owner of storage, epochs, and change evidence. */
166
- private readonly core;
162
+ readonly [worldRead]: WorldRead;
163
+ /** World-local state is kept together so one owner closes each mutation. */
164
+ private executionState;
165
+ private readonly graph;
166
+ private readonly records;
167
+ private readonly freeIndices;
168
+ /** BufferPool for schema-declared variable buffers and arrays. */
169
+ private readonly bufferPool;
170
+ /** Per-World managed unique-ref store used by lifecycle mutations. */
171
+ private readonly uniqueRefs;
172
+ /** Per-World shared-ref store; public read-only for direct handle operations. */
173
+ readonly sharedRefs: SharedRefStore;
174
+ private readonly componentMutationEpochs;
175
+ private readonly structuralEvidence;
176
+ /** One packed reverse index per relationship source component. */
177
+ private readonly relationshipIndexes;
178
+ private mutationEpoch;
179
+ private structureEpoch;
180
+ /** Keep identity as a prototype getter; it is a diagnostic capability, not enumerable state. */
167
181
  get identity(): string;
168
182
  /** Plugin-owned component discovery scoped to this World and removed through leases. */
169
183
  readonly components: ComponentCatalog;
170
- private executionState;
171
- /** Monotonic clock advanced exactly once per successful mutation. */
172
- /** Monotonic revision for successful entity/component structure writes. */
173
- /** Last mutation epoch for each component id, used by component-owned projections. */
174
- /** Ordered, bounded evidence consumed by persistent engine-owned projections. */
175
- private get mutationEpoch();
176
- private set mutationEpoch(value);
177
- private get structureEpoch();
178
- private set structureEpoch(value);
179
- private get componentMutationEpochs();
180
- /** Free index slots (LIFO stack). */
181
- private get records();
182
- private get freeIndices();
183
- /**
184
- * Relationship-sync reentry guard (feat-20260531 M2 / plan-strategy D-7).
185
- /** The archetype graph: manages all archetypes + edge caching. */
186
- private get graph();
187
184
  /** DAG schedules for the two built-in execution scopes. */
188
185
  private readonly schedules;
189
186
  /** Resource store: typed key-value global singletons. */
@@ -191,56 +188,23 @@ export declare class World {
191
188
  private readonly clock;
192
189
  /** Remainder carried between fixed-step runs. */
193
190
  private fixedAccumulator;
194
- /**
195
- * ECS-managed handle store (M1). Owned by the World - constructed eagerly
196
- * so every spawn / despawn / set path can dispatch managed-ref releases
197
- * without caller-side wiring. AI users obtain `Handle<T,'unique'>` values
198
- * by accessing the store through internal channels (the surface is
199
- * private; managed-ref-bearing fields read through `world.get`).
200
- */
201
- private get uniqueRefs();
202
- /**
203
- * Per-World `SharedRefStore` (feat-20260614 M3). Backs every `shared<T>`
204
- * schema field + the `world.allocSharedRef` facade. Public read-only so AI
205
- * users can `retain` / `release` / `resolve` user-tier handles directly off
206
- * the world (the surface is small enough that hiding it behind another
207
- * facade would be a phantom indirection - charter F1 single-entry
208
- * indexability).
209
- *
210
- * Final release publishes structured evidence; there is no callback surface.
211
- * M6 D-15: the store manages only user-tier slots
212
- * (`>= BUILTIN_BASE`); builtin handles are process-static in their
213
- * authoring package and never reference-counted.
214
- */
215
- get sharedRefs(): SharedRefStore;
216
- /**
217
- * BufferPool backing every `buffer:<N>` schema-vocab field (M2). Eagerly
218
- * constructed (per-World, D-2). `spawn` allocs slots for buffer fields and
219
- * stores the slot id in the u32 column; `despawn` / `removeComponent`
220
- * release the slots; `set(e, C, { field: Uint8Array })` copies bytes into
221
- * the live view without re-allocating (schema-declared byteLength is
222
- * fixed in v1; runtime grow is reserved for the M4 carry-over path).
223
- */
224
- private get bufferPool();
225
- private readonly componentAccess;
226
191
  constructor(options?: WorldOptions);
227
192
  /** Immutable integrity state for execution coordinators and headless callers. */
228
193
  get execution(): WorldExecutionState;
229
194
  /** Resolve a schedule token owned by this World realm without package singleton identity. */
230
195
  scheduleToken(name: import('./schedule-token').ScheduleName): import('./schedule-token').ScheduleToken;
231
- /** SharedKernel is the only writer; application code recovers by constructing a new World. */
196
+ /** Seal the first execution fault; application code recovers with a new World. */
232
197
  private internalpoisonExecution;
198
+ /**
199
+ * A poisoned identity is diagnostic evidence, not a mutable recovery path.
200
+ * Public entity mutation therefore returns the same structured fence as
201
+ * `update()` instead of allocating a new reservation or touching a partial
202
+ * row. Recovery remains construction of a fresh World.
203
+ */
204
+ private poisonedResult;
233
205
  query<const R extends readonly Component[] = readonly [], const W extends readonly Component[] = readonly [], const O extends readonly Component[] = readonly []>(descriptor: QueryDescriptor<R, W, O>): Result<Query<R, W, O>, QueryCreationError>;
234
- /** Expose archetype graph for query engine. Not part of public API. */
235
- private internalgetGraph;
236
206
  private componentIsInUse;
237
- /** Current upper bound for mutation observation. */
238
- private internalgetMutationEpoch;
239
- /** Structure snapshot used to invalidate borrowed query facades. */
240
- private internalgetStructureEpoch;
241
- /** Typed structural facts are produced by this World and consumed by projections. */
242
- private internalgetStructuralEvidence;
243
- private internalrecordStructuralEvidence;
207
+ private recordStructuralEvidence;
244
208
  /** Resolve current logical identity for a packed entity handle. */
245
209
  private internalgetEntityArchetype;
246
210
  /** Component change state for query filters. */
@@ -250,55 +214,21 @@ export declare class World {
250
214
  private internalrestoreMutationEpoch;
251
215
  private internalpublishDerivedRange;
252
216
  /** Record one successful structural mutation. */
253
- private internalmarkStructureChanged;
217
+ private advanceStructureEpoch;
254
218
  /** Current structural revision for mounted World projections. */
255
219
  getStructureEpoch(): number;
256
- /** Mark a component as both added and changed at the current tick. */
257
- private internalmarkComponentAdded;
258
220
  /** Mark one mutation's component instances with a shared epoch. */
259
221
  private internalmarkComponentsAdded;
260
222
  /** Mark an existing component as changed at the current tick. */
261
223
  private internalmarkComponentChanged;
262
224
  /** Mark one contiguous component range with a single epoch. */
263
225
  private internalmarkComponentRangeChanged;
264
- /** Latest mutation token for one component-owned projection. */
265
- private internalgetComponentMutationEpoch;
266
- /** Borrow the component-version summary used to skip unrelated value columns. */
267
- private internalgetComponentMutationEpochs;
268
- /** Read a materialized relationship target in O(1 + k). */
269
- private internalgetRelationshipTargetEntities;
270
- /** Monotonic epoch for the materialized relationship index. */
271
- private internalgetRelationshipEpoch;
272
226
  /** Query facade write after the facade has already marked evidence. */
273
227
  private internalsetQueryRow;
274
- /** Query facade read that does not re-enter the public World API. */
275
- private internalgetQueryRow;
276
228
  /** Return resource change ticks for diagnostics and resource-driven systems. */
277
229
  getResourceChange(name: string): ChangeTicks | undefined;
278
- /**
279
- * Route a structured error from
280
- * an engine-internal subsystem (e.g. RenderSystem extract stage, w15).
281
- *
282
- * Mirrors the private `errorHandler(err, ctx)` call sites inside `World`
283
- * itself; the dedicated accessor avoids exposing `errorHandler` directly
284
- * and keeps the routing contract under the `_xxx` `@internal` umbrella so
285
- * AI users do not discover it through IDE autocomplete on `World`.
286
- *
287
- * Not part of the public API.
288
- */
289
- private internalrouteError;
290
- /** */ private internalgetRecords;
291
- /** */ private internalgetFreeIndices;
292
- /** */ private internalgetResources;
293
- /** */ private internalgetFixedAccumulator;
294
- /** */ private internalsetFixedAccumulator;
295
- /** */ private internalgetUniqueRefs;
296
- /** */ private internalgetBufferPool;
297
- /** Scheduler-owned mutable clock capability. */
298
- private internalgetClockWriter;
299
- /** */ private internalgetSchedule;
300
- /** */ private internalgetSchedules;
301
- /** */ private internalgetSharedRefs;
230
+ /** Route an expected internal failure through the host-owned error channel. */
231
+ private routeError;
302
232
  /**
303
233
  * Register a system with query descriptor and optional ordering constraints.
304
234
  *
@@ -516,45 +446,326 @@ export declare class World {
516
446
  internSharedRef<Target extends string, T extends object>(target: Target, payload: T): Handle<Target, 'shared'>;
517
447
  private relationshipTargetWriteError;
518
448
  private relationshipTargetPayloadWrites;
449
+ /** Test live component presence without constructing a Result error. */
450
+ hasComponent(entity: EntityHandle, component: Component): boolean;
451
+ private table;
452
+ private tableRow;
453
+ private markComponentChanged;
454
+ private relationshipIndex;
455
+ /** Read the World-owned materialized target array; never consults a shadow list. */
456
+ private relationshipTargetEntries;
457
+ /** Read a relationship target length without materialising its array view. */
458
+ private relationshipTargetLength;
459
+ private relationshipTargetEntity;
460
+ private preflightComponentFieldValues;
461
+ /**
462
+ * Validate one structural component payload without touching archetypes,
463
+ * columns, relationship mirrors, epochs, or managed-reference stores.
464
+ * CommandBuffer uses this same owner-level gate as the direct World facade;
465
+ * the optional pending set lets a batch refer to an entity reserved earlier
466
+ * in that batch without mistaking it for a stale live handle.
467
+ */
468
+ private preflightComponentData;
469
+ private relationshipCycleHit;
470
+ /**
471
+ * Commit a relationship source through its owner-specific write path.
472
+ * Relationship sources have one entity field, so dispatching before the
473
+ * generic field loop avoids paying the ordinary component-field traversal on
474
+ * every hierarchy reparent while keeping mirror/index publication here.
475
+ */
476
+ private setRelationshipSource;
477
+ /** Prepare the target side before a source archetype mutation commits. */
478
+ private prepareRelationshipInsert;
479
+ /** Append `holder` to the materialized target list. */
480
+ private relationshipOnInsert;
481
+ /** Remove `holder` from the materialized target list. */
482
+ private relationshipOnRemove;
483
+ private linkedSpawnMirrorField;
484
+ private relationshipLinkedSpawnChildren;
485
+ /**
486
+ * Read component data from an entity.
487
+ *
488
+ * **Transient view contract (feat-20260602):** for fixed-capacity
489
+ * `array<T,N>` and `buffer<N>` fields, the returned `TypedArray` (and any
490
+ * subarray of it) aliases the archetype column buffer directly. The view is
491
+ * valid only until the next structural change (`spawn` / `despawn` /
492
+ * `addComponent` / `removeComponent`). Holding a view across a structural
493
+ * change is undefined behaviour -- the backing `ArrayBuffer` is detached on
494
+ * column growth, and swap-remove at the same row index points to the wrong
495
+ * entity. **Re-fetch `world.get(e, C)` on every access.** See
496
+ * `packages/ecs/README.md` Transient view contract section.
497
+ *
498
+ * @returns `Result<ShapeOf<S>, EcsError>` —
499
+ * `ok(ShapeOf<S>)` on success;
500
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
501
+ * `err(ComponentNotPresentError)` (`.code = 'component-not-present'`) if
502
+ * the entity does not have the component (a never-present component on
503
+ * this entity degrades to the same `component-not-present` path — there is
504
+ * no separate "not registered" failure; components are global at
505
+ * `defineComponent` time).
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
510
+ * const world = new World();
511
+ * const e = world.spawn({ component: Position, data: { x: 1, y: 2 } }).unwrap();
512
+ * const r = world.get(e, Position);
513
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
514
+ * const pos = r.value;
515
+ * ```
516
+ */
519
517
  get<S extends ComponentSchema>(entity: EntityHandle, component: Component<string, S>): Result<ShapeOf<S>, EcsError>;
520
518
  /**
521
- * Test live component presence without constructing a Result error.
519
+ * Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
520
+ *
521
+ * Resolves the live byte region for `(entity, component, fieldName)`
522
+ * directly at the column level and returns the element-typed TypedArray
523
+ * aliasing it (`view.buffer` is the SSOT byte region; mutations route
524
+ * through `world.set`). Unlike `get`, this does NOT build the
525
+ * `{}` whole-component object nor walk every schema field. Per-frame
526
+ * consumers that need one column (the resolved world mat4) take this path to
527
+ * avoid the `get` overhead (1 `{}` alloc + N-field readRow walk).
528
+ *
529
+ * Fixed `array<T,N>` columns (feat-20260602) store their elements inline, so
530
+ * the view aliases the archetype column buffer directly (no BufferPool
531
+ * indirection); variable `array<T>` columns still alias the BufferPool slot.
532
+ * The returned view's element type follows the schema element type
533
+ * (`array<entity,N>` -> `Uint32Array`, `array<f32,N>` -> `Float32Array`,
534
+ * etc.) -- the prior f32-only early-return gate is removed.
535
+ *
536
+ * **Transient view contract:** the returned `TypedArray` aliases the column
537
+ * buffer and is valid only until the next structural change (`spawn` /
538
+ * `despawn` / `addComponent` / `removeComponent`). Column growth
539
+ * (`growColumn`) detaches the old `ArrayBuffer` via `transfer()`; a
540
+ * swap-remove at the same row index leaves the view pointing to the wrong
541
+ * entity. **Callers must re-fetch `getArrayView` on every access** and must
542
+ * not hold the view across any operation that may cause archetype migration.
543
+ * All existing per-frame consumers (`propagateTransforms` / `render-extract`
544
+ * / `pick`) already conform -- they fetch the view inside a single pass with
545
+ * no intervening structural changes.
546
+ *
547
+ * Returns `undefined` when the entity is dead, the component is absent, the
548
+ * field does not exist, or the field is not an `array<...>` column.
549
+ *
550
+ * Engine-internal fast path; AI users read the typed view through
551
+ * `world.get(e, GlobalTransform).world`. The accessor is the zero-materialization
552
+ * route the propagate kernel and render walk use.
553
+ */
554
+ private getArrayView;
555
+ /**
556
+ * Write (partial) component data to an entity.
557
+ *
558
+ * @returns `Result<void, EcsError>` —
559
+ * `ok(void)` on success;
560
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
561
+ * `err(ComponentNotPresentError)` (`.code = 'component-not-present'`) if
562
+ * entity does not have the component (F-02: no longer silently ignores).
563
+ *
564
+ * @example
565
+ * ```ts
566
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
567
+ * const world = new World();
568
+ * const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
569
+ * const r = world.set(e, Position, { x: 10 });
570
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
571
+ * r.unwrap();
572
+ * ```
573
+ */
574
+ set<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, component: C & WritableComponent<C>, value: Partial<InputShapeOf<S>>, markChanged?: boolean): Result<void, EcsError>;
575
+ /**
576
+ * Append `value` to the variable `array<T>` field `fieldName` on `entity`.
522
577
  *
523
- * Read projections commonly need to branch on optional components for many
524
- * entities. Calling `get` for that branch allocates a structured
525
- * ComponentNotPresentError on every ordinary miss (and StaleEntityError for
526
- * a dangling handle). This predicate is deliberately non-throwing and
527
- * returns false for both cases; callers that need the detailed error should
528
- * continue to use `get`.
578
+ * BufferPool grow is amortized O(1) via the size-class freelist (research
579
+ * Finding 5). Relationship target arrays grow byte-wise.
580
+ *
581
+ * @returns `Result<void, EcsError>` with the normal stale/component errors.
582
+ *
583
+ * The helper is called only by relationship synchronization.
584
+ */
585
+ private appendArrayElement;
586
+ private ensureArrayCapacity;
587
+ /**
588
+ * Remove one variable-array element at a known slot. Relationship holders
589
+ * supply the slot from their backpointer, so this is O(1) and never scans
590
+ * the materialized target array.
591
+ */
592
+ private removeArrayElementAt;
593
+ /**
594
+ * Add a component to an existing entity, triggering archetype migration.
595
+ *
596
+ * @returns `Result<void, EcsError>` —
597
+ * `ok(void)` on success;
598
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
599
+ * `err(ComponentAlreadyPresentError)` (`.code = 'component-already-present'`)
600
+ * if entity already has the component (E-03).
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
605
+ * const Velocity = defineComponent('Velocity', { dx: 'f32', dy: 'f32' });
606
+ * const world = new World();
607
+ * const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
608
+ * const r = world.addComponent(e, { component: Velocity, data: { dx: 1, dy: 0 } });
609
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
610
+ * r.unwrap();
611
+ * ```
529
612
  */
530
- hasComponent(entity: EntityHandle, component: Component): boolean;
531
- private internalgetArrayView;
532
- private internalgetArrayLength;
533
- private internalgetArrayElement;
534
- private internalgetFieldValue;
535
- set<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, component: C & WritableComponent<C>, value: Partial<InputShapeOf<S>>): Result<void, EcsError>;
536
613
  addComponent<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, componentData: ComponentData<S> & {
537
614
  component: C & WritableComponent<C>;
538
615
  }): Result<void, EcsError>;
539
- private internaladdComponentCore;
616
+ /**
617
+ * Core implementation of `addComponent` with reentry guard.
618
+ *
619
+ * @param internal — `true` when called from relationship maintenance
620
+ * (lazy mirror create or exclusive reparent).
621
+ */
622
+ private addComponentCore;
623
+ /**
624
+ * Remove a component from an existing entity, triggering archetype migration.
625
+ *
626
+ * @returns `Result<void, EcsError>` —
627
+ * `ok(void)` on success;
628
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
629
+ * `err(ComponentNotPresentError)` (`.code = 'component-not-present'`)
630
+ * if entity doesn't have the component (E-04).
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
635
+ * const world = new World();
636
+ * const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
637
+ * const r = world.removeComponent(e, Position);
638
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
639
+ * r.unwrap();
640
+ * ```
641
+ */
540
642
  removeComponent<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, component: C & WritableComponent<C>): Result<void, EcsError>;
541
- private internalremoveComponentCore;
542
- private internalallocatePendingEntity;
543
- /** */ private internalcancelPendingEntity;
544
- private internalmaterializePendingEntity;
545
- /** Shared structural preflight for direct and deferred writes. */
546
- private internalpreflightComponentData;
547
- /** */ private internalallocateIndex;
548
- /** */ private internalrecordIsLive;
549
- /** */ private internallookupAlive;
550
- /** */ private internalreadRow;
551
- /** */ private internalwriteEntitySelf;
552
- /** */ private internalwriteRow;
553
- /** */ private internalreleaseManagedRefsOnRow;
554
- /** */ private internalrelationshipOnInsert;
555
- /** */ private internalprepareRelationshipInsert;
556
- /** */ private internalreleaseRelationshipPreparation;
557
- /** */ private internalrelationshipOnRemove;
643
+ /**
644
+ * Core implementation of `removeComponent` with reentry guard.
645
+ *
646
+ * @param internal — `true` when called from relationship maintenance
647
+ * (exclusive reparent).
648
+ */
649
+ private removeComponentCore;
650
+ /**
651
+ * Allocate a pending entity for deferred spawn.
652
+ * Returns an Entity handle. The entity is "pending" because
653
+ * archetypeId === -1 (set by allocateIndex); no separate flag needed.
654
+ */
655
+ private allocatePendingEntity;
656
+ /**
657
+ * Return a deferred-spawn reservation to the free-list without publishing a
658
+ * row or advancing an epoch. CommandBuffer.abort is the sole caller; a
659
+ * materialized entity is intentionally left untouched so an unexpected
660
+ * post-write failure poisons the World instead of attempting an unsafe undo.
661
+ */
662
+ private cancelPendingEntity;
663
+ /**
664
+ * Materialize one already-reserved entity. Synchronous spawn and deferred
665
+ * command flush share this exact insertion/publication path; only the
666
+ * caller's validation and reservation boundary differs.
667
+ *
668
+ */
669
+ private materializeEntity;
670
+ private poisonAfterEntityMutation;
671
+ /** Deferred commands use the common materialization owner. */
672
+ private materializePendingEntity;
673
+ private allocateIndex;
674
+ /**
675
+ * Single liveness predicate (feat-20260602 / plan-strategy D-4): a slot is
676
+ * live for a given handle generation iff the record exists, its generation
677
+ * still matches the handle (despawn bumps generation, so a stale or recycled
678
+ * handle fails here), and the slot is materialized into an archetype
679
+ * (archetypeId !== -1). Replaces the former `record.alive && record.generation
680
+ * === gen` conjunction and the intermediate `!record.pending` clause. An
681
+ * append in progress keeps `archetypeRow === -1` until both storage indexes
682
+ * exist.
683
+ */
684
+ private recordIsLive;
685
+ private lookupAlive;
686
+ private readArrayView;
687
+ /** Read one scalar column without constructing a component snapshot. */
688
+ private internalgetFieldValue;
689
+ /** Read an array's live logical length without allocating a TypedArray. */
690
+ private internalgetArrayLength;
691
+ /** Read one array element directly from its column or BufferPool slot. */
692
+ private internalgetArrayElement;
693
+ private readRow;
694
+ /**
695
+ * Write the full packed entity handle into the row's essential id=0 `Entity`
696
+ * column (`self` field). Called by `spawn` / `materializePendingEntity`
697
+ * after the row is appended (feat-20260602 / plan-strategy D-3). The column
698
+ * always exists -- `createArchetype` folds the Entity column into every
699
+ * archetype -- so this is a direct u32 store, no readRow/writeRow walk.
700
+ */
701
+ private writeEntitySelf;
702
+ private writeRow;
703
+ /** Release all ECS-owned field handles before a row is removed or overwritten. */
704
+ private releaseManagedRefsOnRow;
705
+ /**
706
+ * Release one ECS-owned field according to its schema. Inline buffers have
707
+ * no pool slot; shared-array elements still release their handles. Store
708
+ * failures use the host error channel so row cleanup remains total.
709
+ */
710
+ private releaseManagedFieldOnRow;
711
+ /** Release one unique-ref handle; sentinel 0 is ignored. */
712
+ private releaseManagedRefHandle;
713
+ /** Release one shared-ref handle, preserving builtin slots and refcounts. */
714
+ private releaseSharedRefHandle;
715
+ /** Retain one shared-ref scalar handle for a World-owned field. */
716
+ private retainSharedScalarHandle;
717
+ /** Release one variable-buffer slot; id 0 is the unallocated sentinel. */
718
+ private releaseManagedBufferSlot;
719
+ /**
720
+ * Attach field context to a `ManagedArrayErrorEnvelope`.
721
+ * The envelope shape (`code / hint / expected / detail`) already mirrors
722
+ * the EcsError contract; this helper only attaches the systemName context
723
+ * so AI users can correlate the error with the holder component / field.
724
+ */
725
+ private routeArrayError;
726
+ /**
727
+ * Write an array field for spawn or set. Fixed arrays stay inline; variable
728
+ * arrays use one BufferPool slot plus a live-count sidecar.
729
+ */
730
+ private writeArrayField;
731
+ /**
732
+ * Walk the first `count` u32 handles in `bytes` and call
733
+ * `SharedRefStore.retain` on each non-sentinel slot id (feat-20260614 M4 /
734
+ * D-3). Failures route via the error channel so the write chain stays
735
+ * total; charter explicit-failure boundary lets AI users see structured
736
+ * `shared-ref-released` payloads when retaining a stale handle.
737
+ *
738
+ * Helper-internal -- only called from `writeArrayField`'s `'shared'` arm.
739
+ */
740
+ private retainSharedArrayElements;
741
+ /**
742
+ * Walk the first `count` u32 handles in `bytes` and call
743
+ * `SharedRefStore.release` on each non-sentinel slot id (feat-20260614 M4 /
744
+ * D-3). Mirrors `retainSharedArrayElements`; called from
745
+ * `releaseManagedFieldOnRow`'s array arm BEFORE the BufferPool slot is
746
+ * released so the underlying bytes are still valid.
747
+ */
748
+ private releaseSharedArrayElements;
749
+ /**
750
+ * Materialize an array snapshot. Fixed arrays alias their inline column;
751
+ * variable arrays alias the live BufferPool slot and use the count sidecar.
752
+ * Both views are transient and must not be held across structural changes.
753
+ */
754
+ private materializeArrayView;
755
+ /**
756
+ * Copy surviving component columns into the target row, then swap-remove
757
+ * the source row. Managed handles and variable-array sidecars are copied
758
+ * verbatim; release remains the responsibility of remove/despawn paths.
759
+ */
760
+ private migrateEntity;
761
+ private moveEntityArchetype;
762
+ /**
763
+ * Retire one live entity and any linked-spawn descendants. The complete
764
+ * row/relationship/managed-data mutation stays on World so a failure after
765
+ * the first write can poison this identity instead of crossing an extraction
766
+ * owner boundary.
767
+ */
768
+ private despawnEntity;
558
769
  /**
559
770
  * Spawn an entity with one or more components.
560
771
  * Multi-component spawn directly targets the correct archetype (AC-06).
@@ -577,13 +788,6 @@ export declare class World {
577
788
  data: Partial<InputShapeOf<SArr[K]>>;
578
789
  };
579
790
  }): Result<EntityHandle, EcsError>;
580
- /**
581
- * Core implementation of `spawn` with reentry guard.
582
- *
583
- * @param internal — `true` when called from relationship maintenance
584
- * (lazy mirror create or exclusive reparent).
585
- */
586
- private internalspawnCore;
587
791
  /**
588
792
  * Despawn an entity. Stale handles are silently ignored (E-01, AC-17).
589
793
  * Generation retirement: gen=255 → index permanently retired (D-08/E-08).
@@ -602,15 +806,6 @@ export declare class World {
602
806
  despawn(entity: EntityHandle): Result<void, EcsError>;
603
807
  /** Despawn every live entity through the normal lifecycle and ref cleanup path. */
604
808
  despawnAll(): Result<void, EcsError>;
605
- /**
606
- * Core implementation of `despawn` with reentry guard.
607
- *
608
- * @param internal — `true` when called from within linkedSpawn cascade.
609
- * Nested despawn skips relationship pruning after the parent is retired;
610
- * the linkedSpawn collection still walks the subtree so grandchildren
611
- * cascade correctly (tweak-20260714 M2, R-6).
612
- */
613
- private internaldespawnCore;
614
809
  addChild<S extends ComponentSchema>(parent: EntityHandle, child: EntityHandle, component: Component<string, S>, data: Partial<InputShapeOf<S>>): Result<void, EcsError>;
615
810
  removeChild<S extends ComponentSchema>(parent: EntityHandle, child: EntityHandle, component: Component<string, S>): Result<void, EcsError>;
616
811
  reparent<S extends ComponentSchema>(child: EntityHandle, newParent: EntityHandle, component: Component<string, S>, data: Partial<InputShapeOf<S>>): Result<void, EcsError>;