@typeonce/ritmo 0.6.0 → 0.8.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 (148) hide show
  1. package/README.md +2 -0
  2. package/dist/Brand.d.ts +9 -4
  3. package/dist/Brand.d.ts.map +1 -1
  4. package/dist/Brand.js +9 -4
  5. package/dist/Brand.js.map +1 -1
  6. package/dist/Clock.d.ts +186 -0
  7. package/dist/Clock.d.ts.map +1 -0
  8. package/dist/Clock.js +111 -0
  9. package/dist/Clock.js.map +1 -0
  10. package/dist/Command.d.ts +250 -41
  11. package/dist/Command.d.ts.map +1 -1
  12. package/dist/Command.js +59 -23
  13. package/dist/Command.js.map +1 -1
  14. package/dist/Condition.d.ts +12 -1
  15. package/dist/Condition.d.ts.map +1 -1
  16. package/dist/Condition.js.map +1 -1
  17. package/dist/Debug.d.ts +163 -15
  18. package/dist/Debug.d.ts.map +1 -1
  19. package/dist/Debug.js.map +1 -1
  20. package/dist/Decode.d.ts +1 -1
  21. package/dist/Decode.js +1 -1
  22. package/dist/Descriptor.d.ts +67 -24
  23. package/dist/Descriptor.d.ts.map +1 -1
  24. package/dist/Descriptor.js +32 -15
  25. package/dist/Descriptor.js.map +1 -1
  26. package/dist/Entity.d.ts +22 -5
  27. package/dist/Entity.d.ts.map +1 -1
  28. package/dist/Entity.js +2 -2
  29. package/dist/Entity.js.map +1 -1
  30. package/dist/EntityScope.d.ts +26 -1
  31. package/dist/EntityScope.d.ts.map +1 -1
  32. package/dist/EntityScope.js +14 -2
  33. package/dist/EntityScope.js.map +1 -1
  34. package/dist/HostClock.d.ts +49 -0
  35. package/dist/HostClock.d.ts.map +1 -0
  36. package/dist/HostClock.js +46 -0
  37. package/dist/HostClock.js.map +1 -0
  38. package/dist/Inspector.d.ts +7 -1
  39. package/dist/Inspector.d.ts.map +1 -1
  40. package/dist/Inspector.js.map +1 -1
  41. package/dist/Machine.d.ts +67 -36
  42. package/dist/Machine.d.ts.map +1 -1
  43. package/dist/Machine.js +10 -3
  44. package/dist/Machine.js.map +1 -1
  45. package/dist/Query.d.ts +74 -26
  46. package/dist/Query.d.ts.map +1 -1
  47. package/dist/Query.js +32 -28
  48. package/dist/Query.js.map +1 -1
  49. package/dist/Runtime.d.ts +39 -16
  50. package/dist/Runtime.d.ts.map +1 -1
  51. package/dist/Runtime.js +556 -87
  52. package/dist/Runtime.js.map +1 -1
  53. package/dist/Schedule.d.ts +217 -10
  54. package/dist/Schedule.d.ts.map +1 -1
  55. package/dist/Schedule.js +5 -0
  56. package/dist/Schedule.js.map +1 -1
  57. package/dist/Schema.d.ts +196 -44
  58. package/dist/Schema.d.ts.map +1 -1
  59. package/dist/Schema.js +29 -11
  60. package/dist/Schema.js.map +1 -1
  61. package/dist/Snapshot.d.ts +14 -0
  62. package/dist/Snapshot.d.ts.map +1 -1
  63. package/dist/Snapshot.js.map +1 -1
  64. package/dist/System.d.ts +212 -58
  65. package/dist/System.d.ts.map +1 -1
  66. package/dist/System.js +84 -6
  67. package/dist/System.js.map +1 -1
  68. package/dist/index.d.ts +8 -7
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +8 -7
  71. package/dist/index.js.map +1 -1
  72. package/dist/internal/cells.d.ts +3 -3
  73. package/dist/internal/cells.d.ts.map +1 -1
  74. package/dist/internal/cells.js +9 -11
  75. package/dist/internal/cells.js.map +1 -1
  76. package/dist/internal/debug.d.ts +13 -0
  77. package/dist/internal/debug.d.ts.map +1 -1
  78. package/dist/internal/debug.js +192 -7
  79. package/dist/internal/debug.js.map +1 -1
  80. package/dist/internal/equal.d.ts +8 -0
  81. package/dist/internal/equal.d.ts.map +1 -0
  82. package/dist/internal/equal.js +38 -0
  83. package/dist/internal/equal.js.map +1 -0
  84. package/dist/internal/fragments.d.ts.map +1 -1
  85. package/dist/internal/fragments.js +11 -1
  86. package/dist/internal/fragments.js.map +1 -1
  87. package/dist/internal/game.d.ts.map +1 -1
  88. package/dist/internal/game.js +41 -6
  89. package/dist/internal/game.js.map +1 -1
  90. package/dist/internal/order.d.ts +10 -0
  91. package/dist/internal/order.d.ts.map +1 -0
  92. package/dist/internal/order.js +122 -0
  93. package/dist/internal/order.js.map +1 -0
  94. package/dist/internal/queries.d.ts.map +1 -1
  95. package/dist/internal/queries.js +32 -10
  96. package/dist/internal/queries.js.map +1 -1
  97. package/dist/internal/requirements.d.ts +26 -0
  98. package/dist/internal/requirements.d.ts.map +1 -0
  99. package/dist/internal/requirements.js +46 -0
  100. package/dist/internal/requirements.js.map +1 -0
  101. package/package.json +1 -1
  102. package/src/Brand.ts +9 -4
  103. package/src/Clock.ts +311 -0
  104. package/src/Command.ts +407 -89
  105. package/src/Condition.ts +17 -1
  106. package/src/Debug.ts +172 -14
  107. package/src/Decode.ts +1 -1
  108. package/src/Descriptor.ts +92 -37
  109. package/src/Entity.ts +24 -5
  110. package/src/EntityScope.ts +27 -2
  111. package/src/HostClock.ts +64 -0
  112. package/src/Inspector.ts +7 -2
  113. package/src/Machine.ts +81 -37
  114. package/src/Query.ts +110 -38
  115. package/src/Runtime.ts +607 -111
  116. package/src/Schedule.ts +380 -13
  117. package/src/Schema.ts +249 -63
  118. package/src/Snapshot.ts +4 -0
  119. package/src/System.ts +248 -93
  120. package/src/index.ts +8 -7
  121. package/src/internal/cells.ts +10 -12
  122. package/src/internal/debug.ts +208 -12
  123. package/src/internal/equal.ts +31 -0
  124. package/src/internal/fragments.ts +12 -2
  125. package/src/internal/game.ts +61 -7
  126. package/src/internal/order.ts +144 -0
  127. package/src/internal/queries.ts +37 -13
  128. package/src/internal/requirements.ts +71 -0
  129. package/dist/Definition.d.ts +0 -99
  130. package/dist/Definition.d.ts.map +0 -1
  131. package/dist/Definition.js +0 -68
  132. package/dist/Definition.js.map +0 -1
  133. package/dist/Feature.d.ts +0 -106
  134. package/dist/Feature.d.ts.map +0 -1
  135. package/dist/Feature.js +0 -87
  136. package/dist/Feature.js.map +0 -1
  137. package/dist/Fx.d.ts +0 -128
  138. package/dist/Fx.d.ts.map +0 -1
  139. package/dist/Fx.js +0 -112
  140. package/dist/Fx.js.map +0 -1
  141. package/dist/internal/definition.d.ts +0 -17
  142. package/dist/internal/definition.d.ts.map +0 -1
  143. package/dist/internal/definition.js +0 -25
  144. package/dist/internal/definition.js.map +0 -1
  145. package/src/Definition.ts +0 -106
  146. package/src/Feature.ts +0 -241
  147. package/src/Fx.ts +0 -176
  148. package/src/internal/definition.ts +0 -56
