@moku-labs/game 0.15.40 → 0.15.41

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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/app/page.d.mts +1 -1
  3. package/dist/app/system.d.mts +3 -3
  4. package/dist/app.d.mts +3 -3
  5. package/dist/app.mjs +1 -1
  6. package/dist/assets.d.mts +1 -1
  7. package/dist/{capture-PTJXDpxK.mjs → capture-BtS6_oWr.mjs} +61 -1
  8. package/dist/{commands-CvwzXCxM.d.mts → commands-DLk06fiL.d.mts} +5 -5
  9. package/dist/control.d.mts +4 -4
  10. package/dist/control.mjs +1 -1
  11. package/dist/{define-DRRs7A6i.d.mts → define-CgU4dz0e.d.mts} +2 -2
  12. package/dist/{headless-CEwDAhgH.d.mts → headless-1e3gEF1Y.d.mts} +1 -1
  13. package/dist/{headless-Bxxl_mQX.mjs → headless-CCiUwvlp.mjs} +1 -1
  14. package/dist/{index-BQGifHXV.d.mts → index-D12s9KZ2.d.mts} +3 -3
  15. package/dist/index.d.mts +5 -5
  16. package/dist/index.mjs +2 -2
  17. package/dist/inspect.d.mts +6 -6
  18. package/dist/jsx-dev-runtime.d.mts +1 -1
  19. package/dist/jsx-runtime.d.mts +1 -1
  20. package/dist/{run-G4m1APMv.mjs → run-8s7kh2QD.mjs} +2 -2
  21. package/dist/{runtime-7RDFd4Ti.d.mts → runtime-Bpg0WmM8.d.mts} +1 -1
  22. package/dist/{src-Cz-QeLTp.mjs → src-CWVwT9UM.mjs} +286 -37
  23. package/dist/testing.d.mts +4 -4
  24. package/dist/testing.mjs +2 -2
  25. package/dist/{types-CP22WjU9.d.mts → types-B-o8Ul1v.d.mts} +2 -2
  26. package/dist/{types-Dnh8LZxo.d.mts → types-CFEtGENK.d.mts} +1 -1
  27. package/dist/{types-B4DmdMle.d.mts → types-DQ9VHXun.d.mts} +4 -4
  28. package/dist/{types-DY_DRbGY.d.mts → types-DrkR03nL.d.mts} +121 -25
  29. package/dist/{types-CoTkzbEB.d.mts → types-gQATSj41.d.mts} +2 -2
  30. package/dist/{visual-BcS4CTps.d.mts → visual-pHKIFBSQ.d.mts} +6 -6
  31. package/dist/visual.d.mts +1 -1
  32. package/dist/visual.mjs +1 -1
  33. package/llms.txt +13 -4
  34. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
