@field/cascade 0.8.0 → 0.10.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 (115) hide show
  1. package/AGENTS.md +17 -1
  2. package/CHANGELOG.md +214 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -25
  5. package/dist/assets/CodeEditor-DSZBpzdy.js +623 -0
  6. package/dist/assets/CodePanel-sY6sZa7B.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRKnqZhp.js} +1 -1
  8. package/dist/assets/{ExportDialog-CjTbbTZJ.js → ExportDialog-C7dVGua4.js} +2 -2
  9. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +1 -0
  10. package/dist/assets/ProjectSettings-Cw17nNe3.js +2 -0
  11. package/dist/assets/SettingsDialog-p25h3tj0.js +1 -0
  12. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-DzFLUotB.js} +2 -2
  13. package/dist/assets/{core-BInLG4gw.js → core-BICrEt0y.js} +8 -8
  14. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-D7TXw57I.js} +1 -1
  15. package/dist/assets/definitions-DUww2Rt-.js +1 -0
  16. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-DvkCO3ko.js} +1 -1
  17. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-BA2lxsF1.js} +1 -1
  18. package/dist/assets/geo-C2RHOVHC.js +1 -0
  19. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-BcDiyPd8.js} +1 -1
  20. package/dist/assets/{html-CVpSv_k9.js → html-C7O4TyhX.js} +1 -1
  21. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-WaDGF3Mj.js} +1 -1
  22. package/dist/assets/{image-BLn--oSF.js → image-DR4oeyJB.js} +3 -3
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-DyvDC6pS.js +219 -0
  25. package/dist/assets/index-client-DqYmKstU.js +1 -0
  26. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-CpIEK7tz.js} +1 -1
  27. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-B2FltW0f.js} +1 -1
  28. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  29. package/dist/assets/{liquid-efUz51Wf.js → liquid-CN6FFHZE.js} +1 -1
  30. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-QhV9XtLp.js} +1 -1
  31. package/dist/assets/math-DaDEgWKt.js +1 -0
  32. package/dist/assets/{mdx-DP_sjGVy.js → mdx-DE89pafV.js} +1 -1
  33. package/dist/assets/pop-pLH7L0PW.js +1 -0
  34. package/dist/assets/{python-aAuP3LUi.js → python-ByTllTB9.js} +1 -1
  35. package/dist/assets/{razor-7M6sMmC5.js → razor-USYlRRCE.js} +1 -1
  36. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  37. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BXpN8iUz.js} +1 -1
  38. package/dist/assets/{typescript-ddhYBGyT.js → typescript-DInBjGQQ.js} +1 -1
  39. package/dist/assets/{workers-X5la8DDT.js → workers-C7Ye5B7D.js} +1 -1
  40. package/dist/assets/{xml-BzbXa3Ps.js → xml-U0Uocqyo.js} +1 -1
  41. package/dist/assets/{yaml-CXs-VzxL.js → yaml-CBw_PVYf.js} +1 -1
  42. package/dist/cli/index.js +19978 -3423
  43. package/dist/cli/runtime/stage.ts +70 -10
  44. package/dist/contracts/bundle.d.ts +75 -0
  45. package/dist/contracts/bundle.js +112 -0
  46. package/dist/contracts/definition.d.ts +67 -4
  47. package/dist/contracts/document.d.ts +29 -0
  48. package/dist/contracts/editor.d.ts +172 -0
  49. package/dist/contracts/editor.js +2 -0
  50. package/dist/contracts/index.d.ts +2 -0
  51. package/dist/contracts/index.js +2 -0
  52. package/dist/contracts/validate.js +59 -3
  53. package/dist/contracts/values.d.ts +3 -3
  54. package/dist/contracts/values.js +1 -0
  55. package/dist/index.html +4 -4
  56. package/dist/player/app.js +17 -5
  57. package/dist/player/editor.js +1894 -0
  58. package/dist/player/editorEngine.js +477 -0
  59. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  60. package/dist/runtime/builtins/core/bundle.js +81 -0
  61. package/dist/runtime/builtins/core/index.d.ts +3 -2
  62. package/dist/runtime/builtins/core/index.js +6 -2
  63. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  64. package/dist/runtime/builtins/core/routing.js +28 -0
  65. package/dist/runtime/edit/document.d.ts +94 -0
  66. package/dist/runtime/edit/document.js +194 -0
  67. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  68. package/dist/runtime/edit/graphEditor.js +893 -0
  69. package/dist/runtime/edit/index.d.ts +12 -0
  70. package/dist/runtime/edit/index.js +6 -0
  71. package/dist/runtime/edit/layout.d.ts +50 -0
  72. package/dist/runtime/edit/layout.js +126 -0
  73. package/dist/runtime/edit/operations.d.ts +65 -0
  74. package/dist/runtime/edit/operations.js +57 -0
  75. package/dist/runtime/edit/parameters.d.ts +75 -0
  76. package/dist/runtime/edit/parameters.js +284 -0
  77. package/dist/runtime/edit/read.d.ts +35 -0
  78. package/dist/runtime/edit/read.js +65 -0
  79. package/dist/runtime/editor/index.d.ts +104 -0
  80. package/dist/runtime/editor/index.js +439 -0
  81. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  82. package/dist/runtime/expressions/bundlePaths.js +55 -0
  83. package/dist/runtime/expressions/index.d.ts +2 -0
  84. package/dist/runtime/expressions/index.js +1 -0
  85. package/dist/runtime/index.d.ts +2 -0
  86. package/dist/runtime/index.js +2 -0
  87. package/dist/runtime/params/animator.d.ts +5 -0
  88. package/dist/runtime/params/animator.js +16 -0
  89. package/dist/runtime/runtime.js +238 -12
  90. package/dist/runtime/seed.d.ts +29 -0
  91. package/dist/runtime/seed.js +52 -0
  92. package/dist/runtime/types.d.ts +12 -0
  93. package/dist/stage.js +40 -9
  94. package/dist/studio/panel.d.ts +2 -0
  95. package/doc/CLI_REFERENCE.md +100 -0
  96. package/doc/EDITOR_EXPORT.md +226 -0
  97. package/doc/HEADLESS_GPU.md +27 -0
  98. package/doc/MCP.md +53 -0
  99. package/doc/NODE_REFERENCE.md +66 -0
  100. package/doc/PROJECT_AUTHORING.md +137 -0
  101. package/package.json +15 -6
  102. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  103. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  104. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  105. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  106. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  107. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  108. package/dist/assets/geo-D2VPjAdt.js +0 -1
  109. package/dist/assets/index-CMiBdEuT.js +0 -212
  110. package/dist/assets/index-DMIuml9a.css +0 -1
  111. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  112. package/dist/assets/legacy-ZA7textG.js +0 -2
  113. package/dist/assets/math-B6hpxPJO.js +0 -1
  114. package/dist/assets/pop-CYUIjT6x.js +0 -1
  115. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -269,6 +269,58 @@ The reason is not tidiness. Two floats cannot connect to a `vec2` output, take t
