@adobe/ecs 0.2.3 → 0.3.0

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 (218) hide show
  1. package/dist/implementation/compiler/augmentations.js +8 -1
  2. package/dist/implementation/compiler/augmentations.js.map +1 -1
  3. package/dist/implementation/compiler/body-access.d.ts +18 -1
  4. package/dist/implementation/compiler/body-access.js +104 -15
  5. package/dist/implementation/compiler/body-access.js.map +1 -1
  6. package/dist/implementation/compiler/estree.d.ts +5 -2
  7. package/dist/implementation/compiler/estree.js +8 -5
  8. package/dist/implementation/compiler/estree.js.map +1 -1
  9. package/dist/implementation/compiler/inferred.js +5 -2
  10. package/dist/implementation/compiler/inferred.js.map +1 -1
  11. package/dist/implementation/compiler/transform.js +5 -4
  12. package/dist/implementation/compiler/transform.js.map +1 -1
  13. package/dist/implementation/compiler/type-schemas.d.ts +1 -0
  14. package/dist/implementation/compiler/type-schemas.js +35 -2
  15. package/dist/implementation/compiler/type-schemas.js.map +1 -1
  16. package/dist/implementation/world/activate.d.ts +14 -3
  17. package/dist/implementation/world/activate.js +28 -10
  18. package/dist/implementation/world/activate.js.map +1 -1
  19. package/dist/implementation/world/buffer.d.ts +7 -0
  20. package/dist/implementation/world/buffer.js +7 -0
  21. package/dist/implementation/world/buffer.js.map +1 -1
  22. package/dist/implementation/world/cascade.d.ts +10 -3
  23. package/dist/implementation/world/cascade.js +48 -23
  24. package/dist/implementation/world/cascade.js.map +1 -1
  25. package/dist/implementation/world/commands.js +41 -5
  26. package/dist/implementation/world/commands.js.map +1 -1
  27. package/dist/implementation/world/create.js +57 -6
  28. package/dist/implementation/world/create.js.map +1 -1
  29. package/dist/implementation/world/execute.d.ts +4 -4
  30. package/dist/implementation/world/execute.js +228 -93
  31. package/dist/implementation/world/execute.js.map +1 -1
  32. package/dist/implementation/world/finalizers.d.ts +10 -0
  33. package/dist/implementation/world/finalizers.js +60 -10
  34. package/dist/implementation/world/finalizers.js.map +1 -1
  35. package/dist/implementation/world/join.js +12 -2
  36. package/dist/implementation/world/join.js.map +1 -1
  37. package/dist/implementation/world/program.js +5 -1
  38. package/dist/implementation/world/program.js.map +1 -1
  39. package/dist/implementation/world/resource-changes.d.ts +18 -0
  40. package/dist/implementation/world/resource-changes.js +66 -0
  41. package/dist/implementation/world/resource-changes.js.map +1 -0
  42. package/dist/implementation/world/rows.js +21 -16
  43. package/dist/implementation/world/rows.js.map +1 -1
  44. package/dist/implementation/world/run-changes.d.ts +38 -0
  45. package/dist/implementation/world/run-changes.js +138 -0
  46. package/dist/implementation/world/run-changes.js.map +1 -0
  47. package/dist/implementation/world/runner.js +16 -12
  48. package/dist/implementation/world/runner.js.map +1 -1
  49. package/dist/implementation/world/state.d.ts +15 -0
  50. package/dist/implementation/world/state.js +22 -1
  51. package/dist/implementation/world/state.js.map +1 -1
  52. package/dist/implementation/world/synchronize.js +81 -24
  53. package/dist/implementation/world/synchronize.js.map +1 -1
  54. package/dist/implementation/world/tables-read.d.ts +5 -3
  55. package/dist/implementation/world/tables-read.js +16 -4
  56. package/dist/implementation/world/tables-read.js.map +1 -1
  57. package/dist/implementation/world/tables.d.ts +4 -2
  58. package/dist/implementation/world/tables.js +137 -38
  59. package/dist/implementation/world/tables.js.map +1 -1
  60. package/dist/implementation/world/thread-observer.js +1 -1
  61. package/dist/implementation/world/thread-observer.js.map +1 -1
  62. package/dist/implementation/world/thread-systems.js +10 -6
  63. package/dist/implementation/world/thread-systems.js.map +1 -1
  64. package/dist/implementation/world/up.js +7 -1
  65. package/dist/implementation/world/up.js.map +1 -1
  66. package/dist/implementation/world/where.d.ts +8 -0
  67. package/dist/implementation/world/where.js +40 -11
  68. package/dist/implementation/world/where.js.map +1 -1
  69. package/dist/interface/finalizer/body.d.ts +40 -4
  70. package/dist/interface/finalizer/create.d.ts +8 -6
  71. package/dist/interface/finalizer/create.js +45 -10
  72. package/dist/interface/finalizer/create.js.map +1 -1
  73. package/dist/interface/finalizer/declaration.d.ts +26 -4
  74. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  75. package/dist/interface/indexes/declaration-of.js +20 -0
  76. package/dist/interface/indexes/declaration-of.js.map +1 -0
  77. package/dist/interface/indexes/index.d.ts +1 -0
  78. package/dist/interface/indexes/index.js +1 -0
  79. package/dist/interface/indexes/index.js.map +1 -1
  80. package/dist/interface/registry/index.d.ts +1 -1
  81. package/dist/interface/registry/index.js +1 -1
  82. package/dist/interface/registry/index.js.map +1 -1
  83. package/dist/interface/registry/names.d.ts +4 -0
  84. package/dist/interface/registry/names.js +20 -0
  85. package/dist/interface/registry/names.js.map +1 -1
  86. package/dist/interface/registry/schedule.d.ts +29 -1
  87. package/dist/interface/registry/schedule.js +261 -25
  88. package/dist/interface/registry/schedule.js.map +1 -1
  89. package/dist/interface/system/access.d.ts +15 -0
  90. package/dist/interface/system/check.d.ts +7 -0
  91. package/dist/interface/system/check.js +21 -3
  92. package/dist/interface/system/check.js.map +1 -1
  93. package/dist/interface/system/commands.d.ts +31 -5
  94. package/dist/interface/system/create-resolved.js +2 -1
  95. package/dist/interface/system/create-resolved.js.map +1 -1
  96. package/dist/interface/system/create.d.ts +10 -6
  97. package/dist/interface/system/create.js +5 -4
  98. package/dist/interface/system/create.js.map +1 -1
  99. package/dist/interface/system/declaration.d.ts +11 -4
  100. package/dist/interface/system/describe.d.ts +120 -0
  101. package/dist/interface/system/describe.js +116 -0
  102. package/dist/interface/system/describe.js.map +1 -0
  103. package/dist/interface/system/index.d.ts +1 -0
  104. package/dist/interface/system/index.js +1 -0
  105. package/dist/interface/system/index.js.map +1 -1
  106. package/dist/interface/system/leaf.d.ts +7 -0
  107. package/dist/interface/system/render.js +2 -1
  108. package/dist/interface/system/render.js.map +1 -1
  109. package/dist/interface/system/resolve.d.ts +14 -1
  110. package/dist/interface/system/resolve.js +50 -1
  111. package/dist/interface/system/resolve.js.map +1 -1
  112. package/dist/interface/system/shared-only.d.ts +7 -0
  113. package/dist/interface/system/shared-only.js +2 -0
  114. package/dist/interface/system/shared-only.js.map +1 -0
  115. package/dist/interface/system/tables.d.ts +9 -1
  116. package/dist/interface/world/table/index.d.ts +3 -2
  117. package/dist/interface/world/world.d.ts +15 -0
  118. package/package.json +11 -2
  119. package/src/guide/01-resources.md +4 -3
  120. package/src/guide/03-systems.out +1 -1
  121. package/src/guide/05-scheduling.md +12 -6
  122. package/src/guide/05-scheduling.out +1 -0
  123. package/src/guide/05-scheduling.ts +17 -0
  124. package/src/guide/06-sync-points.md +20 -8
  125. package/src/guide/06-sync-points.out +3 -2
  126. package/src/guide/06-sync-points.ts +24 -5
  127. package/src/guide/10-finalizers.md +5 -0
  128. package/src/guide/18-identity.out +1 -1
  129. package/src/guide/guide.test.ts +6 -153
  130. package/src/implementation/compiler/augmentations.test.ts +28 -1
  131. package/src/implementation/compiler/augmentations.ts +5 -1
  132. package/src/implementation/compiler/body-access.ts +102 -12
  133. package/src/implementation/compiler/estree.ts +9 -6
  134. package/src/implementation/compiler/inferred.test.ts +1 -1
  135. package/src/implementation/compiler/inferred.ts +5 -2
  136. package/src/implementation/compiler/transform.test.ts +41 -21
  137. package/src/implementation/compiler/transform.ts +5 -3
  138. package/src/implementation/compiler/type-schemas.ts +28 -2
  139. package/src/implementation/world/activate.ts +32 -10
  140. package/src/implementation/world/batches.test.ts +3 -3
  141. package/src/implementation/world/buffer.ts +7 -0
  142. package/src/implementation/world/cascade.ts +46 -22
  143. package/src/implementation/world/commands.ts +37 -5
  144. package/src/implementation/world/create.test.ts +8 -8
  145. package/src/implementation/world/create.ts +61 -6
  146. package/src/implementation/world/defaults.test.ts +24 -1
  147. package/src/implementation/world/despawn-where.test.ts +310 -0
  148. package/src/implementation/world/execute.ts +222 -86
  149. package/src/implementation/world/finalizers.ts +63 -11
  150. package/src/implementation/world/ids.test.ts +1 -1
  151. package/src/implementation/world/indexes.test.ts +2 -1
  152. package/src/implementation/world/join.test.ts +19 -0
  153. package/src/implementation/world/join.ts +16 -2
  154. package/src/implementation/world/lifetime.test-helper.ts +1 -1
  155. package/src/implementation/world/names.test.ts +1 -2
  156. package/src/implementation/world/passes.test.ts +61 -0
  157. package/src/implementation/world/perf.md +12 -1
  158. package/src/implementation/world/program.ts +4 -1
  159. package/src/implementation/world/resource-changes.test.ts +61 -0
  160. package/src/implementation/world/resource-changes.ts +67 -0
  161. package/src/implementation/world/rows.ts +21 -14
  162. package/src/implementation/world/rules.test.ts +37 -0
  163. package/src/implementation/world/run-changes.test.ts +88 -0
  164. package/src/implementation/world/run-changes.ts +160 -0
  165. package/src/implementation/world/run-results.test.ts +17 -0
  166. package/src/implementation/world/runner.ts +17 -13
  167. package/src/implementation/world/schedule.test.ts +42 -1
  168. package/src/implementation/world/shared.test.ts +2 -2
  169. package/src/implementation/world/state.ts +27 -0
  170. package/src/implementation/world/sync-points.test.ts +147 -0
  171. package/src/implementation/world/synchronize.ts +73 -20
  172. package/src/implementation/world/tables-read.ts +16 -5
  173. package/src/implementation/world/tables.test.ts +139 -4
  174. package/src/implementation/world/tables.ts +118 -35
  175. package/src/implementation/world/thread-observer.ts +1 -1
  176. package/src/implementation/world/thread-systems.ts +10 -6
  177. package/src/implementation/world/up.test.ts +13 -1
  178. package/src/implementation/world/up.ts +6 -1
  179. package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
  180. package/src/implementation/world/watch-traits.test-helper.ts +10 -0
  181. package/src/implementation/world/watch-traits.test.ts +62 -0
  182. package/src/implementation/world/where.ts +38 -10
  183. package/src/interface/finalizer/body.ts +33 -4
  184. package/src/interface/finalizer/create.test.ts +6 -1
  185. package/src/interface/finalizer/create.ts +45 -12
  186. package/src/interface/finalizer/declaration.ts +31 -4
  187. package/src/interface/indexes/create.test.ts +10 -0
  188. package/src/interface/indexes/declaration-of.ts +34 -0
  189. package/src/interface/indexes/index.ts +1 -0
  190. package/src/interface/registry/index.ts +1 -1
  191. package/src/interface/registry/names.ts +19 -0
  192. package/src/interface/registry/schedule.ts +275 -29
  193. package/src/interface/rule/rule.type-test.ts +23 -1
  194. package/src/interface/system/access.ts +16 -0
  195. package/src/interface/system/check.ts +17 -2
  196. package/src/interface/system/commands.ts +28 -4
  197. package/src/interface/system/commands.type-test.ts +31 -0
  198. package/src/interface/system/create-resolved.ts +2 -1
  199. package/src/interface/system/create.test.ts +10 -2
  200. package/src/interface/system/create.ts +9 -5
  201. package/src/interface/system/declaration.ts +10 -4
  202. package/src/interface/system/describe.test.ts +98 -0
  203. package/src/interface/system/describe.ts +122 -0
  204. package/src/interface/system/index.ts +1 -0
  205. package/src/interface/system/leaf.ts +7 -0
  206. package/src/interface/system/render.ts +2 -1
  207. package/src/interface/system/resolve.test.ts +17 -1
  208. package/src/interface/system/resolve.ts +60 -2
  209. package/src/interface/system/shared-only.ts +5 -0
  210. package/src/interface/system/system.type-test.ts +6 -0
  211. package/src/interface/system/tables.ts +7 -1
  212. package/src/interface/world/run.type-test.ts +11 -1
  213. package/src/interface/world/table/index.ts +3 -2
  214. package/src/interface/world/world.ts +13 -0
  215. package/src/perftest/benchmarks.ts +20 -0
  216. package/src/perftest/chained.ts +108 -0
  217. package/src/perftest/structural.ts +20 -0
  218. package/src/perftest/tables.ts +107 -0