2
  import { r as systemSource, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { a as finishDrafts, i as enableDraftPatches, n as deepFreeze$1, o as openDrafts, r as dropDrafts } from "./drafts-BawpSaVp.mjs";
4
- import { _ as pushEntry, c as collectFlows, d as findNode, f as framePath, g as compact, h as slotNames, i as servePaused, l as describeGraph, m as isRestCheckpoint, n as captureFrame, o as epochOf, p as graphHash, r as serveCaptures, s as pageNow$1, t as cancelCaptures } from "./capture-PTJXDpxK.mjs";
4
+ import { _ as compact, c as collectFlows, d as findNode, f as framePath, g as slotNames, h as restlessCapOf, i as servePaused, l as describeGraph, m as isRestCheckpoint, n as captureFrame, o as epochOf, p as graphHash, r as serveCaptures, s as pageNow$1, t as cancelCaptures, v as pushEntry } from "./capture-BtS6_oWr.mjs";
5
5
  import { $ as component, A as localPoseOf, B as OverlayLevel, C as builtInStyles, D as textFor, E as readStyle, F as rootPoseOf, G as arcDefaults, H as Shape, I as Display, J as Exiting, K as componentsFor, L as HudPart, M as poseUnder, N as rootPointOf, O as applyPose, P as rootPoseInto, Q as Tree, R as NineSlice, U as Sprite, V as Parent, W as Transform, X as Now, Y as Layer, Z as Order, _ as uiFor, a as visualOf$1, b as Countdown, c as Covered, d as LocalWrite, f as Scroll, h as UiCounters, i as samePose, it as tag, j as parentOf$1, k as isLeaving, l as DebugTrigger, m as UI_OWNER, n as drawnRect, nt as system, o as visualRectOf, p as ScrollState, r as fitScaleOf, rt as systemsFor, s as Box, t as CONTENT$1, tt as resource, u as Escapable, x as Text, z as Overlay } from "./visual-ClVRMUAi.mjs";
6
6
  import { a as isDev } from "./dev-BNndV-hU.mjs";
7
7
  import { n as isComponentDefinition } from "./component-DGg5DqKK.mjs";
@@ -3673,7 +3673,7 @@ function nextNodes(flow, name) {
3673
3673
  */
3674
3674
  function targetProblem(flow, name, outcome, target) {
3675
3675
  if (typeof target !== "string" && target.kind === "exit") {
3676
- if (target.outcome in flow.outcomes) return void 0;
3676
+ if (Object.hasOwn(flow.outcomes, target.outcome)) return void 0;
3677
3677
  return `[game] Flow "${flow.id}": exit("${target.outcome}") of node "${name}" names no outcome of the flow.\n Declare outcomes.${target.outcome} on flow "${flow.id}".`;
3678
3678
  }
3679
3679
  const next = typeof target === "string" ? target : target.target;
@@ -3743,7 +3743,7 @@ function nodeProblems(flow) {
3743
3743
  if (entry.kind === "slot" && !entry.name) problems.push(`[game] Flow "${flow.id}": the slot node "${name}" has no slot name.\n Give it a name: slot("${name}").`);
3744
3744
  if (entry.kind !== "node") continue;
3745
3745
  for (const type of entry.inbox) {
3746
- if (type in entry.outcomes) continue;
3746
+ if (Object.hasOwn(entry.outcomes, type)) continue;
3747
3747
  problems.push(`[game] Flow "${flow.id}": inbox type "${type}" of node "${name}" is not an outcome.\n Add outcomes.${type} to node "${name}".`);
3748
3748
  }
3749
3749
  if (entry.rest && !entry.run && Object.keys(entry.outcomes).length === 0) problems.push(`[game] Flow "${flow.id}": rest node "${name}" has neither outcomes nor a run body.\n Declare outcomes or add a run body.`);
@@ -3889,6 +3889,138 @@ function featureProblems(flows, features) {
3889
3889
  return problems;
3890
3890
  }
3891
3891
  /**
3892
+ * Names the outcomes of a table besides `done`: what a slot declares, or what a contributed flow
3893
+ * may leave its slot on.
3894
+ *
3895
+ * @param outcomes - The outcome table of a slot or a flow.
3896
+ * @returns The outcome names other than `done`, in declaration order.
3897
+ * @example
3898
+ * ```ts
3899
+ * declaredOutcomes({ done: type(), shop: type() }); // ["shop"]
3900
+ * ```
3901
+ */
3902
+ function declaredOutcomes(outcomes) {
3903
+ return Object.keys(outcomes).filter((name) => name !== "done");
3904
+ }
3905
+ /**
3906
+ * Tells whether two lists hold the same names, in any order.
3907
+ *
3908
+ * @param first - One list of names.
3909
+ * @param second - The other list.
3910
+ * @returns True when every name of one is in the other and the lengths match.
3911
+ * @example
3912
+ * ```ts
3913
+ * sameNames(["shop", "board"], ["board", "shop"]); // true
3914
+ * ```
3915
+ */
3916
+ function sameNames$1(first, second) {
3917
+ return first.length === second.length && first.every((name) => second.includes(name));
3918
+ }
3919
+ /**
3920
+ * Collects every slot node of the graph by slot name: the flow that holds it and what it
3921
+ * declares, in the order the graph declares them.
3922
+ *
3923
+ * @param flows - Every collected flow by id.
3924
+ * @returns Slot name to its slot nodes, at least one each.
3925
+ */
3926
+ function slotDeclarations(flows) {
3927
+ const declarations = /* @__PURE__ */ new Map();
3928
+ for (const flow of flows.values()) for (const entry of Object.values(flow.nodes)) {
3929
+ if (entry.kind !== "slot") continue;
3930
+ const declaration = {
3931
+ flow: flow.id,
3932
+ declared: declaredOutcomes(entry.outcomes)
3933
+ };
3934
+ const list = declarations.get(entry.name);
3935
+ if (list === void 0) declarations.set(entry.name, [declaration]);
3936
+ else list.push(declaration);
3937
+ }
3938
+ return declarations;
3939
+ }
3940
+ /**
3941
+ * Reports the contributions of a slot that declares outcomes and that may leave it on an outcome
3942
+ * the slot does not declare: the loop would have no edge for it.
3943
+ *
3944
+ * @param slotName - Name of the slot.
3945
+ * @param declared - The outcomes the slot declares besides `done`.
3946
+ * @param contributions - The contributions of the slot.
3947
+ * @returns One sentence per contribution and outcome.
3948
+ */
3949
+ function undeclaredExits(slotName, declared, contributions) {
3950
+ const problems = [];
3951
+ for (const { feature, flow } of contributions) for (const outcome of declaredOutcomes(flow.outcomes)) {
3952
+ if (declared.includes(outcome)) continue;
3953
+ problems.push(`[game] Slot "${slotName}": flow "${flow.id}" of feature "${feature}" exits "${outcome}", which the slot does not declare.\n Declare slot("${slotName}", { ${outcome}: type() }) or exit "done".`);
3954
+ }
3955
+ return problems;
3956
+ }
3957
+ /**
3958
+ * Reports the named outcomes of slots: one slot name declared with different outcomes in two
3959
+ * places, and a contribution of a slot that declares outcomes which may leave it on another one.
3960
+ * A slot that declares none takes any contribution: what it ignores is a warning.
3961
+ *
3962
+ * @param flows - Every collected flow by id.
3963
+ * @param features - Features API, read for the contributions of every slot.
3964
+ * @returns One sentence per problem.
3965
+ */
3966
+ function slotOutcomeProblems(flows, features) {
3967
+ const problems = [];
3968
+ for (const [slotName, [first, ...others]] of slotDeclarations(flows)) {
3969
+ for (const other of others) {
3970
+ if (sameNames$1(first.declared, other.declared)) continue;
3971
+ problems.push(`[game] Slot "${slotName}" declares different outcomes in flow "${first.flow}" and flow "${other.flow}".\n Declare the slot once and use it in both flows.`);
3972
+ }
3973
+ if (first.declared.length === 0) continue;
3974
+ problems.push(...undeclaredExits(slotName, first.declared, features.contributions(slotName)));
3975
+ }
3976
+ return problems;
3977
+ }
3978
+ /**
3979
+ * Warns about the named exits of contributions that a slot declaring no outcomes ignores: the
3980
+ * slot hands over to its next contribution as after `done`.
3981
+ *
3982
+ * @param slotName - Name of the slot.
3983
+ * @param contributions - The contributions of the slot.
3984
+ * @returns One warning per contribution and outcome.
3985
+ */
3986
+ function ignoredExits(slotName, contributions) {
3987
+ return contributions.flatMap(({ flow }) => declaredOutcomes(flow.outcomes).map((outcome) => `[game] Slot "${slotName}": flow "${flow.id}" exits "${outcome}", which the slot ignores.\n Declare slot("${slotName}", { ${outcome}: type() }) to route it, or exit "done".`));
3988
+ }
3989
+ /**
3990
+ * Warns about contributions without `when` in a slot that declares outcomes: a named exit leaves
3991
+ * the slot, so a later one runs only when the one before it exits `done`.
3992
+ *
3993
+ * @param slotName - Name of the slot.
3994
+ * @param contributions - The contributions of the slot, in order.
3995
+ * @returns One warning per contribution without `when` after the first one.
3996
+ */
3997
+ function unguardedContributions(slotName, contributions) {
3998
+ const warnings = [];
3999
+ let earlier;
4000
+ for (const contribution of contributions) {
4001
+ if (contribution.when !== void 0) continue;
4002
+ if (earlier !== void 0) warnings.push(`[game] Slot "${slotName}": the flows "${earlier.flow.id}" and "${contribution.flow.id}" have no when.\n A named exit leaves the slot, so "${contribution.flow.id}" runs only when "${earlier.flow.id}" exits done; give them when.`);
4003
+ earlier = contribution;
4004
+ }
4005
+ return warnings;
4006
+ }
4007
+ /**
4008
+ * Warns about the named outcomes of slots: a named exit a slot that declares none ignores, and
4009
+ * contributions without `when` in a slot that declares some.
4010
+ *
4011
+ * @param flows - Every collected flow by id.
4012
+ * @param features - Features API, read for the contributions of every slot.
4013
+ * @returns One warning per finding.
4014
+ */
4015
+ function slotOutcomeWarnings(flows, features) {
4016
+ const warnings = [];
4017
+ for (const [slotName, [first]] of slotDeclarations(flows)) {
4018
+ const contributions = features.contributions(slotName);
4019
+ warnings.push(...first.declared.length === 0 ? ignoredExits(slotName, contributions) : unguardedContributions(slotName, contributions));
4020
+ }
4021
+ return warnings;
4022
+ }
4023
+ /**
3892
4024
  * Reads the scene id of one entry of a feature's `scenes` list. The list holds whatever
3893
4025
  * `defineScene` returns; validation reads the `id` and ignores everything else.
3894
4026
  *
@@ -4035,12 +4167,14 @@ function sizeWarnings(flow) {
4035
4167
  * Validates the whole graph at `run()` and returns every problem as one `[game] …` sentence:
4036
4168
  * unreachable node, outcome without an edge, missing target, barrier edge that does not reach a
4037
4169
  * rest node of the same flow, cycle with no rest node, slot without a name, equal contribution
4038
- * `order`, one flow contributed twice to a slot, `inbox` type that is not an outcome, rest node
4039
- * with neither `run` nor outcomes, a `recover` on a node that is not a rest node, a scene id no
4040
- * feature registers, an `over` node that names a scene, `safeNode` that is not a checkpoint,
4041
- * colliding ids. A `recover` on a node that is not the safe node is a warning: it never runs
4042
- * there. Types are bypassed by JSON data and casts, so this runs even though the edge table is
4043
- * checked at compile time.
4170
+ * `order`, one flow contributed twice to a slot, one slot name declared with different outcomes,
4171
+ * a contribution of a slot that declares outcomes which exits another one, `inbox` type that is
4172
+ * not an outcome, rest node with neither `run` nor outcomes, a `recover` on a node that is not a
4173
+ * rest node, a scene id no feature registers, an `over` node that names a scene, `safeNode` that
4174
+ * is not a checkpoint, colliding ids. A `recover` on a node that is not the safe node is a
4175
+ * warning: it never runs there. So are a named exit a slot that declares no outcomes ignores, and
4176
+ * two contributions without `when` in a slot that declares some. Types are bypassed by JSON data
4177
+ * and casts, so this runs even though the edge table is checked at compile time.
4044
4178
  *
4045
4179
  * @param flows - Every collected flow by id, the main flow first.
4046
4180
  * @param features - Features API: slot contributions and feature names.
@@ -4054,8 +4188,8 @@ function validateGraph(flows, features, config) {
4054
4188
  problems.push(...edgeProblems(flow), ...nodeProblems(flow), ...recoverProblems(flow), ...reachProblems(flow), ...cycleProblems(flow));
4055
4189
  warnings.push(...sizeWarnings(flow));
4056
4190
  }
4057
- warnings.push(...slotNameWarnings(flows, features), ...recoverWarnings(flows, features, config));
4058
- problems.push(...flowIdProblems(flows), ...featureProblems(flows, features), ...sceneProblems(flows, features), ...safeNodeProblems(config, features));
4191
+ warnings.push(...slotNameWarnings(flows, features), ...slotOutcomeWarnings(flows, features), ...recoverWarnings(flows, features, config));
4192
+ problems.push(...flowIdProblems(flows), ...featureProblems(flows, features), ...slotOutcomeProblems(flows, features), ...sceneProblems(flows, features), ...safeNodeProblems(config, features));
4059
4193
  return {
4060
4194
  problems,
4061
4195
  warnings
@@ -4545,7 +4679,9 @@ function pickContribution(ctx, features, slotName, from) {
4545
4679
  }
4546
4680
  /**
4547
4681
  * Follows one result to the next position: a node of the same flow, a mapped node, or the exit of
4548
- * a sub-flow, which hands the outcome to the parent.
4682
+ * a sub-flow, which hands the outcome to the parent. The exit of a slot contribution returns to
4683
+ * the slot: on `done` or an outcome the slot does not declare to continue it, on a declared
4684
+ * outcome to leave it.
4549
4685
  *
4550
4686
  * @param ctx - Domain context of the flow plugin.
4551
4687
  * @param frames - The position being planned; mutated.
@@ -4560,19 +4696,78 @@ function planFrom(ctx, frames, location, outcome, payload, depth) {
4560
4696
  const target = location.flow.edges[location.name]?.[outcome];
4561
4697
  if (target === void 0) return { problem: `[game] Flow "${location.flow.id}": outcome "${outcome}" of node "${location.name}" has no edge.\n Add edges.${location.name}.${outcome}.` };
4562
4698
  if (typeof target === "string") return enterTarget(ctx, frames, location.flow, target, payload);
4563
- if (target.kind === "map") {
4564
- const mapped = target.map(payload);
4565
- const input = mapped === void 0 ? null : mapped;
4566
- if (!isJson$1(input)) return { problem: `[game] The mapper of edge "${location.name}.${outcome}" returned a value that is not JSON.\n Return plain JSON from to("${target.target}", ...).` };
4567
- return enterTarget(ctx, frames, location.flow, target.target, input);
4568
- }
4699
+ if (target.kind === "map") return enterMapped(ctx, frames, location, outcome, target, payload);
4569
4700
  frames.pop();
4570
4701
  const parent = locateFrames(ctx, frames);
4571
4702
  if (parent === void 0) return { problem: `[game] The main flow "${location.flow.id}" left with outcome "${target.outcome}".\n Give the top-level flow an edge instead of exit("${target.outcome}").` };
4572
- if (parent.entry.kind === "slot") return continueSlot(frames, location.flow.id);
4703
+ if (parent.entry.kind === "slot") return leavesSlot(parent.entry, target.outcome) ? leaveSlot(frames, target.outcome, payload) : continueSlot(frames, location.flow.id);
4573
4704
  return planFrom(ctx, frames, parent, target.outcome, payload, depth + 1);
4574
4705
  }
4575
4706
  /**
4707
+ * Follows a `to()` edge: maps the payload and enters the target with it. The mapper is game code:
4708
+ * a throw, or a result that is not JSON, is a problem, and the node fails like any other.
4709
+ *
4710
+ * @param ctx - Domain context of the flow plugin.
4711
+ * @param frames - The position being planned; mutated.
4712
+ * @param location - Where the result was produced.
4713
+ * @param outcome - Outcome name of the result.
4714
+ * @param target - The mapped target of the edge.
4715
+ * @param payload - Payload of the result, handed to the mapper.
4716
+ * @returns The plan, or the problem that stops it.
4717
+ */
4718
+ function enterMapped(ctx, frames, location, outcome, target, payload) {
4719
+ let mapped;
4720
+ try {
4721
+ mapped = target.map(payload);
4722
+ } catch (error) {
4723
+ const message = error instanceof Error ? error.message : String(error);
4724
+ return { problem: `[game] The mapper of edge "${location.name}.${outcome}" threw: ${message}.\n Return plain JSON from to("${target.target}", ...).` };
4725
+ }
4726
+ const input = mapped === void 0 ? null : mapped;
4727
+ if (!isJson$1(input)) return { problem: `[game] The mapper of edge "${location.name}.${outcome}" returned a value that is not JSON.\n Return plain JSON from to("${target.target}", ...).` };
4728
+ return enterTarget(ctx, frames, location.flow, target.target, input);
4729
+ }
4730
+ /**
4731
+ * Tells whether a contribution that exits `outcome` leaves its slot at once: the slot declares the
4732
+ * outcome, and it is not `done`, which hands over to the next contribution. Own keys only:
4733
+ * `"constructor" in outcomes` is true for every object.
4734
+ *
4735
+ * @param slot - The slot the contribution runs in.
4736
+ * @param outcome - The outcome the contribution exited with.
4737
+ * @returns True when the slot takes its own edge of that outcome next.
4738
+ * @example
4739
+ * ```ts
4740
+ * leavesSlot(slot("episodeStep", { shop: type() }), "shop"); // true
4741
+ * leavesSlot(slot("episodeStep", { shop: type() }), "done"); // false: the next contribution runs
4742
+ * ```
4743
+ */
4744
+ function leavesSlot(slot, outcome) {
4745
+ return outcome !== "done" && Object.hasOwn(slot.outcomes, outcome);
4746
+ }
4747
+ /**
4748
+ * Plans the early leave of a slot: a contribution exited an outcome the slot declares. The plan
4749
+ * stands on the slot node like `continueSlot`, without `after`, and carries the outcome and its
4750
+ * payload: the loop takes the slot's own edge with them after the commit, so the later
4751
+ * contributions do not run and the next entry of the slot starts at its first one.
4752
+ *
4753
+ * @param frames - The position, already popped back to the slot node.
4754
+ * @param outcome - The declared outcome the contribution left on.
4755
+ * @param payload - Its payload, handed on to the slot's edge.
4756
+ * @returns The plan that stands on the slot node.
4757
+ */
4758
+ function leaveSlot(frames, outcome, payload) {
4759
+ return {
4760
+ stack: frames,
4761
+ next: framePath(frames),
4762
+ rest: false,
4763
+ checkpoint: false,
4764
+ leave: {
4765
+ outcome,
4766
+ payload
4767
+ }
4768
+ };
4769
+ }
4770
+ /**
4576
4771
  * Plans the return to a slot after one of its contributions ended. The next contribution is NOT
4577
4772
  * picked here: `when` reads the committed state, and a plan is made before the commit of the edge.
4578
4773
  * So the plan stands on the slot node and names the contribution that ended; the loop picks the
@@ -4609,7 +4804,7 @@ function asError$4(value) {
4609
4804
  /**
4610
4805
  * Commits the transaction of one node, releases its hints, journals the edge and emits
4611
4806
  * `flow:edge`. This is the only commit of the engine: the edge of a node, and the recovery edge
4612
- * of the safe node, both go through it.
4807
+ * of the safe node, both go through it. It counts the edge among those since the last rest node.
4613
4808
  *
4614
4809
  * @param ctx - Domain context of the flow plugin.
4615
4810
  * @param modules - Injected sibling APIs.
@@ -4619,6 +4814,7 @@ function asError$4(value) {
4619
4814
  */
4620
4815
  function commitEdge(ctx, modules, step, next) {
4621
4816
  const commit = step.transaction.commit();
4817
+ ctx.state.runner.restless += 1;
4622
4818
  ctx.deps.time.wake();
4623
4819
  modules.fx.release();
4624
4820
  const entry = pushEntry(ctx.state.runner, {
@@ -4682,6 +4878,8 @@ function handleFailure(ctx, modules, transaction, error, path, safe) {
4682
4878
  });
4683
4879
  state.stack = [...target];
4684
4880
  state.slotAfter = void 0;
4881
+ state.slotLeave = void 0;
4882
+ state.restless = 0;
4685
4883
  if (!retry) {
4686
4884
  safe.inside = true;
4687
4885
  safe.recover = true;
@@ -4707,9 +4905,11 @@ function handleAbort(modules, transaction, reason) {
4707
4905
  }
4708
4906
  /**
4709
4907
  * Moves the position to a planned place and, when that place is a rest node, makes it the rest
4710
- * point of the loop: the frame rollback and `bookmark` return to, a zeroed failure count, a
4711
- * compacted journal at a checkpoint, `flow:rest` and the walk's rest listeners. The caller marks the
4712
- * rest point in the store first. Used by the edge, and by the loop when it enters a slot.
4908
+ * point of the loop: the frame rollback and `bookmark` return to, a zeroed failure count, a zeroed
4909
+ * count of edges without a rest, a compacted journal at a checkpoint, `flow:rest` and the walk's
4910
+ * rest listeners. The caller marks the rest point in the store first. Used by the edge, and by the
4911
+ * loop when it enters a slot. A slot it stands on keeps the contribution that ended, or the
4912
+ * declared outcome one left on, for the next turn.
4713
4913
  *
4714
4914
  * @param ctx - Domain context of the flow plugin.
4715
4915
  * @param plan - Where the loop stands now.
@@ -4718,9 +4918,11 @@ function arrive(ctx, plan) {
4718
4918
  const state = ctx.state.runner;
4719
4919
  state.stack = plan.stack;
4720
4920
  state.slotAfter = plan.after;
4921
+ state.slotLeave = plan.leave;
4721
4922
  if (!plan.rest) return;
4722
4923
  state.restFrame = [...plan.stack];
4723
4924
  state.failures = 0;
4925
+ state.restless = 0;
4724
4926
  if (plan.checkpoint) compact(state);
4725
4927
  ctx.emit("flow:rest", {
4726
4928
  path: plan.next,
@@ -4730,7 +4932,9 @@ function arrive(ctx, plan) {
4730
4932
  }
4731
4933
  /**
4732
4934
  * Takes the edge of one result: plan, commit, mark the barrier or the rest point, move the
4733
- * position and announce a rest node.
4935
+ * position and announce a rest node. A plan with a problem fails the node, and so does an edge
4936
+ * beyond the cap of edges without a rest node: a loop of transit nodes that never rests would
4937
+ * freeze the game.
4734
4938
  *
4735
4939
  * @param ctx - Domain context of the flow plugin.
4736
4940
  * @param modules - Injected sibling APIs.
@@ -4742,6 +4946,10 @@ async function finishNode(ctx, modules, step, safe) {
4742
4946
  const state = ctx.state.runner;
4743
4947
  const plan = planFrom(ctx, [...state.stack], step.location, step.result.outcome, step.result.payload, 0);
4744
4948
  if ("problem" in plan) return handleFailure(ctx, modules, step.transaction, new Error(plan.problem), step.path, safe);
4949
+ if (state.restless >= state.restlessCap) {
4950
+ const restless = `[game] ${state.restlessCap} edges without a rest node, at "${step.path}".\n Put a rest node in the loop.`;
4951
+ return handleFailure(ctx, modules, step.transaction, new Error(restless), step.path, safe);
4952
+ }
4745
4953
  const entry = commitEdge(ctx, modules, step, plan.next);
4746
4954
  if (step.barrier) await ctx.deps.model.store.markBarrier(`${step.path}#${entry.index}@${step.now}`);
4747
4955
  else if (plan.rest) ctx.deps.model.store.markRest();
@@ -4891,27 +5099,47 @@ function to(target, map) {
4891
5099
  };
4892
5100
  }
4893
5101
  /**
4894
- * Defines an extension point: a node whose body is "run the contributions of this slot in order".
4895
- * Its single outcome is `done`.
5102
+ * Builds the slot. The body works on the widened shapes the runner reads: the signature above has
5103
+ * already kept `done` out of the declared outcomes for a typed caller, and the check here refuses
5104
+ * it past a cast.
4896
5105
  *
4897
5106
  * @param name - Slot name features contribute to.
5107
+ * @param outcomes - The named outcomes besides `done`.
4898
5108
  * @returns The slot node as plain data.
4899
- * @example
4900
- * ```ts
4901
- * // The main flow leaves room after a finished order. Features contribute sub-flows to it.
4902
- * nodes: { boot, home, board: boardFlow, afterOrder: slot("afterOrder") }
4903
- * // slot("afterOrder"): { kind: "slot", name: "afterOrder", input: …, outcomes: { done: … } }
4904
- * ```
5109
+ * @throws {TypeError} When `outcomes` declares `done`.
4905
5110
  */
4906
- function slot(name) {
5111
+ function slot(name, outcomes) {
5112
+ if (outcomes !== void 0 && Object.hasOwn(outcomes, "done")) throw new TypeError(`[game] Slot "${name}" declares the outcome "done".\n done is every slot's own outcome; remove it from slot("${name}", ...).`);
4907
5113
  return {
4908
5114
  kind: "slot",
4909
5115
  name,
4910
5116
  input: type(),
4911
- outcomes: { done: type() }
5117
+ outcomes: {
5118
+ done: type(),
5119
+ ...outcomes
5120
+ }
4912
5121
  };
4913
5122
  }
4914
5123
  /**
5124
+ * Builds the entry. The body works on the widened shapes the runner reads: the signature above
5125
+ * has already checked the flow against the slot.
5126
+ *
5127
+ * @param host - The slot node the flow runs in.
5128
+ * @param spec - The flow, its `order` and an optional `when`.
5129
+ * @returns The entry under the slot's name.
5130
+ */
5131
+ function contribution(host, spec) {
5132
+ const { flow, order, when } = spec;
5133
+ return { [host.name]: when === void 0 ? {
5134
+ flow,
5135
+ order
5136
+ } : {
5137
+ flow,
5138
+ order,
5139
+ when
5140
+ } };
5141
+ }
5142
+ /**
4915
5143
  * Builds the `out` of a node context: one method per declared outcome. A method called without
4916
5144
  * data produces a `null` payload. Called by the loop when it enters a node, never by a game.
4917
5145
  *
@@ -5676,7 +5904,8 @@ function enterSubFlow(ctx, modules, location, sub, safe) {
5676
5904
  }
5677
5905
  /**
5678
5906
  * Enters a slot: the first contribution whose `when` passes, or the slot's `done` edge when the
5679
- * slot has no contribution to run.
5907
+ * slot has no contribution to run. When a contribution just left on an outcome the slot declares,
5908
+ * the slot takes its own edge of that outcome instead, with the contribution's payload.
5680
5909
  *
5681
5910
  * @param ctx - Domain context of the flow plugin.
5682
5911
  * @param modules - Injected sibling APIs.
@@ -5687,6 +5916,14 @@ function enterSubFlow(ctx, modules, location, sub, safe) {
5687
5916
  */
5688
5917
  function enterSlot(ctx, modules, location, slot, safe) {
5689
5918
  const state = ctx.state.runner;
5919
+ const leave = state.slotLeave;
5920
+ if (leave !== void 0) {
5921
+ state.slotLeave = void 0;
5922
+ return applyResult(ctx, modules, location, leave, safe, {
5923
+ barrier: false,
5924
+ now: ctx.deps.clock.now()
5925
+ });
5926
+ }
5690
5927
  const finished = state.slotAfter;
5691
5928
  const contributions = modules.features.contributions(slot.name);
5692
5929
  const from = finished === void 0 ? 0 : contributions.findIndex((item) => item.flow.id === finished) + 1;
@@ -5742,8 +5979,10 @@ function enterBookmark(ctx, modules, bookmark, safe) {
5742
5979
  ctx.deps.model.store.markRest();
5743
5980
  state.stack = framesOf(location.trail, bookmark.input);
5744
5981
  state.slotAfter = void 0;
5982
+ state.slotLeave = void 0;
5745
5983
  state.restFrame = [...state.stack];
5746
5984
  state.failures = 0;
5985
+ state.restless = 0;
5747
5986
  ctx.emit("flow:rest", {
5748
5987
  path: bookmark.path,
5749
5988
  checkpoint: isRestCheckpoint(location.entry)
@@ -5777,7 +6016,8 @@ async function advance$1(ctx, modules, safe) {
5777
6016
  * Runs the graph until aborted: enter node, `onEnter` stages, begin a transaction, await the
5778
6017
  * body, commit on the edge, release hints, journal, emit `flow:edge`, follow the edge. An error
5779
6018
  * discards, rolls back, is logged and retries, then enters `safeNode`, whose `recover` runs first
5780
- * as one committed edge; a failure inside `safeNode` rejects.
6019
+ * as one committed edge; a failure inside `safeNode` rejects. More edges without a rest node than
6020
+ * the graph's cap is such an error too.
5781
6021
  * Abort reasons: `"stop"` ends the loop, `"inbox"` turns the event into the outcome, `"restore"`
5782
6022
  * enters the bookmark's frame.
5783
6023
  *
@@ -5791,6 +6031,7 @@ async function runLoop(ctx, modules) {
5791
6031
  const state = ctx.state.runner;
5792
6032
  state.flows = collectGraph(ctx, modules.features);
5793
6033
  checkGraph(ctx, modules.features);
6034
+ state.restlessCap = restlessCapOf(main, modules.features.contributions);
5794
6035
  modules.features.seal();
5795
6036
  await ctx.deps.model.store.load();
5796
6037
  state.stack = [{
@@ -5800,7 +6041,9 @@ async function runLoop(ctx, modules) {
5800
6041
  }];
5801
6042
  state.restFrame = [...state.stack];
5802
6043
  state.slotAfter = void 0;
6044
+ state.slotLeave = void 0;
5803
6045
  state.failures = 0;
6046
+ state.restless = 0;
5804
6047
  const safe = {
5805
6048
  inside: false,
5806
6049
  recover: false
@@ -6680,6 +6923,9 @@ function createInboxState() {
6680
6923
  //#endregion
6681
6924
  //#region src/plugins/flow/runner/state.ts
6682
6925
  /**
6926
+ * @file flow/runner — state factory.
6927
+ */
6928
+ /**
6683
6929
  * Creates the empty flow registry. It lives in its own function because the plugin's lint rule
6684
6930
  * L5 refuses a collection built inside an exported declaration.
6685
6931
  *
@@ -6709,6 +6955,9 @@ function createRunnerState() {
6709
6955
  stack: [],
6710
6956
  restFrame: void 0,
6711
6957
  slotAfter: void 0,
6958
+ slotLeave: void 0,
6959
+ restless: 0,
6960
+ restlessCap: 64,
6712
6961
  journal: [],
6713
6962
  journalIndex: 0,
6714
6963
  running: void 0,
@@ -66188,4 +66437,4 @@ const screen = [
66188
66437
  uiPlugin
66189
66438
  ];
66190
66439
  //#endregion
66191
- export { sfx as $, flowPlugin as $t, types_exports$10 as A, Glow as At, defineMotion as B, Animation as Bt, types_exports$3 as C, messageDuration as Ct, types_exports$7 as D, Blur as Dt, types_exports$6 as E, Alpha as Et, types_exports$15 as F, audioPlugin as Ft, external as G, MONITOR_SECTIONS as Gt, platformPlugin as H, rendererPlugin as Ht, types_exports$16 as I, scenesPlugin as It, mark as J, PERF_LOG_VERSION as Jt, frames as K, PERF_CAUSES as Kt, types_exports$17 as L, assetsPlugin as Lt, types_exports$12 as M, Outline as Mt, types_exports$13 as N, defineFilter as Nt, types_exports$8 as O, ColorMatrix as Ot, types_exports$14 as P, debugPlugin as Pt, set as Q, worldPlugin as Qt, uiPlugin as R, animPlugin as Rt, types_exports$2 as S, messageArgument as St, types_exports$5 as T, Emitter as Tt, HAPTIC_KINDS as U, PASS_KINDS as Ut, textPlugin as V, Frames as Vt, defineAnimation as W, frameAt as Wt, play$1 as X, SHADER_WHATS as Xt, parallel as Y, PROBLEM_RULES as Yt, sequence as Z, DEMAND_REASONS as Zt, defineBundles as _, Tappable as _t, cubicBezier as a, modelPlugin as an, use as at, types_exports as b, Traced as bt, hint as c, timePlugin as cn, Draggable as ct, defineScene as d, Hovered as dt, refuseStart as en, spawn as et, tr as f, Pointer as ft, music as g, Swipeable as gt, defineEmitter as h, Pressed as ht, screen as i, type as in, tween as it, types_exports$11 as j, Noise as jt, types_exports$9 as k, Displacement as kt, schedule as l, clockPlugin as ln, DropTarget as lt, flattenFeatures as m, Pressable as mt, createPlugin as n, slot as nn, stagger as nt, defineEffect as o, SaveUnreadableError as on, wait as ot, defineFeature as p, PointerOver as pt, haptic as q, PERF_GAP_BUCKETS as qt, defineGame as r, to as rn, stopSfx as rt, guide as s, lifecyclePlugin as sn, inputPlugin as st, createApp as t, exit as tn, spawned as tt, projection as u, Held as ut, load as v, Touchable as vt, types_exports$4 as w, effectsPlugin as wt, types_exports$1 as x, i18nPlugin as xt, ready as y, Traceable as yt, resolve as z, AnimPlayer as zt };
66440
+ export { sfx as $, flowPlugin as $t, types_exports$10 as A, Glow as At, defineMotion as B, Animation as Bt, types_exports$3 as C, messageDuration as Ct, types_exports$7 as D, Blur as Dt, types_exports$6 as E, Alpha as Et, types_exports$15 as F, audioPlugin as Ft, external as G, MONITOR_SECTIONS as Gt, platformPlugin as H, rendererPlugin as Ht, types_exports$16 as I, scenesPlugin as It, mark as J, PERF_LOG_VERSION as Jt, frames as K, PERF_CAUSES as Kt, types_exports$17 as L, assetsPlugin as Lt, types_exports$12 as M, Outline as Mt, types_exports$13 as N, defineFilter as Nt, types_exports$8 as O, ColorMatrix as Ot, types_exports$14 as P, debugPlugin as Pt, set as Q, worldPlugin as Qt, uiPlugin as R, animPlugin as Rt, types_exports$2 as S, messageArgument as St, types_exports$5 as T, Emitter as Tt, HAPTIC_KINDS as U, PASS_KINDS as Ut, textPlugin as V, Frames as Vt, defineAnimation as W, frameAt as Wt, play$1 as X, SHADER_WHATS as Xt, parallel as Y, PROBLEM_RULES as Yt, sequence as Z, DEMAND_REASONS as Zt, defineBundles as _, Tappable as _t, cubicBezier as a, type as an, use as at, types_exports as b, Traced as bt, hint as c, lifecyclePlugin as cn, Draggable as ct, defineScene as d, Hovered as dt, refuseStart as en, spawn as et, tr as f, Pointer as ft, music as g, Swipeable as gt, defineEmitter as h, Pressed as ht, screen as i, to as in, tween as it, types_exports$11 as j, Noise as jt, types_exports$9 as k, Displacement as kt, schedule as l, timePlugin as ln, DropTarget as lt, flattenFeatures as m, Pressable as mt, createPlugin as n, exit as nn, stagger as nt, defineEffect as o, modelPlugin as on, wait as ot, defineFeature as p, PointerOver as pt, haptic as q, PERF_GAP_BUCKETS as qt, defineGame as r, slot as rn, stopSfx as rt, guide as s, SaveUnreadableError as sn, inputPlugin as st, createApp as t, contribution as tn, spawned as tt, projection as u, clockPlugin as un, Held as ut, load as v, Touchable as vt, types_exports$4 as w, effectsPlugin as wt, types_exports$1 as x, i18nPlugin as xt, ready as y, Traceable as yt, resolve as z, AnimPlayer as zt };
@@ -1,7 +1,7 @@
1
- import { $n as FeaturePlugin, Dr as Json, dr as Descriptor, gr as NodeFx, hr as Hint, sr as NodeTable, tr as AnyFlow, xr as FakeClock, yr as ClockSource } from "./types-DY_DRbGY.mjs";
2
- import { t as createApp } from "./index-BQGifHXV.mjs";
3
- import { b as saveOf, v as WithGameState, y as memory } from "./types-B4DmdMle.mjs";
4
- import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-CEwDAhgH.mjs";
1
+ import { Or as Json, Sr as FakeClock, _r as NodeFx, br as ClockSource, cr as NodeTable, er as FeaturePlugin, fr as Descriptor, gr as Hint, nr as AnyFlow } from "./types-DrkR03nL.mjs";
2
+ import { t as createApp } from "./index-D12s9KZ2.mjs";
3
+ import { b as saveOf, v as WithGameState, y as memory } from "./types-DQ9VHXun.mjs";
4
+ import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-1e3gEF1Y.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/plugins/clock/fake.d.ts
package/dist/testing.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { en as refuseStart, in as type, m as flattenFeatures, t as createApp, tn as exit } from "./src-Cz-QeLTp.mjs";
1
+ import { an as type, en as refuseStart, m as flattenFeatures, nn as exit, t as createApp } from "./src-CWVwT9UM.mjs";
2
2
  import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { n as fakeClock } from "./fake-hnJcJeYf.mjs";
4
- import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-Bxxl_mQX.mjs";
4
+ import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-CCiUwvlp.mjs";
5
5
  //#region src/testing/fake-fx.ts
6
6
  /** The node path a refused `fx.start` names when the test gives none. */
7
7
  const defaultPath = "test";
@@ -1,5 +1,5 @@
1
- import { $ as PixiModule, A as SyncApi, An as NumericFields, Dr as Json, En as Ease, Er as Events$3, Fn as TrackSegment, Ln as Api$2, Nn as ReducedMotion, O as FilterSlot, Pn as TrackOptions, Q as PixiFilter, Ur as Events$5, Y as Api$4, b as Events$4, dr as Descriptor, et as PixiSprite, fn as ComponentType, ft as FilterCache, k as FilterSlotInputs, kn as MotionHandle, ln as AnyComponentValue, mn as Entity, ni as Events$2, nn as Api$3, qr as Api$1, ri as Require, s as Events$1, sn as AnyComponent, t as Api$5, tt as PixiTexture, un as ComponentHandle, ur as TypeTag, ut as Point, wr as Api$6 } from "./types-DY_DRbGY.mjs";
2
- import { N as InputApi, R as I18nApi, X as StylesState, c as UiNode, i as Finding, l as LayoutState, n as ElementChange, o as JsxState, r as ElementMotion, w as TextApi } from "./types-Dnh8LZxo.mjs";
1
+ import { $ as PixiModule, A as SyncApi, An as NumericFields, Dr as Events$3, En as Ease, Fn as TrackSegment, Jr as Api$1, Ln as Api$2, Nn as ReducedMotion, O as FilterSlot, Or as Json, Pn as TrackOptions, Q as PixiFilter, Tr as Api$6, Wr as Events$5, Y as Api$4, b as Events$4, dr as TypeTag, et as PixiSprite, fn as ComponentType, fr as Descriptor, ft as FilterCache, ii as Require, k as FilterSlotInputs, kn as MotionHandle, ln as AnyComponentValue, mn as Entity, nn as Api$3, ri as Events$2, s as Events$1, sn as AnyComponent, t as Api$5, tt as PixiTexture, un as ComponentHandle, ut as Point } from "./types-DrkR03nL.mjs";
2
+ import { N as InputApi, R as I18nApi, X as StylesState, c as UiNode, i as Finding, l as LayoutState, n as ElementChange, o as JsxState, r as ElementMotion, w as TextApi } from "./types-CFEtGENK.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
5
5
 
@@ -1,4 +1,4 @@
1
- import { An as NumericFields, Cn as TagType, Dn as Motion, Dr as Json, En as Ease, In as ViewHandle, K as TransformValue, Ln as Api$1, Nn as ReducedMotion, On as MotionCause, Y as Api$3, _r as Answer, cn as AnyComponentType, dr as Descriptor, dt as ViewportSize, fn as ComponentType, hr as Hint, jn as ProjectionMotion, kn as MotionHandle, ln as AnyComponentValue, mn as Entity, nn as Api$2, qr as Api, ri as Require, s as Events$1, sn as AnyComponent, t as Api$4, vr as Api$5, wn as ChangeHook } from "./types-DY_DRbGY.mjs";
1
+ import { An as NumericFields, Cn as TagType, Dn as Motion, En as Ease, In as ViewHandle, Jr as Api, K as TransformValue, Ln as Api$1, Nn as ReducedMotion, On as MotionCause, Or as Json, Y as Api$3, cn as AnyComponentType, dt as ViewportSize, fn as ComponentType, fr as Descriptor, gr as Hint, ii as Require, jn as ProjectionMotion, kn as MotionHandle, ln as AnyComponentValue, mn as Entity, nn as Api$2, s as Events$1, sn as AnyComponent, t as Api$4, vr as Answer, wn as ChangeHook, yr as Api$5 } from "./types-DrkR03nL.mjs";
2
2
  import { Log } from "@moku-labs/common/browser";
3
3
  import { PluginCtx } from "@moku-labs/core";
4
4
  import { Node, Yoga } from "yoga-layout/load";
@@ -1,7 +1,7 @@
1
- import { $ as PixiModule, $n as FeaturePlugin, Dr as Json, Lr as SnapshotOf, Mr as PlayerStateProvider, Nr as ProviderCall, Pr as SaveDoc, Tr as Config, X as Config$2, a as Config$3, d as Manifest, lt as Orientation, n as AssetsIo, tr as AnyFlow, ur as TypeTag, xr as FakeClock, yr as ClockSource, zn as Config$1 } from "./types-DY_DRbGY.mjs";
2
- import { o as AudioContextLike, s as Config$4 } from "./types-CP22WjU9.mjs";
3
- import { R as I18nApi, U as StringTable, W as TextKey } from "./types-Dnh8LZxo.mjs";
4
- import { St as effectsPlugin, Tt as audioPlugin, ht as PlatformProvider, i as screen, pt as platformPlugin, t as createApp } from "./index-BQGifHXV.mjs";
1
+ import { $ as PixiModule, Er as Config, Fr as SaveDoc, Nr as PlayerStateProvider, Or as Json, Pr as ProviderCall, Rr as SnapshotOf, Sr as FakeClock, X as Config$2, a as Config$3, br as ClockSource, d as Manifest, dr as TypeTag, er as FeaturePlugin, lt as Orientation, n as AssetsIo, nr as AnyFlow, zn as Config$1 } from "./types-DrkR03nL.mjs";
2
+ import { o as AudioContextLike, s as Config$4 } from "./types-B-o8Ul1v.mjs";
3
+ import { R as I18nApi, U as StringTable, W as TextKey } from "./types-CFEtGENK.mjs";
4
+ import { St as effectsPlugin, Tt as audioPlugin, ht as PlatformProvider, i as screen, pt as platformPlugin, t as createApp } from "./index-D12s9KZ2.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/plugins/model/store/providers/memory.d.ts