@field/cascade 0.2.22 → 0.3.1

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 (233) hide show
  1. package/AGENTS.md +47 -7
  2. package/ARCHITECTURE.md +30 -2
  3. package/CHANGELOG.md +107 -0
  4. package/DESIGN.md +10 -5
  5. package/README.md +103 -7
  6. package/dist/assets/CodeEditor-D22fVi-4.js +623 -0
  7. package/dist/assets/CodePanel-DPNEYfcR.js +2 -0
  8. package/dist/assets/CustomNodeDialog-DeHCAvgb.js +1 -0
  9. package/dist/assets/{ExportDialog-DWN4nhXI.js → ExportDialog-BJu2Bpvv.js} +2 -2
  10. package/dist/assets/ProjectSettings-DWpVtWEx.js +2 -0
  11. package/dist/assets/SettingsDialog-za00IS3L.js +1 -0
  12. package/dist/assets/VersionHistoryDialog-DTMGhI0_.js +3 -0
  13. package/dist/assets/{core-Bji4aZUU.js → core-JsyKMO1k.js} +29 -38
  14. package/dist/assets/{cssMode-B6Z_Q1Kh.js → cssMode-emdRheFK.js} +1 -1
  15. package/dist/assets/{editor.api-jMoW_43X.js → editor.api-xHX-fMSo.js} +1 -1
  16. package/dist/assets/{freemarker2-DvvPV6m5.js → freemarker2-C3M8_Gs3.js} +1 -1
  17. package/dist/assets/geo-BAR6lQp5.js +1 -0
  18. package/dist/assets/{handlebars-B8rfuSIa.js → handlebars-CBMurtFk.js} +1 -1
  19. package/dist/assets/{html-DyHm6JCp.js → html-xbLQwknw.js} +1 -1
  20. package/dist/assets/{htmlMode-E9XLWE5u.js → htmlMode-2wKdDTon.js} +1 -1
  21. package/dist/assets/{image-BUcQC7PG.js → image-i1j6nWKe.js} +26 -40
  22. package/dist/assets/index-BDV5xGBZ.css +1 -0
  23. package/dist/assets/index-CMcH8AgP.js +212 -0
  24. package/dist/assets/index-client-BoJgsN9M.js +1 -0
  25. package/dist/assets/{javascript-C_bM5PCN.js → javascript-DJaOo_2I.js} +1 -1
  26. package/dist/assets/{jsonMode-BdyVS7G4.js → jsonMode-Bheas8_0.js} +1 -1
  27. package/dist/assets/{legacy-DIu3XRwl.js → legacy-CAj2_7QE.js} +2 -2
  28. package/dist/assets/{liquid-CDkn3spA.js → liquid-BCj_BGbM.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-AB5DMAsr.js → lspLanguageFeatures-BfIsx_xz.js} +1 -1
  30. package/dist/assets/{mdx-BvH_NjDN.js → mdx-DwiQDiVO.js} +1 -1
  31. package/dist/assets/{nodeTypeUtils-CweA3NHd.js → nodeTypeUtils-BBA1JqHx.js} +1 -1
  32. package/dist/assets/{python-B7uii4fi.js → python-BjrB5nqj.js} +1 -1
  33. package/dist/assets/{razor-Z48fWdJc.js → razor-CfoBmGDs.js} +1 -1
  34. package/dist/assets/{tsMode-BDgacVG4.js → tsMode-BcEoMSxp.js} +1 -1
  35. package/dist/assets/{typescript-Derb6Oqa.js → typescript-DIg7TEmQ.js} +1 -1
  36. package/dist/assets/{workers-BtoMX_pm.js → workers-vXYuwtPw.js} +1 -1
  37. package/dist/assets/{xml-B1CaFSrG.js → xml-DexLQIQy.js} +1 -1
  38. package/dist/assets/{yaml-HmIeA0KH.js → yaml-DUnD_1dE.js} +1 -1
  39. package/dist/cli/index.js +16467 -10200
  40. package/dist/cli/runtime/gpu.ts +76 -0
  41. package/dist/cli/runtime/io.ts +93 -7
  42. package/dist/contracts/animation.d.ts +40 -0
  43. package/dist/contracts/animation.js +14 -0
  44. package/dist/contracts/capabilities.d.ts +69 -6
  45. package/dist/contracts/definition.d.ts +60 -2
  46. package/dist/contracts/document.d.ts +15 -0
  47. package/dist/contracts/index.d.ts +1 -0
  48. package/dist/contracts/index.js +1 -0
  49. package/dist/contracts/schema.js +8 -5
  50. package/dist/contracts/validate.d.ts +10 -1
  51. package/dist/contracts/validate.js +20 -3
  52. package/dist/gpu.d.ts +71 -0
  53. package/dist/gpu.js +53 -0
  54. package/dist/graphs/default.cascade +1 -1
  55. package/dist/index.html +5 -5
  56. package/dist/io.d.ts +196 -0
  57. package/dist/io.js +303 -0
  58. package/dist/net.d.ts +1 -0
  59. package/dist/net.js +28 -0
  60. package/dist/runtime/animation/channel.d.ts +108 -0
  61. package/dist/runtime/animation/channel.js +204 -0
  62. package/dist/runtime/animation/frameRange.d.ts +78 -0
  63. package/dist/runtime/animation/frameRange.js +76 -0
  64. package/dist/runtime/animation/index.d.ts +10 -0
  65. package/dist/runtime/animation/index.js +8 -0
  66. package/dist/runtime/builtins/core/random.d.ts +1 -1
  67. package/dist/runtime/builtins/core/remap.d.ts +1 -1
  68. package/dist/runtime/builtins/geo/copyToPoints.d.ts +1 -1
  69. package/dist/runtime/builtins/geo/copyToPoints.js +1 -1
  70. package/dist/runtime/builtins/geo/generators.d.ts +1 -1
  71. package/dist/runtime/builtins/geo/merge.d.ts +1 -1
  72. package/dist/runtime/builtins/geo/svgExport.d.ts +1 -1
  73. package/dist/runtime/builtins/geo/svgExport.js +1 -1
  74. package/dist/runtime/builtins/geo/transform.d.ts +1 -1
  75. package/dist/runtime/builtins/geo/transform.js +1 -1
  76. package/dist/runtime/definition/architecture.d.ts +1 -1
  77. package/dist/runtime/definition/extract.d.ts +1 -1
  78. package/dist/runtime/definition/extract.js +1 -1
  79. package/dist/runtime/error.d.ts +1 -1
  80. package/dist/runtime/expressions/evaluator.d.ts +53 -0
  81. package/dist/runtime/expressions/evaluator.js +193 -0
  82. package/dist/runtime/expressions/index.d.ts +15 -0
  83. package/dist/runtime/expressions/index.js +12 -0
  84. package/dist/runtime/expressions/math.d.ts +75 -0
  85. package/dist/runtime/expressions/math.js +140 -0
  86. package/dist/runtime/expressions/preprocess.d.ts +33 -0
  87. package/dist/runtime/expressions/preprocess.js +102 -0
  88. package/dist/runtime/expressions/scope.d.ts +27 -0
  89. package/dist/runtime/expressions/scope.js +10 -0
  90. package/dist/runtime/expressions/time.d.ts +54 -0
  91. package/dist/runtime/expressions/time.js +71 -0
  92. package/dist/runtime/expressions/types.d.ts +66 -0
  93. package/dist/runtime/expressions/types.js +9 -0
  94. package/dist/runtime/geometry/attributes.d.ts +2 -2
  95. package/dist/runtime/geometry/attributes.js +1 -1
  96. package/dist/runtime/geometry/builder.d.ts +1 -1
  97. package/dist/runtime/geometry/builder.js +1 -1
  98. package/dist/runtime/geometry/copy.d.ts +1 -1
  99. package/dist/runtime/geometry/copy.js +1 -1
  100. package/dist/runtime/geometry/generate.d.ts +1 -1
  101. package/dist/runtime/geometry/groups.d.ts +1 -1
  102. package/dist/runtime/geometry/groups.js +1 -1
  103. package/dist/runtime/geometry/matrix.d.ts +1 -1
  104. package/dist/runtime/geometry/merge.d.ts +1 -1
  105. package/dist/runtime/geometry/merge.js +1 -1
  106. package/dist/runtime/geometry/primitives.d.ts +1 -1
  107. package/dist/runtime/geometry/primitives.js +1 -1
  108. package/dist/runtime/geometry/promote.d.ts +1 -1
  109. package/dist/runtime/geometry/promote.js +1 -1
  110. package/dist/runtime/geometry/resample.d.ts +1 -1
  111. package/dist/runtime/geometry/resample.js +1 -1
  112. package/dist/runtime/geometry/svg.d.ts +1 -1
  113. package/dist/runtime/geometry/svg.js +1 -1
  114. package/dist/runtime/geometry/transform.d.ts +1 -1
  115. package/dist/runtime/geometry/transform.js +1 -1
  116. package/dist/runtime/index.d.ts +4 -1
  117. package/dist/runtime/index.js +4 -1
  118. package/dist/runtime/params/animator.d.ts +74 -0
  119. package/dist/runtime/params/animator.js +137 -0
  120. package/dist/runtime/params/index.d.ts +11 -0
  121. package/dist/runtime/params/index.js +9 -0
  122. package/dist/runtime/params/resolve.d.ts +73 -0
  123. package/dist/runtime/params/resolve.js +62 -0
  124. package/dist/runtime/runtime.d.ts +42 -0
  125. package/dist/runtime/runtime.js +351 -29
  126. package/dist/runtime/types.d.ts +32 -6
  127. package/dist/shell.d.ts +3 -3
  128. package/doc/NODE_REFERENCE.md +395 -0
  129. package/doc/PROJECT_AUTHORING.md +148 -6
  130. package/package.json +30 -3
  131. package/dist/assets/CodeEditor-DoaNEUaM.js +0 -641
  132. package/dist/assets/CodePanel-B0_2XJ-t.js +0 -2
  133. package/dist/assets/CustomNodeDialog-026lzecD.js +0 -1
  134. package/dist/assets/ProjectSettings-BzT164Yx.js +0 -2
  135. package/dist/assets/SettingsDialog-DmHb1V0r.js +0 -1
  136. package/dist/assets/VersionHistoryDialog-hH6VsXLX.js +0 -3
  137. package/dist/assets/geo-CXmCtzMM.js +0 -2
  138. package/dist/assets/index-BXYXSdgh.js +0 -224
  139. package/dist/assets/index-CrNke7SW.css +0 -1
  140. package/dist/assets/index-client-DN_FvjN8.js +0 -1
  141. package/dist/assets/quill-DjZ69wL_.js +0 -121
  142. package/dist/cli/index.js.map +0 -7
  143. package/dist/contracts/capabilities.d.ts.map +0 -1
  144. package/dist/contracts/capabilities.js.map +0 -1
  145. package/dist/contracts/color.d.ts.map +0 -1
  146. package/dist/contracts/color.js.map +0 -1
  147. package/dist/contracts/definition.d.ts.map +0 -1
  148. package/dist/contracts/definition.js.map +0 -1
  149. package/dist/contracts/document.d.ts.map +0 -1
  150. package/dist/contracts/document.js.map +0 -1
  151. package/dist/contracts/geometry.d.ts.map +0 -1
  152. package/dist/contracts/geometry.js.map +0 -1
  153. package/dist/contracts/index.d.ts.map +0 -1
  154. package/dist/contracts/index.js.map +0 -1
  155. package/dist/contracts/schema.d.ts.map +0 -1
  156. package/dist/contracts/schema.js.map +0 -1
  157. package/dist/contracts/validate.d.ts.map +0 -1
  158. package/dist/contracts/validate.js.map +0 -1
  159. package/dist/contracts/values.d.ts.map +0 -1
  160. package/dist/contracts/values.js.map +0 -1
  161. package/dist/graphs/default-orig.cascade +0 -128
  162. package/dist/runtime/browser.d.ts.map +0 -1
  163. package/dist/runtime/browser.js.map +0 -1
  164. package/dist/runtime/builtins/core/index.d.ts.map +0 -1
  165. package/dist/runtime/builtins/core/index.js.map +0 -1
  166. package/dist/runtime/builtins/core/random.d.ts.map +0 -1
  167. package/dist/runtime/builtins/core/random.js.map +0 -1
  168. package/dist/runtime/builtins/core/remap.d.ts.map +0 -1
  169. package/dist/runtime/builtins/core/remap.js.map +0 -1
  170. package/dist/runtime/builtins/core/routing.d.ts.map +0 -1
  171. package/dist/runtime/builtins/core/routing.js.map +0 -1
  172. package/dist/runtime/builtins/core/structural.d.ts.map +0 -1
  173. package/dist/runtime/builtins/core/structural.js.map +0 -1
  174. package/dist/runtime/builtins/geo/copyToPoints.d.ts.map +0 -1
  175. package/dist/runtime/builtins/geo/copyToPoints.js.map +0 -1
  176. package/dist/runtime/builtins/geo/definitions.d.ts.map +0 -1
  177. package/dist/runtime/builtins/geo/definitions.js.map +0 -1
  178. package/dist/runtime/builtins/geo/generators.d.ts.map +0 -1
  179. package/dist/runtime/builtins/geo/generators.js.map +0 -1
  180. package/dist/runtime/builtins/geo/index.d.ts.map +0 -1
  181. package/dist/runtime/builtins/geo/index.js.map +0 -1
  182. package/dist/runtime/builtins/geo/merge.d.ts.map +0 -1
  183. package/dist/runtime/builtins/geo/merge.js.map +0 -1
  184. package/dist/runtime/builtins/geo/namespace.d.ts.map +0 -1
  185. package/dist/runtime/builtins/geo/namespace.js.map +0 -1
  186. package/dist/runtime/builtins/geo/svgExport.d.ts.map +0 -1
  187. package/dist/runtime/builtins/geo/svgExport.js.map +0 -1
  188. package/dist/runtime/builtins/geo/transform.d.ts.map +0 -1
  189. package/dist/runtime/builtins/geo/transform.js.map +0 -1
  190. package/dist/runtime/builtins/index.d.ts.map +0 -1
  191. package/dist/runtime/builtins/index.js.map +0 -1
  192. package/dist/runtime/definition/architecture.d.ts.map +0 -1
  193. package/dist/runtime/definition/architecture.js.map +0 -1
  194. package/dist/runtime/definition/extract.d.ts.map +0 -1
  195. package/dist/runtime/definition/extract.js.map +0 -1
  196. package/dist/runtime/error.d.ts.map +0 -1
  197. package/dist/runtime/error.js.map +0 -1
  198. package/dist/runtime/geometry/attributes.d.ts.map +0 -1
  199. package/dist/runtime/geometry/attributes.js.map +0 -1
  200. package/dist/runtime/geometry/builder.d.ts.map +0 -1
  201. package/dist/runtime/geometry/builder.js.map +0 -1
  202. package/dist/runtime/geometry/copy.d.ts.map +0 -1
  203. package/dist/runtime/geometry/copy.js.map +0 -1
  204. package/dist/runtime/geometry/generate.d.ts.map +0 -1
  205. package/dist/runtime/geometry/generate.js.map +0 -1
  206. package/dist/runtime/geometry/groups.d.ts.map +0 -1
  207. package/dist/runtime/geometry/groups.js.map +0 -1
  208. package/dist/runtime/geometry/index.d.ts.map +0 -1
  209. package/dist/runtime/geometry/index.js.map +0 -1
  210. package/dist/runtime/geometry/matrix.d.ts.map +0 -1
  211. package/dist/runtime/geometry/matrix.js.map +0 -1
  212. package/dist/runtime/geometry/merge.d.ts.map +0 -1
  213. package/dist/runtime/geometry/merge.js.map +0 -1
  214. package/dist/runtime/geometry/primitives.d.ts.map +0 -1
  215. package/dist/runtime/geometry/primitives.js.map +0 -1
  216. package/dist/runtime/geometry/promote.d.ts.map +0 -1
  217. package/dist/runtime/geometry/promote.js.map +0 -1
  218. package/dist/runtime/geometry/resample.d.ts.map +0 -1
  219. package/dist/runtime/geometry/resample.js.map +0 -1
  220. package/dist/runtime/geometry/svg.d.ts.map +0 -1
  221. package/dist/runtime/geometry/svg.js.map +0 -1
  222. package/dist/runtime/geometry/transform.d.ts.map +0 -1
  223. package/dist/runtime/geometry/transform.js.map +0 -1
  224. package/dist/runtime/index.d.ts.map +0 -1
  225. package/dist/runtime/index.js.map +0 -1
  226. package/dist/runtime/node.d.ts.map +0 -1
  227. package/dist/runtime/node.js.map +0 -1
  228. package/dist/runtime/runtime.d.ts.map +0 -1
  229. package/dist/runtime/runtime.js.map +0 -1
  230. package/dist/runtime/types.d.ts.map +0 -1
  231. package/dist/runtime/types.js.map +0 -1
  232. package/dist/shell.js.map +0 -7
  233. package/dist/stage.js.map +0 -7
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The one place a parameter becomes a value.
3
+ *
4
+ * Three bindings, in one order: a keyframe channel, an expression, the stored
5
+ * value. That order was implemented once, in Studio's `Node.evalParm`, which
6
+ * meant the deterministic runtime had no answer at all — a definition-v1 prop
7
+ * carrying `$T` or a drawn curve rendered as a static number offline. Copying
8
+ * `evalParm` into the runtime would have produced a second answer to the same
9
+ * question, which is exactly the two-parameter-stores mistake this codebase
10
+ * has already paid for once. So the order moved here, into the neutral
11
+ * runtime, and both hosts call it.
12
+ *
13
+ * What it deliberately does NOT own: the expression *engine*. Evaluation is
14
+ * passed in as a callback, because each host has its own scope (Studio's
15
+ * `ExpressionEngine` reaches into the compatibility graph; the runtime binds
16
+ * its own node map) and its own error bookkeeping (Studio stores the message
17
+ * on the prop for the Inspector to show). Keeping evaluation out is also what
18
+ * keeps this module dependent on nothing but the channel sampler — no cycle
19
+ * between `animation` and `expressions`, and no clock anywhere: the frame is a
20
+ * value the caller hands in, which is what keeps a cook deterministic.
21
+ */
22
+ import { isEmptyChannel, sampleChannel } from "../animation/channel.js";
23
+ /** True when a parameter carries something beyond its stored value, and so has
24
+ * to be resolved per frame rather than read once. */
25
+ export function isBoundProp(binding) {
26
+ if (!binding)
27
+ return false;
28
+ return !!binding.expression || !isEmptyChannel(binding.channel);
29
+ }
30
+ /**
31
+ * Resolve a parameter at a frame.
32
+ *
33
+ * A parameter may carry both a channel and an expression, and the channel
34
+ * wins. The expression is kept, inert, and comes back when the channel is
35
+ * deleted: silently discarding what an author wrote is the worse of the two
36
+ * failures. `contracts/animation.ts` states the same rule for the document, so
37
+ * the file format and the resolver agree by construction.
38
+ *
39
+ * An empty channel resolves to nothing rather than to zero, so a parameter
40
+ * whose last key was deleted falls through to its expression or its value
41
+ * instead of snapping to the origin.
42
+ */
43
+ export function resolvePropBinding(binding, context) {
44
+ if (!isEmptyChannel(binding.channel)) {
45
+ const sampled = sampleChannel(binding.channel, context.frame);
46
+ if (sampled !== undefined)
47
+ return { value: sampled, source: "channel" };
48
+ }
49
+ if (binding.expression && context.evaluateExpression) {
50
+ const result = context.evaluateExpression(binding.expression);
51
+ // A failed expression falls back to the stored value and reports why. The
52
+ // alternative — a hole in the middle of a render — is the one outcome
53
+ // nobody can act on.
54
+ if (result?.error) {
55
+ return { value: binding.value, source: "value", error: result.error };
56
+ }
57
+ if (result)
58
+ return { value: result.value, source: "expression" };
59
+ }
60
+ return { value: binding.value, source: "value" };
61
+ }
62
+ //# sourceMappingURL=resolve.js.map
@@ -1,3 +1,45 @@
1
1
  import type { CascadeRuntime, CreateRuntimeOptions } from "./types.js";