@@ -49,7 +49,7 @@ describe("System.create", () => {
49
49
  const body = (): void => {};
50
50
  // A body the build doesn't see is given what it uses.
51
51
  const move = System.create(giveAccess({ components: { with: [position] } }, { write: [position], resources: { read: [time] } }), body);
52
- expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body });
52
+ expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body, commands: expect.any(Function) });
53
53
  expect(move.name).toBe("move");
54
54
  expect(definitionOf({ name: "move" })).toBe(undefined);
55
55
  });
@@ -71,7 +71,7 @@ describe("System.create", () => {
71
71
  it("labels a system made with neither an id nor a binding by its kind numbered", () => {
72
72
  const body = ({ commands }: { readonly commands: System.Commands }): void => { commands.spawn([position])({ [position]: 0 }); };
73
73
  const [alone] = [System.create(giveAccess(identify(body, "setup.ts#spawnOnce")))];
74
- expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body });
74
+ expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body, commands: expect.any(Function) });
75
75
  const [first, second] = [System.create(() => {}), System.create({ systems: [alone] })].map(system => system.name);
76
76
  const number = Number(first?.slice("system #".length));
77
77
  expect([first, second]).toEqual([`system #${number}`, `system #${number + 1}`]);
@@ -171,6 +171,14 @@ describe("System.create of a composite", () => {
171
171
  const looped = System.create({ before: [first], after: [second] }, () => {});
172
172
  expect(() => System.create({ systems: [first, second, looped] })).toThrow("Ordering cycle");
173
173
  });
