@oasys/oecs 0.5.2 → 0.5.4

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 (189) hide show
  1. package/CHANGELOG.md +208 -14
  2. package/README.md +198 -145
  3. package/dist/core/ecs/access_check.d.cts +3 -3
  4. package/dist/core/ecs/access_check.d.ts +3 -3
  5. package/dist/core/ecs/archetype.d.cts +103 -43
  6. package/dist/core/ecs/archetype.d.ts +103 -43
  7. package/dist/core/ecs/archetype.d.ts.map +1 -1
  8. package/dist/core/ecs/archetype_graph.d.cts +8 -8
  9. package/dist/core/ecs/archetype_graph.d.ts +8 -8
  10. package/dist/core/ecs/archetype_graph.d.ts.map +1 -1
  11. package/dist/core/ecs/builtin_relations.d.cts +1 -1
  12. package/dist/core/ecs/builtin_relations.d.ts +1 -1
  13. package/dist/core/ecs/command_log.d.cts +4 -4
  14. package/dist/core/ecs/command_log.d.ts +4 -4
  15. package/dist/core/ecs/command_log.d.ts.map +1 -1
  16. package/dist/core/ecs/component.d.cts +4 -4
  17. package/dist/core/ecs/component.d.ts +4 -4
  18. package/dist/core/ecs/deferred_commands.d.cts +1 -1
  19. package/dist/core/ecs/deferred_commands.d.ts +1 -1
  20. package/dist/core/ecs/deferred_commands.d.ts.map +1 -1
  21. package/dist/core/ecs/ecs.d.cts +85 -43
  22. package/dist/core/ecs/ecs.d.ts +85 -43
  23. package/dist/core/ecs/ecs.d.ts.map +1 -1
  24. package/dist/core/ecs/ecs_memory.d.cts +15 -15
  25. package/dist/core/ecs/ecs_memory.d.ts +15 -15
  26. package/dist/core/ecs/ecs_memory.d.ts.map +1 -1
  27. package/dist/core/ecs/entity_allocator.d.cts +6 -6
  28. package/dist/core/ecs/entity_allocator.d.ts +6 -6
  29. package/dist/core/ecs/entity_allocator.d.ts.map +1 -1
  30. package/dist/core/ecs/event.d.cts +2 -2
  31. package/dist/core/ecs/event.d.ts +2 -2
  32. package/dist/core/ecs/event_registry.d.cts +1 -1
  33. package/dist/core/ecs/event_registry.d.ts +1 -1
  34. package/dist/core/ecs/event_registry.d.ts.map +1 -1
  35. package/dist/core/ecs/facades.d.cts +15 -15
  36. package/dist/core/ecs/facades.d.ts +15 -15
  37. package/dist/core/ecs/facades.d.ts.map +1 -1
  38. package/dist/core/ecs/frame_trace.d.cts +2 -2
  39. package/dist/core/ecs/frame_trace.d.ts +2 -2
  40. package/dist/core/ecs/host_commands.d.cts +12 -12
  41. package/dist/core/ecs/host_commands.d.ts +12 -12
  42. package/dist/core/ecs/host_commands.d.ts.map +1 -1
  43. package/dist/core/ecs/index.d.cts +1 -1
  44. package/dist/core/ecs/index.d.ts +1 -1
  45. package/dist/core/ecs/index.d.ts.map +1 -1
  46. package/dist/core/ecs/observer.d.cts +9 -9
  47. package/dist/core/ecs/observer.d.ts +9 -9
  48. package/dist/core/ecs/observer.d.ts.map +1 -1
  49. package/dist/core/ecs/query.d.cts +91 -70
  50. package/dist/core/ecs/query.d.ts +91 -70
  51. package/dist/core/ecs/query.d.ts.map +1 -1
  52. package/dist/core/ecs/ref.d.cts +53 -1
  53. package/dist/core/ecs/ref.d.ts +53 -1
  54. package/dist/core/ecs/ref.d.ts.map +1 -1
  55. package/dist/core/ecs/relation.d.cts +15 -15
  56. package/dist/core/ecs/relation.d.ts +15 -15
  57. package/dist/core/ecs/relation.d.ts.map +1 -1
  58. package/dist/core/ecs/relation_service.d.cts +14 -14
  59. package/dist/core/ecs/relation_service.d.ts +14 -14
  60. package/dist/core/ecs/relation_service.d.ts.map +1 -1
  61. package/dist/core/ecs/resource.d.cts +1 -1
  62. package/dist/core/ecs/resource.d.ts +1 -1
  63. package/dist/core/ecs/resource_registry.d.cts +2 -2
  64. package/dist/core/ecs/resource_registry.d.ts +2 -2
  65. package/dist/core/ecs/resume.d.cts +3 -3
  66. package/dist/core/ecs/resume.d.ts +3 -3
  67. package/dist/core/ecs/resume.d.ts.map +1 -1
  68. package/dist/core/ecs/schedule.d.cts +22 -9
  69. package/dist/core/ecs/schedule.d.ts +22 -9
  70. package/dist/core/ecs/schedule.d.ts.map +1 -1
  71. package/dist/core/ecs/snapshot_service.d.cts +3 -3
  72. package/dist/core/ecs/snapshot_service.d.ts +3 -3
  73. package/dist/core/ecs/sparse_store.d.cts +6 -6
  74. package/dist/core/ecs/sparse_store.d.ts +6 -6
  75. package/dist/core/ecs/sparse_store.d.ts.map +1 -1
  76. package/dist/core/ecs/store.d.cts +189 -110
  77. package/dist/core/ecs/store.d.ts +189 -110
  78. package/dist/core/ecs/store.d.ts.map +1 -1
  79. package/dist/core/ecs/store_layout_listener.d.cts +2 -2
  80. package/dist/core/ecs/store_layout_listener.d.ts +2 -2
  81. package/dist/core/ecs/system.d.cts +8 -8
  82. package/dist/core/ecs/system.d.ts +8 -8
  83. package/dist/core/ecs/system.d.ts.map +1 -1
  84. package/dist/core/ecs/utils/error.d.cts +7 -0
  85. package/dist/core/ecs/utils/error.d.ts +7 -0
  86. package/dist/core/ecs/utils/error.d.ts.map +1 -1
  87. package/dist/core/store/action_ring.d.cts +7 -8
  88. package/dist/core/store/action_ring.d.ts +7 -8
  89. package/dist/core/store/action_ring.d.ts.map +1 -1
  90. package/dist/core/store/allocator.d.cts +58 -35
  91. package/dist/core/store/allocator.d.ts +58 -35
  92. package/dist/core/store/allocator.d.ts.map +1 -1
  93. package/dist/core/store/buffer_backed_column.d.cts +5 -6
  94. package/dist/core/store/buffer_backed_column.d.ts +5 -6
  95. package/dist/core/store/buffer_backed_column.d.ts.map +1 -1
  96. package/dist/core/store/column_store.d.cts +23 -23
  97. package/dist/core/store/column_store.d.ts +23 -23
  98. package/dist/core/store/column_store.d.ts.map +1 -1
  99. package/dist/core/store/command_dispatch.d.cts +1 -1
  100. package/dist/core/store/command_dispatch.d.ts +1 -1
  101. package/dist/core/store/command_ring.d.cts +6 -7
  102. package/dist/core/store/command_ring.d.ts +6 -7
  103. package/dist/core/store/command_ring.d.ts.map +1 -1
  104. package/dist/core/store/descriptor.d.cts +2 -2
  105. package/dist/core/store/descriptor.d.ts +2 -2
  106. package/dist/core/store/entity_index.d.cts +3 -3
  107. package/dist/core/store/entity_index.d.ts +3 -3
  108. package/dist/core/store/event_ring.d.cts +7 -7
  109. package/dist/core/store/event_ring.d.ts +7 -7
  110. package/dist/core/store/extend.d.cts +2 -2
  111. package/dist/core/store/extend.d.ts +2 -2
  112. package/dist/core/store/extend.d.ts.map +1 -1
  113. package/dist/core/store/grow.d.cts +1 -1
  114. package/dist/core/store/grow.d.ts +1 -1
  115. package/dist/core/store/grow.d.ts.map +1 -1
  116. package/dist/core/store/header.d.cts +9 -9
  117. package/dist/core/store/header.d.ts +9 -9
  118. package/dist/core/store/header.d.ts.map +1 -1
  119. package/dist/core/store/layout_ops.d.cts +25 -7
  120. package/dist/core/store/layout_ops.d.ts +25 -7
  121. package/dist/core/store/layout_ops.d.ts.map +1 -1
  122. package/dist/core/store/state_hash.d.cts +3 -3
  123. package/dist/core/store/state_hash.d.ts +3 -3
  124. package/dist/extensions/editor/editor.d.cts +4 -4
  125. package/dist/extensions/editor/editor.d.ts +4 -4
  126. package/dist/extensions/editor/editor.d.ts.map +1 -1
  127. package/dist/extensions/editor/field_handle.d.cts +1 -1
  128. package/dist/extensions/editor/field_handle.d.ts +1 -1
  129. package/dist/extensions/editor/index.d.cts +2 -2
  130. package/dist/extensions/editor/index.d.ts +2 -2
  131. package/dist/extensions/editor/index.development.js +4 -4
  132. package/dist/extensions/editor/index.js +4 -4
  133. package/dist/extensions/reactive/ecs_sync.d.cts +4 -4
  134. package/dist/extensions/reactive/ecs_sync.d.ts +4 -4
  135. package/dist/extensions/reactive/index.d.cts +4 -4
  136. package/dist/extensions/reactive/index.d.ts +4 -4
  137. package/dist/extensions/reactive/index.development.js +3 -3
  138. package/dist/extensions/reactive/index.js +3 -3
  139. package/dist/extensions/solid/index.d.cts +1 -1
  140. package/dist/extensions/solid/index.d.ts +1 -1
  141. package/dist/extensions/solid/index.development.js +1 -1
  142. package/dist/extensions/solid/index.js +1 -1
  143. package/dist/extensions/solid/kernel_solid.d.cts +1 -1
  144. package/dist/extensions/solid/kernel_solid.d.ts +1 -1
  145. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
  146. package/dist/{host_commands-BI8pEmjH.js → host_commands-BlR3oZ3x.js} +473 -393
  147. package/dist/host_commands-C2cpU3yC.cjs +1 -0
  148. package/dist/index.cjs +1 -1
  149. package/dist/index.d.cts +2 -2
  150. package/dist/index.d.ts +2 -2
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.development.cjs +1 -1
  153. package/dist/index.development.js +3345 -2963
  154. package/dist/index.js +1496 -1131
  155. package/dist/{internal-BMDgWZbt.js → internal-BoxWl4dG.js} +410 -330
  156. package/dist/internal-CGBEwfeB.cjs +2 -0
  157. package/dist/internal.cjs +1 -1
  158. package/dist/internal.development.cjs +1 -1
  159. package/dist/internal.development.js +1 -1
  160. package/dist/internal.js +1 -1
  161. package/dist/primitives.development.js +1 -1
  162. package/dist/primitives.js +1 -1
  163. package/dist/reactive/index.d.cts +2 -2
  164. package/dist/reactive/index.d.ts +2 -2
  165. package/dist/reactive/index.development.js +1 -1
  166. package/dist/reactive/index.js +1 -1
  167. package/dist/reactive/kernel.d.cts +5 -5
  168. package/dist/reactive/kernel.d.ts +5 -5
  169. package/dist/reactive/shallow.d.cts +1 -1
  170. package/dist/reactive/shallow.d.ts +1 -1
  171. package/dist/{shallow-DfnAm9vG.js → shallow-Bs39EPh1.js} +1 -1
  172. package/dist/{shallow-9S1pY_Iw.js → shallow-CiFC70Dz.js} +1 -1
  173. package/dist/{shared-BU1Cd40h.js → shared-B4v0L1cw.js} +41 -35
  174. package/dist/shared-DU78k0pU.cjs +1 -0
  175. package/dist/shared.cjs +1 -1
  176. package/dist/shared.d.cts +1 -1
  177. package/dist/shared.d.ts +1 -1
  178. package/dist/shared.development.cjs +1 -1
  179. package/dist/shared.development.js +1 -1
  180. package/dist/shared.js +1 -1
  181. package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +2 -2
  182. package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +2 -2
  183. package/dist/{typed_arrays-BcXcnzB4.js → typed_arrays-DIVwVy5_.js} +1 -1
  184. package/dist/version.d.cts +2 -2
  185. package/dist/version.d.ts +2 -2
  186. package/package.json +1 -1
  187. package/dist/host_commands-CxhpzMx9.cjs +0 -1
  188. package/dist/internal-LiTSB_tG.cjs +0 -2
  189. package/dist/shared-BymrGTyR.cjs +0 -1
