@forgeax/engine-ecs 0.1.20 → 0.1.23

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 (100) hide show
  1. package/README.md +50 -16
  2. package/dist/__tests__/changed-block-summary.candidate.test.d.ts +2 -0
  3. package/dist/__tests__/changed-block-summary.candidate.test.d.ts.map +1 -0
  4. package/dist/__tests__/component-requirements.unit.test.d.ts +2 -0
  5. package/dist/__tests__/component-requirements.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/component-version-surface.test.d.ts +2 -0
  7. package/dist/__tests__/component-version-surface.test.d.ts.map +1 -0
  8. package/dist/__tests__/derived-range-writer.contract.test-d.d.ts +2 -0
  9. package/dist/__tests__/derived-range-writer.contract.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/derived-range-writer.contract.test.d.ts +2 -0
  11. package/dist/__tests__/derived-range-writer.contract.test.d.ts.map +1 -0
  12. package/dist/__tests__/structural-evidence.contract.test-d.d.ts +2 -0
  13. package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +1 -0
  14. package/dist/__tests__/structural-evidence.contract.test.d.ts +2 -0
  15. package/dist/__tests__/structural-evidence.contract.test.d.ts.map +1 -0
  16. package/dist/component-schema.d.ts +17 -0
  17. package/dist/component-schema.d.ts.map +1 -1
  18. package/dist/component.d.ts +7 -1
  19. package/dist/component.d.ts.map +1 -1
  20. package/dist/errors/query-and-component-errors.d.ts +1 -1
  21. package/dist/errors/query-and-component-errors.d.ts.map +1 -1
  22. package/dist/errors/validation-errors.d.ts +24 -0
  23. package/dist/errors/validation-errors.d.ts.map +1 -1
  24. package/dist/errors.d.ts +14 -2
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/externalization/index.mjs.map +1 -1
  27. package/dist/index.mjs +415 -128
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/internal.d.ts +16 -0
  30. package/dist/internal.d.ts.map +1 -0
  31. package/dist/internal.mjs +15 -2
  32. package/dist/internal.mjs.map +1 -1
  33. package/dist/projection/index.d.ts +27 -52
  34. package/dist/projection/index.d.ts.map +1 -1
  35. package/dist/projection/index.mjs +66 -76
  36. package/dist/projection/index.mjs.map +1 -1
  37. package/dist/query/derived-range-writer.d.ts +48 -0
  38. package/dist/query/derived-range-writer.d.ts.map +1 -0
  39. package/dist/query/query.d.ts +1 -1
  40. package/dist/query/query.d.ts.map +1 -1
  41. package/dist/relationship-index.d.ts +8 -0
  42. package/dist/relationship-index.d.ts.map +1 -1
  43. package/dist/shared-ref-store.d.ts +5 -9
  44. package/dist/shared-ref-store.d.ts.map +1 -1
  45. package/dist/shared.mjs.map +1 -1
  46. package/dist/storage/change-detection.d.ts +1 -3
  47. package/dist/storage/change-detection.d.ts.map +1 -1
  48. package/dist/storage/structural-evidence.d.ts +30 -0
  49. package/dist/storage/structural-evidence.d.ts.map +1 -0
  50. package/dist/world-component-access.d.ts +4 -3
  51. package/dist/world-component-access.d.ts.map +1 -1
  52. package/dist/world-core.d.ts +2 -2
  53. package/dist/world-core.d.ts.map +1 -1
  54. package/dist/world-entity-lifecycle.d.ts.map +1 -1
  55. package/dist/world-internal.d.ts +1 -1
  56. package/dist/world-internal.d.ts.map +1 -1
  57. package/dist/world.d.ts +10 -14
  58. package/dist/world.d.ts.map +1 -1
  59. package/package.json +5 -5
  60. package/src/__tests__/changed-block-summary.candidate.test.ts +57 -0
  61. package/src/__tests__/component-requirements.unit.test.ts +76 -0
  62. package/src/__tests__/component-version-surface.test.ts +45 -0
  63. package/src/__tests__/derived-range-writer.contract.test-d.ts +35 -0
  64. package/src/__tests__/derived-range-writer.contract.test.ts +339 -0
  65. package/src/__tests__/errors.unit.test.ts +21 -5
  66. package/src/__tests__/externalization-render-read-lease.test-d.ts +1 -1
  67. package/src/__tests__/externalization-render-read-lease.unit.test.ts +15 -15
  68. package/src/__tests__/query-storage-trends.perf.test.ts +19 -0
  69. package/src/__tests__/query.unit.test.ts +10 -6
  70. package/src/__tests__/shared-ref-store.unit.test.ts +3 -8
  71. package/src/__tests__/structural-evidence.contract.test-d.ts +6 -0
  72. package/src/__tests__/structural-evidence.contract.test.ts +49 -0
  73. package/src/component-schema.ts +51 -0
  74. package/src/component.ts +12 -2
  75. package/src/errors/query-and-component-errors.ts +1 -1
  76. package/src/errors/validation-errors.ts +30 -0
  77. package/src/errors.ts +29 -5
  78. package/src/internal.ts +56 -0
  79. package/src/projection/index.ts +68 -149
  80. package/src/query/derived-range-writer.ts +332 -0
  81. package/src/query/query.ts +89 -15
  82. package/src/relationship-index.ts +13 -1
  83. package/src/shared-ref-store.ts +16 -24
  84. package/src/storage/change-detection.ts +0 -15
  85. package/src/storage/structural-evidence.ts +64 -0
  86. package/src/world-component-access.ts +53 -5
  87. package/src/world-core.ts +2 -2
  88. package/src/world-entity-lifecycle.ts +10 -1
  89. package/src/world-internal.ts +5 -5
  90. package/src/world.ts +45 -50
  91. package/dist/.tsbuildinfo +0 -1
  92. package/dist/__tests__/world-change-journal.unit.test.d.ts +0 -2
  93. package/dist/__tests__/world-change-journal.unit.test.d.ts.map +0 -1
  94. package/dist/__tests__/world-internal-identity.unit.test.d.ts +0 -2
  95. package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +0 -1
  96. package/dist/world-change-journal.d.ts +0 -34
  97. package/dist/world-change-journal.d.ts.map +0 -1
  98. package/src/__tests__/world-change-journal.unit.test.ts +0 -97
  99. package/src/__tests__/world-internal-identity.unit.test.ts +0 -17
  100. package/src/world-change-journal.ts +0 -84