174
+ it("takes a row declared whole as written, every member but references, and says so in a conflict", () => {
175
+ const pass = (row: object): void => { void row; };
176
+ const whole = System.create({ components: { with: [position, parent], whole: true } }, row => { pass(row); });
177
+ const reader = System.create({ components: { with: [position] } }, row => { void row[position]; });
178
+ const definition = definitionOf(whole);
179
+ expect(definition?.kind === "system" ? definition.declaration.components : null).toEqual({ read: [parent], write: [position], whole: true });
180
+ expect(() => System.create([whole, reader])).toThrow(`both access "typesSystemTest/position" and one writes it; order them with before or after, or, if they never match one entity, add a component of one to the other's not. "whole" takes its row whole (components.whole), so it writes every member.`);
181
+ });
174
182
  it("runs readonly members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
175
183
  const write = System.create({ components: { with: [position] } }, row => { row[position] += 1; });
176
184
  const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
@@ -16,8 +16,9 @@ import type { Listed, Property } from './property.js';
16
16
  import type { System } from './system.js';
17
17
  import type { OneThread, ThreadedBy, ThreadsOf } from './threads.js';
18
18
  import type { HeldBy } from './values.js';
19
+ import type { SharedOnly } from './shared-only.js';
19
20
  import type { Shared } from '../component/value.js';
20
- import { accessOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
21
+ import { accessOf, commandListsOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
21
22
 
22
23
  type Lookups<D> = Property<D, "lookup">;
23
24
 
@@ -68,9 +69,10 @@ type Targeted = {
68
69
  readonly order?: never;
69
70
  };
70
71
 
72
+
71
73
  /** `where` names only shared components, and keeps rows unordered across archetypes, so it's without `up`, `order` and a target. */
72
74
  type Where<D> = D extends { readonly components: { readonly where: infer W } }
73
- ? { readonly components?: { readonly where?: { readonly [K in Exclude<keyof W, Shared>]: { readonly "error: where names only shared components, which each archetype holds one value of": K } } } }
75
+ ? { readonly components?: { readonly where?: SharedOnly<W> } }
74
76
  & (D extends { readonly up: object } | { readonly order: object } | { readonly target: string }
75
77
  ? { readonly components?: { readonly where?: { readonly "error: where can't be used with up, order or a target, which run rows across archetypes": true } } } : {})
76
78
  : {};
@@ -99,6 +101,7 @@ type Validate<D extends Declaration> =
99
101
  ? { readonly kernel?: boolean | { readonly "error: a GPU kernel can't have optional components yet": true } } : {})
100
102
  & (D extends { readonly readonly: true } ? Readonly : {})
101
103
  // Tables are copies a readonly system without components reads.
104
+ & (D extends { readonly tables: { readonly where: infer W } } ? { readonly tables?: { readonly where?: SharedOnly<W> } } : {})
102
105
  & (D extends { readonly tables: object }
103
106
  ? (D extends { readonly readonly: true } ? {} : { readonly tables?: { readonly "error: tables are read by a readonly system": true } })
104
107
  & (Property<D, "components"> extends undefined ? {} : { readonly tables?: { readonly "error: a system reading tables has no components": true } })
@@ -175,9 +178,10 @@ export function create(first: Declaration | Leaf["body"] | readonly System<unkno
175
178
  const body = typeof first === "function" ? first : second;
176
179
  if (body === undefined) throw new Error("A system needs a body, and a composite its systems.");
177
180
  const name = nameOf(first, id, "system");
178
- const declaration = resolve(`System "${name}"`, declared, accessOf(`System "${name}"`, first));
181
+ const access = accessOf(`System "${name}"`, first);
182
+ const declaration = resolve(`System "${name}"`, declared, access);
179
183
  const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
180
- const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
184
+ const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: commandListsOf(`System "${name}"`, access) };
181
185
  checkLeaf(system);
182
186
  claimSystem(system, id);
183
187
  return system;
@@ -194,7 +198,7 @@ const resolve = (what: string, declared: Declaration, access: ReturnType<typeof
194
198
  for (const key of [...whereReads, ...declared.tables?.resources ?? []]) if (!uses.read.includes(key) && !uses.write.includes(key) && !conditions.includes(key)) uses.read.push(key);
195
199
  const resolved: Resolved = {
196
200
  ...rest,
197
- ...components === undefined ? {} : { components: componentLists(components, access.components, reading, whereReads) },
201
+ ...components === undefined ? {} : { components: componentLists(components, components.whole === true ? null : access.components, reading, whereReads) },
198
202
  ...lookup === undefined ? {} : { lookup: lookupsLists(lookup, access) },
199
203
  ...up === undefined ? {} : { up: { through: up.through, read: upReads(what, access) } },
200
204
  };
@@ -52,10 +52,16 @@ export type Declaration = {
52
52
  readonly target?: string;
53
53
  /** Makes this a render system (see `render`): its body records GPU work over the GPU copy of the components it names. */
54
54
  readonly shader?: Shader;
55
- /** Hard constraint. */
56
- readonly before?: readonly System<unknown>[];
57
- /** Hard constraint. */
58
- readonly after?: readonly System<unknown>[];
55
+ /**
56
+ * Hard constraint: runs before each system listed, and, for a resource listed, before every other system of the
57
+ * composite that uses it, but those its constraints already order before it and those listing it in `before` too.
58
+ */
59
+ readonly before?: readonly (System<unknown> | Resource.Key)[];
60
+ /**
61
+ * Hard constraint: runs after each system listed, and, for a resource listed, after every other system of the
62
+ * composite that uses it, but those its constraints already order after it and those listing it in `after` too.
63
+ */
64
+ readonly after?: readonly (System<unknown> | Resource.Key)[];
59
65
  /** Runs inside the span of the systems listed: after what they follow and before what follows them. */
60
66
  readonly during?: readonly System<unknown>[];
61
67
  /**
@@ -0,0 +1,98 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it } from "vitest";
3
+ import { Index } from "../indexes/index-type.js";
4
+ import { System } from "./system.js";
5
+
6
+ const position = Symbol("describeTest/position");
7
+ const cell = Symbol("describeTest/cell");
8
+ const speed = Symbol("describeTest/speed");
9
+ const hover = Symbol("describeTest/hover");
10
+
11
+ declare module "@adobe/ecs" {
12
+ export interface Components {
13
+ [position]: { type: number };
14
+ [cell]: { type: number };
15
+ }
16
+ export interface Resources {
17
+ [speed]: { type: number };
18
+ [hover]: { type: number };
19
+ }
20
+ }
21
+
22
+ const byCell = Index.create({ key: [cell] });
23
+
24
+ const spawnCells = System.create(({ commands }) => { commands.spawn([position])({ [position]: 1 }); });
25
+ const move = System.create({ components: { with: [position] }, after: [spawnCells] }, (row, { resources }) => { row[position] += resources[speed]; });
26
+ const deriveCells = System.create({ components: { with: [position], not: [cell] }, after: [move] }, (row, { commands }) => { commands.add(row.entity, cell, row[position]); });
27
+ const pick = System.create({ lookup: { cells: { index: byCell } }, after: [deriveCells] }, ({ lookup, resources }) => { resources[hover] = lookup.cells(1).length; });
28
+ const nudge = System.create({ components: { with: [position], whole: true } }, row => { Reflect.set(row, position, 0); });
29
+ const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
30
+ const frame = System.create([spawnCells, move, deriveCells, pick, watch]);
31
+
32
+ describe("System.describe", () => {
33
+ it("gives tiers in run order, readonly systems apart, and what each system uses, as data", () => {
34
+ const described = System.describe(frame);
35
+ expect(described.tiers.map(tier => tier.systems.map(system => system.name))).toEqual([["spawnCells"], ["move"], ["deriveCells"], ["pick"]]);
36
+ expect(described.readonly.map(system => system.system)).toEqual([watch]);
37
+ const [[spawning], [moving], [deriving], [picking]] = described.tiers.map(tier => tier.systems);
38
+ expect(spawning?.commands).toEqual({ spawn: [position], add: [], remove: [], despawn: false });
39
+ expect([moving?.components.write, moving?.resources.read, moving?.commands?.add]).toEqual([[position], [speed], []]);
40
+ expect([deriving?.components.read, deriving?.components.not, deriving?.commands?.add]).toEqual([[position], [cell], [cell]]);
41
+ expect([picking?.lookups, picking?.resources.write]).toEqual([[{ name: "cells", index: "byCell", read: [] }], [hover]]);
42
+ expect(moving?.after).toEqual([{ system: spawnCells, name: "spawnCells", because: "after", conflicts: [] }]);
43
+ expect(deriving?.after).toEqual([{ system: move, name: "move", because: "after", conflicts: [position] }]);
44
+ expect(System.describe(nudge).tiers[0]?.systems[0]?.whole).toBe(true);
45
+ });
46
+ it("gives the fewest sync points between tiers whose commands a later system depends on, and why", () => {
47
+ expect(System.describe(frame).tiers.map(tier => tier.sync)).toEqual([
48
+ [{ from: spawnCells, to: move, keys: [position] }, { from: spawnCells, to: deriveCells, keys: [position] }],
49
+ null,
50
+ [{ from: deriveCells, to: pick, keys: [cell] }],
51
+ null,
52
+ ]);
53
+ // Despawning may change anything a later system matches; writing values needs no sync point.
54
+ const clear = System.create({ components: { with: [cell] } }, (row, { commands }) => { commands.despawn(row.entity); });
55
+ const count = System.create({ components: { with: [position] }, after: [clear] }, row => { void row[position]; });
56
+ const tally = System.create({ after: [count] }, ({ resources }) => { resources[hover] = resources[speed]; });
57
+ expect(System.describe(System.create([clear, count, tally])).tiers.map(tier => tier.sync)).toEqual([[{ from: clear, to: count, keys: [] }], null, null]);
58
+ });
59
+ it("lists no conflict on a key two systems reach through rows that never match one entity", () => {
60
+ const placeCells = System.create({ components: { with: [position, cell] } }, row => { row[cell] = row[position]; });
61
+ const clearCells = System.create({ components: { with: [cell], not: [position] }, after: [placeCells] }, row => { row[cell] = 0; });
62
+ expect(System.describe(System.create([placeCells, clearCells])).tiers[1]?.systems[0]?.after).toEqual([{ system: placeCells, name: "placeCells", because: "after", conflicts: [] }]);
63
+ });
64
+ it("says a system follows another by a resource one lists in before or after", () => {
65
+ const setSpeed = System.create({ before: [speed] }, ({ resources }) => { resources[speed] = 2; });
66
+ const readSpeed = System.create({ components: { with: [position] } }, (row, { resources }) => { row[position] = resources[speed]; });
67
+ const readHover = System.create({ after: [hover] }, ({ resources }) => resources[hover] + resources[speed]);
68
+ const setHover = System.create({ after: [setSpeed] }, ({ resources }) => { resources[hover] = 1; });
69
+ const described = System.describe(System.create([readHover, readSpeed, setHover, setSpeed]));
70
+ expect(described.tiers.map(tier => tier.systems.map(system => [system.name, system.after.map(placement => [placement.name, placement.because])]))).toEqual([
71
+ [["setSpeed", []]],
72
+ [["readSpeed", [["setSpeed", "before"]]], ["setHover", [["setSpeed", "after"]]]],
73
+ [["readHover", [["setHover", "after"]]]],
74
+ ]);
75
+ // @ts-expect-error ordering takes resources, not components
76
+ expect(() => System.create({ after: [position] }, () => {})).toThrow(`uses resource "describeTest/position", which isn't a registered resource`);
77
+ });
78
+ it("gives a text form to read", () => {
79
+ expect(System.describe(frame).text).toBe([
80
+ "tier 0 spawnCells commands spawn describeTest/position",
81
+ "sync spawnCells's commands of describeTest/position, for move",
82
+ " spawnCells's commands of describeTest/position, for deriveCells",
83
+ "tier 1 move writes describeTest/position",
84
+ " resources reads describeTest/speed",
85
+ " after spawnCells (after)",
86
+ "tier 2 deriveCells reads describeTest/position",
87
+ " requires not describeTest/cell",
88
+ " commands add describeTest/cell",
89
+ " after move (after: describeTest/position)",
90
+ "sync deriveCells's commands of describeTest/cell, for pick",
91
+ "tier 3 pick resources writes describeTest/hover",
92
+ " looks up cells through byCell",
93
+ " after deriveCells (after)",
94
+ "readonly",
95
+ " watch reads describeTest/position",
96
+ ].join("\n"));
97
+ });
98
+ });
@@ -0,0 +1,122 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { indexDefinitionOf } from '../registry/indexes.js';
3
+ import { labelOf } from '../registry/names.js';
4
+ import { requireDefinition } from '../registry/program.js';
5
+ import { isLeaf, placementsOf, scheduleOf, type Placement } from '../registry/schedule.js';
6
+ import type { Leaf } from './leaf.js';
7
+ import type { System } from './system.js';
8
+
9
+ type Key = symbol;
10
+
11
+ const none: readonly never[] = [];
12
+
13
+ /** A leaf as the system value it is. */
14
+ const asSystem = (leaf: Leaf): System<unknown, unknown> => leaf;
15
+
16
+ /** What the build inferred one system uses, and what placed it, as plain data. */
17
+ const describeLeaf = (leaf: Leaf, placements: readonly Placement[]) => {
18
+ const { components, resources, lookup, up, order } = leaf.declaration;
19
+ const commands = leaf.commands();
20
+ return {
21
+ /** The system, the value its constructor returned. */
22
+ system: asSystem(leaf),
23
+ name: leaf.name,
24
+ components: {
25
+ read: [...components?.read ?? none, ...up === undefined ? none : [up.through]],
26
+ write: components?.write ?? none,
27
+ /** What it requires without reading: `with` members it doesn't use, `added` and `changed` filters' among them. */
28
+ has: components?.has ?? none,
29
+ optional: components?.optional ?? none,
30
+ not: components?.not ?? none,
31
+ added: components?.added ?? none,
32
+ changed: components?.changed ?? none,
33
+ /** What it reads of each row's parent, through `up`. */
34
+ parent: up?.read ?? none,
35
+ },
36
+ resources: {
37
+ read: resources?.read ?? none,
38
+ write: resources?.write ?? none,
39
+ reduce: resources?.reduce ?? none,
40
+ changed: resources?.changed ?? none,
41
+ is: resources?.is ?? none,
42
+ not: resources?.not ?? none,
43
+ },
44
+ lookups: Object.entries(lookup ?? {}).map(([name, used]) => ({
45
+ name,
46
+ /** The label of the index it looks up through, or null for a lookup by entity. */
47
+ index: "index" in used && used.index !== undefined ? indexDefinitionOf(used.index)?.name ?? null : null,
48
+ read: used.read ?? none,
49
+ })),
50
+ /** The label of the sorted index ordering its rows, or null. */
51
+ order: indexDefinitionOf(order)?.name ?? null,
52
+ /** What its commands name by kind, and whether it despawns; null where the build couldn't follow them. */
53
+ commands: commands === null ? null : {
54
+ spawn: commands?.spawn ?? none,
55
+ add: commands?.add ?? none,
56
+ remove: commands?.remove ?? none,
57
+ despawn: commands?.despawn === true,
58
+ },
59
+ /** Whether it takes its row whole (`components: { whole: true }`), every member written. */
60
+ whole: components?.whole === true,
61
+ /** The systems it follows directly, with what ordered it after each and the keys that need that order. */
62
+ after: placements.map(placement => ({ system: asSystem(placement.leaf), name: placement.leaf.name, because: placement.because, conflicts: placement.conflicts })),
63
+ };
64
+ };
65
+
66
+ type Described = ReturnType<typeof describeLeaf>;
67
+
68
+ const keys = (list: readonly Key[]): string => list.map(labelOf).join(" ");
69
+
70
+ /** A system's lines of text, after its name in a column of `width`. */
71
+ const linesOf = (system: Described, width: number): readonly string[] => {
72
+ const lines: string[] = [];
73
+ const add = (label: string, text: string): void => { if (text !== "") lines.push(`${label.padEnd(10)}${text}`); };
74
+ const { components, resources, commands } = system;
75
+ add("reads", keys(components.read));
76
+ add("writes", `${keys(components.write)}${system.whole ? " (its whole row)" : ""}`);
77
+ add("requires", [keys(components.has), components.not.length === 0 ? "" : `not ${keys(components.not)}`, components.optional.length === 0 ? "" : `optional ${keys(components.optional)}`].filter(text => text !== "").join(", "));
78
+ add("filters", [...components.added.map(key => `added ${labelOf(key)}`), ...components.changed.map(key => `changed ${labelOf(key)}`), ...resources.changed.map(key => `changed ${labelOf(key)}`)].join(", "));
79
+ add("parent", keys(components.parent));
80
+ add("resources", [resources.read.length === 0 ? "" : `reads ${keys(resources.read)}`, resources.write.length === 0 ? "" : `writes ${keys(resources.write)}`,
81
+ resources.reduce.length === 0 ? "" : `reduces ${keys(resources.reduce)}`, resources.is.length === 0 ? "" : `while ${keys(resources.is)}`,
82
+ resources.not.length === 0 ? "" : `unless ${keys(resources.not)}`].filter(text => text !== "").join(", "));
83
+ for (const lookup of system.lookups) add("looks up", `${lookup.name}${lookup.index === null ? "" : ` through ${lookup.index}`}${lookup.read.length === 0 ? "" : `: ${keys(lookup.read)}`}`);
84
+ if (system.order !== null) add("ordered", `by ${system.order}`);
85
+ if (commands === null) add("commands", "unknown: the build couldn't follow them");
86
+ else add("commands", [commands.spawn.length === 0 ? "" : `spawn ${keys(commands.spawn)}`, commands.add.length === 0 ? "" : `add ${keys(commands.add)}`,
87
+ commands.remove.length === 0 ? "" : `remove ${keys(commands.remove)}`, commands.despawn ? "despawn" : ""].filter(text => text !== "").join(", "));
88
+ add("after", system.after.map(placement => `${placement.name} (${placement.because}${placement.conflicts.length === 0 ? "" : `: ${keys(placement.conflicts)}`})`).join(", "));
89
+ const indent = " ".repeat(width);
90
+ return lines.length === 0 ? [system.name] : lines.map((line, i) => `${i === 0 ? system.name.padEnd(width) : indent}${line}`);
91
+ };
92
+
93
+ /**
94
+ * Describes how a system or composite runs: its tiers in run order, systems in one tier having no ordering path between
95
+ * them, each with the sync point after it, if any, and its readonly systems apart, which run after its last sync
96
+ * point. For each system, what the build inferred it reads, writes, looks up and commands, whether it takes its row
97
+ * whole, and the systems it follows directly, with what ordered it after each. `text` gives it all to read; the rest
98
+ * is data to assert on.
99
+ */
100
+ export const describe = <I, R>(system: System<I, R>) => {
101
+ const definition = requireDefinition(system);
102
+ const schedule = scheduleOf(definition);
103
+ const placements = placementsOf(definition);
104
+ const tiers = schedule.tiers.map((tier, t) => ({
105
+ systems: tier.map(leaf => describeLeaf(leaf, placements.get(leaf) ?? none)),
106
+ /**
107
+ * Why a sync point follows the tier, applying the commands so far: each system whose commands may change what a
108
+ * later one that follows it uses, through `keys`, none listed where it despawns or a cascade could; null for none.
109
+ */
110
+ sync: schedule.syncs[t]?.map(reason => ({ from: asSystem(reason.from), to: asSystem(reason.to), keys: reason.keys })) ?? null,
111
+ }));
112
+ const readonly = schedule.observer.flatMap(member => isLeaf(member) ? [describeLeaf(member, none)] : []);
113
+ const width = Math.max(0, ...[...tiers.flatMap(tier => tier.systems), ...readonly].map(described => described.name.length)) + 2;
114
+ const text = [
115
+ ...tiers.flatMap((tier, t) => [
116
+ ...tier.systems.flatMap((described, i) => linesOf(described, width).map((line, l) => `${(i === 0 && l === 0 ? `tier ${t}` : "").padEnd(8)}${line}`)),
117
+ ...(tier.sync ?? none).map((reason, r) => `${(r === 0 ? "sync" : "").padEnd(8)}${reason.from.name}'s commands${reason.keys.length === 0 ? "" : ` of ${keys(reason.keys)}`}, for ${reason.to.name}`),
118
+ ]),
119
+ ...readonly.length === 0 ? none : ["readonly", ...readonly.flatMap(described => linesOf(described, width).map(line => `${"".padEnd(8)}${line}`))],
120
+ ].map(line => line.trimEnd()).join("\n");
121
+ return { tiers, readonly, text };
122
+ };
@@ -1,4 +1,5 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export type { Commands } from './commands.js';
3
3
  export * from './create.js';
4
+ export { describe } from './describe.js';
4
5
  export { render, type RenderContext, type RenderTable } from './render.js';
@@ -1,5 +1,6 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Schema } from '../schema/schema.js';
3
+ import type { CommandLists } from './access.js';
3
4
  import type { Resolved } from './declaration.js';
4
5
  import type { Name } from './name.js';
5
6
 
@@ -17,4 +18,10 @@ export type Leaf = {
17
18
  readonly declaration: Resolved;
18
19
  readonly input: InputSchema | undefined;
19
20
  readonly body: (...parameters: never[]) => void;
21
+ /**
22
+ * What its body's commands name, as the build inferred them, read at first need: null where it couldn't follow
23
+ * them, undefined for none. Apart from its declaration, so a module evaluated again with other commands declares the
24
+ * same system.
25
+ */
26
+ readonly commands: () => CommandLists | null | undefined;
20
27
  };
@@ -11,7 +11,7 @@ import type { Body } from './body.js';
11
11
  import type { ReturnsNoPromise } from './input.js';
12
12
  import { accessorsOf } from '../gpu/accessors.js';
13
13
  import { checkLeaf } from './check.js';
14
- import { accessOf, compact, resourceUses } from './resolve.js';
14
+ import { accessOf, compact, noCommands, resourceUses } from './resolve.js';
15
15
  import type { Leaf } from './leaf.js';
16
16
  import type { StructComponent } from './struct.js';
17
17
  import type { Shared } from '../component/value.js';
@@ -96,6 +96,7 @@ export const render = <const D extends RenderDeclaration, B extends Body<D>>(
96
96
  },
97
97
  input: undefined,
98
98
  body,
99
+ commands: noCommands,
99
100
  };
100
101
  if (uses.write.length > 0 || uses.reduce.length > 0) throw new Error(`${what} writes or reduces into resources; a render system only reads.`);
101
102
  checkLeaf(system);
@@ -1,7 +1,7 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import { describe, expect, it } from "vitest";
3
3
  import "../registry/declarations.js";
4
- import { componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
4
+ import { commandListsOf, componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
5
5
 
6
6
  const mass = Symbol("resolveTest/mass");
7
7
  const pool = Symbol("resolveTest/pool");
@@ -47,3 +47,19 @@ describe("resourceUses", () => {
47
47
  expect(() => resourceUses("System \"s\"", changing(pool), undefined)).not.toThrow();
48
48
  });
49
49
  });
50
+
51
+ describe("commandListsOf", () => {
52
+ const commanding = (commands: () => unknown): Access => ({ components: null, resources: null, lookup: null, up: null, commands, at: "a test" });
53
+ it("reads what the commands name at the first call, once", () => {
54
+ let reads = 0;
55
+ const lists = commandListsOf("System \"s\"", commanding(() => { reads++; return { spawn: [[mass, position]], add: [mass], remove: [], despawn: false }; }));
56
+ expect(reads).toBe(0);
57
+ expect([lists(), lists(), reads]).toEqual([{ spawn: [mass, position], add: [mass] }, { spawn: [mass, position], add: [mass] }, 1]);
58
+ });
59
+ it("names the system and the declaration its commands name before it's initialized", () => {
60
+ for (const message of ["Cannot access 'standard' before initialization", "can't access lexical declaration 'standard' before initialization"]) {
61
+ const lists = commandListsOf("System \"s\"", commanding(() => { throw new ReferenceError(message); }));
62
+ expect(lists).toThrow(`System "s" names "standard" in its commands before it's initialized: declare it above the system.`);
63
+ }
64
+ });
65
+ });
@@ -3,7 +3,7 @@ import { takeAccessOf } from '../registry/ids.js';
3
3
  import { indexDefinitionOf } from '../registry/indexes.js';
4
4
  import { declarationOf, labelOf } from '../registry/names.js';
5
5
  import { isString, isStruct } from '../registry/schemas.js';
6
- import type { ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
6
+ import type { CommandLists, ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
7
7
 
8
8
  type Key = symbol;
9
9
 
@@ -23,11 +23,26 @@ export type Access = {
23
23
  readonly up: readonly Key[] | null;
24
24
  /** The resources the predicates of its declaration's `where` read; absent without predicates. */
25
25
  readonly where?: readonly Key[] | null;
26
+ /**
27
+ * What its commands name, keys or lists of them, by kind, and whether it despawns, or a function giving that; absent
28
+ * where no build said.
29
+ */
30
+ readonly commands?: unknown;
26
31
  readonly at: string;
27
32
  };
28
33
 
34
+ type Named = readonly (Key | readonly Key[])[];
35
+ type Commanded = { readonly spawn: Named; readonly add: Named; readonly remove: Named; readonly despawn: boolean };
36
+
29
37
  const isKeys = (value: unknown): value is readonly Key[] => Array.isArray(value) && value.every(key => typeof key === "symbol");
30
38
 
39
+ const isNamed = (value: unknown): value is Named => Array.isArray(value) && value.every(item => typeof item === "symbol" || isKeys(item));
40
+
41
+ /** A binding a command named that holds neither a key nor a list of them isn't followed. */
42
+ const isCommanded = (value: unknown): value is Commanded => typeof value === "object" && value !== null
43
+ && isNamed(Reflect.get(value, "spawn")) && isNamed(Reflect.get(value, "add")) && isNamed(Reflect.get(value, "remove"))
44
+ && typeof Reflect.get(value, "despawn") === "boolean";
45
+
31
46
  const isUsed = (value: unknown): value is Used => typeof value === "object" && value !== null
32
47
  && isKeys(Reflect.get(value, "read")) && isKeys(Reflect.get(value, "write"))
33
48
  && (Reflect.get(value, "mutate") === undefined || isKeys(Reflect.get(value, "mutate")));
@@ -106,7 +121,7 @@ export const componentLists = (components: ComponentAccess, used: Used | null, r
106
121
  : used.write;
107
122
  const read = used === null ? without(held, write) : without(used.read, write);
108
123
  const has = [...without(members, read, write), ...without(filtered, read, write)];
109
- return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads });
124
+ return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads, whole: components.whole });
110
125
  };
111
126
 
112
127
  /**
@@ -161,5 +176,48 @@ export const whereReadsOf = (what: string, components: ComponentAccess | undefin
161
176
  return reads;
162
177
  };
163
178
 
179
+ /**
180
+ * What a body's commands name, as the world takes it, read at the first call and kept: null where the build couldn't
181
+ * follow them, nothing for none. The build gives them as a function, so a list declared below the system is read once
182
+ * its module has loaded; one still uninitialized then, as an import cycle leaves it, throws naming it.
183
+ */
184
+ export const commandListsOf = (what: string, access: Access): (() => CommandLists | null | undefined) => {
185
+ let read = false;
186
+ let lists: CommandLists | null | undefined;
187
+ return () => {
188
+ if (!read) {
189
+ lists = listsOf(what, access.commands);
190
+ read = true;
191
+ }
192
+ return lists;
193
+ };
194
+ };
195
+
196
+ /** The command lists of a system whose body issues none. */
197
+ export const noCommands = (): undefined => undefined;
198
+
199
+ const listsOf = (what: string, given: unknown): CommandLists | null | undefined => {
200
+ const commands = typeof given === "function" ? evaluated(what, () => Reflect.apply(given, undefined, [])) : given;
201
+ if (commands === undefined) return undefined;
202
+ if (!isCommanded(commands)) return null;
203
+ if (!commands.despawn && commands.spawn.length + commands.add.length + commands.remove.length === 0) return undefined;
204
+ const flat = (named: Named): Key[] => [...new Set(named.flat())];
205
+ const lists = compact({ spawn: flat(commands.spawn), add: flat(commands.add), remove: flat(commands.remove), despawn: commands.despawn ? true as const : undefined });
206
+ return Object.keys(lists).length === 0 ? undefined : lists;
207
+ };
208
+
209
+ /** What the build's function of a body's commands gives. */
210
+ const evaluated = (what: string, commands: () => unknown): unknown => {
211
+ try {
212
+ return commands();
213
+ } catch (error) {
214
+ if (!(error instanceof ReferenceError)) throw error;
215
+ // Engines quote the binding in the message: "Cannot access 'standard' before initialization", or "can't access
216
+ // lexical declaration 'standard' before initialization".
217
+ const binding = /'([A-Za-z_$][\w$]*)'/.exec(error.message)?.[1];
218
+ throw new Error(`${what} names ${binding === undefined ? "a declaration" : `"${binding}"`} in its commands before it's initialized: declare it above the system.`, { cause: error });
219
+ }
220
+ };
221
+
164
222
  /** What a body reads of its parent's values. */
165
223
  export const upReads = (what: string, access: Access): readonly Key[] => followed(what, access.up, "its parent's values", access);
@@ -0,0 +1,5 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Shared } from '../component/value.js';
3
+
4
+ /** A `where`'s keys that aren't shared components, each an error: `where` names only shared components. */
5
+ export type SharedOnly<W> = { readonly [K in Exclude<keyof W, Shared>]: { readonly "error: where names only shared components, which each archetype holds one value of": K } };
@@ -154,6 +154,12 @@ System.create({
154
154
  // @ts-expect-error ordering takes systems
155
155
  after: ["test/unregistered"],
156
156
  }, () => {});