@@ -1,4 +1,5 @@
1
1
  import { EntityID } from './entity.js';
2
+ import { CursorBinder } from './ref.js';
2
3
  import { FrameTraceSink } from './frame_trace.js';
3
4
  import { ComponentDef, ComponentHandle, ComponentSchema, FieldValues } from './component.js';
4
5
  import { SparseComponentDef, SparseComponentID } from './sparse_store.js';
@@ -21,14 +22,14 @@ export interface ComponentMeta {
21
22
  obsAdd: boolean;
22
23
  /** Has an onRemove observer — collect effective removes for this component. */
23
24
  obsRem: boolean;
24
- /** Has an onDisable observer (#677) — collect effective disables for this
25
+ /** Has an onDisable observer — collect effective disables for this
25
26
  * component at the toggle drain. */
26
27
  obsDisable: boolean;
27
- /** Has an onEnable observer (#677) — collect effective enables for this
28
+ /** Has an onEnable observer — collect effective enables for this
28
29
  * component at the toggle drain. */
29
30
  obsEnable: boolean;
30
31
  /** Has a per-entity onSet observer — record dirty rows on the write path
31
- * (the ADR-0012 opt-in dirty list). */
32
+ * (the opt-in dirty list). */
32
33
  trackDirty: boolean;
33
34
  }
34
35
  /**
@@ -45,13 +46,13 @@ export interface StructuralObserverEvents {
45
46
  remComp: number[];
46
47
  remEid: number[];
47
48
  remLen: number;
48
- /** Effective disable events (#677) — collected during the toggle drain
49
+ /** Effective disable events — collected during the toggle drain
49
50
  * (`_flushToggles`), one per `(component, entity)` of each net-disabled
50
51
  * entity's mask. Empty on a structural (add/remove/destroy) round. */
51
52
  disComp: number[];
52
53
  disEid: number[];
53
54
  disLen: number;
54
- /** Effective enable events (#677), symmetric with the disable arrays. */
55
+ /** Effective enable events, symmetric with the disable arrays. */
55
56
  enaComp: number[];
56
57
  enaEid: number[];
57
58
  enaLen: number;
@@ -75,14 +76,14 @@ export type TemplateOverrides<Defs extends readonly ComponentDef[]> = {
75
76
  readonly [K in TemplateFieldNames<Defs>]?: number;
76
77
  };
77
78
  declare const __templateDefs: unique symbol;