269
269
 
270
270
  Keep them separate only when the components differ in kind or in range — a `width` and a `depth` that mean different things are two props, not a `vec2`.
271
271
 
272
+ #### Bundle ports
273
+
274
+ When a node would fan out more than three or four outputs that travel together, export one `bundle` instead: a single port carrying named fields. Each field is any Cascade type; an image or cache entry travels as its reference (`ImageRef`, `AssetRef`), never its pixels.
275
+
276
+ ```ts
277
+ // Producer: declares the fields it carries, and optionally a schema name.
278
+ outputs: {
279
+ format: {
280
+ kind: 'data', type: 'bundle', schema: 'format',
281
+ fields: {
282
+ size: 'vec2i',
283
+ size_mm: 'vec2',
284
+ dpi: { type: 'float', description: 'Resolution of the working canvas' },
285
+ paper: 'string',
286
+ },
287
+ },
288
+ },
289
+ // in execute:
290
+ context.outputs.format.set({ size, size_mm, dpi, paper });
291
+ ```
292
+
293
+ ```ts
294
+ // Consumer: declares the fields it reads.
295
+ inputs: {
296
+ format: { kind: 'data', type: 'bundle', schema: 'format', fields: ['size', 'paper'] },
297
+ },
298
+ // in execute:
299
+ const { size, paper } = context.inputs.format ?? {};
300
+ ```
301
+
302
+ - **`fields` on an output** maps each field to its type. A producer may set only declared fields; a declared field may be left unset. Without `fields` the bundle is untyped and nothing can check it.
303
+ - **`fields` on an input** lists the fields the node reads. The node then sees only those, and Studio re-cooks it only when one of them changed: a change to `dpi` does not re-cook a consumer that reads `size` and `paper`. Without `fields` a consumer depends on the whole bundle. Field access is not tracked at run time, so declare the selection when the node is expensive.
304
+ - **`schema`** names the bundle's shape. Two named schemas must match to connect; a port without one takes any bundle.
305
+ - **Expressions** read a field of a wired bundle input with a dotted path: `ch("format.dpi")`, `chv("format.size_mm")`, `chs("format.paper")`. The path names an input on the same node, not another node's output, so the wire is the dependency and the producer always cooks first. `ch("/spread-format/format.dpi")` is refused by `cascade check`, which says which wire to add. A field read in an expression counts as a declared field for caching.
306
+ - **`cascade.core.Pick`** reads one field (`field` prop) for a node that wants a single value, and **`cascade.core.Pack`** builds a bundle from loose values, one name per input in its comma-separated `names` prop. Pick is cheap and its output only changes when the picked field does, so it is also how a node that cannot declare fields depends on one.
307
+ - **`cascade check`** reports a field that the producer does not declare, in a consumer's `fields`, in an expression, or on a Pick, with the nearest declared name: `dot-layer.radius reads unknown bundle field "dpu" — did you mean "dpi"?`.
308
+ - **Studio** draws a bundle as one port in the neutral wire colour. Hovering it lists every field with its type and current value, and the Inspector shows each field with its own control.
309
+
310
+ A bundle passed to a Python stage is an ordinary JSON object, so the worker receives a `dict`; reference fields arrive as their small JSON form, e.g. `args["format"]["preview"]["path"]`. A stage that returns a dict can be set straight onto a bundle output:
311
+
312
+ ```ts
313
+ const result = await runStage('signals', { format: context.inputs.format });
314
+ context.outputs.signals.set(result as Record<string, unknown>);
315
+ ```
316
+
317
+ ```python
318
+ def signals(args):
319
+ fmt = args["format"] # a dict
320
+ w, h = fmt["size"]
321
+ return {"brightness": {"path": cache_png(...), "size": [w, h], ...}, "edge_count": 412}
322
+ ```
323
+
272
324
  ### dynamic