157
+ // Ordering by a resource: before or after every other system of the composite that uses it.
158
+ System.create({ before: [time], after: [integrate, score] }, ({ resources }) => { resources[time] = resources[score]; });
159
+ System.create({
160
+ // @ts-expect-error ordering takes systems and resources, not components
161
+ after: [position],
162
+ }, () => {});
157
163
 
158
164
  // @ts-expect-error a system with input can't use change filters
159
165
  System.create({ components: { changed: [position] } }, (_row, _context, input: { readonly distance: number }) => { void input; });
@@ -1,16 +1,22 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Component } from '../component/component.js';
3
+ import type { Shared } from '../component/value.js';
3
4
  import type { Resource } from '../resource/resource.js';
4
5
  import type { Table } from '../world/table/table.js';
5
6
 
6
7
  /**
7
- * What a readonly system's `tables` reads: of every archetype matching `with` and `not` that holds any of
8
+ * What a readonly system's `tables` reads: of every archetype matching `with`, `not` and `where` that holds any of
8
9
  * `components`, a table of those it holds; and, with `resources`, each resource's value.
9
10
  */
10
11
  export type TablesDeclaration = {
11
12
  readonly components: readonly Component.Key[];
12
13
  readonly with?: readonly Component.Key[];
13
14
  readonly not?: readonly Component.Key[];
15
+ /**
16
+ * Keeps the tables to the archetypes holding these values of shared components, each one required as `with`
17
+ * requires it, as a system's `where` of values keeps its system to them.
18
+ */
19
+ readonly where?: { readonly [K in Shared]?: Component<K> };
14
20
  readonly resources?: readonly Resource.Key[];
15
21
  };