78
- /** A resolved template (#462) — an archetype template produced by
79
+ /** A resolved template — an archetype template produced by
79
80
  * `ECS.template(...)`. **Opaque** apart from `defs`: callers hold it and pass
80
81
  * it to `ECS.spawn` / `ECS.spawnMany` (and may reference it in a system's
81
82
  * `spawns` / `despawns` access declaration — the scheduler expands it to
82
83
  * `defs`); the remaining fields are engine-internal and may change. `spawn`
83
84
  * lands an entity directly in `archetype_id` with zero archetype transitions,
84
85
  * writing `flatValues` (defaults in `_flatColumns` order) in one append
85
- * pass. See ADR-0010. */
86
+ * pass. */
86
87
  export interface Template<Defs extends readonly ComponentDef[] = readonly ComponentDef[]> {
87
88
  readonly archetypeId: ArchetypeID;
88
89
  readonly flatValues: number[];
@@ -95,13 +96,13 @@ export interface StoreOptions {
95
96
  initialCapacity?: number;
96
97
  /** Pluggable SAB buffer source. When provided, `createColumnStore`,
97
98
  * `extendColumnStore`, and `growColumnStore` route through it. Default is
98
- * `growableSabAllocator()`. Typed `InPlaceBufferAllocator` (#682): a live
99
+ * `growableSabAllocator`. Typed `InPlaceBufferAllocator`: a live
99
100
  * Store's flush loops hoist entity-index views across grows, so only
100
- * in-place allocators may back one (ADR-0008) — the constructor also
101
+ * in-place allocators may back one — the constructor also
101
102
  * runtime-asserts the marker for untyped JS callers. Consumers normally
102
103
  * don't touch this directly; `ECSOptions.memory` resolves to it. */
103
104
  bufferAllocator?: InPlaceBufferAllocator;
104
- /** Sizing intent the world was constructed with (#682), used to phrase
105
+ /** Sizing intent the world was constructed with, used to phrase
105
106
  * allocator-cap and entity-index-overflow errors in the caller's own
106
107
  * terms ("3.2× the declared budget") instead of raw bytes. Wired by
107
108
  * `ECS` from `resolveECSMemory`; absent for bare test Stores. */
@@ -111,29 +112,29 @@ export interface StoreOptions {
111
112
  * views by the time this fires. Used by ECS to call
112
113
  * `sim.setLayout(0)` so WASM-side cached pointers re-walk. */
113
114
  onBufferResized?: () => void;
114
- /** Max live entities the SAB entity-index region holds (#245 / PR 4B).
115
+ /** Max live entities the SAB entity-index region holds.
115
116
  * Default `ENTITY_INDEX_DEFAULT_CAPACITY` (`1 << 20` — the full EntityID
116
117
  * index space). Exceeding this at runtime throws `EID_MAX_INDEX_OVERFLOW`.
117
118
  * Tests with small entity counts may set lower to bench the SAB region size
118
119
  * or to make index exhaustion reachable; a 1000-entity workload fits
119
120
  * comfortably in the default. */
120
121
  entityIndexCapacity?: number;
121
- /** Consumer-declared SAB regions (#623), forwarded verbatim to
122
+ /** Consumer-declared SAB regions, forwarded verbatim to
122
123
  * `createColumnStore`. Each `StoreRegionSpec` carries an opaque `region_id`,
123
124
  * a precomputed byte size, and an `init` closure; the engine lays them out
124
125
  * generically and exposes them via `regionHandle(id)` / `regionOffset(id)`.
125
126
  * A game (e.g. `@internal/sim`'s region specs) supplies these — the engine
126
127
  * ships no game regions of its own. Omitted ⇒ none. */
127
128
  regions?: readonly StoreRegionSpec[];
128
- /** Byte size of the opt-in sim-bindings region (#625), forwarded verbatim to
129
+ /** Byte size of the opt-in sim-bindings region, forwarded verbatim to
129
130
  * `createColumnStore`. A consumer that attaches a WASM backend passes its own
130
131
  * size (`@internal/sim`'s `SIM_BINDINGS_BYTES`, computed from the binding
131
132
  * manifest); the host then writes the `(component_id, field_id)` IDs into the
132
133
  * region. Omitted / 0 ⇒ no region (a pure-TS game pays nothing for the WASM
133
- * seam). De-welded from the engine ABI in #625 so a manifest edit doesn't
134
+ * seam). De-welded from the engine ABI so a manifest edit doesn't
134
135
  * drift an engine golden. */
135
136
  bindingsRegionBytes?: number;
136
- /** Opt into the **determinism surface** (#626 / ADR-0020). Default `false`.
137
+ /** Opt into the **determinism surface**. Default `false`.
137
138
  * Gates the three methods that fold/serialize state in canonical (sorted)
138
139
  * order: `stateHash`, `snapshotSparse`, `restoreSparse`. When `false`
139
140
  * those throw `DETERMINISM_DISABLED` — the canonical-ordering tax (sparse
@@ -141,7 +142,7 @@ export interface StoreOptions {
141
142
  * consumer can't accidentally read a non-canonical digest. When `true`,
142
143
  * today's behavior is reproduced bit-for-bit. This is the ONLY effect of the
143
144
  * flag: it does not touch the per-tick path, the in-place-allocator invariant
144
- * (ADR-0008, a memory-safety requirement that holds regardless), or the
145
+ * (a memory-safety requirement that holds regardless), or the
145
146
  * always-on `enabled_count` partition maintenance. The flag's value is a
146
147
  * capability gate, not a hot-path switch — `stateHash`/snapshot are never
147
148
  * called per tick. */
@@ -164,7 +165,7 @@ export declare class Store implements ObserverHost, QueryHost {
164
165
  private readonly _deferred;
165
166
  private readonly _snapshots;
166
167
  _tick: number;
167
- /** Per-world frame-trace sink (ADR-0030), installed via `ECS.setTrace`.
168
+ /** Per-world frame-trace sink, installed via `ECS.setTrace`.
168
169
  * `null` unless a consumer attaches a recorder. Every call site is
169
170
  * `if (DEV) store._trace?.…`, so production builds dead-code-eliminate
170
171
  * the seam and pay only this one nullable field. The sink observes; it never
@@ -172,7 +173,7 @@ export declare class Store implements ObserverHost, QueryHost {
172
173
  * observer state below). */
173
174
  _trace: FrameTraceSink | null;
174
175
  private _structuralObserverCount;
175
- /** Count of components with any onDisable/onEnable observer (#677). While 0
176
+ /** Count of components with any onDisable/onEnable observer. While 0
176
177
  * (with `_structuralObserverCount` also 0), `flushStructural` takes the
177
178
  * byte-for-byte fast path and the toggle drain skips event collection. */
178
179
  private _toggleObserverCount;
@@ -184,14 +185,14 @@ export declare class Store implements ObserverHost, QueryHost {
184
185
  private _structuralObserverHook;
185
186
  /** Install the structural-observer dispatch hook (called once by `ECS`
186
187
  * during construction) — the named seam replacing direct writes to the
187
- * previously-public field (M1). */
188
+ * previously-public field. */
188
189
  setStructuralObserverHook(fn: (ev: StructuralObserverEvents) => void): void;
189
190
  private _collectDestroyEid;
190
191
  private readonly _collectDestroyRemoveBit;
191
192
  private _collectToggleEid;
192
193
  private readonly _collectDisableBit;
193
194
  private readonly _collectEnableBit;
194
- /** Net-transition snapshot for the toggle drain (#677): entity → its disabled
195
+ /** Net-transition snapshot for the toggle drain: entity → its disabled
195
196
  * state at the START of the drain. Reused, cleared each drain. Lets
196
197
  * `_flushToggles` emit one event per *net* transition (disable→enable→disable
197
198
  * within a tick = a single onDisable) instead of one per buffered op — required
@@ -206,7 +207,7 @@ export declare class Store implements ObserverHost, QueryHost {
206
207
  * (`flushStructural`/`flushDestroyed` when they did work; immediate
207
208
  * `destroyEntity`, `addComponent(s)`, `removeComponent(s)` on the
208
209
  * Store). Cleared by `publishRowCountsToDescriptor`. Lets read-only
209
- * phases' `ctx.flush()` skip the descriptor walk entirely (#324). */
210
+ * phases' `ctx.flush` skip the descriptor walk entirely. */
210
211
  private _rowCountsDirty;
211
212
  /** Monotonic counter bumped by every membership-changing path (immediate
212
213
  * `addComponent(s)`, `removeComponent(s)`, `destroyEntity`,
@@ -214,7 +215,7 @@ export declare class Store implements ObserverHost, QueryHost {
214
215
  * `flushDestroyed`, and new-archetype installs in `ArchetypeGraph.install`).
215
216
  * Read by `Query._nonEmpty()` via `QueryResolver._getQueryDirtyEpoch`
216
217
  * — a query whose stored `_lastSeenEpoch` matches the current epoch
217
- * reuses its cached non-empty list (#327). Replaces the previous walk
218
+ * reuses its cached non-empty list. Replaces the previous walk
218
219
  * over `registeredQueries` that wrote one dirty bit per query per
219
220
  * mutation; 5000 startup adds × Q queries used to be 5000×Q writes,
220
221
  * now it's 5000 integer increments. Public so ECS can forward through
@@ -229,45 +230,45 @@ export declare class Store implements ObserverHost, QueryHost {
229
230
  * to whatever fits `arch.length + additional`, whichever is larger),
230
231
  * reallocs the SAB via `growColumnStore` (live rows of every archetype
231
232
  * are carried forward), and republishes column views to every
232
- * SAB-backed archetype. Plan §8.1 / §8.3 / §8.4. (#171 §6.1.9 Phase 3) */
233
+ * SAB-backed archetype. */
233
234
  private readonly _growHandler;
234
- /** Build the intent-aware fatal for an allocator cap hit (#682). The
235
+ /** Build the intent-aware fatal for an allocator cap hit. The
235
236
  * allocator can only name raw bytes; the Store knows what the caller
236
237
  * declared (`capContext`) and how many entities are live, so the error
237
238
  * says "3.2× the declared budget — runaway creation upstream?" instead
238
239
  * of leaving the caller to reverse-engineer byte counts. Fatality is
239
- * unchanged (#380: no grow-beyond-cap fallback). */
240
+ * unchanged (no grow-beyond-cap fallback). */
240
241
  private _capExceededError;
241
242
  private readonly _bufferAllocator;
242
243
  private readonly _capContext;
243
244
  private readonly _onBufferResized;
244
245
  /** Construct with an `initialCapacity` number (legacy form) or an
245
- * options object (new in PR 3D — adds `bufferAllocator` and
246
+ * options object (adds `bufferAllocator` and
246
247
  * `onBufferResized` callback). Both signatures coexist so test fixtures
247
248
  * that pass `new Store(4)` keep working. */
248
249
  constructor(arg?: number | StoreOptions);
249
250
  /** Capacity of the entity-index SAB region (max slots ≈ max live
250
- * entities). Fixed at construction in #245's PR 4B scope; a future
251
+ * entities). Fixed at construction; a future
251
252
  * follow-up will grow it via `growColumnStore` when `entityHighWater`
252
253
  * hits the cap. */
253
254
  private readonly _entityIndexCapacity;
254
- /** Consumer-declared SAB regions (#623), captured so the realloc path
255
+ /** Consumer-declared SAB regions, captured so the realloc path
255
256
  * re-lays them out. `undefined` when no consumer regions were declared.
256
257
  * The region contents survive a grow via the self-describing region table
257
258
  * (`extend.ts` snapshot/restore), so this is only the layout recipe. */
258
259
  private readonly _regions;
259
- /** Byte size of the opt-in sim-bindings region (#625). 0 ⇒ no region (the
260
+ /** Byte size of the opt-in sim-bindings region. 0 ⇒ no region (the
260
261
  * pure-TS default). Captured so the initial `createColumnStore` reserves it;
261
262
  * across a realloc the size is re-derived from the old header by
262
263
  * `optionsFromOld`, so it is not threaded through the grow/extend path. */
263
264
  private readonly _bindingsRegionBytes;
264
- /** Determinism opt-in (#626 / ADR-0020). When `false` (the default), the
265
+ /** Determinism opt-in. When `false` (the default), the
265
266
  * canonical-ordering determinism surface (`stateHash` / `snapshotSparse` /
266
267
  * `restoreSparse`) throws `DETERMINISM_DISABLED` rather than running its
267
- * sort. Memory-safety invariants (the in-place allocator, ADR-0008) and the
268
+ * sort. Memory-safety invariants (the in-place allocator) and the
268
269
  * `enabled_count` partition are unaffected — they hold regardless. */
269
270
  private readonly _deterministic;
270
- /** Whether the determinism surface is enabled (#626). `false` ⇒ `stateHash`
271
+ /** Whether the determinism surface is enabled. `false` ⇒ `stateHash`
271
272
  * / `snapshotSparse` / `restoreSparse` throw `DETERMINISM_DISABLED`. */
272
273
  get deterministic(): boolean;
273
274
  /** Guard the canonical-ordering determinism surface. Throws
@@ -277,11 +278,11 @@ export declare class Store implements ObserverHost, QueryHost {
277
278
  * check is free, and a silent non-canonical digest is the failure mode we're
278
279
  * preventing. */
279
280
  private _requireDeterministic;
280
- /** Reject `f32`/`f64` fields on a `deterministic: true` world at registration
281
- * (#777). IEEE-754 rounds differently across V8 / Bun / Zig at the 1-ULP
281
+ /** Reject `f32`/`f64` fields on a `deterministic: true` world at registration.
282
+ * IEEE-754 rounds differently across V8 / Bun / Zig at the 1-ULP
282
283
  * level, so a float column in a fixed-update path is a silent per-tick
283
284
  * `stateHash` divergence between client and server — the one thing the
284
- * determinism opt-in (ADR-0020) exists to prevent. Non-deterministic worlds
285
+ * determinism opt-in exists to prevent. Non-deterministic worlds
285
286
  * skip this entirely (floats stay allowed), so it costs the default path
286
287
  * nothing. `kind` names the storage class in the error ("component" /
287
288
  * "sparse component"); the array shorthand's `f64` default lands here too, so
@@ -306,7 +307,7 @@ export declare class Store implements ObserverHost, QueryHost {
306
307
  get columnStore(): ColumnStore;
307
308
  /** Resolve a consumer-declared SAB region's byte offset by `region_id`, or
308
309
  * 0 when the region is absent (no region was declared with that id). The
309
- * generic, de-gamed replacement (#623) for the removed game-named accessors
310
+ * generic, de-gamed replacement for the removed game-named accessors
310
311
  * (`terrain_view` / `spatial_grid_view` / … ); a consumer pairs this with
311
312
  * its own region module (e.g. `@internal/sim`'s region helpers) to
312
313
  * materialise a typed view. TS twin of Zig `abi.find_region`. */
@@ -315,16 +316,16 @@ export declare class Store implements ObserverHost, QueryHost {
315
316
  * `null` when absent. Carries the live `buffer`/`view` plus the region's byte
316
317
  * `offset` and `bytes`, so a consumer's region module can build a TypedArray
317
318
  * view over exactly the region's span without re-reading the directory.
318
- * Re-fetch after a SAB grow (the offset/view may have moved). (#623) */
319
+ * Re-fetch after a SAB grow (the offset/view may have moved). */
319
320
  regionHandle(regionId: number): ColumnStoreRegionHandle | null;
320
321
  /**
321
322
  * Stamp every SAB-backed archetype's live `length` into its descriptor's
322
- * `row_count` field (#252 / Phase 4 PR 4E). `extendColumnStore` /
323
+ * `row_count` field. `extendColumnStore` /
323
324
  * `growColumnStore` are the only other writers of `row_count`, and they
324
325
  * record the count at the moment of the resize — `Archetype.addEntity`
325
326
  * does not update it, so any insertion after the most recent resize
326
327
  * leaves the descriptor stale. Zig systems that drive their per-row loop
327
- * off `arch_hdr.row_count` (every `tick_*` export added since PR 3B)
328
+ * off `arch_hdr.row_count` (every `tick_*` export)
328
329
  * read those stale bytes and silently skip the just-spawned rows.
329
330
  *
330
331
  * Lockstep walk: SAB descriptors are written by `extendColumnStore` in
@@ -333,10 +334,10 @@ export declare class Store implements ObserverHost, QueryHost {
333
334
  * once, skipping non-SAB entries, and advancing an `archAddr` cursor
334
335
  * by the descriptor's `column_count` lets us write `row_count` without
335
336
  * the throwaway `Map<archId, length>` the previous version allocated
336
- * on every call (#323). Cheap: descriptor-region seeks only, no column
337
+ * on every call. Cheap: descriptor-region seeks only, no column
337
338
  * I/O.
338
339
  *
339
- * Gated by `_rowCountsDirty` (#324) — mutation paths
340
+ * Gated by `_rowCountsDirty` — mutation paths
340
341
  * (`flushStructural`, `flushDestroyed`, immediate `destroyEntity`,
341
342
  * `addComponent(s)`, `removeComponent(s)`) set the flag; this method
342
343
  * clears it. Read-only phases that flush only to drain empty buffers
@@ -344,12 +345,11 @@ export declare class Store implements ObserverHost, QueryHost {
344
345
  publishRowCountsToDescriptor(): void;
345
346
  /** FNV-1a-style 32-bit digest over (archetype_id, live_row_count, live
346
347
  * column bytes) for each archetype in id order, followed by the sparse
347
- * stores (out-of-identity components, ADR-0011) in registration order.
348
- * Replaces the per-networked-component fold that compute_state_hash used
349
- * pre-#171 §6.1.9 Phase 5 — this is the canonical "live ECS state digest"
350
- * for cross-replay determinism.
348
+ * stores (out-of-identity components) in registration order.
349
+ * This is the canonical "live ECS state digest" for cross-replay
350
+ * determinism. It replaces the earlier per-networked-component fold.
351
351
  *
352
- * **Sparse coverage (#470).** Sparse data lives outside the archetype
352
+ * **Sparse coverage.** Sparse data lives outside the archetype
353
353
  * graph, so it is folded separately after the archetype loop — per store:
354
354
  * the sparse-component id, the member count, then each member's source
355
355
  * entity index + f64 field words, walked in CANONICAL ascending-index order
@@ -364,11 +364,11 @@ export declare class Store implements ObserverHost, QueryHost {
364
364
  * components), and strictly tighter than `columnStoreStateHash(...)`
365
365
  * which scans the full SAB including trailing unused capacity.
366
366
  *
367
- * **Per-word fold (#326).** The inner column loop folds one 32-bit
367
+ * **Per-word fold.** The inner column loop folds one 32-bit
368
368
  * word at a time using FNV-1a's `xor + imul(PRIME)` step. This is NOT
369
369
  * byte-for-byte FNV-1a-32 of the column bytes — it's a deterministic
370
- * digest with the same equality semantics, ~4× faster than the per-
371
- * byte loop it replaces. Trailing 0–3 tail bytes (only possible for
370
+ * digest with the same equality semantics, and much quicker than the
371
+ * per-byte loop it replaces. Trailing 0–3 tail bytes (only possible for
372
372
  * u8/u16 columns at odd row counts) are folded together as a single
373
373
  * little-endian word so the algorithm stays branch-free in the inner
374
374
  * loop. The 4-byte `id` and `len` headers are folded as words for the
@@ -381,7 +381,7 @@ export declare class Store implements ObserverHost, QueryHost {
381
381
  * processes on the same architecture (which is all `replay_match`
382
382
  * needs — both replays run the same algorithm on the same words).
383
383
  *
384
- * **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
384
+ * **Opt-in.** Throws `DETERMINISM_DISABLED` unless the
385
385
  * Store was constructed with `{ deterministic: true }`. The canonical
386
386
  * ordering this fold relies on (sparse `canonicalIndices`, sorted relation
387
387
  * target sets) is the determinism tax the flag gates. */
@@ -391,8 +391,7 @@ export declare class Store implements ObserverHost, QueryHost {
391
391
  * by a WASM system to resolve an
392
392
  * `EntityID` from an event-ring payload — Zig writes
393
393
  * `(archId, row, …)` to the event ring,
394
- * and TS bridges it back through `ctx.emit(...)` via this method. (#250 /
395
- * Phase 4 PR 4D)
394
+ * and TS bridges it back through `ctx.emit(...)` via this method.
396
395
  *
397
396
  * Throws `ECSError` if `archetype_id` is out of range or `row` is
398
397
  * past the archetype's live row count — these would indicate a
@@ -404,7 +403,7 @@ export declare class Store implements ObserverHost, QueryHost {
404
403
  * `ArchetypeGraph.getOrCreateFromMask`. */
405
404
  private archGetOrCreateFromMask;
406
405
  /** Bulk variant of `archGetOrCreateFromMask` — one `extendColumnStore`
407
- * call for the whole batch (Phase C prewarm, #213). See
406
+ * call for the whole batch (the prewarm pass). See
408
407
  * `ArchetypeGraph.createManyFromMasks`. */
409
408
  archCreateManyFromMasks(masks: readonly BitSet[]): ArchetypeID[];
410
409
  /** Snapshot every existing archetype's SAB rows, call `extendColumnStore`
@@ -435,11 +434,11 @@ export declare class Store implements ObserverHost, QueryHost {
435
434
  * generation so `isAlive` is already true), so the caller MUST have
436
435
  * reserved the column capacity for the row first
437
436
  * (`Archetype.ensureRowCapacity`) — otherwise a cap throw from the
438
- * subsequent append leaves the slot phantom-alive (#775). */
437
+ * subsequent append leaves the slot phantom-alive. */
439
438
  private _allocEntity;
440
439
  /** Pre-check that `count` fresh entity slots can be allocated without
441
- * exhausting the entity-index space, so `spawnMany` commits all-or-nothing
442
- * (#775). `_allocEntity`'s own per-call high-water guard would otherwise
440
+ * exhausting the entity-index space, so `spawnMany` commits all-or-nothing.
441
+ * `_allocEntity`'s own per-call high-water guard would otherwise
443
442
  * throw `EID_MAX_INDEX_OVERFLOW` partway through the alloc loop, leaving the
444
443
  * slots it already committed phantom-alive. Free-list reuse covers the first
445
444
  * `entityFreeIndices.length` slots; only the remainder draws down the
@@ -475,7 +474,7 @@ export declare class Store implements ObserverHost, QueryHost {
475
474
  * With no `delete`/`clear` target-cleanup policy registered (the common
476
475
  * case) this tears the one entity down and returns — no allocation. When a
477
476
  * policy is in play, a `delete`-target's sources are appended to a local
478
- * work-list this method then drains in the same iterative pass (#473, #492):
477
+ * work-list this method then drains in the same iterative pass:
479
478
  * the `work.length` re-read drives chains and trees out without recursion, so
480
479
  * depth is bounded by entity count, not tree depth. This mirrors the deferred
481
480
  * `flushDestroyed` buffer mechanism — both paths are iterative and reach the
@@ -488,38 +487,65 @@ export declare class Store implements ObserverHost, QueryHost {
488
487
  * then recycle (or retire) its slot. Shared by both immediate-destroy entry
489
488
  * points (the fast no-cascade path and the work-list driver in
490
489
  * `destroyEntity`). When `cascade` is non-null, a `delete`-policy target's
491
- * surviving sources are appended to it for the driver to drain (#473, #492);
490
+ * surviving sources are appended to it for the driver to drain;
492
491
  * `null` skips that collection for callers that cannot cascade. The caller
493
492
  * must have already confirmed `id` is alive. */
494
493
  private _destroyOne;
495
494
  /**
496
495
  * Liveness check, **fail-closed** against forged / retired / out-of-bounds
497
- * handles (#778). For a general-purpose engine that may receive a handle from
496
+ * handles. For a general-purpose engine that may receive a handle from
498
497
  * serialization, IPC, or any untrusted caller, three malformed inputs must read
499
498
  * dead rather than alias a slot:
500
499
  * - **Out of range** — an `id` outside the 31-bit packed space (`< 0` or
501
500
  * `> MAX_ENTITY_ID`). Without this, the 20-bit index mask below silently
502
501
  * folds garbage high bits onto a valid slot. (Same bound the snapshot /
503
- * postMessage decode applies, #723.)
502
+ * postMessage decode applies.)
504
503
  * - **Tombstone generation** — a handle carrying `RETIRED_GENERATION`, which
505
504
  * the allocator stamps into a retired slot and never issues to a live
506
505
  * entity, would otherwise match a retired slot's parked generation and read
507
506
  * alive (the ABA tombstone, previously documented as a known gap).
508
507
  * Both guards are comparisons predicted not-taken on the live path, so a
509
- * well-formed handle pays two branches and nothing else (#778 measured).
508
+ * well-formed handle pays two branches and nothing else.
510
509
  */
511
510
  isAlive(id: EntityID): boolean;
511
+ /**
512
+ * Liveness and the packed index in one result: the entity index if `id` is
513
+ * live, else `-1`. Same three fail-closed guards as `isAlive` (documented
514
+ * above), and the sole implementation of them — `isAlive` is a comparison on
515
+ * top of this.
516
+ *
517
+ * Why it returns the index instead of a boolean: every by-id caller needs
518
+ * BOTH answers, and the pair used to cost two derivations of the same index.
519
+ * `hasComponent` called `isAlive(id)` — which computed `getEntityIndex(id)`
520
+ * internally — and then computed `getEntityIndex(id)` again to reach
521
+ * `entityArchetype`. The generational check has already touched the index; a
522
+ * caller that is about to index a parallel array with it should be handed the
523
+ * one that was computed, not re-derive it. The generations read is inlined
524
+ * here for the same reason, rather than delegated to
525
+ * `entityAllocator.isAliveIndex` — which stays as the index-domain entry point
526
+ * for callers that already hold an index.
527
+ *
528
+ * **This gives much less than it appears to give.** The estimate for this
529
+ * change was a large decrease. The measurement shows that `has` and `isAlive`
530
+ * are only a little faster. The decrease is real, but it is much smaller than
531
+ * the estimate. The work that we removed was truly not necessary. But V8
532
+ * already made both one-line functions inline, and it already removed most of
533
+ * the duplicated calculation. Therefore "the work is plainly not necessary" is
534
+ * not an argument about performance for a JIT compiler. This note stops the
535
+ * next reader from making the same estimate again.
536
+ */
537
+ private _liveIndex;
512
538
  get entityCount(): number;
513
539
  /** An archetype's row count moved from `preLen` to its current
514
540
  * `arch.length` on a **shrink** (rows removed: the source of a transition, a
515
- * destroy, a batch-source drain). Always marks SAB row counts dirty (#324 —
516
- * the descriptor walk just needs "something moved"); bumps the query-dirty
517
- * epoch (#327) only on a `length` 0/non-zero crossing, the only case where
518
- * `Query._nonEmptyArchetypes` can change on a shrink (#328). Mutations that
541
+ * destroy, a batch-source drain). Always marks SAB row counts dirty
542
+ * (the descriptor walk just needs "something moved"); bumps the query-dirty
543
+ * epoch only on a `length` 0/non-zero crossing, the only case where
544
+ * `Query._nonEmptyArchetypes` can change on a shrink. Mutations that
519
545
  * move row counts within the same side (6→5) leave the non-empty set unchanged
520
546
  * and skip the bump.
521
547
  *
522
- * A shrink does **not** need the `enabledCount` crossing test (#812): the
548
+ * A shrink does **not** need the `enabledCount` crossing test: the
523
549
  * only enabled-count move it can make is 1→0 (the last enabled row leaves an
524
550
  * archetype that keeps disabled rows), which leaves the archetype in a default
525
551
  * query's non-empty list as a harmless stale *inclusion* — `count`/`forEach`
@@ -530,29 +556,29 @@ export declare class Store implements ObserverHost, QueryHost {
530
556
  * **Inlining-sensitive — keep the body tiny.** This function is called
531
557
  * once or twice per immediate-mode `addComponent` / `removeComponent` and the
532
558
  * mutation hot path depends on it being inlined at every call site.
533
- * #351 / PR #353 added an `if (registeredQueries.length === 0) return;`
534
- * gate to skip the bump for no-query workloads bench showed it
535
- * regressed `mutation: churn_loop` 16-18% because the extra statement
536
- * pushed the function past V8's per-call inlining budget. Reverted in
537
- * PR #355. Any future change here needs a `bench-vs-commit` run before
538
- * merging, not just code review. */
559
+ * An earlier change added an `if (registeredQueries.length === 0) return;`
560
+ * gate to skip the bump for no-query workloads. The bench showed a large
561
+ * regression of the mutation churn loop, because the extra statement pushed
562
+ * the function past V8's per-call inlining budget. The gate is no longer in
563
+ * the code. Do a bench run before you merge a change here. Code review
564
+ * alone is not sufficient. */
539
565
  private _onArchLenChange;
540
566
  /** An archetype **grew** — rows were appended (the target of a transition, a
541
567
  * spawn, a batch-target fill). Like `_onArchLenChange` it marks row counts
542
568
  * dirty and bumps the query-dirty epoch on a `length` 0/non-zero crossing
543
569
  * (`includeDisabled` membership), but it *also* bumps on an `enabledCount`
544
- * 0→1 crossing (#812). The non-empty filter is field-split (#577): a default
570
+ * 0→1 crossing. The non-empty filter is field-split: a default
545
571
  * query keeps archetypes with `enabledCount > 0`. An enabled row appended to
546
572
  * an archetype that is non-empty but all-disabled (`length > 0,
547
573
  * enabledCount == 0`) crosses `enabledCount` 0→1 without touching `length`,
548
- * so the `preLen` test alone (the valid-while-`enabledCount === length`
549
- * pre-#577 proxy) misses it and a cached default query keeps a stale
574
+ * so the `preLen` test alone (the earlier proxy, valid only while
575
+ * `enabledCount === length`) misses it and a cached default query keeps a stale
550
576
  * `_nonEmpty` list. Only grows can do this, so only grow sites carry the test.
551
577
  *
552
578
  * **Precondition: ≥1 row was appended** (every caller adds at least one row),
553
579
  * so `arch.length > 0` afterward — which is why the crossings simplify and the
554
580
  * body stays inlinable (the inlining caveat on `_onArchLenChange` applies
555
- * here too; verified with `bench-vs-commit mutation`). The general
581
+ * here too; this is bench-verified). The general
556
582
  * `(pre === 0) !== (post === 0)` boundary test collapses given the post side:
557
583
  * - `length`: post > 0 always ⇒ a crossing iff `preLen === 0`.
558
584
  * - `enabledCount`: non-decreasing on a grow ⇒ a 0-crossing iff it was 0
@@ -561,7 +587,7 @@ export declare class Store implements ObserverHost, QueryHost {
561
587
  * read is short-circuited away on the hot path (`preLen` or `preEnabled`
562
588
  * non-zero), so a no-disabled workload pays only two scalar compares. */
563
589
  private _onArchGrow;
564
- /** Dirty bookkeeping for an enable/disable toggle (#577). `length` is
590
+ /** Dirty bookkeeping for an enable/disable toggle. `length` is
565
591
  * unchanged (no row added/removed) but `enabled_count` moved, so: republish
566
592
  * row counts (the descriptor's `enabled_count` changed, so the WASM sim and
567
593
  * snapshot see the new partition), and bump the query epoch only when the
@@ -580,7 +606,7 @@ export declare class Store implements ObserverHost, QueryHost {
580
606
  isDisabled(id: EntityID): boolean;
581
607
  /** 0-crossing detection for the per-entity flush paths (`_flushAdds`,
582
608
  * `_flushRemoves`) without per-entity Map traffic — the same cost the
583
- * destroy drain shed in #457. Each touched archetype is stamped with the
609
+ * destroy drain also avoids. Each touched archetype is stamped with the
584
610
  * current flush epoch (`Archetype._flushSeenEpoch`), its pre-length and
585
611
  * pre-enabled-count recorded on first sight (`_flushPreLen` /
586
612
  * `_flushPreEnabled`), and pushed onto this scratch list;
@@ -594,12 +620,12 @@ export declare class Store implements ObserverHost, QueryHost {
594
620
  * the query epoch once if any touched archetype crossed the 0 boundary on
595
621
  * *either* `length` (includeDisabled membership) or `enabledCount`
596
622
  * (default-query membership) — the deferred analog of the immediate
597
- * `_onArchLenChange` two-field check (#812). A single bump is sufficient
623
+ * `_onArchLenChange` two-field check. A single bump is sufficient
598
624
  * (queries only need to know "something changed"). Clears the touched list
599
625
  * and advances the flush epoch on exit. */
600
626
  private _settleFlushDirty;
601
627
  destroyEntityDeferred(id: EntityID): void;
602
- /** Buffer an enable/disable toggle for the phase flush (#577). The row swap a
628
+ /** Buffer an enable/disable toggle for the phase flush. The row swap a
603
629
  * toggle performs would corrupt a `forEach` over that archetype if applied
604
630
  * mid-system, so it is deferred like add/remove. */
605
631
  disableEntityDeferred(id: EntityID): void;
@@ -610,13 +636,13 @@ export declare class Store implements ObserverHost, QueryHost {
610
636
  * entity's final archetype placement for the tick.
611
637
  *
612
638
  * When an onDisable/onEnable observer is registered (`_toggleObserverCount >
613
- * 0`, #677) this also collects effective toggle events into `_obsEvents` for
639
+ * 0`) this also collects effective toggle events into `_obsEvents` for
614
640
  * the dispatch hook, collapsed to one event per *net* transition across the
615
641
  * drain (see `_toggleInitial`). The no-observer path is byte-for-byte the
616
- * pre-#677 drain. */
642
+ * earlier drain. */
617
643
  private _flushToggles;
618
644
  /** Fan one entity's net toggle transition out to an onDisable / onEnable event
619
- * per carried component (#677). Walks the entity's archetype mask through the
645
+ * per carried component. Walks the entity's archetype mask through the
620
646
  * matching pre-bound bit visitor; a component-less entity (no row) carries
621
647
  * nothing and is skipped. */
622
648
  private _collectToggle;
@@ -631,7 +657,7 @@ export declare class Store implements ObserverHost, QueryHost {
631
657
  * Same commit-then-observe discipline as `_flushRemoves`: the entity is fully
632
658
  * freed before the callback runs, so onRemove receives the (now dead) eid as
633
659
  * the identity of what was destroyed, not a live handle to read. The
634
- * no-observer path is byte-for-byte unchanged (`collecting` gate). #531.
660
+ * no-observer path is byte-for-byte unchanged (`collecting` gate).
635
661
  *
636
662
  * Re-entrancy: while the observed fixed point owns the flush, the loop
637
663
  * drains destroys itself via `_drainDestroyed`, so a re-entrant
@@ -647,7 +673,7 @@ export declare class Store implements ObserverHost, QueryHost {
647
673
  /** Phase-boundary structural flush. The drain policy — no-observer fast
648
674
  * path, observed fixed point (adds/removes → destroys → toggles),
649
675
  * convergence guard, re-entrancy — lives in `DeferredCommandBuffer`
650
- * (H1 step 4); the batch appliers it drives are the `_flush*` /
676
+ *; the batch appliers it drives are the `_flush*` /
651
677
  * `_drainDestroyed` methods below. */
652
678
  flushStructural(): void;
653
679
  /** Batch-apply all deferred component additions. */
@@ -657,13 +683,13 @@ export declare class Store implements ObserverHost, QueryHost {
657
683
  get pendingStructuralCount(): number;
658
684
  /** Set the per-component observation flags from the registry's aggregate of
659
685
  * live observers for `cid`. Maintains `_structuralObserverCount` and
660
- * `_toggleObserverCount` (#677) (the fast-path gates) and lazily allocates the
686
+ * `_toggleObserverCount` (the fast-path gates) and lazily allocates the
661
687
  * dirty list when per-entity onSet tracking turns on. */
662
688
  _configureComponentObservation(cid: number, hasAdd: boolean, hasRem: boolean, hasDisable: boolean, hasEnable: boolean, trackDirty: boolean): void;
663
689
  /** Record a per-entity onSet "changed" event for the entity. Called from the
664
690
  * field-write path (`SystemContext.setField` / `markChanged`) and gated by
665
691
  * the caller on `_anyDirtyTracked`. Appends to the dirty list only if the
666
- * dedup bit was clear (the ADR-0012 list + dedup-bit mechanism). */
692
+ * dedup bit was clear (the dirty list + dedup-bit mechanism). */
667
693
  _noteSet(def: ComponentHandle, eid: EntityID): void;
668
694
  private _growDirtyMarks;
669
695
  /** Detach and return the dirty-row list for `cid`, clearing its dedup bits and
@@ -681,7 +707,7 @@ export declare class Store implements ObserverHost, QueryHost {
681
707
  * per-archetype change tick (free; no write-path cost). */
682
708
  _forEachChangedArchetype(cid: number, baseline: number, cb: (arch: Archetype) => void): void;
683
709
  /** Enabled live entities currently carrying `cid`, used by `yieldExisting` to
684
- * replay onAdd on registration. Bounded by `enabled_count` (#677): a disabled
710
+ * replay onAdd on registration. Bounded by `enabled_count`: a disabled
685
711
  * entity is excluded from default queries, so seeding it via onAdd would
686
712
  * publish a row that an immediate onDisable should have removed — it is simply
687
713
  * absent at seed (the "delete on disable" semantics). Unordered here — the
@@ -694,16 +720,16 @@ export declare class Store implements ObserverHost, QueryHost {
694
720
  /** Return the field index assigned to `(def, fieldName)` at component
695
721
  * registration. Indexes are insertion-order, zero-based, and stable for
696
722
  * the lifetime of the ECS. Used by systems that pass `(component_id,
697
- * field_id)` pairs across the WASM FFI (PR 3C / #231). */
723
+ * field_id)` pairs across the WASM FFI. */
698
724
  fieldIdOf(def: ComponentHandle, fieldName: string): number;
699
725
  /** Register a sparse component or tag. Unlike `registerComponent`, this
700
726
  * allocates from a separate id space and never touches the archetype mask,
701
727
  * so it does **not** count against `STORE_DESCRIPTOR_COMPONENT_LIMIT`. See
702
- * ADR-0011 and `sparse_store.ts`. */
728
+ * `sparse_store.ts`. */
703
729
  registerSparseComponent<S extends Record<string, TypedArrayTag>>(schema: S, name?: string): SparseComponentDef<S>;
704
730
  /** Sparse sibling of `componentLabel` — sparse ids are a separate id space. */
705
731
  sparseLabel(sid: number): string;
706
- /** Allocate the backing sparse store WITHOUT the #777 float guard, for
732
+ /** Allocate the backing sparse store WITHOUT the float guard, for
707
733
  * engine-internal backings whose `f64` holds an EXACT integer rather than a
708
734
  * user quantity: the exclusive-relation `{ target }` slot stores an `EntityID`
709
735
  * (≤ 2^53, so f64 is bit-exact and cross-host identical — the ban targets float
@@ -731,11 +757,11 @@ export declare class Store implements ObserverHost, QueryHost {
731
757
  * by the relation side data (`snapshotRelations` — multi forward target
732
758
  * sets, which live outside the sparse store). Both are written in canonical
733
759
  * entity-index order, so two worlds with identical contents inserted in
734
- * different orders snapshot byte-for-byte the same (#470). The reverse index
760
+ * different orders snapshot byte-for-byte the same. The reverse index
735
761
  * is derived and never serialized — `restoreSparse` rebuilds it. Pairs with
736
762
  * `restoreSparse`.
737
763
  *
738
- * **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
764
+ * **Opt-in.** Throws `DETERMINISM_DISABLED` unless the
739
765
  * Store was constructed with `{ deterministic: true }` — the canonical
740
766
  * entity-index ordering is the determinism tax the flag gates. */
741
767
  snapshotSparse(): Uint8Array;
@@ -749,7 +775,7 @@ export declare class Store implements ObserverHost, QueryHost {
749
775
  * code). Throws `SparseRestoreError` if the snapshot's shape, field identity,
750
776
  * entity-index bounds, or frame length don't validate.
751
777
  *
752
- * **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
778
+ * **Opt-in.** Throws `DETERMINISM_DISABLED` unless the
753
779
  * Store was constructed with `{ deterministic: true }`; paired with
754
780
  * `snapshotSparse`, which produces the canonical bytes restore consumes. */
755
781
  restoreSparse(bytes: Uint8Array): void;
@@ -763,7 +789,7 @@ export declare class Store implements ObserverHost, QueryHost {
763
789
  * order is load-bearing for byte-identical resume), the alive count, and each
764
790
  * archetype's `length` / `enabledCount`.
765
791
  *
766
- * **Opt-in (ADR-0020).** Throws `DETERMINISM_DISABLED` unless constructed with
792
+ * **Opt-in.** Throws `DETERMINISM_DISABLED` unless constructed with
767
793
  * `{ deterministic: true }` — the sparse section rides the canonical-ordering
768
794
  * surface and byte-identical resume is a determinism property. Pairs with
769
795
  * `restoreInto`.
@@ -784,7 +810,7 @@ export declare class Store implements ObserverHost, QueryHost {
784
810
  *
785
811
  * Requires a world whose SAB-backed archetype set + column layout match the
786
812
  * snapshot's exactly (prewarm so the archetype set is stable) and the same
787
- * entity-index capacity. **Opt-in (ADR-0020):** throws `DETERMINISM_DISABLED`
813
+ * entity-index capacity. **Opt-in:** throws `DETERMINISM_DISABLED`
788
814
  * unless `{ deterministic: true }`. See `snapshot()` for the v1 scope. */
789
815
  restoreInto(bytes: Uint8Array): void;
790
816
  /** Adopt a restored dense store (`SnapshotService.restoreInto`'s mount
@@ -797,7 +823,7 @@ export declare class Store implements ObserverHost, QueryHost {
797
823
  * `_entityIds` after the dense backing was swapped in `restoreInto`. `length`
798
824
  * + the per-row entity-id back-reference come from a scan of the restored
799
825
  * entity-index region (which entity occupies which row); `enabledCount` comes
800
- * from the captured host-state (the #577 partition boundary is positional only
826
+ * from the captured host-state (the partition boundary is positional only
801
827
  * — it has no per-entity byte source). */
802
828
  private _reconstructHostRows;
803
829
  registerRelation(opts?: RelationOptions): RelationDef;
@@ -820,7 +846,7 @@ export declare class Store implements ObserverHost, QueryHost {
820
846
  ancestorsOf(src: EntityID, def: RelationDef): EntityID[];
821
847
  rootOf(src: EntityID, def: RelationDef): EntityID;
822
848
  cascadeOf(root: EntityID, def: RelationDef): EntityID[];
823
- /** Second query-match path (#469 / ADR-0011): iterate entities matching a
849
+ /** Second query-match path: iterate entities matching a
824
850
  * dense mask **and** sparse-membership terms, invoking `cb` per entity.
825
851
  * Yields `EntityID`s, not archetype spans — sparse members are scattered
826
852
  * across archetypes, so there is no SoA column to hand back. Driven by the
@@ -836,13 +862,20 @@ export declare class Store implements ObserverHost, QueryHost {
836
862
  * - **neither** → walk `denseArchetypes`' entity ids (dense-only fallback).
837
863
  *
838
864
  * Only reached via `Query.forEachEntity`; dense `forEach` never consults
839
- * the sparse stores, so dense-only queries are unaffected (#469 AC). */
865
+ * the sparse stores, so dense-only queries are unaffected. */
840
866
  _forEachSparseMatch(include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], denseArchetypes: readonly Archetype[], cb: (entityId: EntityID) => void, includeDisabled: boolean): void;
841
- /** Fourth query-match path (#581): the matched set in hierarchy depth order
867
+ /** Fourth query-match path: the matched set in hierarchy depth order
842
868
  * (parents before children) — see `RelationService.forEachHierarchyMatch`. */
843
869
  _forEachHierarchyMatch(include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], denseArchetypes: readonly Archetype[], relation: RelationDef, maxDepth: number, includeDisabled: boolean, cb: (entityId: EntityID) => void): void;
844
870
  addComponent(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
845
871
  addComponent<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
872
+ /** @internal — cold tail of `addComponent`: the entity already holds `def`
873
+ * (overwrite in place, no transition), or the (source, component) add edge
874
+ * has not been cached yet (first time this pair is seen — resolve, which
875
+ * plants the edge, then re-enter the hot body). Split out so the edge-hit
876
+ * path above stays a straight line with one holey-array probe; this runs at
877
+ * most once per (archetype, component) pair plus on in-place overwrites. */
878
+ private _addComponentCold;
846
879
  /** Add multiple components in one transition (resolves final archetype, then moves once).
847
880
  *
848
881
  * Final-mask resolve, not graph walk. The previous implementation called
@@ -851,22 +884,21 @@ export declare class Store implements ObserverHost, QueryHost {
851
884
  * triggered a fresh `extendColumnStore` even though no entity ever lived
852
885
  * there. Computing the union mask up front and resolving once via
853
886
  * `archGetOrCreateFromMask` collapses N-1 intermediate-archetype
854
- * creations into zero for the batched case (#211 follow-up). The lazy
887
+ * creations into zero for the batched case. The lazy
855
888
  * single-mask path remains the same; this just avoids feeding it
856
889
  * archetypes the entity never visits.
857
890
  *
858
- * #659 — composite-add edge cache. The final-mask resolve, unlike the
891
+ * Composite-add edge cache. The final-mask resolve, unlike the
859
892
  * single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `ArchetypeGraph.lookup`
860
893
  * (the Map-of-buckets + `equals` scan), and `getBatchTransitionMap` on
861
- * every call a ~2× gap vs a cached edge walk that the decomposition probe
862
- * pinned on the two `Map.get`s, not the hash. So a repeated (source, added-
863
- * set) add now resolves through `currentArch`'s composite-add cache: one
894
+ * every call. That is much slower than a cached edge walk, and a probe put the
895
+ * cost on the two `Map.get` calls, and not on the hash. So a repeated (source,
896
+ * added-set) add now resolves through `currentArch`'s composite-add cache: one
864
897
  * `Map.get` on an exact packed key yields the target + transition map, and we
865
898
  * skip the union-mask build entirely. First call per key still resolves via
866
- * the final-mask path below (no intermediate planting) and plants the edge.
867
- * See docs/reports/bench/regressions/add-components-composite-edge.md. */
899
+ * the final-mask path below (no intermediate planting) and plants the edge. */
868
900
  addComponents(entityId: EntityID, entries: readonly TemplateEntryData[]): void;
869
- /** Shared move+write tail of `addComponents` (#659): place the entity into
901
+ /** Shared move+write tail of `addComponents`: place the entity into
870
902
  * the already-resolved `targetArch` — a `moveEntityFrom` along the cached
871
903
  * `map` when it has a row, else a fresh append (the rowless empty-archetype
872
904
  * source ignores `map`) — then write every entry's fields. Both the
@@ -874,6 +906,10 @@ export declare class Store implements ObserverHost, QueryHost {
874
906
  * the placement logic lives once. */
875
907
  private _addComponentsInto;
876
908
  removeComponent(entityId: EntityID, def: ComponentDef): void;
909
+ /** @internal — cold tail of `removeComponent`: the entity doesn't hold `def`
910
+ * (no-op), or the (source, component) remove edge has not been cached yet.
911
+ * Mirror of `_addComponentCold`; same depth-1 re-entry argument. */
912
+ private _removeComponentCold;
877
913
  /** Remove multiple components in one transition (resolves final archetype, then moves once).
878
914
  *
879
915
  * Final-mask resolve, not graph walk. Same rationale as `addComponents`
@@ -882,7 +918,7 @@ export declare class Store implements ObserverHost, QueryHost {
882
918
  * removal path. Computing the difference mask up front and resolving
883
919
  * once avoids planting N-1 intermediates the entity never lives in. */
884
920
  removeComponents(entityId: EntityID, defs: ComponentDef[]): void;
885
- /** Total (POLISH_AUDIT #9): a dead/stale `entityId` returns `false` rather
921
+ /** Total: a dead/stale `entityId` returns `false` rather
886
922
  * than throwing — a "has" probe is exactly what callers reach for to avoid
887
923
  * touching dead entities, so it must be safe to ask. */
888
924
  hasComponent(entityId: EntityID, def: ComponentHandle): boolean;
@@ -900,6 +936,49 @@ export declare class Store implements ObserverHost, QueryHost {
900
936
  batchRemoveComponent(src: ArchetypeID, def: ComponentDef): void;
901
937
  getEntityArchetype(entityId: EntityID): Archetype;
902
938
  getEntityRow(entityId: EntityID): number;
939
+ /**
940
+ * The row `resolveEntity` placed the entity at — the alloc-free second
941
+ * return value of a resolve, read immediately after the call. Same out-param
942
+ * pattern as `EntityAllocator.lastIndex` and `_moveResult`; returning a
943
+ * `{ arch, row }` pair instead would allocate on every by-id read.
944
+ */
945
+ resolvedRow: number;
946
+ /**
947
+ * (archetype, row) for a by-id access, derived from ONE index computation.
948
+ *
949
+ * `getEntityArchetype` and `getEntityRow` are each one line, and every by-id
950
+ * caller needs both — so the pair cost two derivations of the same packed
951
+ * index and two call frames to read two elements of two parallel arrays
952
+ * addressed identically. This is that pair, fused: index once, publish the
953
+ * row on `resolvedRow`, return the archetype.
954
+ *
955
+ * The two single-purpose accessors stay above — tests reach for one half at a
956
+ * time — but no runtime path uses them in a pair any more.
957
+ */
958
+ resolveEntity(entityId: EntityID): Archetype;
959
+ /**
960
+ * Build the `at(entity)` binder a `ComponentCursor` repoints itself through
961
+ * (ref.ts). Handing the cursor a closure rather than the Store keeps the
962
+ * mutation surface out of a value that user code holds onto.
963
+ *
964
+ * `stampTick` distinguishes the two variants once, here, instead of per
965
+ * `at()`: a mutable cursor bumps the component's change tick on every
966
+ * repoint (matching `ctx.ref`), a read-only one never does.
967
+ *
968
+ * The access check lives HERE, in the binder, and not only at the call that
969
+ * creates the cursor. A cursor is made one time and then kept, so it outlives
970
+ * the span that made it: a cursor made at host level (where no system is
971
+ * active, and the check at creation therefore passes) writes an undeclared
972
+ * component when a system body uses it, and a `ctx.cursor` that a system
973
+ * stores in an outer variable does the same in the NEXT system. Both slip
974
+ * past a check that only runs at creation. `at()` is the point of use, so the
975
+ * check belongs on it. The creation-site check stays as well: it fails early,
976
+ * and its stack names the line that made the cursor.
977
+ */
978
+ cursorBinder(def: ComponentHandle, stampTick: boolean): CursorBinder;
979
+ /** A component's field names in schema order — the cursor prototype key and
980
+ * ordinal source (ref.ts). One array per component, owned by its meta. */
981
+ componentFieldNames(def: ComponentHandle): readonly string[];
903
982
  /**
904
983
  * Find all archetypes matching the given masks.
905
984
  * Uses the inverted componentIndex to start from the component with the
@@ -929,7 +1008,7 @@ export declare class Store implements ObserverHost, QueryHost {
929
1008
  getResource(key: symbol): unknown;
930
1009
  setResource(key: symbol, value: unknown): void;
931
1010
  /** Fails closed on a missing key; the present → absent → present
932
- * lifecycle (#798) — see `ResourceRegistry.remove`. */
1011
+ * lifecycle — see `ResourceRegistry.remove`. */
933
1012
  removeResource(key: symbol): void;
934
1013
  hasResource(key: symbol): boolean;
935
1014
  }