2
+ /**
3
+ * Preflight codes that describe something other than "this graph cannot run
4
+ * here", so `run` proceeds and a checker reports them as warnings.
5
+ *
6
+ * False positives are worse than blindness: a validator that cries wolf gets
7
+ * ignored, and then it is no better than the one that saw nothing. So the line
8
+ * is drawn at what the diagnostic is about.
9
+ *
10
+ * `preflight-environment` is about the checker's reach — a node saying
11
+ * `runsOn: 'browser'` claims nothing about the CLI and is not defective
12
+ * because somebody validated it from a terminal.
13
+ *
14
+ * `stray-params` is about the document — values were lost on the way in, which
15
+ * is worth saying loudly and is not a reason to refuse to run: a graph with a
16
+ * leftover `params` array runs perfectly well on its defaults, which is
17
+ * precisely the problem.
18
+ *
19
+ * It was briefly an error, and Marcus reverted that on 2026-09-08 with the
20
+ * evidence that decides it: while converting sixty-two nodes across four
21
+ * sketches, every one of them was a mixed graph for most of the day, and as an
22
+ * error an unconverted or half-converted sketch **stops validating at all** —
23
+ * which is the one thing you need while converting it. The warning is the
24
+ * conversion checklist; as an error it is a locked door on the room you are
25
+ * working in.
26
+ *
27
+ * The fair case for the error was that the warning never fired, and that was
28
+ * true: `check` bailed on dynamic modules before preflight ran. The fix for
29
+ * that was the reporting *order*, not the severity.
30
+ *
31
+ * This set lives here rather than in the CLI because both paths read it, and a
32
+ * duplicated copy is what let `validate` and `check` pass graphs `run` then
33
+ * rejected.
34
+ *
35
+ * **It classifies a static check, and it is not the run gate.** Those are
36
+ * different questions and conflating them was a real bug of mine, caught by
37
+ * Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
38
+ * still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
39
+ * it means executing and failing on an undefined capability instead of saying
40
+ * why. What a static check downgrades is the *checker's reach*; what a run
41
+ * tolerates is much narrower. See `RUN_TOLERATED_CODES`.
42
+ */
43
+ export declare const PREFLIGHT_WARNING_CODES: ReadonlySet<string>;
2
44
  export declare function createRuntime(options: CreateRuntimeOptions): CascadeRuntime;
