@forgeax/engine-ecs 0.1.21 → 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
package/dist/index.mjs CHANGED
@@ -21,6 +21,31 @@ function componentDefinition(component) {
21
21
  }
22
22
  return definition;
23
23
  }
24
+ function componentRequirements(component) {
25
+ return definitions.definitions.get(component)?.policy.requires ?? [];
26
+ }
27
+ function expandComponentRequirements(componentDatas) {
28
+ let hasRequirements = false;
29
+ for (const entry of componentDatas) {
30
+ if (componentRequirements(entry.component).length !== 0) {
31
+ hasRequirements = true;
32
+ break;
33
+ }
34
+ }
35
+ if (!hasRequirements) return componentDatas;
36
+ const expanded = [...componentDatas];
37
+ const seen = new Set(expanded.map((entry) => entry.component));
38
+ for (let index = 0; index < expanded.length; index++) {
39
+ const component = expanded[index]?.component;
40
+ if (component === void 0) continue;
41
+ for (const required of componentRequirements(component)) {
42
+ if (seen.has(required)) continue;
43
+ seen.add(required);
44
+ expanded.push({ component: required, data: {} });
45
+ }
46
+ }
47
+ return expanded;
48
+ }
24
49
  function deepFreeze(value) {
25
50
  if (value === null || typeof value !== "object" && typeof value !== "function") {
26
51
  return value;
@@ -654,6 +679,17 @@ var ChangeEpochExhaustedError = class extends Error {
654
679
  this.detail = { epoch };
655
680
  }
656
681
  };
682
+ var DerivedRangeOutOfBoundsError = class extends Error {
683
+ name = "DerivedRangeOutOfBoundsError";
684
+ code = "derived-range-out-of-bounds";
685
+ expected = "a non-negative span-relative range with start + count <= span.length";
686
+ hint = "check start and count against the QuerySpan length, then retry without changing World state";
687
+ detail;
688
+ constructor(start, count, spanLength) {
689
+ super(`Derived range [${start}, ${start + count}) exceeds QuerySpan length ${spanLength}.`);
690
+ this.detail = { start, count, spanLength };
691
+ }
692
+ };
657
693
  var ProtectedResourceError = class extends Error {
658
694
  name = "ProtectedResourceError";
659
695
  code = "resource-protected";
@@ -1254,7 +1290,11 @@ function defineComponent(name, fields, options) {
1254
1290
  registerComponentDefinition(token, {
1255
1291
  fields: frozenFields,
1256
1292
  defaults: frozenDefaults,
1257
- policy: { transient: options?.transient ?? false, meta }
1293
+ policy: {
1294
+ transient: options?.transient ?? false,
1295
+ meta,
1296
+ requires: Object.freeze([...options?.requires ?? []])
1297
+ }
1258
1298
  });
1259
1299
  return Object.freeze(token);
1260
1300
  }
@@ -1385,7 +1425,11 @@ function defineRelationship(options) {
1385
1425
  };
1386
1426
  const target = defineComponent(options.targetName, targetFields);
1387
1427
  const sourceFields = { [options.sourceField]: { type: "entity" } };
1388
- const source = defineComponent(options.sourceName, sourceFields);
1428
+ const source = defineComponent(
1429
+ options.sourceName,
1430
+ sourceFields,
1431
+ options.sourceRequires === void 0 ? void 0 : { requires: options.sourceRequires }
1432
+ );
1389
1433
  roles.set(source, {
1390
1434
  kind: "source",
1391
1435
  target,
@@ -1563,51 +1607,10 @@ function entityGeneration(entity) {
1563
1607
  return unpackGen(entity);
1564
1608
  }
1565
1609
 
1566
- // src/world-change-journal.ts
1567
- var EMPTY_CHANGES = Object.freeze([]);
1568
- var WorldChangeJournal = class {
1569
- constructor(capacity = 65536) {
1570
- this.capacity = capacity;
1571
- if (!Number.isSafeInteger(capacity) || capacity <= 0) {
1572
- throw new RangeError("WorldChangeJournal capacity must be a positive safe integer");
1573
- }
1574
- this.records = new Array(capacity);
1575
- }
1576
- capacity;
1577
- records;
1578
- nextSequence = 1;
1579
- cursor() {
1580
- return this.nextSequence - 1;
1581
- }
1582
- append(change) {
1583
- const sequence = this.nextSequence;
1584
- this.nextSequence += 1;
1585
- this.records[(sequence - 1) % this.capacity] = { ...change, sequence };
1586
- return sequence;
1587
- }
1588
- readAfter(cursor) {
1589
- const latest = this.cursor();
1590
- if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
1591
- throw new RangeError(`WorldChangeJournal cursor ${cursor} is outside 0..${latest}`);
1592
- }
1593
- const oldestAvailable = Math.max(1, latest - this.capacity + 1);
1594
- if (cursor < oldestAvailable - 1) {
1595
- return { status: "overflow", cursor: latest, oldestAvailable };
1596
- }
1597
- if (cursor === latest) {
1598
- return { status: "ok", cursor: latest, records: EMPTY_CHANGES };
1599
- }
1600
- const records = [];
1601
- for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
1602
- const record = this.records[(sequence - 1) % this.capacity];
1603
- if (record === void 0 || record.sequence !== sequence) {
1604
- return { status: "overflow", cursor: latest, oldestAvailable };
1605
- }
1606
- records.push(record);
1607
- }
1608
- return { status: "ok", cursor: latest, records };
1609
- }
1610
- };
1610
+ // src/internal.ts
1611
+ var DERIVED_WRITER = /* @__PURE__ */ Symbol.for(
1612
+ "forgeax.ecs.query.derivedWriter"
1613
+ );
1611
1614
 
1612
1615
  // src/storage/archetype.ts
1613
1616
  var INITIAL_CAPACITY = 64;
@@ -2039,10 +2042,162 @@ function markComponentChanged(graph, location, entity, componentId2, epoch) {
2039
2042
  const tableRow2 = archetype.rows[location.archetypeRow] ?? -1;
2040
2043
  epochs.changed[tableRow2] = epoch();
2041
2044
  }
2042
- function readTableEntityRange(table, rowStart, rowCount) {
2043
- const entities = table.storage.get(componentId(Entity))?.fields.get("self")?.view;
2044
- if (entities === void 0) return [];
2045
- return entities.subarray(rowStart, rowStart + rowCount);
2045
+ function createDerivedRangeWriter(world, component, source) {
2046
+ let boundEpoch = -1;
2047
+ let bindingTables = [];
2048
+ let bindings = [];
2049
+ let runStartBuffers = [];
2050
+ let runCountBuffers = [];
2051
+ const rebind = () => {
2052
+ const tables = source.tables();
2053
+ const nextBindings = [];
2054
+ const nextRunStartBuffers = [];
2055
+ const nextRunCountBuffers = [];
2056
+ for (const table of tables) {
2057
+ nextBindings.push({
2058
+ read: buildWholeColumnShape(table, source.readComponents),
2059
+ write: buildWholeColumnShape(table, source.writeComponents),
2060
+ rowCapacity: table.size
2061
+ });
2062
+ const runCapacity = Math.max(1, table.size);
2063
+ nextRunStartBuffers.push(new Int32Array(runCapacity));
2064
+ nextRunCountBuffers.push(new Int32Array(runCapacity));
2065
+ }
2066
+ bindingTables = tables;
2067
+ bindings = nextBindings;
2068
+ runStartBuffers = nextRunStartBuffers;
2069
+ runCountBuffers = nextRunCountBuffers;
2070
+ boundEpoch = source.structureEpoch();
2071
+ };
2072
+ rebind();
2073
+ const writer = {
2074
+ get bindings() {
2075
+ if (boundEpoch !== source.structureEpoch()) rebind();
2076
+ return bindings;
2077
+ },
2078
+ writeRange(bindingIndex, base, start, count, kernel, context) {
2079
+ if (world.execution.health === "poisoned") {
2080
+ return err(new WorldPoisonedError(world.identity, world.execution.fault));
2081
+ }
2082
+ if (boundEpoch !== source.structureEpoch()) rebind();
2083
+ const binding = bindings[bindingIndex];
2084
+ const table = bindingTables[bindingIndex];
2085
+ if (binding === void 0 || table === void 0 || !Number.isSafeInteger(bindingIndex) || !Number.isSafeInteger(base) || !Number.isSafeInteger(start) || !Number.isSafeInteger(count) || bindingIndex < 0 || base < 0 || start < 0 || count < 0 || base + start + count > binding.rowCapacity || table.storage.get(componentId(component)) === void 0) {
2086
+ return err(new DerivedRangeOutOfBoundsError(start, count, binding?.rowCapacity ?? 0));
2087
+ }
2088
+ if (count === 0) return ok(void 0);
2089
+ const previousEpoch = world[worldInternal].getMutationEpoch();
2090
+ let epoch;
2091
+ try {
2092
+ epoch = world[worldInternal].nextMutationEpoch();
2093
+ } catch (cause) {
2094
+ return err(cause);
2095
+ }
2096
+ try {
2097
+ kernel(binding, base, start, count, context);
2098
+ world[worldInternal].publishDerivedRange(
2099
+ table,
2100
+ componentId(component),
2101
+ base + start,
2102
+ count,
2103
+ epoch
2104
+ );
2105
+ return ok(void 0);
2106
+ } catch (cause) {
2107
+ world[worldInternal].restoreMutationEpoch(previousEpoch);
2108
+ world[worldInternal].poisonExecution({
2109
+ code: "shared-kernel-failed",
2110
+ kernelName: `derived-range:${component.name}`,
2111
+ cause,
2112
+ partialWrite: true,
2113
+ retryable: false
2114
+ });
2115
+ return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
2116
+ }
2117
+ },
2118
+ probeAndCommitRange(bindingIndex, base, start, count, probe, commit, context) {
2119
+ if (world.execution.health === "poisoned") {
2120
+ return err(new WorldPoisonedError(world.identity, world.execution.fault));
2121
+ }
2122
+ if (boundEpoch !== source.structureEpoch()) rebind();
2123
+ const binding = bindings[bindingIndex];
2124
+ const table = bindingTables[bindingIndex];
2125
+ const runStarts = runStartBuffers[bindingIndex];
2126
+ const runCounts = runCountBuffers[bindingIndex];
2127
+ if (binding === void 0 || table === void 0 || runStarts === void 0 || runCounts === void 0 || !Number.isSafeInteger(bindingIndex) || !Number.isSafeInteger(base) || !Number.isSafeInteger(start) || !Number.isSafeInteger(count) || bindingIndex < 0 || base < 0 || start < 0 || count < 0 || base + start + count > binding.rowCapacity || table.storage.get(componentId(component)) === void 0) {
2128
+ return err(new DerivedRangeOutOfBoundsError(start, count, binding?.rowCapacity ?? 0));
2129
+ }
2130
+ if (count === 0) return ok(void 0);
2131
+ const previousEpoch = world[worldInternal].getMutationEpoch();
2132
+ let runCount = 0;
2133
+ let epoch = 0;
2134
+ let epochReserved = false;
2135
+ let runStart = -1;
2136
+ try {
2137
+ for (let offset = 0; offset < count; offset += 1) {
2138
+ const row = base + start + offset;
2139
+ const changed = probe(binding, row, context);
2140
+ if (changed) {
2141
+ if (!epochReserved) {
2142
+ try {
2143
+ epoch = world[worldInternal].nextMutationEpoch();
2144
+ epochReserved = true;
2145
+ } catch (cause) {
2146
+ return err(cause);
2147
+ }
2148
+ }
2149
+ if (runStart < 0) runStart = row;
2150
+ commit(binding, row, context);
2151
+ } else if (runStart >= 0) {
2152
+ runStarts[runCount] = runStart;
2153
+ runCounts[runCount] = row - runStart;
2154
+ runCount += 1;
2155
+ runStart = -1;
2156
+ }
2157
+ }
2158
+ if (runStart >= 0) {
2159
+ runStarts[runCount] = runStart;
2160
+ runCounts[runCount] = base + start + count - runStart;
2161
+ runCount += 1;
2162
+ }
2163
+ if (runCount > 0) {
2164
+ const componentIdentifier = componentId(component);
2165
+ for (let index = 0; index < runCount; index += 1) {
2166
+ const runStartValue = runStarts[index] ?? 0;
2167
+ const runLength = runCounts[index] ?? 0;
2168
+ world[worldInternal].publishDerivedRange(
2169
+ table,
2170
+ componentIdentifier,
2171
+ runStartValue,
2172
+ runLength,
2173
+ epoch
2174
+ );
2175
+ }
2176
+ }
2177
+ return ok(void 0);
2178
+ } catch (cause) {
2179
+ if (epochReserved) world[worldInternal].restoreMutationEpoch(previousEpoch);
2180
+ world[worldInternal].poisonExecution({
2181
+ code: "shared-kernel-failed",
2182
+ kernelName: `derived-range:${component.name}:probe`,
2183
+ cause,
2184
+ partialWrite: true,
2185
+ retryable: false
2186
+ });
2187
+ return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
2188
+ }
2189
+ }
2190
+ };
2191
+ return writer;
2192
+ }
2193
+ function buildWholeColumnShape(table, components) {
2194
+ const shape = {};
2195
+ for (const component of components) {
2196
+ const fields = table.storage.get(componentId(component))?.fields;
2197
+ if (fields === void 0) continue;
2198
+ for (const [fieldName, column] of fields) shape[fieldName] = column.view;
2199
+ }
2200
+ return shape;
2046
2201
  }
2047
2202
 
2048
2203
  // src/query/query.ts
@@ -2154,7 +2309,7 @@ var QuerySpanFacade = class {
2154
2309
  this.rowStart = rowStart;
2155
2310
  this.length = length;
2156
2311
  const entityColumn = table.storage.get(componentId(Entity))?.fields.get("self");
2157
- this.entities = entityColumn?.view.subarray(rowStart, rowStart + length) ?? new Uint32Array(0);
2312
+ this.entities = entityColumn === void 0 ? new Uint32Array(0) : rowStart === 0 && length === entityColumn.view.length ? entityColumn.view : entityColumn.view.subarray(rowStart, rowStart + length);
2158
2313
  }
2159
2314
  world;
2160
2315
  table;
@@ -2185,7 +2340,7 @@ var QuerySpanFacade = class {
2185
2340
  }
2186
2341
  };
2187
2342
  function makeManagedColumnReader(view, length, fieldType) {
2188
- const slots = view.subarray(0, length);
2343
+ const slots = view.length === length ? view : view.subarray(0, length);
2189
2344
  return Object.freeze({
2190
2345
  length,
2191
2346
  get(i) {
@@ -2201,7 +2356,7 @@ function buildColumnShape(table, component, rowStart, rowCount) {
2201
2356
  for (const [fieldName, column] of fields) {
2202
2357
  const start = rowStart * column.arity;
2203
2358
  const end = start + rowCount * column.arity;
2204
- const view = column.view.subarray(start, end);
2359
+ const view = start === 0 && end === column.view.length ? column.view : column.view.subarray(start, end);
2205
2360
  const fieldType = componentSchema(component)[fieldName];
2206
2361
  shape[fieldName] = fieldType !== void 0 && isManagedField(fieldType) ? makeManagedColumnReader(view, view.length, fieldType) : view;
2207
2362
  }
@@ -2216,6 +2371,7 @@ var ExecutableQuery = class {
2216
2371
  compiled;
2217
2372
  matchedArchetypes = [];
2218
2373
  matchedTables = [];
2374
+ matchedTableObjects = [];
2219
2375
  lastGraphGeneration = -1;
2220
2376
  lastObservedEpoch = 0;
2221
2377
  active = false;
@@ -2306,7 +2462,8 @@ var ExecutableQuery = class {
2306
2462
  query.refreshMatches();
2307
2463
  const structureEpoch = query.world[worldInternal].getStructureEpoch();
2308
2464
  const upperBound = query.world[worldInternal].getMutationEpoch();
2309
- let index = 0;
2465
+ let tableIndex = 0;
2466
+ let rowIndex = 0;
2310
2467
  let finished = false;
2311
2468
  const close = (commit) => {
2312
2469
  if (finished) return;
@@ -2324,12 +2481,37 @@ var ExecutableQuery = class {
2324
2481
  query.world[worldInternal].getStructureEpoch()
2325
2482
  );
2326
2483
  }
2327
- while (index < query.matchedTables.length) {
2328
- const table = query.world[worldInternal].getGraph().tables[query.matchedTables[index++] ?? -1];
2329
- if (table === void 0 || table.size === 0) continue;
2484
+ while (tableIndex < query.matchedTables.length) {
2485
+ const table = query.world[worldInternal].getGraph().tables[query.matchedTables[tableIndex] ?? -1];
2486
+ if (table === void 0 || table.size === 0) {
2487
+ tableIndex += 1;
2488
+ rowIndex = 0;
2489
+ continue;
2490
+ }
2491
+ if (query.compiled.changedIds.length === 0 && query.compiled.addedIds.length === 0) {
2492
+ tableIndex += 1;
2493
+ rowIndex = 0;
2494
+ return {
2495
+ done: false,
2496
+ value: new QuerySpanFacade(query.world, table, 0, table.size)
2497
+ };
2498
+ }
2499
+ while (rowIndex < table.size && !query.denseChangeMatches(table, rowIndex, upperBound)) {
2500
+ rowIndex += 1;
2501
+ }
2502
+ if (rowIndex >= table.size) {
2503
+ tableIndex += 1;
2504
+ rowIndex = 0;
2505
+ continue;
2506
+ }
2507
+ const start = rowIndex;
2508
+ rowIndex += 1;
2509
+ while (rowIndex < table.size && query.denseChangeMatches(table, rowIndex, upperBound)) {
2510
+ rowIndex += 1;
2511
+ }
2330
2512
  return {
2331
2513
  done: false,
2332
- value: new QuerySpanFacade(query.world, table, 0, table.size)
2514
+ value: new QuerySpanFacade(query.world, table, start, rowIndex - start)
2333
2515
  };
2334
2516
  }
2335
2517
  close(true);
@@ -2343,6 +2525,25 @@ var ExecutableQuery = class {
2343
2525
  }
2344
2526
  });
2345
2527
  }
2528
+ [DERIVED_WRITER](component) {
2529
+ const reason = this.spanUnavailableReason();
2530
+ if (reason !== void 0) return err(new QuerySpanUnavailableError(reason));
2531
+ return ok(
2532
+ createDerivedRangeWriter(
2533
+ this.world,
2534
+ component,
2535
+ {
2536
+ structureEpoch: () => this.world[worldInternal].getStructureEpoch(),
2537
+ tables: () => {
2538
+ this.refreshMatches();
2539
+ return this.matchedTableObjects;
2540
+ },
2541
+ readComponents: this.compiled.descriptor.read ?? [],
2542
+ writeComponents: this.compiled.descriptor.write ?? []
2543
+ }
2544
+ )
2545
+ );
2546
+ }
2346
2547
  combinations(k = 2) {
2347
2548
  const query = this;
2348
2549
  return {
@@ -2393,6 +2594,10 @@ var ExecutableQuery = class {
2393
2594
  if (this.lastGraphGeneration === graph.generation) return;
2394
2595
  this.matchedArchetypes = graph.archetypes.filter((archetype) => this.archetypeMatches(archetype)).map((archetype) => archetype.id);
2395
2596
  this.matchedTables = graph.tables.filter((table) => this.tableMatches(table)).map((table) => table.id);
2597
+ this.matchedTableObjects = this.matchedTables.flatMap((id) => {
2598
+ const table = graph.tables[id];
2599
+ return table === void 0 ? [] : [table];
2600
+ });
2396
2601
  this.lastGraphGeneration = graph.generation;
2397
2602
  }
2398
2603
  archetypeMatches(archetype) {
@@ -2431,11 +2636,20 @@ var ExecutableQuery = class {
2431
2636
  }
2432
2637
  return true;
2433
2638
  }
2639
+ denseChangeMatches(table, row, upperBound) {
2640
+ for (const componentId2 of this.compiled.changedIds) {
2641
+ const epoch = table.storage.get(componentId2)?.epochs.changed[row] ?? 0;
2642
+ if (epoch <= this.lastObservedEpoch || epoch > upperBound) return false;
2643
+ }
2644
+ for (const componentId2 of this.compiled.addedIds) {
2645
+ const epoch = table.storage.get(componentId2)?.epochs.added[row] ?? 0;
2646
+ if (epoch <= this.lastObservedEpoch || epoch > upperBound) return false;
2647
+ }
2648
+ return true;
2649
+ }
2434
2650
  spanUnavailableReason() {
2435
2651
  if (this.compiled.sparseRoute) return "sparse-component";
2436
2652
  if ((this.compiled.descriptor.optional?.length ?? 0) > 0) return "optional-data";
2437
- if ((this.compiled.descriptor.changed?.length ?? 0) > 0 || (this.compiled.descriptor.added?.length ?? 0) > 0)
2438
- return "row-change-filter";
2439
2653
  return void 0;
2440
2654
  }
2441
2655
  };
@@ -3394,8 +3608,8 @@ var ComponentStorage = class {
3394
3608
  get sharedRefs() {
3395
3609
  return this.state.sharedRefs;
3396
3610
  }
3397
- routeError(err11, ctx) {
3398
- this.state.routeError(err11, ctx);
3611
+ routeError(err12, ctx) {
3612
+ this.state.routeError(err12, ctx);
3399
3613
  }
3400
3614
  readArrayView(arch, component, row, fieldName) {
3401
3615
  const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
@@ -3762,11 +3976,11 @@ var ComponentStorage = class {
3762
3976
  * the EcsError contract; this helper only attaches the systemName context
3763
3977
  * so AI users can correlate the error with the holder component / field.
3764
3978
  */
3765
- routeArrayError(err11, componentName, fieldName) {
3979
+ routeArrayError(err12, componentName, fieldName) {
3766
3980
  const ctx = {
3767
3981
  systemName: `World.write (${componentName}.${fieldName})`
3768
3982
  };
3769
- this.routeError(err11, ctx);
3983
+ this.routeError(err12, ctx);
3770
3984
  }
3771
3985
  /**
3772
3986
  * Write the value of an `array<T>` / `array<T,N>` field at `row` (D-3 +
@@ -4211,8 +4425,8 @@ var WorldComponentAccess = class {
4211
4425
  get uniqueRefs() {
4212
4426
  return this.state.uniqueRefs;
4213
4427
  }
4214
- routeError(err11, ctx) {
4215
- this.state.routeError(err11, ctx);
4428
+ routeError(err12, ctx) {
4429
+ this.state.routeError(err12, ctx);
4216
4430
  }
4217
4431
  relationshipIndex(component) {
4218
4432
  if (relationshipRole(component)?.kind !== "source") return void 0;
@@ -4557,7 +4771,7 @@ var WorldComponentAccess = class {
4557
4771
  * field does not exist, or the field is not an `array<...>` column.
4558
4772
  *
4559
4773
  * @internal Engine-internal fast path; AI users read the typed view through
4560
- * `world.get(e, Transform).world`. The accessor is the zero-materialization
4774
+ * `world.get(e, GlobalTransform).world`. The accessor is the zero-materialization
4561
4775
  * route the propagate kernel and render walk use.
4562
4776
  */
4563
4777
  _getArrayView(entity, component, fieldName) {
@@ -4884,11 +5098,11 @@ var WorldComponentAccess = class {
4884
5098
  * (lazy mirror create or exclusive reparent).
4885
5099
  * @internal
4886
5100
  */
4887
- _addComponentCore(entity, componentData, internal) {
5101
+ _addComponentCore(entity, componentData, internal, resolveRequirements = true) {
4888
5102
  const record = this.lookupAlive(entity, "addComponent", componentData.component.name);
4889
5103
  if (!record.ok) return record;
4890
5104
  const rec = record.value;
4891
- const srcArch = this.graph.archetypes[rec.archetypeId];
5105
+ let srcArch = this.graph.archetypes[rec.archetypeId];
4892
5106
  if (!srcArch) {
4893
5107
  return err(
4894
5108
  new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
@@ -4930,6 +5144,29 @@ var WorldComponentAccess = class {
4930
5144
  if (enumErr !== null) {
4931
5145
  return err(enumErr);
4932
5146
  }
5147
+ if (resolveRequirements) {
5148
+ const required = expandComponentRequirements([componentData]).slice(1);
5149
+ for (const requirement of required) {
5150
+ if (srcArch.components.some(
5151
+ (candidate) => componentId(candidate) === componentId(requirement.component)
5152
+ )) {
5153
+ continue;
5154
+ }
5155
+ const added = this._addComponentCore(entity, requirement, internal, false);
5156
+ if (!added.ok) return added;
5157
+ srcArch = this.graph.archetypes[rec.archetypeId];
5158
+ if (!srcArch) {
5159
+ return err(
5160
+ new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
5161
+ operation: "addComponent",
5162
+ component: componentData.component.name,
5163
+ expectedGeneration: rec.generation,
5164
+ actualGeneration: rec.generation
5165
+ })
5166
+ );
5167
+ }
5168
+ }
5169
+ }
4933
5170
  const localId = componentId(componentData.component);
4934
5171
  if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
4935
5172
  const role = relationshipRole(componentData.component);
@@ -4985,6 +5222,11 @@ var WorldComponentAccess = class {
4985
5222
  if (!relationshipResult.ok) return relationshipResult;
4986
5223
  }
4987
5224
  this.markStructureChanged();
5225
+ this.state.recordStructuralEvidence({
5226
+ kind: "component-added",
5227
+ entity,
5228
+ componentId: localId
5229
+ });
4988
5230
  return ok(void 0);
4989
5231
  }
4990
5232
  /**
@@ -5062,8 +5304,12 @@ var WorldComponentAccess = class {
5062
5304
  } else {
5063
5305
  this.storage.migrateEntity(rec, srcArch, targetArch);
5064
5306
  }
5065
- this.state.removeComponentChange(entity, componentId(component));
5066
5307
  this.markStructureChanged();
5308
+ this.state.recordStructuralEvidence({
5309
+ kind: "component-removed",
5310
+ entity,
5311
+ componentId: localId
5312
+ });
5067
5313
  return ok(void 0);
5068
5314
  }
5069
5315
  // ──────────────────────────────────────────────────────────────────────────
@@ -5097,6 +5343,7 @@ var WorldComponentAccess = class {
5097
5343
  * Idempotent: a record with archetypeId !== -1 is already materialized.
5098
5344
  */
5099
5345
  _materializePendingEntity(entity, componentDatas) {
5346
+ componentDatas = expandComponentRequirements(componentDatas);
5100
5347
  const slot = entityIndex(entity);
5101
5348
  const record = this.records[slot];
5102
5349
  if (!record || record.archetypeId !== -1) return ok(void 0);
@@ -5131,6 +5378,10 @@ var WorldComponentAccess = class {
5131
5378
  }
5132
5379
  }
5133
5380
  this.markStructureChanged();
5381
+ this.state.recordStructuralEvidence({
5382
+ kind: "spawn",
5383
+ entity
5384
+ });
5134
5385
  return ok(void 0);
5135
5386
  }
5136
5387
  // ──────────────────────────────────────────────────────────────────────────
@@ -5411,7 +5662,7 @@ var BufferPool = class {
5411
5662
  return n;
5412
5663
  }
5413
5664
  };
5414
- var SHARED_REF_MUTATION_JOURNAL_CAPACITY = 4096;
5665
+ var SHARED_REF_RELEASE_EVIDENCE_CAPACITY = 4096;
5415
5666
  var SharedRefStore = class {
5416
5667
  payloads = /* @__PURE__ */ new Map();
5417
5668
  refcounts = /* @__PURE__ */ new Map();
@@ -5420,7 +5671,8 @@ var SharedRefStore = class {
5420
5671
  internedKeys = /* @__PURE__ */ new Map();
5421
5672
  nextSlot = BUILTIN_BASE;
5422
5673
  mutationEpoch = 0;
5423
- mutationJournal = [];
5674
+ /** Latest published mutation epoch per live handle; not an event journal. */
5675
+ mutationEpochs = /* @__PURE__ */ new Map();
5424
5676
  releaseJournal = [];
5425
5677
  /**
5426
5678
  * Generation table indexed by slot (D-6). Each entry tracks the current
@@ -5524,26 +5776,23 @@ var SharedRefStore = class {
5524
5776
  throw new RangeError("SharedRefStore mutation epoch exhausted");
5525
5777
  }
5526
5778
  this.mutationEpoch += 1;
5527
- this.mutationJournal.push({ epoch: this.mutationEpoch, handle: unwrapHandle(handle) });
5528
- if (this.mutationJournal.length > SHARED_REF_MUTATION_JOURNAL_CAPACITY) {
5529
- this.mutationJournal.shift();
5530
- }
5779
+ this.mutationEpochs.set(unwrapHandle(handle), this.mutationEpoch);
5531
5780
  return ok(void 0);
5532
5781
  }
5533
5782
  /** Current upper bound for explicitly published payload mutations. */
5534
5783
  getMutationEpoch() {
5535
5784
  return this.mutationEpoch;
5536
5785
  }
5537
- /** Read exact changed handles after `cursor`; overflow requires consumer resync. */
5786
+ /** Read each live handle whose latest published mutation is after `cursor`. */
5538
5787
  readChangesSince(cursor) {
5539
- const oldestAvailable = this.mutationJournal[0]?.epoch ?? this.mutationEpoch + 1;
5540
- if (cursor < oldestAvailable - 1) {
5541
- return { status: "overflow", cursor: this.mutationEpoch, oldestAvailable };
5788
+ const records = [];
5789
+ for (const [handle, epoch] of this.mutationEpochs) {
5790
+ if (epoch > cursor) records.push({ epoch, handle });
5542
5791
  }
5792
+ records.sort((left, right) => left.epoch - right.epoch || left.handle - right.handle);
5543
5793
  return {
5544
- status: "ok",
5545
5794
  cursor: this.mutationEpoch,
5546
- records: this.mutationJournal.filter((record) => record.epoch > cursor)
5795
+ records
5547
5796
  };
5548
5797
  }
5549
5798
  /**
@@ -5609,6 +5858,7 @@ var SharedRefStore = class {
5609
5858
  }
5610
5859
  this.refcounts.delete(raw);
5611
5860
  this.payloads.delete(raw);
5861
+ this.mutationEpochs.delete(raw);
5612
5862
  const generation = storeGen + 1;
5613
5863
  this._generations[slot] = generation;
5614
5864
  if (!isRetiredSlot(generation)) {
@@ -5621,7 +5871,7 @@ var SharedRefStore = class {
5621
5871
  evidence: "released"
5622
5872
  });
5623
5873
  this.releaseJournal.push(evidence);
5624
- if (this.releaseJournal.length > SHARED_REF_MUTATION_JOURNAL_CAPACITY) {
5874
+ if (this.releaseJournal.length > SHARED_REF_RELEASE_EVIDENCE_CAPACITY) {
5625
5875
  this.releaseJournal.shift();
5626
5876
  }
5627
5877
  return ok(evidence);
@@ -5645,6 +5895,47 @@ var SharedRefStore = class {
5645
5895
  return this.payloads.size;
5646
5896
  }
5647
5897
  };
5898
+
5899
+ // src/storage/structural-evidence.ts
5900
+ var StructuralEvidenceRing = class {
5901
+ constructor(capacity = 1024) {
5902
+ this.capacity = capacity;
5903
+ if (!Number.isSafeInteger(capacity) || capacity <= 0) {
5904
+ throw new RangeError("StructuralEvidenceRing capacity must be a positive safe integer");
5905
+ }
5906
+ this.events = new Array(capacity);
5907
+ }
5908
+ capacity;
5909
+ events;
5910
+ nextSequence = 1;
5911
+ get cursor() {
5912
+ return this.nextSequence - 1;
5913
+ }
5914
+ append(input) {
5915
+ const sequence = this.nextSequence;
5916
+ this.nextSequence += 1;
5917
+ this.events[(sequence - 1) % this.capacity] = { ...input, sequence };
5918
+ return sequence;
5919
+ }
5920
+ readAfter(cursor) {
5921
+ const latest = this.cursor;
5922
+ if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
5923
+ throw new RangeError(`StructuralEvidenceRing cursor ${cursor} is outside 0..${latest}`);
5924
+ }
5925
+ const oldestAvailable = Math.max(1, latest - this.capacity + 1);
5926
+ if (cursor < oldestAvailable - 1)
5927
+ return { status: "overflow", cursor: latest, oldestAvailable };
5928
+ const events = [];
5929
+ for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
5930
+ const event = this.events[(sequence - 1) % this.capacity];
5931
+ if (event === void 0 || event.sequence !== sequence) {
5932
+ return { status: "overflow", cursor: latest, oldestAvailable };
5933
+ }
5934
+ events.push(event);
5935
+ }
5936
+ return { status: "ok", cursor: latest, events };
5937
+ }
5938
+ };
5648
5939
  var UniqueRefStore = class {
5649
5940
  payloads = /* @__PURE__ */ new Map();
5650
5941
  freeSlots = [];
@@ -5801,7 +6092,7 @@ var WorldCore = class {
5801
6092
  sharedRefs = new SharedRefStore();
5802
6093
  storage;
5803
6094
  componentMutationEpochs = [];
5804
- changeJournal = new WorldChangeJournal();
6095
+ structuralEvidence = new StructuralEvidenceRing();
5805
6096
  /** One packed reverse index per relationship source component. */
5806
6097
  relationshipIndexes = /* @__PURE__ */ new Map();
5807
6098
  mutationEpoch = 0;
@@ -5830,6 +6121,7 @@ function tableRow(world, record) {
5830
6121
  return archetype?.rows[record.archetypeRow] ?? -1;
5831
6122
  }
5832
6123
  function spawnCore(world, componentDatas, internal) {
6124
+ componentDatas = expandComponentRequirements(componentDatas);
5833
6125
  const filledData = [];
5834
6126
  for (const cd of componentDatas) {
5835
6127
  const preflight = world[worldInternal].preflightComponentData(null, cd);
@@ -5886,6 +6178,10 @@ function spawnCore(world, componentDatas, internal) {
5886
6178
  }
5887
6179
  }
5888
6180
  world[worldInternal].markStructureChanged();
6181
+ world[worldInternal].recordStructuralEvidence({
6182
+ kind: "spawn",
6183
+ entity: spawnedEntity
6184
+ });
5889
6185
  return ok(spawnedEntity);
5890
6186
  }
5891
6187
  function despawnCore(world, entity, internal) {
@@ -5936,8 +6232,11 @@ function despawnCore(world, entity, internal) {
5936
6232
  }
5937
6233
  }
5938
6234
  }
6235
+ world[worldInternal].recordStructuralEvidence({
6236
+ kind: "despawn",
6237
+ entity
6238
+ });
5939
6239
  if (record) {
5940
- world[worldInternal].removeEntityChanges(entity);
5941
6240
  record.archetypeId = -1;
5942
6241
  record.archetypeRow = -1;
5943
6242
  record.generation += 1;
@@ -6586,9 +6885,6 @@ var World = class {
6586
6885
  get componentMutationEpochs() {
6587
6886
  return this.core.componentMutationEpochs;
6588
6887
  }
6589
- get changeJournal() {
6590
- return this.core.changeJournal;
6591
- }
6592
6888
  /** Free index slots (LIFO stack). */
6593
6889
  get records() {
6594
6890
  return this.core.records;
@@ -6668,8 +6964,8 @@ var World = class {
6668
6964
  markComponentAdded: (entity, component) => this.internalmarkComponentAdded(entity, component),
6669
6965
  markComponentsAdded: (entity, components) => this.internalmarkComponentsAdded(entity, components),
6670
6966
  markComponentChanged: (entity, component) => this.internalmarkComponentChanged(entity, component),
6671
- removeComponentChange: (entity, component) => this.internalremoveComponentChange(entity, component),
6672
6967
  markStructureChanged: () => this.internalmarkStructureChanged(),
6968
+ recordStructuralEvidence: (evidence) => this.internalrecordStructuralEvidence(evidence),
6673
6969
  routeError: (error, context) => this.internalrouteError(error, context)
6674
6970
  });
6675
6971
  this.clock = createWorldClock({ ...DEFAULT_TIME_POLICY, ...options.time });
@@ -6691,10 +6987,10 @@ var World = class {
6691
6987
  despawnCore: this.internaldespawnCore.bind(this),
6692
6988
  getArrayView: this.internalgetArrayView.bind(this),
6693
6989
  getBufferPool: this.internalgetBufferPool.bind(this),
6694
- getChangeCursor: this.internalgetChangeCursor.bind(this),
6695
6990
  getClockWriter: this.internalgetClockWriter.bind(this),
6696
6991
  getComponentChange: this.internalgetComponentChange.bind(this),
6697
6992
  getComponentMutationEpoch: this.internalgetComponentMutationEpoch.bind(this),
6993
+ getComponentMutationEpochs: this.internalgetComponentMutationEpochs.bind(this),
6698
6994
  getEntityArchetype: this.internalgetEntityArchetype.bind(this),
6699
6995
  getFixedAccumulator: this.internalgetFixedAccumulator.bind(this),
6700
6996
  getFreeIndices: this.internalgetFreeIndices.bind(this),
@@ -6709,28 +7005,28 @@ var World = class {
6709
7005
  getSchedules: this.internalgetSchedules.bind(this),
6710
7006
  getSharedRefs: this.internalgetSharedRefs.bind(this),
6711
7007
  getStructureEpoch: this.internalgetStructureEpoch.bind(this),
7008
+ getStructuralEvidence: this.internalgetStructuralEvidence.bind(this),
6712
7009
  getUniqueRefs: this.internalgetUniqueRefs.bind(this),
6713
7010
  lookupAlive: this.internallookupAlive.bind(this),
6714
7011
  markComponentAdded: this.internalmarkComponentAdded.bind(this),
6715
7012
  markComponentChanged: this.internalmarkComponentChanged.bind(this),
6716
7013
  markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
6717
7014
  markComponentsAdded: this.internalmarkComponentsAdded.bind(this),
6718
- markDerivedComponentChanges: this.internalmarkDerivedComponentChanges.bind(this),
6719
7015
  markStructureChanged: this.internalmarkStructureChanged.bind(this),
6720
7016
  materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
6721
7017
  nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
6722
7018
  poisonExecution: this.internalpoisonExecution.bind(this),
7019
+ publishDerivedRange: this.internalpublishDerivedRange.bind(this),
6723
7020
  preflightComponentData: this.internalpreflightComponentData.bind(this),
6724
- readChangesSince: this.internalreadChangesSince.bind(this),
6725
7021
  readRow: this.internalreadRow.bind(this),
7022
+ recordStructuralEvidence: this.internalrecordStructuralEvidence.bind(this),
6726
7023
  recordIsLive: this.internalrecordIsLive.bind(this),
6727
7024
  relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
6728
7025
  relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
6729
7026
  releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
6730
- removeComponentChange: this.internalremoveComponentChange.bind(this),
6731
7027
  removeComponentCore: this.internalremoveComponentCore.bind(this),
6732
- removeEntityChanges: this.internalremoveEntityChanges.bind(this),
6733
7028
  routeError: this.internalrouteError.bind(this),
7029
+ restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
6734
7030
  setFixedAccumulator: this.internalsetFixedAccumulator.bind(this),
6735
7031
  setQueryRow: this.internalsetQueryRow.bind(this),
6736
7032
  spawnCore: this.internalspawnCore.bind(this),
@@ -6776,18 +7072,17 @@ var World = class {
6776
7072
  internalgetMutationEpoch() {
6777
7073
  return this.mutationEpoch;
6778
7074
  }
6779
- /** Current cursor for a persistent projection subscriber. */
6780
- internalgetChangeCursor() {
6781
- return this.changeJournal.cursor();
6782
- }
6783
- /** Read complete mutation evidence or an explicit rebuild signal. */
6784
- internalreadChangesSince(cursor) {
6785
- return this.changeJournal.readAfter(cursor);
6786
- }
6787
7075
  /** Structure snapshot used to invalidate borrowed query facades. */
6788
7076
  internalgetStructureEpoch() {
6789
7077
  return this.structureEpoch;
6790
7078
  }
7079
+ /** Typed structural facts are produced by this World and consumed by projections. */
7080
+ internalgetStructuralEvidence() {
7081
+ return this.core.structuralEvidence;
7082
+ }
7083
+ internalrecordStructuralEvidence(evidence) {
7084
+ this.core.structuralEvidence.append(evidence);
7085
+ }
6791
7086
  /** Resolve current logical identity for a packed entity handle. */
6792
7087
  internalgetEntityArchetype(entity) {
6793
7088
  const record = this.records[entityIndex(entity)];
@@ -6808,6 +7103,17 @@ var World = class {
6808
7103
  this.mutationEpoch += 1;
6809
7104
  return this.mutationEpoch;
6810
7105
  }
7106
+ internalrestoreMutationEpoch(epoch) {
7107
+ this.mutationEpoch = epoch;
7108
+ }
7109
+ internalpublishDerivedRange(table, componentId2, rowStart, rowCount, epoch) {
7110
+ const epochs = table.storage.get(componentId2)?.epochs;
7111
+ if (epochs === void 0) {
7112
+ throw new Error(`Derived component ${componentId2} is not in the table.`);
7113
+ }
7114
+ epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
7115
+ this.componentMutationEpochs[componentId2] = epoch;
7116
+ }
6811
7117
  /** Record one successful structural mutation. */
6812
7118
  internalmarkStructureChanged() {
6813
7119
  this.structureEpoch += 1;
@@ -6828,7 +7134,6 @@ var World = class {
6828
7134
  markComponentsAdded(this.graph, record, entity, componentIds, epoch);
6829
7135
  for (const componentId2 of componentIds) {
6830
7136
  this.componentMutationEpochs[componentId2] = epoch;
6831
- this.changeJournal.append({ kind: "component-added", entity, componentId: componentId2 });
6832
7137
  }
6833
7138
  }
6834
7139
  /** Mark an existing component as changed at the current tick. */
@@ -6842,7 +7147,6 @@ var World = class {
6842
7147
  });
6843
7148
  if (epoch !== void 0) {
6844
7149
  this.componentMutationEpochs[componentId2] = epoch;
6845
- this.changeJournal.append({ kind: "component-changed", entity, componentId: componentId2 });
6846
7150
  }
6847
7151
  }
6848
7152
  /** Mark one contiguous component range with a single epoch. */
@@ -6852,18 +7156,15 @@ var World = class {
6852
7156
  const epoch = this.internalnextMutationEpoch();
6853
7157
  epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
6854
7158
  this.componentMutationEpochs[componentId2] = epoch;
6855
- for (const entity of readTableEntityRange(table, rowStart, rowCount)) {
6856
- this.changeJournal.append({
6857
- kind: "component-changed",
6858
- entity,
6859
- componentId: componentId2
6860
- });
6861
- }
6862
7159
  }
6863
7160
  /** Latest mutation token for one component-owned projection. */
6864
7161
  internalgetComponentMutationEpoch(componentId2) {
6865
7162
  return this.componentMutationEpochs[componentId2] ?? 0;
6866
7163
  }
7164
+ /** Borrow the component-version summary used to skip unrelated value columns. */
7165
+ internalgetComponentMutationEpochs() {
7166
+ return this.componentMutationEpochs;
7167
+ }
6867
7168
  /** Read a materialized relationship target in O(1 + k). */
6868
7169
  internalgetRelationshipTargetEntities(source, target) {
6869
7170
  return this.componentAccess.relationshipTargetEntries(source, target);
@@ -6872,12 +7173,6 @@ var World = class {
6872
7173
  internalgetRelationshipEpoch(source) {
6873
7174
  return this.core.relationshipIndexes.get(componentId(source))?.epoch ?? 0;
6874
7175
  }
6875
- /** Publish changes to a value derived without an authored mutation epoch. */
6876
- internalmarkDerivedComponentChanges(componentId2, entities) {
6877
- for (const entity of entities) {
6878
- this.changeJournal.append({ kind: "derived-component-changed", entity, componentId: componentId2 });
6879
- }
6880
- }
6881
7176
  /** Query facade write after the facade has already marked evidence. */
6882
7177
  internalsetQueryRow(entity, component, value) {
6883
7178
  return this.componentAccess.set(entity, component, value, false);
@@ -6886,14 +7181,6 @@ var World = class {
6886
7181
  internalgetQueryRow(entity, component) {
6887
7182
  return this.componentAccess.get(entity, component);
6888
7183
  }
6889
- /** Remove one component's change state after archetype removal. */
6890
- internalremoveComponentChange(entity, componentId2) {
6891
- this.changeJournal.append({ kind: "component-removed", entity, componentId: componentId2 });
6892
- }
6893
- /** Remove all change state before an entity handle is retired. */
6894
- internalremoveEntityChanges(entity) {
6895
- this.changeJournal.append({ kind: "entity-removed", entity });
6896
- }
6897
7184
  /** Return resource change ticks for diagnostics and resource-driven systems. */
6898
7185
  getResourceChange(name) {
6899
7186
  const entry = this.resources.entries.get(name);
@@ -6910,8 +7197,8 @@ var World = class {
6910
7197
  *
6911
7198
  * Not part of the public API.
6912
7199
  */
6913
- internalrouteError(err11, ctx) {
6914
- console.error(`[${ctx?.systemName ?? "World"}]`, err11);
7200
+ internalrouteError(err12, ctx) {
7201
+ console.error(`[${ctx?.systemName ?? "World"}]`, err12);
6915
7202
  }
6916
7203
  // ──────────────────────────────────────────────────────────────────────────
6917
7204
  // accessors — M1 extraction seam