package/src/System.ts CHANGED
@@ -57,7 +57,6 @@
57
57
  */
58
58
  import type { Descriptor } from "./Descriptor.ts"
59
59
  import type * as Entity from "./Entity.ts"
60
- import type { Fx } from "./Fx.ts"
61
60
  import * as Machine from "./Machine.ts"
62
61
  import type * as Relation from "./Relation.ts"
63
62
  import type { Query, QueryMatch } from "./Query.ts"
@@ -65,7 +64,7 @@ import type { ConstructedWriteCell, ReadCell, ReadonlyValue, WriteCell } from ".
65
64
  import * as Requirement from "./Requirement.ts"
66
65
  import type * as Result from "./Result.ts"
67
66
  import type { Schema } from "./Schema.ts"
68
- import type { CommandsApi } from "./Command.ts"
67
+ import type { CommandAccess, CommandsApi, NotDeclared } from "./Command.ts"
69
68
 
70
69
  /**
71
70
  * System declarations, typed execution context, and runtime requirements.
@@ -194,6 +193,50 @@ export const writeResource = <D extends Descriptor<"resource", string, any>>(
194
193
  descriptor
195
194
  })
196
195
 
196
+ /**
197
+ * Declares read access to a resource plus whether it changed since this
198
+ * system's previous completed run.
199
+ */
200
+ export type ResourceChanged<D extends Descriptor<"resource", string, any>> = {
201
+ readonly mode: "changed"
202
+ readonly descriptor: D
203
+ }
204
+
205
+ /**
206
+ * Creates a resource read that also reports `changed()`: whether a committed
207
+ * write left a different value since this system's previous completed run.
208
+ *
209
+ * Use it for work worth doing only when a value moved: rewriting a HUD label,
210
+ * rebuilding a cache from settings. Use `readResource` when the system needs
211
+ * the value every run anyway.
212
+ *
213
+ * A write that leaves an equal value is not a change: primitives compare
214
+ * with `Object.is`, plain objects and arrays by their entries, other objects
215
+ * by reference. This differs from `Game.Query.changed`, where every write
216
+ * through a component write cell counts. The first run reports `true`; a
217
+ * failed run (rolled back) is not a change, and a failed reader sees the same
218
+ * change again next run. Its own writes are not reported to the writer.
219
+ *
220
+ * Conditions (`Game.Condition.check`) cannot declare it: they own no cursor.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * const SyncHud = Game.System("SyncHud", {
225
+ * resources: { score: Game.System.changedResource(Score) },
226
+ * services: { hud: Game.System.service(Hud) }
227
+ * }, ({ resources, services }) => {
228
+ * if (!resources.score.changed()) return
229
+ * services.hud.score.textContent = `${resources.score.get()}`
230
+ * })
231
+ * ```
232
+ */
233
+ export const changedResource = <D extends Descriptor<"resource", string, any>>(
234
+ descriptor: D
235
+ ): ResourceChanged<D> => ({
236
+ mode: "changed",
237
+ descriptor
238
+ })
239
+
197
240
  /**
198
241
  * Declares read-only access to an event stream.
199
242
  */