273
325
 
274
326
  A dynamic module declares its interface imperatively, inside `execute`, which runs once to discover the interface and again on every cook. There is no `definition` export; `runsOn` and `icon` are plain exports.
@@ -512,6 +564,60 @@ It may also accept inline JSON after `--args` for manual compatibility. Projects
512
564
  with expensive imports may additionally declare a newline-delimited JSON
513
565
  `worker`; Cascade keeps that process warm and correlates requests by `id`.
514
566
 
567
+ Cascade sends the worker one request at a time. When a newer edit supersedes a
568
+ running stage, Cascade withdraws it: a queued request is never sent, and the
569
+ one in flight gets `SIGINT`. A worker that catches `KeyboardInterrupt` around a
570
+ request should answer `{"id": <id>, "ok": false, "cancelled": true}` and keep
571
+ serving, so warm models survive a scrub; it should also ignore an interrupt
572
+ that arrives between requests. A worker that does not handle it exits and is
573
+ respawned on the next call, and one that has not answered within three seconds
574
+ is killed. Whatever a cancelled request returns is discarded. Write cache
575
+ entries atomically (temp file plus rename), since a stage can now be
576
+ interrupted part-way. Nodes can pass `{ signal }` as `runStage`'s third
577
+ argument; without it the signal of the cook in flight is used.
578
+
579
+ #### Stage code, cache keys and the run trace
580
+
581
+ Cascade keys every stage call on four things: the stage code (a hash of every
582
+ `.py` file under `exec.stages.sources`), the arguments with each input file
583
+ replaced by a hash of its bytes, those input hashes, and the Cascade version.
584
+ The key and the cache state are reported on every call, hit or miss: in the
585
+ server log (`[stage] ink: miss (no entry) key 3f2a… code 99dad29d… 1204 ms`),
586
+ in `cascade run --json` under `stages`, and in the run trace.
587
+
588
+ ```json
589
+ {
590
+ "exec": {
591
+ "stages": {
592
+ "entrypoint": "runtime/node_cli.py",
593
+ "worker": "runtime/worker.py",
594
+ "sources": ["runtime", "src", "shared/bridge"],
595
+ "cache": { "exclude": ["plot-status"], "outputs": ["out", "svg_out"] }
596
+ }
597
+ }
598
+ }
599
+ ```
600
+
601
+ - `sources` lists the files and folders whose `.py` files are the stage code.
602
+ Without it: the folders of the entrypoint and the worker, plus `src/`. A
603
+ symlinked folder (such as `shared/`) is followed only when it is listed.
604
+ - The warm worker fingerprints those sources when it starts and again before
605
+ every request, and restarts first when they changed, so an edited stage never
606
+ keeps running its old code in a live Studio. Its bytecode cache is keyed by
607
+ the code hash, because Python accepts a `.pyc` whose source kept its size
608
+ within the same second.
609
+ - `cache` turns on Cascade's own result cache: `true`, or an object with
610
+ `exclude` (stages whose answer changes under an identical call) and
611
+ `outputs` (arguments naming files a stage writes, which are not inputs). Off
612
+ by default. A stored result is served only while every file it names still
613
+ has the bytes it had, so a stage that writes a fixed path and was re-run with
614
+ other inputs misses rather than serving the overwritten file.
615
+ - A request carries `"cascade": {"key", "code", "cache"}` beside `args` (the
616
+ worker), or `CASCADE_STAGE_ENVELOPE` in the environment (one-shot). A
617
+ project that keeps its own cache should stand aside when `cache` is `"off"`,
618
+ which `cascade parity` sets, and may answer `"cache": "hit" | "miss"` beside
619
+ `result` so the trace shows its state too.
620
+
515
621
  ## Work with `.cascade` files