@@ -0,0 +1,64 @@
1
+ import type { EntityHandle } from '../entity-handle';
2
+
3
+ export type StructuralEvidenceKind = 'spawn' | 'despawn' | 'component-added' | 'component-removed';
4
+
5
+ export interface StructuralEvidenceInput {
6
+ readonly kind: StructuralEvidenceKind;
7
+ readonly entity: EntityHandle;
8
+ readonly componentId?: number;
9
+ }
10
+
11
+ export interface StructuralEvidence extends StructuralEvidenceInput {
12
+ readonly sequence: number;
13
+ }
14
+
15
+ export type StructuralEvidenceRead =
16
+ | {
17
+ readonly status: 'ok';
18
+ readonly cursor: number;
19
+ readonly events: readonly StructuralEvidence[];
20
+ }
21
+ | { readonly status: 'overflow'; readonly cursor: number; readonly oldestAvailable: number };
22
+
23
+ /** Bounded producer-owned structural evidence; consumers never infer facts by scanning. */
24
+ export class StructuralEvidenceRing {
25
+ private readonly events: Array<StructuralEvidence | undefined>;
26
+ private nextSequence = 1;
27
+
28
+ constructor(readonly capacity = 1024) {
29
+ if (!Number.isSafeInteger(capacity) || capacity <= 0) {
30
+ throw new RangeError('StructuralEvidenceRing capacity must be a positive safe integer');
31
+ }
32
+ this.events = new Array<StructuralEvidence | undefined>(capacity);
33
+ }
34
+
35
+ get cursor(): number {
36
+ return this.nextSequence - 1;
37
+ }
38
+
39
+ append(input: StructuralEvidenceInput): number {
40
+ const sequence = this.nextSequence;
41
+ this.nextSequence += 1;
42
+ this.events[(sequence - 1) % this.capacity] = { ...input, sequence };
43
+ return sequence;
44
+ }
45
+
46
+ readAfter(cursor: number): StructuralEvidenceRead {
47
+ const latest = this.cursor;
48
+ if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
49
+ throw new RangeError(`StructuralEvidenceRing cursor ${cursor} is outside 0..${latest}`);
50
+ }
51
+ const oldestAvailable = Math.max(1, latest - this.capacity + 1);
52
+ if (cursor < oldestAvailable - 1)
53
+ return { status: 'overflow', cursor: latest, oldestAvailable };
54
+ const events: StructuralEvidence[] = [];
55
+ for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
56
+ const event = this.events[(sequence - 1) % this.capacity];
57
+ if (event === undefined || event.sequence !== sequence) {
58
+ return { status: 'overflow', cursor: latest, oldestAvailable };
59
+ }
60
+ events.push(event);
61
+ }
62
+ return { status: 'ok', cursor: latest, events };
63
+ }
64
+ }
@@ -22,7 +22,7 @@ import {
22
22
  TYPE_METADATA,
23
23
  } from './component';