3
45
  //# sourceMappingURL=runtime.d.ts.map
@@ -1,6 +1,66 @@
1
- import { canConnectTypes, validateNodeDefinition, } from "cascade/contracts";
1
+ import { canConnectTypes, validateNodeDefinition, } from "@field/cascade/contracts";
2
2
  import { CascadeRuntimeError } from "./error.js";
3
3
  import { builtinNodeRegistrations } from "./builtins/index.js";
4
+ import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
5
+ import { PropAnimator, isBoundProp } from "./params/index.js";
6
+ import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
7
+ /**
8
+ * Preflight codes that describe something other than "this graph cannot run
9
+ * here", so `run` proceeds and a checker reports them as warnings.
10
+ *
11
+ * False positives are worse than blindness: a validator that cries wolf gets
12
+ * ignored, and then it is no better than the one that saw nothing. So the line
13
+ * is drawn at what the diagnostic is about.
14
+ *
15
+ * `preflight-environment` is about the checker's reach — a node saying
16
+ * `runsOn: 'browser'` claims nothing about the CLI and is not defective
17
+ * because somebody validated it from a terminal.
18
+ *
19
+ * `stray-params` is about the document — values were lost on the way in, which
20
+ * is worth saying loudly and is not a reason to refuse to run: a graph with a
21
+ * leftover `params` array runs perfectly well on its defaults, which is
22
+ * precisely the problem.
23
+ *
24
+ * It was briefly an error, and Marcus reverted that on 2026-09-08 with the
25
+ * evidence that decides it: while converting sixty-two nodes across four
26
+ * sketches, every one of them was a mixed graph for most of the day, and as an
27
+ * error an unconverted or half-converted sketch **stops validating at all** —
28
+ * which is the one thing you need while converting it. The warning is the
29
+ * conversion checklist; as an error it is a locked door on the room you are
30
+ * working in.
31
+ *
32
+ * The fair case for the error was that the warning never fired, and that was
33
+ * true: `check` bailed on dynamic modules before preflight ran. The fix for
34
+ * that was the reporting *order*, not the severity.
35
+ *
36
+ * This set lives here rather than in the CLI because both paths read it, and a
37
+ * duplicated copy is what let `validate` and `check` pass graphs `run` then
38
+ * rejected.
39
+ *
40
+ * **It classifies a static check, and it is not the run gate.** Those are
41
+ * different questions and conflating them was a real bug of mine, caught by
42
+ * Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
43
+ * still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
44
+ * it means executing and failing on an undefined capability instead of saying
45
+ * why. What a static check downgrades is the *checker's reach*; what a run
46
+ * tolerates is much narrower. See `RUN_TOLERATED_CODES`.
47
+ */
48
+ export const PREFLIGHT_WARNING_CODES = new Set([
49
+ "runtime/preflight-environment",
50
+ "runtime/stray-params",
51
+ ]);
52
+ /**
53
+ * The only preflight code that does not stop a run.
54
+ *
55
+ * A stranded `params` value is a fact about the document: the graph cooks
56
+ * perfectly well on its defaults, which is exactly the problem, and refusing to
57
+ * run is the worse answer. Everything else in the preflight is a reason this
58
+ * host cannot execute this graph — an environment mismatch and a missing
59
+ * capability both mean the cook would fail partway with a worse message.
60
+ */
61
+ const RUN_TOLERATED_CODES = new Set([
62
+ "runtime/stray-params",
63
+ ]);
4
64
  class MutableAbortSignal {
5
65
  report;
6
66
  aborted = false;
@@ -120,6 +180,10 @@ class Graph {
120
180
  onDispose;
121
181
  graphState = "ready";
122
182
  byId;
183
+ dataConnectionsByInput;
184
+ dataConnectionsByNode;
185
+ connectedInputs;
186
+ connectedOutputs;
123
187
  listeners = new Set();
124
188
  diagnostics = [];
125
189
  active;
@@ -129,19 +193,91 @@ class Graph {
129
193
  authoredAnnotations;
130
194
  graphInputs;
131
195
  graphOutputs;
196
+ /**
197
+ * The graph's animated parameters, and the clock they resolve against.
198
+ *
199
+ * The clock is a value the caller sets per run (`run({ frame })`), never one
200
+ * this graph reads: same graph plus same frame gives the same output, which
201
+ * is the only reason a headless render can be trusted.
202
+ */
203
+ animator;
204
+ frame = MIN_FRAME;
205
+ fps = DEFAULT_FPS;
132
206
  constructor(host, nodes, connections, onDispose, annotations = []) {
133
207
  this.host = host;
134
208
  this.nodes = nodes;
135
209
  this.connections = connections;
136
210
  this.onDispose = onDispose;
137
211
  this.byId = new Map(nodes.map((node) => [node.id, node]));
212
+ const byInput = new Map();
213
+ const byNode = new Map();
214
+ const connectedInputs = new Set();
215
+ const connectedOutputs = new Set();
216
+ for (const connection of connections) {
217
+ connectedInputs.add(`${connection.target.nodeId}\0${connection.target.inputName}`);
218
+ connectedOutputs.add(`${connection.source.nodeId}\0${connection.source.outputName}`);
219
+ if (connection.kind !== "data")
220
+ continue;
221
+ const inputKey = `${connection.target.nodeId}\0${connection.target.inputName}`;
222
+ const inputConnections = byInput.get(inputKey) ?? [];
223
+ inputConnections.push(connection);
224
+ byInput.set(inputKey, inputConnections);
225
+ const nodeConnections = byNode.get(connection.target.nodeId) ?? [];
226
+ nodeConnections.push(connection);
227
+ byNode.set(connection.target.nodeId, nodeConnections);
228
+ }
229
+ this.dataConnectionsByInput = new Map([...byInput].map(([key, inputConnections]) => [
230
+ key,
231
+ Object.freeze(inputConnections.sort((first, second) => (first.variadicIndex ?? first.order) -
232
+ (second.variadicIndex ?? second.order))),
233
+ ]));
234
+ this.dataConnectionsByNode = new Map([...byNode].map(([key, nodeConnections]) => [
235
+ key,
236
+ Object.freeze(nodeConnections.sort((first, second) => first.order - second.order)),
237
+ ]));
238
+ this.connectedInputs = connectedInputs;
239
+ this.connectedOutputs = connectedOutputs;
138
240
  this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
139
241
  this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
140
242
  this.authoredAnnotations = Object.freeze(annotations.map((annotation) => snapshot(annotation)));
243
+ this.animator = new PropAnimator({
244
+ nodes,
245
+ node: (id) => this.byId.get(id),
246
+ inputNode: (node, index) => {
247
+ const feeding = this.dataConnectionsByNode.get(node.id) ?? [];
248
+ const source = feeding[index]?.source.nodeId;
249
+ return (source ? this.byId.get(source) : undefined) ?? null;
250
+ },
251
+ // A failed expression has already fallen back to the stored value, so it
252
+ // is a diagnostic rather than a stopped run — but it is never silent: a
253
+ // parameter that quietly reverts is the hardest fault to see in a
254
+ // hundred rendered frames.
255
+ report: (nodeId, propName, error) => this.reportDiagnostic({
256
+ phase: "run",
257
+ code: "runtime/expression-failed",
258
+ message: `${nodeId}.${propName}: ${error}`,
259
+ path: `${nodeId}.${propName}`,
260
+ }),
261
+ });
262
+ this.animator.setFps(this.fps);
263
+ this.animator.setFrame(this.frame);
264
+ // Resolve once at load, so `inspect()` and a preset applied before any run
265
+ // both see the parameter at the graph's own first frame rather than the
266
+ // number that was last saved.
267
+ this.animator.resolveAll();
141
268
  }
142
269
  get state() {
143
270
  return this.graphState;
144
271
  }
272
+ /**
273
+ * Everything that would stop this host running the graph, without running
274
+ * it: a node whose environment this host is not, a bridged node that cannot
275
+ * be bridged, a declared capability this host does not install. Empty means
276
+ * the graph clears the same gate `run` puts it through.
277
+ */
278
+ preflight() {
279
+ return this.preflightDiagnostics(this.nodes);
280
+ }
145
281
  inspect() {
146
282
  const nodes = this.nodes.map((node) => ({
147
283
  id: node.id,
@@ -153,8 +289,7 @@ class Graph {
153
289
  {
154
290
  kind: definition.kind,
155
291
  type: definition.kind === "trigger" ? "trigger" : definition.type,
156
- connected: this.connections.some((connection) => connection.target.nodeId === node.id &&
157
- connection.target.inputName === name),
292
+ connected: this.connectedInputs.has(`${node.id}\0${name}`),
158
293
  ...(definition.kind === "data"
159
294
  ? { value: snapshot(node.inputs[name]) }
160
295
  : {}),
@@ -165,8 +300,7 @@ class Graph {
165
300
  {
166
301
  kind: definition.kind,
167
302
  type: definition.kind === "trigger" ? "trigger" : definition.type,
168
- connected: this.connections.some((connection) => connection.source.nodeId === node.id &&
169
- connection.source.outputName === name),
303
+ connected: this.connectedOutputs.has(`${node.id}\0${name}`),
170
304
  ...(definition.kind === "data" && node.outputs.has(name)
171
305
  ? { value: snapshot(node.outputs.get(name)) }
172
306
  : {}),
@@ -212,7 +346,26 @@ class Graph {
212
346
  misuse("runtime/structural-prop", `${nodeId}.${propName} is structural and requires a graph reload`);
213
347
  if (!node.definition.props?.[propName])
214
348
  misuse("runtime/invalid-prop", `${nodeId}.${propName} is not a prop`);
215
- node.props[propName] = runtimeValue(value, node.definition.props[propName].type, false, `${nodeId}.${propName}`);
349
+ const resolved = runtimeValue(value, node.definition.props[propName].type, false, `${nodeId}.${propName}`);
350
+ node.props[propName] = resolved;
351
+ this.rebind(node, propName, resolved);
352
+ }
353
+ /**
354
+ * Keep a bound prop's STORED value in step with a direct write.
355
+ *
356
+ * A channel or an expression still wins on the next resolve — setting a
357
+ * keyed parameter's value does not delete its keys, in Studio or here — but
358
+ * the value written is the one the parameter falls back to, so dropping it
359
+ * would make a later `clearChannel` reinstate a number nobody chose.
360
+ */
361
+ rebind(node, propName, value) {
362
+ const binding = node.bindings[propName];
363
+ if (binding)
364
+ node.bindings[propName] = { ...binding, value };
365
+ }
366
+ reportDiagnostic(diagnostic) {
367
+ this.diagnostics.push(diagnostic);
368
+ this.emit({ type: "diagnostic", diagnostic });
216
369
  }
217
370
  async applyPreset(preset) {
218
371
  this.requireReady();
@@ -247,10 +400,50 @@ class Graph {
247
400
  }
248
401
  for (const [node, name, value] of inputDrafts)
249
402
  node.inputs[name] = value;
250
- for (const [node, name, value] of propDrafts)
403
+ for (const [node, name, value] of propDrafts) {
251
404
  node.props[name] = value;
405
+ this.rebind(node, name, value);
406
+ }
407
+ }
408
+ /**
409
+ * The frame this graph is cooking at, and the rate frames are counted in.
410
+ *
411
+ * Separate from `run` so a caller can scrub without cooking — and so that
412
+ * `setFrame` then `inspect()` reports a keyed parameter at that frame, which
413
+ * is what a panel needs and what `run` alone cannot give it.
414
+ */
415
+ setFrame(frame) {
416
+ if (!Number.isFinite(frame))
417
+ return;
418
+ this.frame = Math.max(MIN_FRAME, frame);
419
+ this.animator.setFrame(this.frame);
420
+ this.animator.resolveAll();
421
+ }
422
+ setFps(fps) {
423
+ if (!Number.isFinite(fps))
424
+ return;
425
+ this.fps = Math.max(1, fps);
426
+ this.animator.setFps(this.fps);
427
+ this.animator.resolveAll();
428
+ }
429
+ getFrame() {
430
+ return this.frame;
431
+ }
432
+ getFps() {
433
+ return this.fps;
252
434
  }
253
435
  run(options = {}) {
436
+ // The frame belongs to the run, not to the graph's history: a render loop
437
+ // states which frame it wants and gets exactly that, with no dependence on
438
+ // what the previous run left behind.
439
+ if (options.fps !== undefined)
440
+ this.setFps(options.fps);
441
+ if (options.frame !== undefined)
442
+ this.setFrame(options.frame);
443
+ // Every bound parameter, resolved before any node executes — so a node
444
+ // still receives a plain number and never reads a clock.
445
+ else
446
+ this.animator.resolveAll();
254
447
  return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId);
255
448
  }
256
449
  trigger(nodeId, inputName, payload, options = {}) {
@@ -339,7 +532,7 @@ class Graph {
339
532
  }
340
533
  : this.runSelection(target);
341
534
  const { reachable, initial } = selection;
342
- this.preflight(reachable);
535
+ this.assertPreflight(reachable);
343
536
  if (!signal.aborted)
344
537
  await Promise.all(reachable
345
538
  .filter((node) => !this.isRemote(node))
@@ -415,10 +608,7 @@ class Graph {
415
608
  const index = integerProp(node.props.inputIndex);
416
609
  return [name, this.dataInputValue(parent, `input_${index}`)];
417
610
  }
418
- const connections = this.connections.filter((item) => item.kind === "data" &&
419
- item.target.nodeId === node.id &&
420
- item.target.inputName === name).sort((first, second) => (first.variadicIndex ?? first.order) -
421
- (second.variadicIndex ?? second.order));
611
+ const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
422
612
  if (definition.variadic) {
423
613
  const values = connections.map((connection) => this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
424
614
  if (!values.length && node.inputs[name] !== undefined)
@@ -504,6 +694,7 @@ class Graph {
504
694
  report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
505
695
  };
506
696
  await node.execute({
697
+ nodeId: node.id,
507
698
  inputs,
508
699
  outputs,
509
700
  props,
@@ -544,33 +735,70 @@ class Graph {
544
735
  }
545
736
  this.emit({ type: "node:complete", runId, nodeId: node.id });
546
737
  }
547
- preflight(nodes) {
738
+ /**
739
+ * Every reason this host cannot run these nodes, collected rather than
740
+ * thrown. `assertPreflight` is the run path and raises the first of them;
741
+ * `preflight()` is the static path and hands the whole list to a caller that
742
+ * is not executing anything.
743
+ *
744
+ * There is deliberately one implementation. `validate` and `check` used to
745
+ * answer "will this graph run?" without ever consulting a host, so a node
746
+ * declaring a capability nobody installs passed both and then failed here —
747
+ * the two commands could not see the fault they exist to report. Any second
748
+ * copy of these rules would drift back into that.
749
+ */
750
+ preflightDiagnostics(nodes) {
751
+ const diagnostics = [];
752
+ const push = (code, nodeId, message) => diagnostics.push({ phase: "preflight", code, path: nodeId, message });
548
753
  for (const node of nodes) {
754
+ if (node.strayParams.length)
755
+ push("runtime/stray-params", node.id, `${node.moduleId} stores ${node.strayParams.join(", ")} under "params", which this runtime does not read — move ${node.strayParams.length === 1 ? "it" : "them"} to "props" or "inputs", whichever the definition declares, or the ${node.strayParams.length === 1 ? "value is" : "values are"} silently the default`);
549
756
  const remote = this.isRemote(node);
550
757
  const locationWorks = node.definition.runsOn === "portable" ||
551
758
  node.definition.runsOn === this.host.environment ||
552
759
  remote;
553
- if (!locationWorks)
554
- throw runtimeError("runtime/preflight-environment", `${node.moduleId} requires ${node.definition.runsOn}`);
760
+ if (!locationWorks) {
761
+ push("runtime/preflight-environment", node.id, `${node.moduleId} requires ${node.definition.runsOn}`);
762
+ continue;
763
+ }
555
764
  if (remote) {
556
765
  const hasTriggers = [
557
766
  ...Object.values(node.definition.inputs ?? {}),
558
767
  ...Object.values(node.definition.outputs ?? {}),
559
768
  ].some((port) => port.kind === "trigger");
560
769
  if (hasTriggers)
561
- throw runtimeError("runtime/preflight-bridge-trigger", `${node.moduleId} cannot send triggers over the server bridge`);
770
+ push("runtime/preflight-bridge-trigger", node.id, `${node.moduleId} cannot send triggers over the server bridge`);
562
771
  const hasTexture = [
563
772
  ...Object.values(node.definition.inputs ?? {}),
564
773
  ...Object.values(node.definition.outputs ?? {}),
565
774
  ].some((port) => port.kind === "data" && port.type === "texture");
566
775
  if (hasTexture)
567
- throw runtimeError("runtime/preflight-bridge-texture", `${node.moduleId} cannot send textures over the server bridge`);
776
+ push("runtime/preflight-bridge-texture", node.id, `${node.moduleId} cannot send textures over the server bridge`);
568
777
  }
569
778
  else
570
779
  for (const capability of node.definition.capabilities ?? [])
571
780
  if (!this.host.capabilities[capability])
572
- throw runtimeError("runtime/missing-capability", `Missing capability ${capability} for ${node.moduleId}`);
781
+ push("runtime/missing-capability", node.id, `Missing capability ${capability} for ${node.moduleId} (node ${node.id}) — the ${this.host.environment} host ${this.hostProvides()}`);
573
782
  }
783
+ return diagnostics;
784
+ }
785
+ /**
786
+ * What the host actually installed, asked of the host rather than written
787
+ * down. `serverBridge` is left out: it is how a node runs somewhere else,
788
+ * not a capability a definition can declare.
789
+ */
790
+ hostProvides() {
791
+ const supplied = Object.keys(this.host.capabilities)
792
+ .filter((name) => name !== "serverBridge" && !!this.host.capabilities[name])
793
+ .sort();
794
+ return supplied.length
795
+ ? `provides ${supplied.join(", ")}`
796
+ : `provides no capabilities`;
797
+ }
798
+ assertPreflight(nodes) {
799
+ const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !RUN_TOLERATED_CODES.has(diagnostic.code));
800
+ if (first)
801
+ throw new CascadeRuntimeError(first.code, [first]);
574
802
  }
575
803
  isRemote(node) {
576
804
  return (this.host.environment === "browser" &&
@@ -582,7 +810,8 @@ class Graph {
582
810
  }
583
811
  runSelection(target) {
584
812
  if (target.kind === "all") {
585
- const initial = this.nodes.filter((node) => !isGated(node) && !this.hasGatedDataAncestor(node.id));
813
+ const gatedDataDescendants = this.gatedDataDescendants();
814
+ const initial = this.nodes.filter((node) => !isGated(node) && !gatedDataDescendants.has(node.id));
586
815
  const ids = new Set(initial.map((node) => node.id));
587
816
  let changed = true;
588
817
  while (changed) {
@@ -627,8 +856,25 @@ class Graph {
627
856
  }
628
857
  return this.nodes.filter((node) => ids.has(node.id));
629
858
  }
630
- hasGatedDataAncestor(nodeId) {
631
- return this.dataClosure(nodeId).some(isGated);
859
+ gatedDataDescendants() {
860
+ const outgoing = new Map();
861
+ for (const dependency of dataDependencies(this.nodes, this.connections)) {
862
+ const targets = outgoing.get(dependency.sourceId) ?? [];
863
+ targets.push(dependency.targetId);
864
+ outgoing.set(dependency.sourceId, targets);
865
+ }
866
+ const queue = this.nodes.filter(isGated).map((node) => node.id);
867
+ const descendants = new Set(queue);
868
+ let queueIndex = 0;
869
+ while (queueIndex < queue.length) {
870
+ for (const targetId of outgoing.get(queue[queueIndex++]) ?? []) {
871
+ if (descendants.has(targetId))
872
+ continue;
873
+ descendants.add(targetId);
874
+ queue.push(targetId);
875
+ }
876
+ }
877
+ return descendants;
632
878
  }
633
879
  triggerDataAncestors(nodeId) {
634
880
  const dependencies = this.dataClosure(nodeId).filter((node) => node.id !== nodeId);
@@ -776,12 +1022,52 @@ function materialize(authored, registration) {
776
1022
  }
777
1023
  for (const [name, definition] of Object.entries(registration.definition.props ?? {}))
778
1024
  props[name] = runtimeValue(definition.default, definition.type, false, `${authored.id}.${name}`);
779
- for (const [name, saved] of Object.entries(authored.props ?? {})) {
1025
+ const bindings = {};
1026
+ // A declared default expression, applied only where the document stored
1027
+ // nothing at all for the prop. The loop below then runs over what *was*
1028
+ // stored, so an authored value or an authored expression overwrites this —
1029
+ // which is the whole contract: a default expression is a default, and a file
1030
+ // that saved a plain number keeps its number rather than quietly animating.
1031
+ const storedProps = authored.props ?? {};
1032
+ for (const [name, definition] of Object.entries(registration.definition.props ?? {})) {
1033
+ if (!definition.expression || name in storedProps)
1034
+ continue;
1035
+ bindings[name] = {
1036
+ value: props[name],
1037
+ expression: definition.expression,
1038
+ };
1039
+ }
1040
+ for (const [name, saved] of Object.entries(storedProps)) {
780
1041
  const definition = registration.definition.props?.[name];
781
- if (definition)
782
- props[name] = runtimeValue(saved && typeof saved === "object" && "value" in saved
783
- ? saved.value
784
- : saved, definition.type, false, `${authored.id}.${name}`);
1042
+ if (!definition)
1043
+ continue;
1044
+ // `{ value, expression?, channel? }` — the object form declared in
1045
+ // contracts/animation.ts. A bare value is the same prop with nothing more
1046
+ // to say, which is what every older file on disk contains.
1047
+ const object = saved && typeof saved === "object" && "value" in saved
1048
+ ? saved
1049
+ : null;
1050
+ const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
1051
+ props[name] = value;
1052
+ // A stored plain value is an author's decision and beats the declared
1053
+ // default expression, so any binding seeded above has to go.
1054
+ if (!object) {
1055
+ delete bindings[name];
1056
+ continue;
1057
+ }
1058
+ const channel = deserializeChannel(object.channel);
1059
+ const expression = typeof object.expression === "string" && object.expression
1060
+ ? object.expression
1061
+ : undefined;
1062
+ const binding = {
1063
+ value,
1064
+ ...(expression ? { expression } : {}),
1065
+ ...(isEmptyChannel(channel) ? {} : { channel }),
1066
+ };
1067
+ // Only a prop that actually carries something, so `animated` stays a real
1068
+ // question and an unanimated graph pays nothing for this.
1069
+ if (isBoundProp(binding))
1070
+ bindings[name] = binding;
785
1071
  }
786
1072
  return {
787
1073
  id: authored.id,
@@ -791,9 +1077,43 @@ function materialize(authored, registration) {
791
1077
  definition: registration.definition,
792
1078
  inputs,
793
1079
  props,
1080
+ bindings,
1081
+ strayParams: strayParams(authored, registration),
794
1082
  outputs: new Map(),
795
1083
  };
796
1084
  }
1085
+ /**
1086
+ * The names a document stored under `params` that this runtime will not read.
1087
+ *
1088
+ * Nothing above consults `params`: the loops that fill `inputs` and `props`
1089
+ * read the definition and then the document's own `inputs` and `props`, so a
1090
+ * value left in `params` is dropped without a word and the parameter keeps its
1091
+ * default. That is not a hypothetical — on `cascade-logo` it put a seed back to
1092
+ * 7 and two rasterizers back to 1024, so three nodes wrote one file and the
1093
+ * drawing changed, with every command still reporting success.
1094
+ *
1095
+ * **Inputs count, not just props.** The first version of this checked props
1096
+ * alone and so was blind to exactly the case the conversions produce most: a
1097
+ * parameter another node drives has to be declared as an `inputs` entry,
1098
+ * because a v1 prop cannot be promoted to a pin. `field-logo`'s `angle` is one,
1099
+ * and a negative control over the pre-migration document named five nodes and
1100
+ * missed that one.
1101
+ *
1102
+ * Only names the definition actually declares are reported. A leftover key
1103
+ * naming nothing is a different fault, and reporting it would make this fire on
1104
+ * documents where no value was lost.
1105
+ */
1106
+ function strayParams(authored, registration) {
1107
+ const definition = registration.definition;
1108
+ const declared = new Set([
1109
+ ...Object.keys(definition.props ?? {}),
1110
+ ...Object.keys(definition.inputs ?? {}),
1111
+ ]);
1112
+ const names = Array.isArray(authored.params)
1113
+ ? authored.params.map((saved) => saved?.name)
1114
+ : Object.keys(authored.params ?? {});
1115
+ return names.filter((name) => typeof name === "string" && declared.has(name));
1116
+ }
797
1117
  function specializeCoreRegistration(registration, authored, nodes) {
798
1118
  if (registration.moduleId === "cascade.core.Input" ||
799
1119
  registration.moduleId === "cascade.core.Output") {
@@ -1037,6 +1357,7 @@ function validateTopology(nodes, connections) {
1037
1357
  topological(nodes, connections);
1038
1358
  }
1039
1359
  function topological(nodes, connections) {
1360
+ const byId = new Map(nodes.map((node) => [node.id, node]));
1040
1361
  const indegree = new Map(nodes.map((node) => [node.id, 0]));
1041
1362
  const outgoing = new Map(nodes.map((node) => [node.id, []]));
1042
1363
  for (const dependency of graphDependencies(nodes, connections)) {
@@ -1045,13 +1366,14 @@ function topological(nodes, connections) {
1045
1366
  }
1046
1367
  const queue = nodes.filter((node) => indegree.get(node.id) === 0);
1047
1368
  const result = [];
1048
- while (queue.length) {
1049
- const node = queue.shift();
1369
+ let queueIndex = 0;
1370
+ while (queueIndex < queue.length) {
1371
+ const node = queue[queueIndex++];
1050
1372
  result.push(node);
1051
1373
  for (const id of outgoing.get(node.id)) {
1052
1374
  indegree.set(id, indegree.get(id) - 1);
1053
1375
  if (indegree.get(id) === 0)
1054
- queue.push(nodes.find((candidate) => candidate.id === id));
1376
+ queue.push(byId.get(id));
1055
1377
  }
1056
1378
  }
1057
1379
  if (result.length !== nodes.length)