16
22
 
@@ -62,7 +62,17 @@ void ecs.Report;
62
62
  // @ts-expect-error no run numbers
63
63
  const numbered: Promise<number> = world.run(main);
64
64
 
65
- void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, nothing, numbered, time];
65
+ // Asked for its changes, after its input, a run resolves to them, in the shape changes finalizers take.
66
+ const changes = world.run(move, { distance: 1 }, { changes: true });
67
+ const alone = world.run(main, { changes: true });
68
+ check<Equal<typeof changes, typeof alone>>();
69
+ check<Equal<Awaited<typeof alone>["resources"][number]["key"], Ecs.Resource.Key>>();
70
+ // @ts-expect-error its input comes first
71
+ void world.run(move, { changes: true }, { distance: 1 });
72
+ // @ts-expect-error a run asks only for its changes
73
+ void world.run(main, { changes: false });
74
+
75
+ void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, nothing, numbered, time, changes];
66
76
 
67
77
  // A body returns no promise: it starts the work, and a later run finishes it.
68
78
  // @ts-expect-error a body returns no promise
@@ -13,7 +13,8 @@ export type Column =
13
13
  | { readonly kind: "value"; readonly value: unknown };
14
14
 
15
15
  /**
16
- * What `commands.spawnTables` spawned: each table's new entities in its row order, `Entity.none` for a row it didn't
17
- * spawn, and how many rows it didn't, those owned by an entity outside the tables.
16
+ * What `commands.spawnTables` spawned: each table's entities in its row order, the one spawned for each row, the one
17
+ * existing for a row its `existing` maps, and `Entity.none` for a row it didn't spawn; and how many rows it didn't,
18
+ * those owned by an entity outside the tables.
18
19
  */