24
24
  import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
25
- import { componentDefinition } from './component-schema';
25
+ import { componentDefinition, expandComponentRequirements } from './component-schema';
26
26
  import { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
27
27
  import { Entity as EntityComponent } from './entity';
28
28
  import {
@@ -63,6 +63,7 @@ import {
63
63
  } from './storage/archetype-graph';
64
64
  import { removeSparseTag } from './storage/change-detection';
65
65
  import { arrayCountColumnName, type FieldView, normalizeBufferWrite } from './storage/column';
66
+ import type { StructuralEvidenceInput } from './storage/structural-evidence';
66
67
  import { appendTableRow, type Table } from './storage/table';
67
68
  import type { UniqueRefStore } from './unique-ref-store';
68
69
  import type { ComponentData, EcsError, EntityRecord } from './world';
@@ -106,8 +107,8 @@ export interface ComponentAccessState {
106
107
  readonly markComponentAdded: (entity: EntityHandle, componentId: number) => void;
107
108
  readonly markComponentsAdded: (entity: EntityHandle, componentIds: readonly number[]) => void;
108
109
  readonly markComponentChanged: (entity: EntityHandle, componentId: number) => void;
109
- readonly removeComponentChange: (entity: EntityHandle, componentId: number) => void;
110
110
  readonly markStructureChanged: () => void;
111
+ readonly recordStructuralEvidence: (evidence: StructuralEvidenceInput) => void;
111
112
  routeError(err: unknown, ctx: ErrorContext): void;
112
113
  }
113
114
 
@@ -568,7 +569,7 @@ export class WorldComponentAccess {
568
569
  * field does not exist, or the field is not an `array<...>` column.
569
570
  *
570
571
  * @internal Engine-internal fast path; AI users read the typed view through
571
- * `world.get(e, Transform).world`. The accessor is the zero-materialization
572
+ * `world.get(e, GlobalTransform).world`. The accessor is the zero-materialization
572
573
  * route the propagate kernel and render walk use.
573
574
  */
574
575
  _getArrayView(
@@ -1011,12 +1012,13 @@ export class WorldComponentAccess {
1011
1012
  entity: EntityHandle,
1012
1013
  componentData: ComponentData<S>,
1013
1014
  internal: boolean,
1015
+ resolveRequirements = true,
1014
1016
  ): Result<void, EcsError> {
1015
1017
  const record = this.lookupAlive(entity, 'addComponent', componentData.component.name);
1016
1018
  if (!record.ok) return record;
1017
1019
 
1018
1020
  const rec = record.value;
1019
- const srcArch = this.graph.archetypes[rec.archetypeId];
1021
+ let srcArch = this.graph.archetypes[rec.archetypeId];
1020
1022
  /* istanbul ignore next -- defensive: alive record always has valid archetypeId */
1021
1023
  if (!srcArch) {
1022
1024
  return err(
@@ -1066,6 +1068,38 @@ export class WorldComponentAccess {
1066
1068
  return err(enumErr as unknown as EcsError);
1067
1069
  }
1068
1070
 
1071
+ // Generic component requirements are resolved once at the structural
1072
+ // boundary. Explicit data remains authoritative; only missing required
1073
+ // identities are added before the requested component is migrated.
1074
+ if (resolveRequirements) {
1075
+ const required = expandComponentRequirements([componentData]).slice(1);
1076
+ for (const requirement of required) {
1077
+ if (
1078
+ srcArch.components.some(
1079
+ (candidate) => componentId(candidate) === componentId(requirement.component),
1080
+ )
1081
+ ) {
1082
+ continue;
1083
+ }
1084
+ // The closure is expanded once above. Bypass requirement expansion for
1085
+ // each member so malformed dependency cycles remain finite and the
1086
+ // structural work still happens in one deterministic sequence.
1087
+ const added = this._addComponentCore(entity, requirement as ComponentData, internal, false);
1088
+ if (!added.ok) return added;
1089
+ srcArch = this.graph.archetypes[rec.archetypeId];
1090
+ if (!srcArch) {
1091
+ return err(
1092
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
1093
+ operation: 'addComponent',
1094
+ component: componentData.component.name,
1095
+ expectedGeneration: rec.generation,
1096
+ actualGeneration: rec.generation,
1097
+ }),
1098
+ );
1099
+ }
1100
+ }
1101
+ }
1102
+
1069
1103
  // Check if entity already has this component (using World-local ID).
1070
1104
  const localId = componentId(componentData.component);
1071
1105
  if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
@@ -1144,6 +1178,11 @@ export class WorldComponentAccess {
1144
1178
  }
1145
1179
 
1146
1180
  this.markStructureChanged();
1181
+ this.state.recordStructuralEvidence({
1182
+ kind: 'component-added',
1183
+ entity,
1184
+ componentId: localId,
1185
+ });
1147
1186
  return ok(undefined);
1148
1187
  }
1149
1188
 
@@ -1249,8 +1288,12 @@ export class WorldComponentAccess {
1249
1288
  } else {
1250
1289
  this.storage.migrateEntity(rec, srcArch, targetArch);
1251
1290
  }
1252
- this.state.removeComponentChange(entity, componentId(component));
1253
1291
  this.markStructureChanged();
1292
+ this.state.recordStructuralEvidence({
1293
+ kind: 'component-removed',
1294
+ entity,
1295
+ componentId: localId,
1296
+ });
1254
1297
  return ok(undefined);
1255
1298
  }
1256
1299
 
@@ -1292,6 +1335,7 @@ export class WorldComponentAccess {
1292
1335
  entity: EntityHandle,
1293
1336
  componentDatas: ComponentData[],
1294
1337
  ): Result<void, EcsError> {
1338
+ componentDatas = expandComponentRequirements(componentDatas);
1295
1339
  const slot = entityIndex(entity);
1296
1340
  const record = this.records[slot];
1297
1341
  if (!record || record.archetypeId !== -1) return ok(undefined);
@@ -1341,6 +1385,10 @@ export class WorldComponentAccess {
1341
1385
  }
1342
1386
  }
1343
1387
  this.markStructureChanged();
1388
+ this.state.recordStructuralEvidence({
1389
+ kind: 'spawn',
1390
+ entity,
1391
+ });
1344
1392
  return ok(undefined);
1345
1393
  }
1346
1394
 
package/src/world-core.ts CHANGED
@@ -4,7 +4,7 @@ import type { RelationshipIndex } from './relationship-index';
4
4
  import type { SharedRefStore } from './shared-ref-store';
5
5
  import { SharedRefStore as SharedRefStoreImpl } from './shared-ref-store';
6
6
  import { type ArchetypeGraph, createArchetypeGraph } from './storage/archetype-graph';
7
- import { WorldChangeJournal } from './storage/change-detection';
7
+ import { StructuralEvidenceRing } from './storage/structural-evidence';
8
8
  import type { UniqueRefStore } from './unique-ref-store';
9
9
  import { UniqueRefStore as UniqueRefStoreImpl } from './unique-ref-store';
10
10
  import type { EntityRecord } from './world';
@@ -43,7 +43,7 @@ export class WorldCore {
43
43
  readonly sharedRefs: SharedRefStore = new SharedRefStoreImpl();
44
44
  readonly storage: WorldStorageCore;
45
45
  readonly componentMutationEpochs: number[] = [];
46
- readonly changeJournal = new WorldChangeJournal();
46
+ readonly structuralEvidence = new StructuralEvidenceRing();
47
47
  /** One packed reverse index per relationship source component. */
48
48
  readonly relationshipIndexes = new Map<number, RelationshipIndex>();
49
49
  mutationEpoch = 0;
@@ -8,6 +8,7 @@ import { err, isRetiredSlot, ok, pack, type Result } from '@forgeax/engine-types
8
8
  import type { Component, ComponentSchema, InputShapeOf, ShapeOf } from './component';
9
9
  import { componentId, componentSchema } from './component';
10
10
  import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
11
+ import { expandComponentRequirements } from './component-schema';
11
12
  import { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
12
13
  import { Entity } from './entity';
13
14
  import {
@@ -48,6 +49,7 @@ export function spawnCore(
48
49
  componentDatas: { component: Component; data: Partial<Record<string, unknown>> }[],
49
50
  internal: boolean,
50
51
  ): Result<EntityHandle, EcsError> {
52
+ componentDatas = expandComponentRequirements(componentDatas);
51
53
  const filledData: Record<string, unknown>[] = [];
52
54
  for (const cd of componentDatas) {
53
55
  const preflight = world[worldInternal].preflightComponentData(null, cd);
@@ -104,6 +106,10 @@ export function spawnCore(
104
106
  }
105
107
  }
106
108
  world[worldInternal].markStructureChanged();
109
+ world[worldInternal].recordStructuralEvidence({
110
+ kind: 'spawn',
111
+ entity: spawnedEntity,
112
+ });
107
113
  return ok(spawnedEntity);
108
114
  }
109
115
 
@@ -169,8 +175,11 @@ export function despawnCore(
169
175
  }
170
176
  }
171
177
  }
178
+ world[worldInternal].recordStructuralEvidence({
179
+ kind: 'despawn',
180
+ entity,
181
+ });
172
182
  if (record) {
173
- world[worldInternal].removeEntityChanges(entity);
174
183
  record.archetypeId = -1;
175
184
  record.archetypeRow = -1;
176
185
  record.generation += 1;
@@ -23,10 +23,10 @@ type InternalName =
23
23
  | 'despawnCore'
24
24
  | 'getArrayView'
25
25
  | 'getBufferPool'
26
- | 'getChangeCursor'
27
26
  | 'getClockWriter'
28
27
  | 'getComponentChange'
29
28
  | 'getComponentMutationEpoch'
29
+ | 'getComponentMutationEpochs'
30
30
  | 'getEntityArchetype'
31
31
  | 'getFixedAccumulator'
32
32
  | 'getFreeIndices'
@@ -41,28 +41,28 @@ type InternalName =
41
41
  | 'getSchedules'
42
42
  | 'getSharedRefs'
43
43
  | 'getStructureEpoch'
44
+ | 'getStructuralEvidence'
44
45
  | 'getUniqueRefs'
45
46
  | 'lookupAlive'
46
47
  | 'markComponentAdded'
47
48
  | 'markComponentChanged'
48
49
  | 'markComponentRangeChanged'
49
50
  | 'markComponentsAdded'
50
- | 'markDerivedComponentChanges'
51
51
  | 'markStructureChanged'
52
52
  | 'materializePendingEntity'
53
53
  | 'nextMutationEpoch'
54
54
  | 'poisonExecution'
55
+ | 'publishDerivedRange'
55
56
  | 'preflightComponentData'
56
- | 'readChangesSince'
57
57
  | 'readRow'
58
+ | 'recordStructuralEvidence'
58
59
  | 'recordIsLive'
59
60
  | 'relationshipOnInsert'
60
61
  | 'relationshipOnRemove'
61
62
  | 'releaseManagedRefsOnRow'
62
- | 'removeComponentChange'
63
63
  | 'removeComponentCore'
64
- | 'removeEntityChanges'
65
64
  | 'routeError'
65
+ | 'restoreMutationEpoch'
66
66
  | 'setFixedAccumulator'
67
67
  | 'setQueryRow'
68
68
  | 'spawnCore'
package/src/world.ts CHANGED
@@ -28,6 +28,7 @@ import type {
28
28
  ComponentNotDefinedError,
29
29
  ComponentNotPresentError,
30
30
  ComponentNumericValueInvalidError,
31
+ DerivedRangeOutOfBoundsError,
31
32
  FixedSizeMismatchError,
32
33
  ManagedArrayInvalidValueError,
33
34
  ManagedBufferOutOfBoundsError,
@@ -71,9 +72,11 @@ import {
71
72
  markComponentChanged,
72
73
  markComponentsAdded,
73
74
  readComponentChange,
74
- readTableEntityRange,
75
- type WorldChangeRead,
76
75
  } from './storage/change-detection';
76
+ import type {
77
+ StructuralEvidenceInput,
78
+ StructuralEvidenceRing,
79
+ } from './storage/structural-evidence';
77
80
  import {
78
81
  type ClockWriter,
79
82
  createWorldClock,
@@ -144,6 +147,8 @@ export type EcsError =
144
147
  | ScheduleScopeMismatchError
145
148
  | SharedKernelEligibilityError
146
149
  | SharedKernelFailureError
150
+ | DerivedRangeOutOfBoundsError
151
+ | ChangeEpochExhaustedError
147
152
  | WorldPoisonedError;
148
153
 
149
154
  /** Component data for spawn/addComponent: component token + initial values.
@@ -324,9 +329,6 @@ export class World {
324
329
  private get componentMutationEpochs() {
325
330
  return this.core.componentMutationEpochs;
326
331
  }
327
- private get changeJournal() {
328
- return this.core.changeJournal;
329
- }
330
332
  /** Free index slots (LIFO stack). */
331
333
  private get records() {
332
334
  return this.core.records;
@@ -409,9 +411,8 @@ export class World {
409
411
  this.internalmarkComponentsAdded(entity, components),
410
412
  markComponentChanged: (entity, component) =>
411
413
  this.internalmarkComponentChanged(entity, component),
412
- removeComponentChange: (entity, component) =>
413
- this.internalremoveComponentChange(entity, component),
414
414
  markStructureChanged: () => this.internalmarkStructureChanged(),
415
+ recordStructuralEvidence: (evidence) => this.internalrecordStructuralEvidence(evidence),
415
416
  routeError: (error, context) => this.internalrouteError(error as EcsError, context),
416
417
  });
417
418
  this.clock = createWorldClock({ ...DEFAULT_TIME_POLICY, ...options.time });
@@ -433,10 +434,10 @@ export class World {
433
434
  despawnCore: this.internaldespawnCore.bind(this),
434
435
  getArrayView: this.internalgetArrayView.bind(this),
435
436
  getBufferPool: this.internalgetBufferPool.bind(this),
436
- getChangeCursor: this.internalgetChangeCursor.bind(this),
437
437
  getClockWriter: this.internalgetClockWriter.bind(this),
438
438
  getComponentChange: this.internalgetComponentChange.bind(this),
439
439
  getComponentMutationEpoch: this.internalgetComponentMutationEpoch.bind(this),
440
+ getComponentMutationEpochs: this.internalgetComponentMutationEpochs.bind(this),
440
441
  getEntityArchetype: this.internalgetEntityArchetype.bind(this),
441
442
  getFixedAccumulator: this.internalgetFixedAccumulator.bind(this),
442
443
  getFreeIndices: this.internalgetFreeIndices.bind(this),
@@ -451,28 +452,28 @@ export class World {
451
452
  getSchedules: this.internalgetSchedules.bind(this),
452
453
  getSharedRefs: this.internalgetSharedRefs.bind(this),
453
454
  getStructureEpoch: this.internalgetStructureEpoch.bind(this),
455
+ getStructuralEvidence: this.internalgetStructuralEvidence.bind(this),
454
456
  getUniqueRefs: this.internalgetUniqueRefs.bind(this),
455
457
  lookupAlive: this.internallookupAlive.bind(this),
456
458
  markComponentAdded: this.internalmarkComponentAdded.bind(this),
457
459
  markComponentChanged: this.internalmarkComponentChanged.bind(this),
458
460
  markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
459
461
  markComponentsAdded: this.internalmarkComponentsAdded.bind(this),
460
- markDerivedComponentChanges: this.internalmarkDerivedComponentChanges.bind(this),
461
462
  markStructureChanged: this.internalmarkStructureChanged.bind(this),
462
463
  materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
463
464
  nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
464
465
  poisonExecution: this.internalpoisonExecution.bind(this),
466
+ publishDerivedRange: this.internalpublishDerivedRange.bind(this),
465
467
  preflightComponentData: this.internalpreflightComponentData.bind(this),
466
- readChangesSince: this.internalreadChangesSince.bind(this),
467
468
  readRow: this.internalreadRow.bind(this),
469
+ recordStructuralEvidence: this.internalrecordStructuralEvidence.bind(this),
468
470
  recordIsLive: this.internalrecordIsLive.bind(this),
469
471
  relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
470
472
  relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
471
473
  releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
472
- removeComponentChange: this.internalremoveComponentChange.bind(this),
473
474
  removeComponentCore: this.internalremoveComponentCore.bind(this),
474
- removeEntityChanges: this.internalremoveEntityChanges.bind(this),
475
475
  routeError: this.internalrouteError.bind(this),
476
+ restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
476
477
  setFixedAccumulator: this.internalsetFixedAccumulator.bind(this),
477
478
  setQueryRow: this.internalsetQueryRow.bind(this),
478
479
  spawnCore: this.internalspawnCore.bind(this),
@@ -536,19 +537,18 @@ export class World {
536
537
  return this.mutationEpoch;
537
538
  }
538
539
 
539
- /** Current cursor for a persistent projection subscriber. */
540
- private internalgetChangeCursor(): number {
541
- return this.changeJournal.cursor();
540
+ /** Structure snapshot used to invalidate borrowed query facades. */
541
+ private internalgetStructureEpoch(): number {
542
+ return this.structureEpoch;
542
543
  }
543
544
 
544
- /** Read complete mutation evidence or an explicit rebuild signal. */
545
- private internalreadChangesSince(cursor: number): WorldChangeRead {
546
- return this.changeJournal.readAfter(cursor);
545
+ /** Typed structural facts are produced by this World and consumed by projections. */
546
+ private internalgetStructuralEvidence(): StructuralEvidenceRing {
547
+ return this.core.structuralEvidence;
547
548
  }
548
549
 
549
- /** Structure snapshot used to invalidate borrowed query facades. */
550
- private internalgetStructureEpoch(): number {
551
- return this.structureEpoch;
550
+ private internalrecordStructuralEvidence(evidence: StructuralEvidenceInput): void {
551
+ this.core.structuralEvidence.append(evidence);
552
552
  }
553
553
 
554
554
  /** Resolve current logical identity for a packed entity handle. */
@@ -577,6 +577,25 @@ export class World {
577
577
  return this.mutationEpoch;
578
578
  }
579
579
 
580
+ private internalrestoreMutationEpoch(epoch: number): void {
581
+ this.mutationEpoch = epoch;
582
+ }
583
+
584
+ private internalpublishDerivedRange(
585
+ table: ArchetypeGraph['tables'][number],
586
+ componentId: number,
587
+ rowStart: number,
588
+ rowCount: number,
589
+ epoch: number,
590
+ ): void {
591
+ const epochs = table.storage.get(componentId)?.epochs;
592
+ if (epochs === undefined) {
593
+ throw new Error(`Derived component ${componentId} is not in the table.`);
594
+ }
595
+ epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
596
+ this.componentMutationEpochs[componentId] = epoch;
597
+ }
598
+
580
599
  /** Record one successful structural mutation. */
581
600
  private internalmarkStructureChanged(): void {
582
601
  this.structureEpoch += 1;
@@ -600,7 +619,6 @@ export class World {
600
619
  markComponentsAdded(this.graph, record, entity, componentIds, epoch);
601
620
  for (const componentId of componentIds) {
602
621
  this.componentMutationEpochs[componentId] = epoch;
603
- this.changeJournal.append({ kind: 'component-added', entity, componentId });
604
622
  }
605
623
  }
606
624
 
@@ -615,7 +633,6 @@ export class World {
615
633
  });
616
634
  if (epoch !== undefined) {
617
635
  this.componentMutationEpochs[componentId] = epoch;
618
- this.changeJournal.append({ kind: 'component-changed', entity, componentId });
619
636
  }
620
637
  }
621
638
 
@@ -631,13 +648,6 @@ export class World {
631
648
  const epoch = this.internalnextMutationEpoch();
632
649
  epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
633
650
  this.componentMutationEpochs[componentId] = epoch;
634
- for (const entity of readTableEntityRange(table, rowStart, rowCount)) {
635
- this.changeJournal.append({
636
- kind: 'component-changed',
637
- entity,
638
- componentId,
639
- });
640
- }
641
651
  }
642
652
 
643
653
  /** Latest mutation token for one component-owned projection. */
@@ -645,6 +655,11 @@ export class World {
645
655
  return this.componentMutationEpochs[componentId] ?? 0;
646
656
  }
647
657
 
658
+ /** Borrow the component-version summary used to skip unrelated value columns. */
659
+ private internalgetComponentMutationEpochs(): readonly number[] {
660
+ return this.componentMutationEpochs;
661
+ }
662
+
648
663
  /** Read a materialized relationship target in O(1 + k). */
649
664
  private internalgetRelationshipTargetEntities(
650
665
  source: Component,
@@ -658,16 +673,6 @@ export class World {
658
673
  return this.core.relationshipIndexes.get(componentId(source))?.epoch ?? 0;
659
674
  }
660
675
 
661
- /** Publish changes to a value derived without an authored mutation epoch. */
662
- private internalmarkDerivedComponentChanges(
663
- componentId: number,
664
- entities: Iterable<EntityHandle>,
665
- ): void {
666
- for (const entity of entities) {
667
- this.changeJournal.append({ kind: 'derived-component-changed', entity, componentId });
668
- }
669
- }
670
-
671
676
  /** Query facade write after the facade has already marked evidence. */
672
677
  private internalsetQueryRow(
673
678
  entity: EntityHandle,
@@ -685,16 +690,6 @@ export class World {
685
690
  return this.componentAccess.get(entity, component) as Result<Record<string, unknown>, EcsError>;
686
691
  }
687
692
 
688
- /** Remove one component's change state after archetype removal. */
689
- private internalremoveComponentChange(entity: EntityHandle, componentId: number): void {
690
- this.changeJournal.append({ kind: 'component-removed', entity, componentId });
691
- }
692
-
693
- /** Remove all change state before an entity handle is retired. */
694
- private internalremoveEntityChanges(entity: EntityHandle): void {
695
- this.changeJournal.append({ kind: 'entity-removed', entity });
696
- }
697
-
698
693
  /** Return resource change ticks for diagnostics and resource-driven systems. */
699
694
  getResourceChange(name: string): ChangeTicks | undefined {
700
695
  const entry = this.resources.entries.get(name);