516
622
 
517
623
  A graph document stores authored elements separately from runtime state. Nodes and annotations remain flat even inside subnets:
@@ -550,6 +656,11 @@ cascade run index.cascade --frames 1-100x2 --fps 25 --out frames
550
656
  cascade run index.cascade --frames 42 --entry-node logo-1024
551
657
  ```
552
658
 
659
+ Every run writes a trace to `.cascade/runs/<time>-<graph>.json` (the last 200
660
+ are kept): each node that cooked, its time, and every stage call it made with
661
+ the key and cache state. `--verbose` prints the stage lines as they happen, and
662
+ `--json` prints the status, the seed, the trace path and the stage calls.
663
+
553
664
  `--frames` takes a range, a stepped range, or a single frame, and refuses a range it cannot parse rather than repairing it. Files are written as `<out>/<node id>.<frame>.<ext>`, zero-padded to at least four digits, into `renders/` unless `--out` says otherwise. One rule whether the graph has one output or three. Every image output with nothing connected downstream is saved, or the outputs of `--entry-node` when one is named. Cascade stops at the image sequence; a directory of numbered frames is what an encoder takes.
554
665
 
555
666
  Offline, drawing goes through Skia: the optional `@napi-rs/canvas` supplies `OffscreenCanvas`, `Image`, `Path2D` and the rest as globals in the Node process, deliberately not `document`, because that is how a node tells which host it is in. This is why a node that draws on an `OffscreenCanvas` needs no second implementation for headless runs, and why the built-in `cascade.image.*` nodes render under `cascade run` as well as in Studio. A graph that draws nothing runs without the renderer installed.
@@ -583,6 +694,32 @@ testing the exported artifact for the particular graph. Prefer `cascade build`
583
694
  for definition-v1 browser graphs, or embed `cascade/runtime` directly when an
584
695
  application needs custom capabilities and presentation.
585
696
 
697
+ ## Seeds and parity
698
+
699
+ A graph has one `seed` (an unsigned 32-bit integer, 0 when absent) and each
700
+ node gets `context.seed`, derived from it and the node id, plus
701
+ `context.random()`, a stream seeded with it that restarts at every cook. Both
702
+ hosts derive them the same way, so a node that draws with `context.random`
703
+ draws the same thing headless and in Studio. `Math.random` cannot be
704
+ reproduced by any host; pass `context.seed` on to a Python stage instead of
705
+ seeding there from the clock. `cascade run --seed N` cooks with another seed
706
+ without editing the file.
707
+
708
+ ```bash
709
+ cascade parity index.cascade # every node
710
+ cascade parity index.cascade --node ink # one node and what feeds it
711
+ cascade parity index.cascade --tolerance 2 --json
712
+ ```
713
+
714
+ `parity` cooks three times with every cache off: headless, headless again, and
715
+ through Studio's own executor (the compatibility graph and its definition
716
+ adapter) run headlessly with the same Dawn GPU and project capabilities. It
717
+ prints one row per node output with both comparisons side by side (`same`,
718
+ `within tolerance`, or the difference: pixels and the largest channel delta
719
+ for an image, both values for a number) and exits 1 when anything differs.
720
+ See [Headless GPU](HEADLESS_GPU.md#determinism) for what makes a GPU stage
721
+ repeatable.
722
+
586
723
  ## Verify a project
587
724
 
588
725
  Before committing a custom node or graph:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Visual programming for generative art",
5
5
  "license": "MIT",
6
6
  "author": "FIELD.IO <marcus@field.io>",
@@ -35,6 +35,9 @@
35
35
  "doc/PROJECT_AUTHORING.md",
36
36
  "doc/HEADLESS_GPU.md",
37
37
  "doc/WEB_PLAYER.md",
38
+ "doc/EDITOR_EXPORT.md",
39
+ "doc/CLI_REFERENCE.md",
40
+ "doc/MCP.md",
38
41
  "README.md",
39
42
  "LICENSE"
40
43
  ],
@@ -86,6 +89,10 @@
86
89
  "types": "./dist/runtime/scene/index.d.ts",
87
90
  "default": "./dist/runtime/scene/index.js"
88
91
  },
92
+ "./runtime/editor": {
93
+ "types": "./dist/runtime/editor/index.d.ts",
94
+ "default": "./dist/runtime/editor/index.js"
95
+ },
89
96
  "./runtime/definition/extract": {
90
97
  "types": "./dist/runtime/definition/extract.d.ts",
91
98
  "default": "./dist/runtime/definition/extract.js"
@@ -121,12 +128,12 @@
121
128
  "scripts": {
122
129
  "dev": "vite",
123
130
  "dev:client": "vite",
124
- "build": "vite build",
125
- "postbuild": "npm run build:cli",
126
- "build:packages": "npm run build --workspace @cascade/contracts && npm run build --workspace @cascade/runtime && node scripts/copy-package-builds.mjs",
131
+ "build": "node scripts/build.mjs",
132
+ "build:workspaces": "npm run build --workspace @cascade/contracts && npm run build --workspace @cascade/runtime",
133
+ "build:packages": "npm run build:workspaces && node scripts/copy-package-builds.mjs",
127
134
  "check:packages": "npm run check --workspace @cascade/contracts && npm run build --workspace @cascade/contracts && npm run check --workspace @cascade/runtime",
128
135
  "test:packages": "npm run test --workspace @cascade/contracts && npm run test --workspace @cascade/runtime",
129
- "build:cli": "npm run build:packages && node scripts/build-cli.mjs && npm run build:node-reference",
136
+ "build:cli": "node scripts/build.mjs --cli",
130
137
  "build:server": "npm --prefix server run build",
131
138
  "prepack": "npm run build",
132
139
  "preview": "vite preview",
@@ -136,7 +143,9 @@
136
143
  "test:coverage": "vitest run --coverage",
137
144
  "test:package": "node scripts/smoke-package.mjs",
138
145
  "test:player": "node scripts/smoke-player.mjs",
139
- "build:node-reference": "tsx scripts/build-node-reference.mjs"
146
+ "build:node-reference": "tsx scripts/build-node-reference.mjs",
147
+ "site:versions": "node site/build-versions.mjs",
148
+ "site:deploy": "node site/build-versions.mjs && vercel deploy --prod --yes --cwd site"
140
149
  },
141
150
  "dependencies": {
142
151
  "esbuild": "^0.28.2",