@@ -246,7 +289,7 @@ export const readEvent = <D extends Descriptor<"event", string, any>>(
246
289
  *
247
290
  * If the payload needs to name an entity for later work, emit a durable
248
291
  * `Game.Entity.handle(...)` (optionally with an intent component) and let the later
249
- * reader re-resolve it through `lookup.getHandle(...)`.
292
+ * reader re-resolve it through `queries.x.getHandle(...)` (through a declared query).
250
293
  *
251
294
  * @example
252
295
  * ```ts
@@ -266,6 +309,58 @@ export const writeEvent = <D extends Descriptor<"event", string, any>>(
266
309
  descriptor
267
310
  })
268
311
 
312
+ /** The marker `thisTick(...)` adds to a read. */
313
+ export interface ThisTick {
314
+ readonly thisTick: true
315
+ }
316
+
317
+ /**
318
+ * Requires that every system writing this event or resource runs earlier in
319
+ * the same `runtime.tick(...)` call than the reading system: the reader then
320
+ * sees this tick's data, never last tick's. The library never reorders
321
+ * anything; it only checks.
322
+ *
323
+ * `runtime.tick(...)` checks it at compile time over everything passed to that
324
+ * call, in order (gated groups count by position), and rejects a writer that
325
+ * runs after the reader and a call whose order is not statically known. A
326
+ * read with no writer in the call compiles (a reader reused in several
327
+ * ticks); the `event-never-written` lint reports data nobody writes.
328
+ * `tryTick` checks the same at runtime.
329
+ *
330
+ * Use it on reads whose correctness depends on running after their writers
331
+ * (damage applied the tick it is dealt). A plain read is right when last
332
+ * tick's data is acceptable.
333
+ *
334
+ * On a query (`queries: { enemies: Game.System.thisTick(Enemies) }`), the
335
+ * writers are systems whose declared commands can change what it matches:
336
+ * `spawns` covering every component it requires, or `inserts` / `removes` of
337
+ * any component it names (selection slots, optional ones included, `with`,
338
+ * `without`, filters). Each must run earlier with an `applyDeferred()` (or
339
+ * `applyStateTransitions(...)`) between it and the reader, since commands
340
+ * apply only there. `despawns` never count: a despawned entity stays visible
341
+ * to queries until the next `applyDeferred()`. Nested schedules are
342
+ * flattened, gated groups count by position, and a transition bundle counts
343
+ * at its `applyStateTransitions(...)` step (which applies queued commands
344
+ * first), as if every schedule in it runs, markers inside included.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const ApplyDamage = Game.System("ApplyDamage", {
349
+ * events: { damage: Game.System.thisTick(Game.System.readEvent(Damage)) }
350
+ * }, ({ events }) => { ... })
351
+ *
352
+ * runtime.tick(Game.Schedule(DealDamage, ApplyDamage)) // compiles
353
+ * runtime.tick(Game.Schedule(ApplyDamage, DealDamage)) // "ApplyDamage needs Damage from this tick, but its writer DealDamage runs after it"
354
+ *
355
+ * const AimAtNewest = Game.System("AimAtNewest", { queries: { enemies: Game.System.thisTick(Enemies) } }, ...)
356
+ * runtime.tick(Game.Schedule(SpawnWave, Game.Schedule.applyDeferred(), AimAtNewest)) // compiles
357
+ * runtime.tick(Game.Schedule(SpawnWave, AimAtNewest)) // "AimAtNewest needs this tick's commands for queries.enemies, but SpawnWave queues them with no applyDeferred() in between"
358
+ * ```
359
+ */
360
+ export const thisTick = <
361
+ Read extends EventRead<Descriptor<"event", string, any>> | ResourceRead<Descriptor<"resource", string, any>> | ResourceChanged<Descriptor<"resource", string, any>> | Query.Any<any>
362
+ >(read: Read): Read & ThisTick => ({ ...read, thisTick: true })
363
+
269
364
  /**
270
365
  * Declares read access to the current committed value of a finite-state machine.
271
366
  *
@@ -307,7 +402,7 @@ export const transition = <M extends Machine.StateMachine.Any>(
307
402
  * normal events: each run sees those published since its previous run.
308
403
  *
309
404
  * This is one of the clearest signs that the modeled value should be a machine
310
- * rather than a plain state descriptor.
405
+ * rather than a resource or a `Descriptor.EntityState` component.
311
406
  */
312
407
  export const readTransitionEvent = <M extends Machine.StateMachine.Any>(
313
408
  stateMachine: M
@@ -316,7 +411,10 @@ export const readTransitionEvent = <M extends Machine.StateMachine.Any>(
316
411
  /**
317
412
  * Internal union for all supported resource access declarations.
318
413
  */
319
- type ResourceAccess = ResourceRead<Descriptor<"resource", string, any>> | ResourceWrite<Descriptor<"resource", string, any>>
414
+ type ResourceAccess =
415
+ | ResourceRead<Descriptor<"resource", string, any>>
416
+ | ResourceChanged<Descriptor<"resource", string, any>>
417
+ | ResourceWrite<Descriptor<"resource", string, any>>
320
418
  /**
321
419
  * Internal union for all supported event access declarations.
322
420
  */
@@ -327,6 +425,15 @@ type EventAccess = EventRead<Descriptor<"event", string, any>> | EventWrite<Desc
327
425
  */
328
426
  export interface ResourceReadView<T> extends ReadCell<T> {}
329
427
 
428
+ /**
429
+ * A read-only view over a resource value that also reports whether it
430
+ * changed since this system's previous completed run (`changedResource`).
431
+ */
432
+ export interface ResourceChangedView<T> extends ReadCell<T> {
433
+ /** Whether a committed write left a different value since this system's previous completed run. `true` on the first run. */
434
+ changed(): boolean
435
+ }
436
+
330
437
  /**
331
438
  * A mutable view over a resource value.
332
439
  */
@@ -361,7 +468,8 @@ export interface EventWriteView<T> {
361
468
  * A read-only view over one committed finite-state-machine value.
362
469
  */
363
470
  export interface MachineReadView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
364
- get(): Machine.StateMachine.Value<M>
471
+ /** The committed value; `undefined` only for a sub-state whose parent is in another state. */
472
+ get(): Machine.CurrentValue<M>
365
473
  is(value: Machine.StateMachine.Value<M>): boolean
366
474
  }
367
475
 
@@ -370,20 +478,31 @@ export interface MachineReadView<M extends Machine.StateMachine.Any = Machine.St
370
478
  */
371
479
  export interface NextMachineWriteView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
372
480
  getPending(): Machine.StateMachine.Value<M> | undefined
373
- set(value: Machine.StateMachine.Value<M>): void
374
- setIfChanged(value: Machine.StateMachine.Value<M>): void
481
+ /**
482
+ * Queues `value`, committed at the next `applyStateTransitions(...)`. For a
483
+ * sub-state it returns a `Result` that fails with `SubStateInactive` (and
484
+ * queues nothing) while the parent is in another state.
485
+ */
486
+ set(value: Machine.StateMachine.Value<M>): Machine.SetResult<M>
487
+ /** Like `set`, but committing the current value again is skipped (no exit or enter schedules). */
488
+ setIfChanged(value: Machine.StateMachine.Value<M>): Machine.SetResult<M>
375
489
  reset(): void
376
490
  }
377
491
 
378
492
  /**
379
493
  * A read-only view over the last applied transition payload for one machine.
494
+ *
495
+ * Only meaningful inside transition schedules, so it is only given to
496
+ * systems that declare `transitions: { x: Game.System.transition(M) }`.
380
497
  */
381
498
  export interface TransitionReadView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
382
499
  get(): Machine.TransitionSnapshot<M>
383
500
  }
384
501
 
385
502
  /**
386
- * A read-only event stream of committed machine transitions.
503
+ * A read-only event stream of committed machine transitions, published by
504
+ * `applyStateTransitions(...)`. Like events, each reader sees the
505
+ * transitions published since its own previous run.
387
506
  */
388
507
  export interface TransitionEventReadView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
389
508
  all(): ReadonlyArray<Machine.TransitionSnapshot<M>>
@@ -430,8 +549,9 @@ export interface RelationFailureRead<R extends Relation.Relation.Any> {
430
549
  * ticked less often than the one that removes (rendering after several fixed
431
550
  * updates) still sees every removal. A system skipped by its run conditions
432
551
  * keeps its position, like `added`/`changed`, and sees the removals when it
433
- * runs again. Each log is capped at `Runtime.streamCapacity` entries; drops
434
- * show up as `missed` reads in the debug trace. Systems usually pair this
552
+ * runs again. Each log is capped at `Runtime.streamCapacity` entries; a
553
+ * reader that missed dropped ones sees `lagged() === true` (and a `missed`
554
+ * read in the debug trace). Systems usually pair this
435
555
  * with host cleanup such as removing renderer-owned nodes. {@link readDespawned}
436
556
  * complements this for whole-entity teardown.
437
557
  *
@@ -493,6 +613,14 @@ export const readDespawned = (): DespawnedRead => ({
493
613
  */
494
614
  export interface RemovedReadView<S extends Schema.Any, Root = unknown> {
495
615
  all(): ReadonlyArray<import("./Entity.ts").EntityId<S, Root>>
616
+ /**
617
+ * Whether records published since this system's previous run were dropped
618
+ * before it read them. Records wait for every reading system, so this only
619
+ * happens when a reader has not run for so long that the log exceeded its
620
+ * capacity (see `Runtime.streamCapacity`). Check it where a missed record
621
+ * would leak host state (a renderer node never destroyed).
622
+ */
623
+ lagged(): boolean
496
624
  }
497
625
 
498
626
  /**
@@ -500,6 +628,14 @@ export interface RemovedReadView<S extends Schema.Any, Root = unknown> {
500
628
  */
501
629
  export interface DespawnedReadView<S extends Schema.Any, Root = unknown> {
502
630
  all(): ReadonlyArray<import("./Entity.ts").EntityId<S, Root>>
631
+ /**
632
+ * Whether records published since this system's previous run were dropped
633
+ * before it read them. Records wait for every reading system, so this only
634
+ * happens when a reader has not run for so long that the log exceeded its
635
+ * capacity (see `Runtime.streamCapacity`). Check it where a missed record
636
+ * would leak host state (a renderer node never destroyed).
637
+ */
638
+ lagged(): boolean
503
639
  }
504
640
 
505
641
  /**
@@ -531,8 +667,46 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
531
667
  each(): ReadonlyArray<QueryMatch<S, Q>>
532
668
  /**
533
669
  * Retrieves the match for one specific entity id when it satisfies the query.
670
+ *
671
+ * Every read and write of an entity goes through a query the system
672
+ * declares, so its access is visible in the spec: there is no lookup that
673
+ * takes a query at call time.
534
674
  */
535
675
  get(entityId: import("./Entity.ts").EntityId<S, Query.Root<Q>>): Result.Result<QueryMatch<S, Q>, Query.LookupError>
676
+ /**
677
+ * Resolves a stored durable handle back into this query's access.
678
+ *
679
+ * Use this for handles carried in events, resources, or components. A handle
680
+ * is storage-safe, not proof of liveness, so stale or mismatched handles stay
681
+ * explicit typed failures. A handle with an intent component needs a query
682
+ * that proves it (selects or requires it).
683
+ */
684
+ getHandle<H extends Entity.Handle<Query.Root<Q>, any>>(
685
+ handle: [Entity.Handle.Intent<H>] extends [undefined]
686
+ ? H
687
+ : Query.ProvesComponent<Q, Entity.Handle.Intent<H> & Descriptor<"component", string, any>> extends true
688
+ ? H
689
+ : "this query does not prove the handle's intent component: select it, or add it to with"
690
+ ): Result.Result<QueryMatch<S, Q>, Query.LookupError>
691
+ /**
692
+ * The direct children of one hierarchy parent that match this query, in the
693
+ * stored child order. Children that do not match are skipped; a missing
694
+ * parent is a `MissingEntity` failure.
695
+ */
696
+ children<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
697
+ entityId: Entity.EntityId<S, Query.Root<Q>>,
698
+ relation: R
699
+ ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
700
+ /**
701
+ * The hierarchy descendants of one entity that match this query, in
702
+ * `options.order` (depth-first by default). Descendants that do not match
703
+ * are skipped. `lookup.descendants` returns every descendant's id instead.
704
+ */
705
+ descendants<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
706
+ entityId: Entity.EntityId<S, Query.Root<Q>>,
707
+ relation: R,
708
+ options?: { readonly order?: "breadth" | "depth" }
709
+ ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
536
710
  /**
537
711
  * Returns a single match when exactly one entity satisfies the query.
538
712
  */
@@ -558,40 +732,19 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
558
732
  }
559
733
 
560
734
  /**
561
- * Typed entity lookup API exposed to systems.
735
+ * Relation lookups exposed to systems: entity ids along relation edges.
562
736
  *
563
- * Use this when you already have an entity id and want a validated, typed view
564
- * over a specific component access specification.
737
+ * Lookups return ids only. To read or write the entities they lead to, pass
738
+ * the ids to a declared query (`queries.x.get(id)`), or use the query
739
+ * handle's `children` / `descendants`, so every access stays in the spec.
565
740
  *
566
741
  * All lookup methods are total and explicit:
567
742
  *
568
743
  * - no hidden exceptions
569
- * - stale handles are treated as normal typed failures
744
+ * - missing entities are normal typed failures
570
745
  * - hierarchy traversal is available only for hierarchy relations
571
746
  */
572
747
  export interface LookupApi<S extends Schema.Any, Root = unknown> {
573
- get<Q extends Query.Any<Root>>(
574
- entityId: Entity.EntityId<S, Root>,
575
- query: Q
576
- ): Result.Result<QueryMatch<S, Q>, Query.LookupError>
577
- /**
578
- * Resolves a stored durable handle back into current-world query access.
579
- *
580
- * Use this for handles carried in events, resources, or components. A handle is
581
- * storage-safe, not proof of liveness, so stale or mismatched handles remain
582
- * explicit typed failures.
583
- */
584
- getHandle<
585
- H extends Entity.Handle<Root, any>,
586
- Q extends Query.Any<Root>
587
- >(
588
- handle: [Entity.Handle.Intent<H>] extends [undefined]
589
- ? H
590
- : Query.ProvesComponent<Q, Entity.Handle.Intent<H> & Descriptor<"component", string, any>> extends true
591
- ? H
592
- : never,
593
- query: Q
594
- ): Result.Result<QueryMatch<S, Q>, Query.LookupError>
595
748
  related<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Any>>(
596
749
  entityId: Entity.EntityId<S, Root>,
597
750
  relation: R
@@ -600,21 +753,6 @@ export interface LookupApi<S extends Schema.Any, Root = unknown> {
600
753
  entityId: Entity.EntityId<S, Root>,
601
754
  relation: R
602
755
  ): Result.Result<ReadonlyArray<Entity.EntityId<S, Root>>, Relation.Relation.MissingEntityError>
603
- /**
604
- * Reads the direct children of one hierarchy parent as typed query matches.
605
- *
606
- * This preserves the stored child order for that hierarchy relation and skips
607
- * entities that do not satisfy the query. Missing parents remain explicit
608
- * `MissingEntity` failures.
609
- */
610
- childMatches<
611
- R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>,
612
- Q extends Query.Any<Root>
613
- >(
614
- entityId: Entity.EntityId<S, Root>,
615
- relation: R,
616
- query: Q
617
- ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
618
756
  parent<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
619
757
  entityId: Entity.EntityId<S, Root>,
620
758
  relation: R
@@ -630,24 +768,6 @@ export interface LookupApi<S extends Schema.Any, Root = unknown> {
630
768
  readonly order?: "breadth" | "depth"
631
769
  }
632
770
  ): Result.Result<ReadonlyArray<Entity.EntityId<S, Root>>, Relation.Relation.MissingEntityError>
633
- /**
634
- * Traverses hierarchy descendants and resolves only the entities that match
635
- * the given query.
636
- *
637
- * Traversal order stays explicit through `options.order`, and non-matching
638
- * descendants are skipped without turning traversal into a failure.
639
- */
640
- descendantMatches<
641
- R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>,
642
- Q extends Query.Any<Root>
643
- >(
644
- entityId: Entity.EntityId<S, Root>,
645
- relation: R,
646
- query: Q,
647
- options?: {
648
- readonly order?: "breadth" | "depth"
649
- }
650
- ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
651
771
  root<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
652
772
  entityId: Entity.EntityId<S, Root>,
653
773
  relation: R
@@ -686,6 +806,25 @@ export interface SystemAccessInput {
686
806
  readonly removed?: Record<string, RemovedRead<Descriptor<"component", string, any>>>
687
807
  readonly despawned?: Record<string, DespawnedRead>
688
808
  readonly relationFailures?: Record<string, RelationFailureRead<Relation.Relation.Any>>
809
+ /**
810
+ * The commands the system may queue: `Game.System.commands(...)` with
811
+ * `spawns`, `inserts`, `removes`, `despawns`, `despawnsScope`, and
812
+ * `relates` permissions. Without it the system has no `commands`.
813
+ */
814
+ readonly commands?: CommandAccess.Any
815
+ /**
816
+ * Run conditions that skip the system: while one fails, the system does not
817
+ * run and the events, transition events, and relation failures published
818
+ * meanwhile are dropped for it. Use it for a mode change after which those
819
+ * messages are stale (menu, game over); use `pauseWhen` instead for a freeze
820
+ * whose messages stay valid. `Schedule.when(...)` adds conditions to a whole
821
+ * group.
822
+ *
823
+ * Four gating forms, one rule: `when` drops the events published while
824
+ * gated (the system resumes on fresh messages), `pauseWhen` keeps them (the
825
+ * system resumes and reads the backlog). Put them on one system's spec, or
826
+ * on a group with `Schedule.when(...)` / `Schedule.pauseWhen(...)`.
827
+ */
689
828
  readonly when?: ReadonlyArray<Machine.Condition>
690
829
  /**
691
830
  * Run conditions that pause the system instead of skipping it: while one
@@ -763,6 +902,7 @@ export type SystemSpec<
763
902
  readonly pauseWhen: AccessField<Access, "pauseWhen", readonly []>
764
903
  readonly transitions: AccessField<Access, "transitions", {}>
765
904
  readonly local: LocalFactory<Access>
905
+ readonly commands: "commands" extends keyof Access ? NonNullable<Access["commands"]> : undefined
766
906
  readonly schema: S
767
907
  readonly __schemaRoot: Root
768
908
  }
@@ -775,6 +915,7 @@ export type AnySystemSpec = SystemSpec<any, any, any>
775
915
  type ResourceContext<Spec extends AnySystemSpec> = {
776
916
  readonly [K in keyof Spec["resources"]]:
777
917
  Spec["resources"][K] extends ResourceRead<infer D> ? ResourceReadView<Descriptor.Value<D>>
918
+ : Spec["resources"][K] extends ResourceChanged<infer D> ? ResourceChangedView<Descriptor.Value<D>>
778
919
  : Spec["resources"][K] extends ResourceWrite<infer D> ? ResourceWriteView<D>
779
920
  : never
780
921
  }
@@ -857,6 +998,11 @@ type QueryContext<Spec extends AnySystemSpec> = {
857
998
  type LocalValue<Factory> = Factory extends () => infer Local ? Local : undefined
858
999
  type LocalContext<Spec extends AnySystemSpec> = LocalValue<Spec["local"]>
859
1000
 
1001
+ type CommandsContext<Spec extends AnySystemSpec> =
1002
+ Spec["commands"] extends CommandAccess<infer Spawns, infer Inserts, infer Removes, infer Despawns, infer Scopes, infer Relates>
1003
+ ? CommandsApi<Spec["schema"], Spec["__schemaRoot"], Spawns, Inserts, Removes, Despawns, Scopes, Relates>
1004
+ : NotDeclared
1005
+
860
1006
  /**
861
1007
  * The implementation context derived from a system spec.
862
1008
  *
@@ -876,16 +1022,12 @@ export interface SystemContext<Spec extends AnySystemSpec> {
876
1022
  readonly relationFailures: RelationFailureContext<Spec>
877
1023
  readonly transitions: TransitionContext<Spec>
878
1024
  readonly services: ServiceContext<Spec>
879
- readonly commands: CommandsApi<Spec["schema"], Spec["__schemaRoot"]>
1025
+ /** The commands the spec declares (`commands: Game.System.commands(...)`). */
1026
+ readonly commands: CommandsContext<Spec>
880
1027
  /** The system's own scratch state (see `local` in the spec); `undefined` when it declares none. */
881
1028
  readonly local: LocalContext<Spec>
882
1029
  }
883
1030
 
884
- /**
885
- * The service environment required by a system.
886
- */
887
- export type SystemDependencies<Spec extends AnySystemSpec> = ServiceContext<Spec>
888
-
889
1031
  type DescriptorFromAccess<Access> =
890
1032
  Access extends { readonly descriptor: infer D extends Requirement.Requirement } ? D : never
891
1033
 
@@ -949,7 +1091,7 @@ export interface SystemFailure<
949
1091
 
950
1092
  /** Extracts the normalized failure carried by one system definition. */
951
1093
  export type FailureOf<Value> =
952
- Value extends SystemDefinition<any, any, infer Error, any, infer Name, any>
1094
+ Value extends SystemDefinition<any, infer Error, any, infer Name, any>
953
1095
  ? [Error] extends [never] ? never : SystemFailure<Name, Error>
954
1096
  : never
955
1097
 
@@ -958,7 +1100,6 @@ export type FailureOf<Value> =
958
1100
  */
959
1101
  export interface SystemDefinition<
960
1102
  Spec extends AnySystemSpec,
961
- out A = void,
962
1103
  out E = never,
963
1104
  out Root = unknown,
964
1105
  out Name extends string = string,
@@ -991,32 +1132,46 @@ export interface SystemDefinition<
991
1132
  /**
992
1133
  * The executable implementation of the system.
993
1134
  */
994
- readonly run: SystemRun<Spec, A, E>
1135
+ readonly run: SystemRun<Spec, E>
995
1136
  /**
996
1137
  * The system this one is a gated copy of, made by `Schedule.when(...)`:
997
1138
  * the same system with extra run conditions. The runtime keys per-system
998
1139
  * state (change detection, event cursors) by the original, so a system and
999
1140
  * its gated copies are one reader.
1000
1141
  */
1001
- readonly base?: SystemDefinition<any, any, any, any, any, any> | undefined
1142
+ readonly base?: SystemDefinition<any, any, any, any, any> | undefined
1002
1143
  }
1003
1144
 
1004
1145
  /**
1005
1146
  * A system implementation.
1006
1147
  *
1007
- * Return nothing for a system that cannot fail. Return an `Fx` when the system
1008
- * has an expected failure (`Fx.fail(...)`) or wants to compose effects; the
1009
- * failure type becomes part of every schedule that contains the system.
1148
+ * Return nothing for a system that cannot fail. Return
1149
+ * `Result.failure(error)` for an expected failure: the system's ECS writes
1150
+ * are rolled back, its commands and events are dropped, and the tick stops
1151
+ * with a typed `SystemFailure` whose `error` is inferred from every
1152
+ * `Result.failure(...)` the body returns. A returned success
1153
+ * (`Result.success(...)`) behaves like returning nothing. Exceptions are
1154
+ * defects, not failures: they roll back the same way and are rethrown.
1155
+ *
1156
+ * @example
1157
+ * ```ts
1158
+ * const Spend = Game.System("Spend", {
1159
+ * resources: { gold: Game.System.writeResource(Gold) }
1160
+ * }, ({ resources }) => {
1161
+ * if (resources.gold.get() < 10) return Result.failure("NotEnoughGold" as const)
1162
+ * resources.gold.update((gold) => gold - 10)
1163
+ * })
1164
+ * ```
1010
1165
  */
1011
- export type SystemRun<Spec extends AnySystemSpec, A = void, E = never> =
1012
- (context: SystemContext<Spec>) => Fx<A, E, SystemDependencies<Spec>> | void
1166
+ export type SystemRun<Spec extends AnySystemSpec, E = never> =
1167
+ (context: SystemContext<Spec>) => Result.Result<unknown, E> | void
1013
1168
 
1014
1169
  /**
1015
1170
  * Defines a system from an explicit spec and a typed implementation.
1016
1171
  *
1017
1172
  * This is the public entrypoint for authoring systems. The implementation only
1018
- * receives the capabilities declared in the spec, and the returned effect keeps
1019
- * service dependencies tracked in the type system.
1173
+ * receives the capabilities declared in the spec; return
1174
+ * `Result.failure(error)` for an expected failure (see {@link SystemRun}).
1020
1175
  *
1021
1176
  * Use the string-name overload in normal code. The name is turned into an
1022
1177
  * internal ordering token automatically, so the system can participate in
@@ -1075,7 +1230,6 @@ const collectSystemRequirements = (
1075
1230
 
1076
1231
  export function System<
1077
1232
  const Access extends SystemAccessInput & { readonly schema: Schema.Any },
1078
- A = void,
1079
1233
  E = never,
1080
1234
  Root = unknown,
1081
1235
  const Name extends string = string
@@ -1084,14 +1238,14 @@ export function System<
1084
1238
  spec: Access & {
1085
1239
  readonly [Key in Exclude<keyof Access, keyof SystemAccessInput | "schema">]: never
1086
1240
  },
1087
- run: SystemRun<SystemSpec<Access["schema"], Access, Root>, A, E>
1088
- ): SystemDefinition<SystemSpec<Access["schema"], Access, Root>, A, E, Root, Name, SystemAccessNeeds<Access>>
1241
+ run: SystemRun<SystemSpec<Access["schema"], Access, Root>, E>
1242
+ ): SystemDefinition<SystemSpec<Access["schema"], Access, Root>, E, Root, Name, SystemAccessNeeds<Access>>
1089
1243
 
1090
1244
  export function System(
1091
1245
  name: string,
1092
1246
  spec: { readonly schema: Schema.Any } & SystemAccessInput,
1093
- run: (context: any) => Fx<any, any, any> | void
1094
- ): SystemDefinition<any, any, any, any, any, any> {
1247
+ run: (context: any) => Result.Result<any, any> | void
1248
+ ): SystemDefinition<any, any, any, any, any> {
1095
1249
  type Spec = SystemSpec<Schema.Any, SystemAccessInput, unknown>
1096
1250
 
1097
1251
  const normalizedSpec = {
@@ -1110,6 +1264,7 @@ export function System(
1110
1264
  pauseWhen: spec.pauseWhen ?? [],
1111
1265
  transitions: spec.transitions ?? {},
1112
1266
  local: spec.local,
1267
+ commands: spec.commands,
1113
1268
  __schemaRoot: undefined
1114
1269
  } as Spec
1115
1270
 
@@ -1123,5 +1278,5 @@ export function System(
1123
1278
  },
1124
1279
  spec: normalizedSpec,
1125
1280
  run
1126
- } as SystemDefinition<any, any, any, any, any, any>
1281
+ } as SystemDefinition<any, any, any, any, any>
1127
1282
  }
package/src/index.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * small number of stable namespaces instead of many deep internal paths.
6
6
  */
7
7
  /**
8
- * Branding helpers for validated values.
8
+ * Branding helpers for packages that author validated value types (like
9
+ * `@typeonce/ritmo-math`). Game code validates through descriptors instead.
9
10
  */
10
11
  export * as Brand from "./Brand.ts"
11
12
  /**
@@ -17,13 +18,13 @@ export * as Decode from "./Decode.ts"
17
18
  */
18
19
  export * as Debug from "./Debug.ts"
19
20
  /**
20
- * Descriptor authoring helpers.
21
+ * Simulation time: the fixed step, the clock resource, durations in whole steps.
21
22
  */
22
- export * as Descriptor from "./Descriptor.ts"
23
+ export * as Clock from "./Clock.ts"
23
24
  /**
24
- * Reusable validated authored definitions.
25
+ * Descriptor authoring helpers.
25
26
  */
26
- export * as Definition from "./Definition.ts"
27
+ export * as Descriptor from "./Descriptor.ts"
27
28
  /**
28
29
  * Entity identity and proof helpers.
29
30
  */
@@ -33,9 +34,9 @@ export * as Entity from "./Entity.ts"
33
34
  */
34
35
  export * as EntityScope from "./EntityScope.ts"
35
36
  /**
36
- * Minimal effect-style computation type.
37
+ * The host's wall clock as an injected contract (`standard`, `test()`).
37
38
  */
38
- export * as Fx from "./Fx.ts"
39
+ export * as HostClock from "./HostClock.ts"
39
40
  /**
40
41
  * Read-only typed world projections.
41
42
  */