19
20
  export type Spawned = { readonly entities: readonly Float64Array[]; readonly dropped: number };
@@ -1,6 +1,9 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Input } from '../system/input.js';
3
3
  import type { System } from '../system/system.js';
4
+ import type { ChangeSet } from '../finalizer/body.js';
5
+ import type { Essential } from '../component/value.js';
6
+ import type { Resource } from '../resource/resource.js';
4
7
 
5
8
  /**
6
9
  * A world. Systems read and change it; readonly systems read it after each run's and tick's sync point. Every member's
@@ -23,6 +26,16 @@ export type World = {
23
26
  * added or GPU work, is reported out of band, as ticks' failures are (see `add`).
24
27
  */
25
28
  run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
29
+ /**
30
+ * Runs as `run` does, and resolves to this run's own changes, in the shape `Finalizer.changes` gives, rather than its
31
+ * value: the structural changes its commands made, rules and cascades included, to every component that isn't
32
+ * derived or an object a joined thread fills, the values its adds gave components entities had before and after, and
33
+ * each resource its systems write or reduce into whose value changed, compared as `Finalizer.changes` compares them;
34
+ * `previous` and `next` give values before and after the run. Commands the finalizers of an earlier sync point
35
+ * queued apply first, at a sync point of their own. Values written to rows aren't structural changes, so they're in
36
+ * none. The run pays for this only when asked. Only on the world's thread: a joined thread's rejects.
37
+ */
38
+ run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
26
39
  /**
27
40
  * Adds a system to every tick, from the next one on, ordered with those already added by its constraints, as a
28
41
  * composite's members are. A tick runs on a beat of the `cadence` resource where `always` is set, `wake` is due or