@field/cascade 0.10.0 → 0.11.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 (133) hide show
  1. package/AGENTS.md +20 -3
  2. package/CHANGELOG.md +187 -0
  3. package/README.md +1 -2
  4. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-4O7FQ__R.js} +4 -4
  5. package/dist/assets/{CodePanel-sY6sZa7B.js → CodePanel-BU-autmq.js} +2 -2
  6. package/dist/assets/{CustomNodeDialog-BRKnqZhp.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  7. package/dist/assets/{OpenGraphDialog-ZsgL-iwr.js → OpenGraphDialog-BwbRlC0X.js} +1 -1
  8. package/dist/assets/{ProjectSettings-Cw17nNe3.js → ProjectSettings-DLOXLxcA.js} +1 -1
  9. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-Bz8JxISH.js} +1 -1
  10. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-D5Z0nJ5-.js} +1 -1
  11. package/dist/assets/{core-BICrEt0y.js → core-Yqi0Ni61.js} +1 -1
  12. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-CxUzc4vp.js} +1 -1
  13. package/dist/assets/{definitions-DUww2Rt-.js → definitions-GreEMXLV.js} +1 -1
  14. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-BNFb25CX.js} +1 -1
  15. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-0ZDnWxeM.js} +1 -1
  16. package/dist/assets/geo-u0n_jOsD.js +1 -0
  17. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-Cy18MoaD.js} +1 -1
  18. package/dist/assets/{html-C7O4TyhX.js → html-B-2Dx7nq.js} +1 -1
  19. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUAc9B_d.js} +1 -1
  20. package/dist/assets/{image-DR4oeyJB.js → image-cAa-Nu9O.js} +1 -1
  21. package/dist/assets/{index-DyvDC6pS.js → index-5ZtDAuy2.js} +42 -41
  22. package/dist/assets/{javascript-CpIEK7tz.js → javascript-DeQsDqP5.js} +1 -1
  23. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-BiFwJVSk.js} +1 -1
  24. package/dist/assets/{liquid-CN6FFHZE.js → liquid-BLoHH68a.js} +1 -1
  25. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  26. package/dist/assets/math-DioIQOPJ.js +1 -0
  27. package/dist/assets/{mdx-DE89pafV.js → mdx-CFXQVNfs.js} +1 -1
  28. package/dist/assets/pop-DC_TQG9t.js +1 -0
  29. package/dist/assets/{python-ByTllTB9.js → python-Cf7S7p2E.js} +1 -1
  30. package/dist/assets/{razor-USYlRRCE.js → razor-CMHBcG-g.js} +1 -1
  31. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BEih0GDj.js} +1 -1
  32. package/dist/assets/{typescript-DInBjGQQ.js → typescript-COEKYD_a.js} +1 -1
  33. package/dist/assets/{workers-C7Ye5B7D.js → workers-BNhAhMNn.js} +1 -1
  34. package/dist/assets/{xml-U0Uocqyo.js → xml-CPiMpnPl.js} +1 -1
  35. package/dist/assets/{yaml-CBw_PVYf.js → yaml-Bgp1ITLR.js} +1 -1
  36. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  37. package/dist/cli/chunk-2KXB47FP.js +115 -0
  38. package/dist/cli/chunk-35D343DC.js +1447 -0
  39. package/dist/cli/chunk-36P5K3SD.js +631 -0
  40. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  41. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  42. package/dist/cli/chunk-53NMBJAO.js +35 -0
  43. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  44. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  45. package/dist/cli/chunk-BP7UKF36.js +376 -0
  46. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  47. package/dist/cli/chunk-CO22XHNG.js +593 -0
  48. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  49. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  50. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  51. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  52. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  53. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  54. package/dist/cli/chunk-HFHWYL55.js +139 -0
  55. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  56. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  57. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  58. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  59. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  60. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  61. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  62. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  63. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  64. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  65. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  66. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  67. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  68. package/dist/cli/chunk-SGJAET2L.js +197 -0
  69. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  70. package/dist/cli/chunk-WD62JIP6.js +118 -0
  71. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  72. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  73. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  74. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  75. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  76. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  77. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  78. package/dist/cli/commands-Q23M26QM.js +1292 -0
  79. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  80. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  81. package/dist/cli/edit-JHCPTQFT.js +28 -0
  82. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  83. package/dist/cli/encode-HAUZTYPT.js +17 -0
  84. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  85. package/dist/cli/geo-4RNK3QVN.js +43 -0
  86. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  87. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  88. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  89. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  90. package/dist/cli/index.js +100 -30756
  91. package/dist/cli/math-ZRMLKU2D.js +16 -0
  92. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  93. package/dist/cli/parity-KINARCP2.js +328 -0
  94. package/dist/cli/pop-USDFXB7H.js +39 -0
  95. package/dist/cli/project-K3QIPGBB.js +12 -0
  96. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  97. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  98. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  99. package/dist/cli/render-YURAPPUI.js +436 -0
  100. package/dist/cli/runCommand-PI764LEG.js +198 -0
  101. package/dist/cli/runner-RA6UKEPD.js +37 -0
  102. package/dist/cli/runtime/image.ts +277 -0
  103. package/dist/cli/runtime/stage.ts +4 -1
  104. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  105. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  106. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  107. package/dist/contracts/documentFormat.d.ts +18 -0
  108. package/dist/contracts/documentFormat.js +195 -0
  109. package/dist/contracts/editor.d.ts +17 -0
  110. package/dist/contracts/index.d.ts +1 -0
  111. package/dist/contracts/index.js +1 -0
  112. package/dist/image.d.ts +141 -0
  113. package/dist/image.js +127 -0
  114. package/dist/index.html +1 -1
  115. package/dist/player/editor.js +32 -7
  116. package/dist/runtime/builtins/headless.d.ts +19 -0
  117. package/dist/runtime/builtins/headless.js +25 -0
  118. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  119. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  120. package/dist/runtime/builtins/image/index.d.ts +9 -0
  121. package/dist/runtime/builtins/image/index.js +9 -0
  122. package/dist/stage.js +4 -1
  123. package/doc/CLI_REFERENCE.md +73 -2
  124. package/doc/EDITOR_EXPORT.md +11 -2
  125. package/doc/MCP.md +17 -1
  126. package/doc/NODE_REFERENCE.md +142 -0
  127. package/doc/PROJECT_AUTHORING.md +127 -7
  128. package/package.json +16 -4
  129. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  130. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  131. package/dist/assets/geo-C2RHOVHC.js +0 -1
  132. package/dist/assets/math-DaDEgWKt.js +0 -1
  133. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -83,6 +83,13 @@ my-artwork/
83
83
 
84
84
  Keep generated output in a project-specific directory such as `renders/`. Keep reusable algorithms in `nodes/` or a normal TypeScript package rather than in Studio components.
85
85
 
86
+ Before writing OKLCH conversions, a seeded generator, noise, paper, a pen plot,
87
+ an ffmpeg call, a Cairn or Observatory loader or a disk cache, use the sketch
88
+ kit, `@field/cascade-kit` (`packages/kit`, a `file:` dependency). It also
89
+ generates the stubs for using another project's nodes without a symlink
90
+ (`npx cascade-kit nodes <provider>`). Read [KIT.md](KIT.md). The kit is
91
+ outside the core: Cascade never imports it.
92
+
86
93
  ## Add a project panel
87
94
 
88
95
  A project can provide an optional dockable Studio panel without adding UI code to Cascade. Create `panels/asset-browser/index.ts`:
@@ -191,6 +198,19 @@ image-api:
191
198
 
192
199
  The proxy accepts HTTPS only, refuses undeclared hosts, strips caller-supplied authorization headers, and never returns the credential value to the page. Studio defers its initial cook when a required credential is missing and links to File → Project Settings.
193
200
 
201
+ ## Design the graph, not one big node
202
+
203
+ A piece is several nodes, each doing one job: load the data, lay it out, colour it, draw it, export it. The usage survey of November 2026 found the opposite in most projects (1 to 5 nodes of 300+ lines each over `lib/`), which leaves the graph as a parameter panel around opaque code. The graph is the record Marcus inspects, so each node boundary should be a place worth looking at, with the parameters that matter on it.
204
+
205
+ Clean graph design is clean code design:
206
+
207
+ - **One responsibility per node**, named for what it does (`layout_rings`, not `main`).
208
+ - **Data flows through typed ports**, never through module-level state or files one node writes and another reads behind the graph's back.
209
+ - **What varies is a parameter**; seeds, counts and colours are on the node, not constants in the code.
210
+ - **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse.
211
+
212
+ Split a node when it does two of load, generate, style, render and export, or when its `execute` grows past about 150 lines.
213
+
194
214
  ## Add a custom node
195
215
 
196
216
  Before writing one, check whether you need one. A parameter can hold an expression, so anything time-based is usually a parameter rather than a node. See [Animate a parameter](#animate-a-parameter). A node earns its place when it produces geometry or pixels, or when several parameters share a computation. The built-in library is listed in `NODE_REFERENCE.md`, generated from the definitions themselves; read that before searching for what a node does.
@@ -345,7 +365,7 @@ export async function execute(node: any) {
345
365
 
346
366
  `param()` is a method on the node the engine hands to `execute`, not an import. It is re-declared on every cook, so it never resets a value, an expression, or a keyframe someone set: reading `.value` gives the *resolved* value for the current frame, whichever of the three bindings is in force. The loader imports the module as real JavaScript, so top-level code does run at import time. Keep dynamic modules free of top-level effects and put interface declaration and cooking inside `execute`.
347
367
 
348
- Prefer `new OffscreenCanvas(width, height)` to `document.createElement('canvas')` when both hosts can run the node. A portable declaration still requires output parity checks; installing Skia does not override a browser-only declaration. Use Python for libraries or tools that require a server. Measure transport, serialization, and cold-start costs; persistent workers can avoid repeated process/model startup.
368
+ Prefer `new OffscreenCanvas(width, height)` to `document.createElement('canvas')` when both hosts can run the node; a definition-v1 node uses `cascade/image` (see "Draw an image in any host"). A portable declaration still requires output parity checks; installing Skia does not override a browser-only declaration. Use Python for libraries or tools that require a server. Measure transport, serialization, and cold-start costs; persistent workers can avoid repeated process/model startup.
349
369
 
350
370
  Choose the narrowest execution locus:
351
371
 
@@ -367,6 +387,64 @@ export const definition = {
367
387
 
368
388
  The runtime refuses unsupported host/capability combinations before executing the graph.
369
389
 
390
+ ### Draw an image in any host
391
+
392
+ A node that draws, reads or writes an image uses `cascade/image`, and then one
393
+ body cooks in Studio, under `cascade run`/`render`/MCP, and in an exported
394
+ editor's worker. Declare `runsOn: 'portable'` and `capabilities: ['assets']`,
395
+ and import nothing host-specific: no `node:*`, no `@napi-rs/canvas`, no
396
+ `document`.
397
+
398
+ ```ts
399
+ import type { NodeDefinition, NodeExecutionContext } from 'cascade/contracts';
400
+ import { readBytes, readImage, registerFont, surface, writeImage } from 'cascade/image';
401
+
402
+ export const definition = {
403
+ apiVersion: 1,
404
+ label: 'Poster',
405
+ runsOn: 'portable',
406
+ capabilities: ['assets'],
407
+ inputs: { photo: { kind: 'data', type: 'image' } },
408
+ outputs: { image: { kind: 'data', type: 'image' } },
409
+ props: { title: { type: 'string', default: 'Hello' }, font: { type: 'string', default: 'assets/fonts/Display.otf' } },
410
+ } as const satisfies NodeDefinition;
411
+
412
+ export async function execute(context: NodeExecutionContext<typeof definition>) {
413
+ const photo = await readImage(context, context.inputs.photo); // an image input, asset or project path
414
+ await registerFont('Display', await readBytes(context, context.props.font));
415
+ const { canvas, context: g } = surface(photo.width, photo.height); // OffscreenCanvas, CPU-backed 2D
416
+ g.drawImage(photo, 0, 0);
417
+ g.font = '96px Display';
418
+ g.fillText(context.props.title, 40, 140);
419
+ context.outputs.image.set(await writeImage(context, canvas, `${context.nodeId}.png`));
420
+ }
421
+ ```
422
+
423
+ | | Browser (Studio, exported editor) | Headless (CLI, MCP) |
424
+ | --- | --- | --- |
425
+ | `createCanvas`, `surface` | the realm's `OffscreenCanvas` | Skia's, from `@napi-rs/canvas` |
426
+ | `decodeImage`, `readImage` | `createImageBitmap` | Skia's decoder |
427
+ | `writeImage` | an in-memory generated asset | `.cascade-cache/<name>.<hash>.png` |
428
+ | `registerFont` | `FontFace` added to the page's or worker's font set | Skia's `GlobalFonts` |
429
+ | `readBytes` | shipped files only (`editor.assets`, literal asset props, frozen files) | any project file |
430
+
431
+ - The host chooses where a written image lands; never name a path or read an
432
+ output file back by a path you built. `cachePath` and `saveImage` from
433
+ `cascade/io` remain for dynamic nodes.
434
+ - `surface(w, h)` asks for a `willReadFrequently` context, which keeps Chrome
435
+ on the same software Skia rasteriser the headless host uses. Expect the same
436
+ image, not the same bytes: on commons `plotter-social` the paper, foil and
437
+ wordmark match exactly and scanline marks differ at stroke edges (mean
438
+ channel delta under 0.5 of 255 with the same fonts).
439
+ - A font only matches across hosts when both register the same bytes. A file
440
+ that is not there (a licensed face the export never ships) should fall back
441
+ to a named system face, not fail; read it with `readBytes` in a `try`.
442
+ - What has no browser form is refused, not imitated: no canvas in the realm,
443
+ or `assets` not declared, throws a sentence saying which. A node that needs
444
+ the filesystem, a network fetch or ffmpeg stays `runsOn: 'server'`; to put
445
+ its output in an editor, freeze it (`cascade freeze <graph> <node> --fields
446
+ path,size,channels,depth,space` ships the image file with the snapshot).
447
+
370
448
  ### GPU nodes today
371
449
 
372
450
  A definition-v1 node may declare `capabilities: ['gpu']`. Studio and the optional
@@ -606,12 +684,54 @@ in `cascade run --json` under `stages`, and in the run trace.
606
684
  keeps running its old code in a live Studio. Its bytecode cache is keyed by
607
685
  the code hash, because Python accepts a `.pyc` whose source kept its size
608
686
  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.
687
+ - `cache` controls Cascade's own result cache. A stored result is served only
688
+ while every file it names still has the bytes it had, so a stage that writes
689
+ a fixed path and was re-run with other inputs misses rather than serving the
690
+ overwritten file. See "The stage cache default" below.
691
+
692
+ #### The stage cache default
693
+
694
+ Since 6 Nov 2026 the cache is **on by default for the calls a node's cook
695
+ makes**, and off for everything else. A node is a pure function of its inputs,
696
+ so a stage it calls through `runStage` from `cascade/stage` is treated as pure:
697
+ same stage code, same arguments, same input file bytes, same answer. A second
698
+ identical cook is served from `.cascade/cache/stages/` without starting Python
699
+ at all; on `cloud-plots/relief.cascade` that takes a re-cook from about 4.2 s
700
+ to 0.45 s. A call from a panel (`api.runStage`) or a hand-rolled
701
+ `fetch('/api/exec')` is an action — `plot-start`, `plot-status` — and is not
702
+ cached unless you say so.
703
+
704
+ What is not pure, and must be opted out:
705
+
706
+ - a stage whose answer changes under an identical call: it reads the clock,
707
+ draws fresh randomness, queries a device or a job's progress, or calls a
708
+ network service whose answer you want fresh each time;
709
+ - a stage that reads a file its arguments do not name (a config file, a folder
710
+ it scans, a model downloaded on the side). Only files named in the arguments
711
+ are hashed into the key, so an edit to an unnamed file is invisible;
712
+ - a stage whose purpose is a side effect (copying to a named place, posting
713
+ somewhere). A result whose named files still have their bytes is served, so
714
+ a copy that already happened is not repeated; when the effect must happen
715
+ every time, opt the stage out.
716
+
717
+ How to opt out:
718
+
719
+ ```json
720
+ "cache": { "exclude": ["plot-status", "camera-grab"], "outputs": ["out", "svg_out"] }
721
+ ```
722
+
723
+ | Declaration | What is cached |
724
+ |---|---|
725
+ | absent | calls from node cooks, except `exclude` (none) |
726
+ | `{ "exclude": [...], "outputs": [...] }` | calls from node cooks, except the named stages |
727
+ | `{ "scope": "all", ... }` or `true` | every call, panel actions included, except `exclude` |
728
+ | `false` or `{ "enabled": false }` | nothing |
729
+
730
+ `CASCADE_STAGE_CACHE=off` in the environment still turns it off for one
731
+ process (`cascade parity` sets it). Every call's trace says what happened and
732
+ why: `hit`, `miss (no entry)`, `off (excluded)`, `off (not a cook call)`,
733
+ `off (not enabled)`. A project that keeps its own Python-side cache can keep
734
+ it; Cascade's sits in front, and on a hit the project's code is not called.
615
735
  - A request carries `"cascade": {"key", "code", "cache"}` beside `args` (the
616
736
  worker), or `CASCADE_STAGE_ENVELOPE` in the environment (one-shot). A
617
737
  project that keeps its own cache should stand aside when `cache` is `"off"`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Visual programming for generative art",
5
5
  "license": "MIT",
6
6
  "author": "FIELD.IO <marcus@field.io>",
@@ -43,7 +43,8 @@
43
43
  ],
44
44
  "workspaces": [
45
45
  "packages/contracts",
46
- "packages/runtime"
46
+ "packages/runtime",
47
+ "packages/kit"
47
48
  ],
48
49
  "bin": {
49
50
  "cascade": "./dist/cli/index.js"
@@ -105,6 +106,10 @@
105
106
  "types": "./dist/io.d.ts",
106
107
  "default": "./dist/io.js"
107
108
  },
109
+ "./image": {
110
+ "types": "./dist/image.d.ts",
111
+ "default": "./dist/image.js"
112
+ },
108
113
  "./gpu": {
109
114
  "types": "./dist/gpu.d.ts",
110
115
  "default": "./dist/gpu.js"
@@ -129,15 +134,22 @@
129
134
  "dev": "vite",
130
135
  "dev:client": "vite",
131
136
  "build": "node scripts/build.mjs",
132
- "build:workspaces": "npm run build --workspace @cascade/contracts && npm run build --workspace @cascade/runtime",
137
+ "build:workspaces": "npm run build --workspace @cascade/contracts && npm run build --workspace @cascade/runtime && npm run build --workspace @field/cascade-kit",
138
+ "build:kit": "npm run build --workspace @field/cascade-kit",
139
+ "test:kit": "npm run test --workspace @field/cascade-kit",
133
140
  "build:packages": "npm run build:workspaces && node scripts/copy-package-builds.mjs",
134
141
  "check:packages": "npm run check --workspace @cascade/contracts && npm run build --workspace @cascade/contracts && npm run check --workspace @cascade/runtime",
135
- "test:packages": "npm run test --workspace @cascade/contracts && npm run test --workspace @cascade/runtime",
142
+ "test:packages": "npm run test --workspace @cascade/contracts && npm run test --workspace @cascade/runtime && npm run test --workspace @field/cascade-kit",
136
143
  "build:cli": "node scripts/build.mjs --cli",
137
144
  "build:server": "npm --prefix server run build",
138
145
  "prepack": "npm run build",
139
146
  "preview": "vite preview",
140
147
  "check": "svelte-check --tsconfig ./tsconfig.json",
148
+ "verify": "node scripts/verify.mjs",
149
+ "verify:fast": "node scripts/verify.mjs --fast",
150
+ "check:architecture": "vitest run tests/architecture-",
151
+ "check:bundle": "node scripts/bundle-budget.mjs",
152
+ "check:node-reference": "tsx scripts/build-node-reference.mjs --check",
141
153
  "test": "vitest",
142
154
  "test:run": "vitest run",
143
155
  "test:coverage": "vitest run --coverage",
@@ -1,358 +0,0 @@
1
- import{At as e,Dt as t,G as n,Ot as r,Q as i,T as a,U as o,V as s,W as c,X as l,at as u,b as d,ct as f,d as p,dt as m,i as h,it as g,jt as _,l as v,lt as y,ot as b,pt as x,q as S,st as C,t as w,v as T,w as E,z as D}from"./legacy-CTEeXeqF.js";function O(e){let t=e.elements.filter(e=>!e.isAnnotation).map(e=>{let t=e;return{id:t.id,type:t.type,code:t.code,position:t.position,inputs:t.inputs.map(e=>({id:e.id,name:e.name,portType:e.portType,dataType:e.dataType,value:e.value,defaultValue:e.defaultValue})),outputs:t.outputs.map(e=>({id:e.id,name:e.name,portType:e.portType,dataType:e.dataType,value:e.value}))}}),n=e.connections.map(e=>({id:e.id,from:e.from,to:e.to,type:e.type})),r=e.elements.filter(e=>!e.isAnnotation).map(e=>e).filter(e=>e.inputs.every(e=>e.connections.length===0)).map(e=>e.id);return`
2
- const graphData = {
3
- version: '1.0.0',
4
- nodes: ${JSON.stringify(t,null,2)},
5
- connections: ${JSON.stringify(n,null,2)},
6
- entryPoints: ${JSON.stringify(r)}
7
- };
8
- `.trim()}async function k(e){return new Promise((t,n)=>{let r=new FileReader;r.onloadend=()=>{let e=r.result.split(`,`)[1];t(e)},r.onerror=n,r.readAsDataURL(e)})}async function A(e){let t=[];for(let n of e)try{let e;if(n.type===`image`&&n.data instanceof HTMLImageElement){let t=document.createElement(`canvas`);t.width=n.data.width,t.height=n.data.height;let r=t.getContext(`2d`);r?(r.drawImage(n.data,0,0),e=t.toDataURL(`image/png`)):e=n.data.src}else if(n.type===`json`)e=`data:application/json;base64,${btoa(JSON.stringify(n.data))}`;else if(n.type===`text`)e=`data:text/plain;base64,${btoa(n.data)}`;else{let t=await(await fetch(n.path)).blob(),r=await k(t);e=`data:${t.type};base64,${r}`}t.push({id:n.id,path:n.path,dataUrl:e,type:n.type})}catch(e){console.warn(`Failed to embed asset ${n.path}:`,e)}return t}async function j(e,t=`Cascade Project`){let n=await A(e.assetManager.list()),r=O(e);return`<!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="UTF-8">
12
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
- <title>${t}</title>
14
- <style>
15
- * {
16
- margin: 0;
17
- padding: 0;
18
- box-sizing: border-box;
19
- }
20
- body {
21
- width: 100vw;
22
- height: 100vh;
23
- overflow: hidden;
24
- background: #0a0a0a;
25
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
26
- }
27
- #cascade-root {
28
- width: 100%;
29
- height: 100%;
30
- }
31
- </style>
32
- </head>
33
- <body>
34
- <div id="cascade-root"></div>
35
-
36
- <script>
37
- // Minimal Cascade Runtime
38
- ${M()}
39
-
40
- // Embedded assets
41
- const ASSETS = ${JSON.stringify(n,null,2)};
42
-
43
- // Override fetch for asset loading
44
- const originalFetch = window.fetch;
45
- window.fetch = function(url) {
46
- const asset = ASSETS.find(a => url.includes(a.path) || url.includes(a.id));
47
- if (asset) {
48
- return Promise.resolve(new Response(
49
- dataUrlToBlob(asset.dataUrl)
50
- ));
51
- }
52
- return originalFetch.apply(this, arguments);
53
- };
54
-
55
- // Helper function
56
- function dataUrlToBlob(dataUrl) {
57
- const arr = dataUrl.split(',');
58
- const mime = arr[0].match(/:(.*?);/)?.[1] || 'application/octet-stream';
59
- const bstr = atob(arr[1]);
60
- let n = bstr.length;
61
- const u8arr = new Uint8Array(n);
62
- while (n--) {
63
- u8arr[n] = bstr.charCodeAt(n);
64
- }
65
- return new Blob([u8arr], { type: mime });
66
- }
67
-
68
- // Compiled graph
69
- ${r}
70
-
71
- // Start execution
72
- Cascade.run(graphData, document.getElementById('cascade-root'));
73
- <\/script>
74
- </body>
75
- </html>`}function M(){return`(function() {
76
- 'use strict';
77
-
78
- class CascadeRuntime {
79
- static run(graphData, container) {
80
- const graph = this.buildGraph(graphData);
81
-
82
- // Find entry points and start execution
83
- const entryNodes = graphData.entryPoints
84
- .map(id => graph.nodes.find(e => e.id === id))
85
- .filter(Boolean);
86
-
87
- entryNodes.forEach(node => {
88
- graph.execute(node);
89
- });
90
-
91
- // Mount scene container
92
- if (container && graph.sceneContainer) {
93
- container.appendChild(graph.sceneContainer);
94
- }
95
- }
96
-
97
- static buildGraph(graphData) {
98
- // Minimal Node class
99
- class Node {
100
- constructor(id, type) {
101
- this.id = id;
102
- this.type = type;
103
- this.inputs = [];
104
- this.outputs = [];
105
- this.position = { x: 0, y: 0 };
106
- this.code = '';
107
- }
108
-
109
- in(name, defaultValue, options = {}) {
110
- const port = {
111
- id: this.id + '_in_' + name,
112
- name,
113
- portType: name === 'trigger' ? 'trigger' : 'param',
114
- dataType: options.type || 'any',
115
- value: defaultValue,
116
- defaultValue,
117
- options,
118
- connections: []
119
- };
120
- this.inputs.push(port);
121
- return port;
122
- }
123
-
124
- out(name, portType = 'param') {
125
- const port = {
126
- id: this.id + '_out_' + name,
127
- name,
128
- portType,
129
- dataType: 'any',
130
- value: undefined,
131
- connections: [],
132
- setValue: (value) => {
133
- port.value = value;
134
- port.connections.forEach(conn => {
135
- const targetNode = graph.nodes.find(e => e.id === conn.to.nodeId);
136
- if (targetNode) {
137
- const targetPort = targetNode.inputs.find(p => p.id === conn.to.portId);
138
- if (targetPort) {
139
- targetPort.value = value;
140
- if (targetPort.onChange) targetPort.onChange(value);
141
- }
142
- }
143
- });
144
- },
145
- trigger: (props) => {
146
- port.connections.forEach(conn => {
147
- const targetNode = graph.nodes.find(e => e.id === conn.to.nodeId);
148
- if (targetNode) {
149
- const targetPort = targetNode.inputs.find(p => p.id === conn.to.portId);
150
- if (targetPort && targetPort.onTrigger) {
151
- targetPort.onTrigger(props);
152
- }
153
- }
154
- });
155
- }
156
- };
157
- this.outputs.push(port);
158
- return port;
159
- }
160
-
161
- setFunction(fn) {
162
- this.nodeFunction = fn;
163
- }
164
-
165
- async execute() {
166
- if (this.nodeFunction) {
167
- try {
168
- await this.nodeFunction(this, graph);
169
- } catch (err) {
170
- console.error('Node execution error:', err);
171
- }
172
- }
173
- }
174
-
175
- log(...args) {
176
- console.log('[' + this.id + ']', ...args);
177
- }
178
-
179
- async require(packageName, version) {
180
- return graph.packageManager.load(packageName, version);
181
- }
182
- }
183
-
184
- // Minimal PackageManager for runtime
185
- class PackageManager {
186
- constructor() {
187
- this.cache = new Map();
188
- }
189
-
190
- async load(packageName, version = 'latest') {
191
- const key = packageName + '@' + version;
192
- if (this.cache.has(key)) {
193
- return this.cache.get(key);
194
- }
195
-
196
- const versionPart = version === 'latest' ? '' : '@' + version;
197
- const url = 'https://esm.sh/' + packageName + versionPart;
198
-
199
- try {
200
- const module = await import(url);
201
- const result = module.default || module;
202
- this.cache.set(key, result);
203
- return result;
204
- } catch (error) {
205
- throw new Error('Failed to load package ' + key + ': ' + error.message);
206
- }
207
- }
208
- }
209
-
210
- // Minimal Graph class
211
- class Graph {
212
- constructor() {
213
- this.nodes = [];
214
- this.connections = [];
215
- this.sceneContainer = document.createElement('div');
216
- this.sceneContainer.id = 'cascade-scene';
217
- this.sceneContainer.style.width = '100%';
218
- this.sceneContainer.style.height = '100%';
219
- this.packageManager = new PackageManager();
220
- }
221
-
222
- addNode(type, position) {
223
- const id = 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
224
- const node = new Node(id, type);
225
- node.position = position;
226
- this.nodes.push(node);
227
- return node;
228
- }
229
-
230
- getNode(nodeId) {
231
- return this.nodes.find(n => n.id === nodeId) || null;
232
- }
233
-
234
- connect(fromPort, toPort) {
235
- // Parse port IDs to extract element IDs and indices
236
- const fromParts = fromPort.id.split('_');
237
- const toParts = toPort.id.split('_');
238
- const fromElementId = fromParts.slice(0, -2).join('_');
239
- const toElementId = toParts.slice(0, -2).join('_');
240
-
241
- const connection = {
242
- id: 'conn_' + Date.now(),
243
- from: { nodeId: fromElementId, portId: fromPort.id },
244
- to: { nodeId: toElementId, portId: toPort.id },
245
- type: fromPort.portType
246
- };
247
- this.connections.push(connection);
248
- fromPort.connections.push(connection);
249
- toPort.connections.push(connection);
250
- return connection;
251
- }
252
-
253
- execute(entryNode) {
254
- if (entryNode) {
255
- entryNode.execute();
256
- } else {
257
- this.nodes.forEach(node => {
258
- if (node.inputs.every(p => p.connections.length === 0)) {
259
- node.execute();
260
- }
261
- });
262
- }
263
- }
264
- }
265
-
266
- const graph = new Graph();
267
-
268
- // Create nodes
269
- graphData.nodes.forEach(nodeData => {
270
- const node = graph.addNode(nodeData.type, nodeData.position);
271
- node.id = nodeData.id;
272
- node.code = nodeData.code || '';
273
-
274
- // Restore ports
275
- nodeData.inputs.forEach(portData => {
276
- const port = node.in(portData.name, portData.defaultValue, { type: portData.dataType });
277
- port.id = portData.id;
278
- port.value = portData.value;
279
- });
280
-
281
- nodeData.outputs.forEach(portData => {
282
- const port = node.out(portData.name, portData.portType);
283
- port.id = portData.id;
284
- port.value = portData.value;
285
- });
286
-
287
- // Compile and set function if code exists
288
- if (node.code) {
289
- try {
290
- // Wrap code in async function to support top-level await
291
- const wrappedCode = 'return (async function(node, graph) {
292
- ' + node.code + '
293
- })(node, graph);';
294
- const nodeFunction = new Function('node', 'graph', wrappedCode);
295
- node.setFunction(nodeFunction);
296
- } catch (err) {
297
- console.warn('Failed to compile node ' + node.id + ':', err);
298
- }
299
- }
300
- });
301
-
302
- // Restore connections
303
- graphData.connections.forEach(connData => {
304
- const fromNode = graph.getNode(connData.from.nodeId);
305
- const toNode = graph.getNode(connData.to.nodeId);
306
- if (fromNode && toNode) {
307
- const fromPort = fromNode.outputs.find(p => p.id === connData.from.portId);
308
- const toPort = toNode.inputs.find(p => p.id === connData.to.portId);
309
- if (fromPort && toPort) {
310
- graph.connect(fromPort, toPort);
311
- }
312
- }
313
- });
314
-
315
- return graph;
316
- }
317
- }
318
-
319
- // Export to global
320
- window.Cascade = CascadeRuntime;
321
- })();`}function N(e,t,n=`text/html`){let r=new Blob([e],{type:n}),i=URL.createObjectURL(r),a=document.createElement(`a`);a.href=i,a.download=t,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(i)}async function P(e,t=`Cascade Project`){let n=e.assetManager.list(),r=O(e),i=M(),a=`<!DOCTYPE html>
322
- <html lang="en">
323
- <head>
324
- <meta charset="UTF-8">
325
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
326
- <title>${t}</title>
327
- <style>
328
- * {
329
- margin: 0;
330
- padding: 0;
331
- box-sizing: border-box;
332
- }
333
- body {
334
- width: 100vw;
335
- height: 100vh;
336
- overflow: hidden;
337
- background: #0a0a0a;
338
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
339
- }
340
- #cascade-root {
341
- width: 100%;
342
- height: 100%;
343
- }
344
- </style>
345
- </head>
346
- <body>
347
- <div id="cascade-root"></div>
348
-
349
- <script src="./runtime.js"><\/script>
350
- <script src="./graph.js"><\/script>
351
- <script>
352
- // Start execution
353
- Cascade.run(graphData, document.getElementById('cascade-root'));
354
- <\/script>
355
- </body>
356
- </html>`,o=`// Minimal Cascade Runtime
357
- ${i}`,s=`// Compiled graph
358
- ${r}`,c=JSON.stringify(n.map(e=>({id:e.id,path:e.path,type:e.type,size:e.size})),null,2);N(a,`index.html`,`text/html`),setTimeout(()=>{N(o,`runtime.js`,`application/javascript`)},100),setTimeout(()=>{N(s,`graph.js`,`application/javascript`)},200),setTimeout(()=>{N(c,`assets.json`,`application/json`)},300),console.log(`Folder export initiated. Download the files and organize them in a folder structure:`),console.log(`- index.html`),console.log(`- runtime.js`),console.log(`- graph.js`),console.log(`- assets.json`),console.log(`- assets/ (create this folder and add your asset files)`)}var F=n(`<p class="svelte-12jesfv">This will export your project as a standalone HTML file with all assets embedded.</p> <ul class="svelte-12jesfv"><li class="svelte-12jesfv">✅ All nodes and connections</li> <li class="svelte-12jesfv">✅ All node code</li> <li class="svelte-12jesfv">✅ All assets (embedded as base64)</li> <li class="svelte-12jesfv">✅ Minimal runtime (~50KB)</li> <li class="svelte-12jesfv">✅ Single file - easy to share</li></ul>`,1),I=n(`<p class="svelte-12jesfv">This will export your project as a folder structure with separate files.</p> <ul class="svelte-12jesfv"><li class="svelte-12jesfv">✅ index.html - Main HTML file</li> <li class="svelte-12jesfv">✅ runtime.js - Cascade runtime</li> <li class="svelte-12jesfv">✅ graph.js - Compiled graph</li> <li class="svelte-12jesfv">✅ assets.json - Asset manifest</li> <li class="svelte-12jesfv">✅ assets/ - Asset files folder</li> <li class="svelte-12jesfv">⚠️ You'll need to manually organize asset files</li></ul>`,1),L=n(`<div class="error svelte-12jesfv"> </div>`),R=n(`<div class="overlay svelte-12jesfv"><div class="dialog svelte-12jesfv" role="dialog" aria-modal="true" aria-labelledby="export-title"><div class="header svelte-12jesfv"><h2 id="export-title" class="svelte-12jesfv">Export Project</h2> <button class="close-button svelte-12jesfv" aria-label="Close">×</button></div> <div class="content svelte-12jesfv"><div class="form-group svelte-12jesfv"><label for="project-name" class="svelte-12jesfv">Project Name</label> <input id="project-name" type="text" placeholder="My Project" class="svelte-12jesfv"/></div> <div class="form-group svelte-12jesfv"><label for="export-format" class="svelte-12jesfv">Export Format</label> <select id="export-format" class="svelte-12jesfv"><option>Single HTML File (all assets embedded)</option><option>Folder Structure (separate files)</option></select></div> <div class="info svelte-12jesfv"><!></div> <!></div> <div class="footer svelte-12jesfv"><button class="cancel-button svelte-12jesfv">Cancel</button> <button class="export-button svelte-12jesfv"><!></button></div></div></div>`);function z(n,O){r(O,!1);let k=h(O,`graph`,8),A=h(O,`open`,12,!1),M=w(),z=m(`Cascade Project`),B=m(!1),V=m(null),H=m(`html`);async function U(){if(!(!k()||i(B))){x(B,!0),x(V,null);try{i(H)===`html`?N(await j(k(),i(z)),`${i(z).replace(/[^a-z0-9]/gi,`_`).toLowerCase()}.html`,`text/html`):await P(k(),i(z)),setTimeout(()=>{A(!1),M(`close`)},500)}catch(e){x(V,e.message||`Failed to export project`),console.error(`Export error:`,e)}finally{x(B,!1)}}}function W(){A(!1),M(`close`)}function G(e){e.key===`Escape`&&W()}v();var K=c();l(`keydown`,u,G);var q=C(K),J=t=>{var n=R(),r=b(n),c=b(r),u=y(b(c),2);_(c);var m=y(c,2),h=b(m),v=y(b(h),2);d(v),_(h);var C=y(h,2),w=y(b(C),2),O=b(w);O.value=O.__value=`html`;var k=y(O);k.value=k.__value=`folder`,_(w),a(w),_(C);var A=y(C,2),j=b(A),M=t=>{var n=F();e(2),o(t,n)},N=t=>{var n=I();e(2),o(t,n)};D(j,e=>{i(H)===`html`?e(M):e(N,-1)}),_(A);var P=y(A,2),G=e=>{var t=L(),n=f(t,!0);g(()=>s(n,i(V))),o(e,t)};D(P,e=>{i(V)&&e(G)}),_(m);var K=y(m,2),q=b(K),J=y(q,2),Y=b(J),X=e=>{var t=S(`Exporting...`);o(e,t)},Z=e=>{var t=S();g(()=>s(t,`Export ${i(H)===`html`?`HTML`:`Folder`}`)),o(e,t)};D(Y,e=>{i(B)?e(X):e(Z,-1)}),_(J),_(K),_(r),_(n),g(()=>{v.disabled=i(B),w.disabled=i(B),q.disabled=i(B),J.disabled=i(B)}),l(`click`,u,W),T(v,()=>i(z),e=>x(z,e)),E(w,()=>i(H),e=>x(H,e)),l(`click`,q,W),l(`click`,J,U),l(`click`,n,p(W)),o(t,n)};D(q,e=>{A()&&e(J)}),o(n,K),t()}export{z as default};
@@ -1 +0,0 @@
1
- .overlay.svelte-12jesfv{background:var(--shade-stronger);backdrop-filter:blur(4px);z-index:1000;justify-content:center;align-items:center;animation:.2s ease-out svelte-12jesfv-fadeIn;display:flex;position:fixed;inset:0}@keyframes svelte-12jesfv-fadeIn{0%{opacity:0}to{opacity:1}}.dialog.svelte-12jesfv{background:var(--surface-panel-alt);box-shadow:0 10px 40px var(--shadow);border-radius:8px;flex-direction:column;width:90%;max-width:500px;max-height:90vh;animation:.2s ease-out svelte-12jesfv-slideUp;display:flex}@keyframes svelte-12jesfv-slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header.svelte-12jesfv{border-bottom:1px solid var(--tint);justify-content:space-between;align-items:center;padding:20px;display:flex}.header.svelte-12jesfv h2:where(.svelte-12jesfv){color:var(--text-bright);margin:0;font-size:20px;font-weight:600}.close-button.svelte-12jesfv{color:var(--text-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;width:32px;height:32px;padding:0;font-size:24px;transition:all .15s;display:flex}.close-button.svelte-12jesfv:hover{background:var(--tint);color:var(--text-bright)}.content.svelte-12jesfv{flex:1;padding:20px;overflow-y:auto}.form-group.svelte-12jesfv{margin-bottom:20px}.form-group.svelte-12jesfv label:where(.svelte-12jesfv){color:var(--text-muted);margin-bottom:8px;font-size:12px;font-weight:500;display:block}.form-group.svelte-12jesfv input:where(.svelte-12jesfv){background:var(--shade-weak);border:1px solid var(--tint);width:100%;color:var(--text-bright);box-sizing:border-box;border-radius:4px;padding:10px;font-size:14px}.form-group.svelte-12jesfv input:where(.svelte-12jesfv):focus{border-color:var(--accent);outline:none}.form-group.svelte-12jesfv input:where(.svelte-12jesfv):disabled,.form-group.svelte-12jesfv select:where(.svelte-12jesfv):disabled{opacity:.5;cursor:not-allowed}.form-group.svelte-12jesfv select:where(.svelte-12jesfv){background:var(--shade-weak);border:1px solid var(--tint);width:100%;color:var(--text-bright);box-sizing:border-box;cursor:pointer;border-radius:4px;padding:10px;font-size:14px}.form-group.svelte-12jesfv select:where(.svelte-12jesfv):focus{border-color:var(--accent);outline:none}.info.svelte-12jesfv{background:var(--accent-tint-weak);border:1px solid var(--accent-tint-medium);border-radius:4px;margin-top:20px;padding:16px}.info.svelte-12jesfv p:where(.svelte-12jesfv){color:var(--text-bright);margin:0 0 12px;font-size:14px}.info.svelte-12jesfv ul:where(.svelte-12jesfv){color:var(--text-muted);margin:0;padding-left:20px;font-size:13px}.info.svelte-12jesfv li:where(.svelte-12jesfv){margin:4px 0}.error.svelte-12jesfv{background:var(--status-error-tint-weak);border:1px solid var(--status-error-tint-strong);color:var(--status-error);border-radius:4px;margin-top:16px;padding:12px;font-size:13px}.footer.svelte-12jesfv{border-top:1px solid var(--tint);justify-content:flex-end;gap:12px;padding:20px;display:flex}.cancel-button.svelte-12jesfv,.export-button.svelte-12jesfv{cursor:pointer;border:none;border-radius:4px;padding:10px 20px;font-size:14px;font-weight:500;transition:all .15s}.cancel-button.svelte-12jesfv{color:var(--text-muted);background:0 0}.cancel-button.svelte-12jesfv:hover:not(:disabled){background:var(--tint);color:var(--text-bright)}.export-button.svelte-12jesfv{background:var(--accent);color:var(--text-on-accent)}.export-button.svelte-12jesfv:hover:not(:disabled){background:var(--accent-strong)}.export-button.svelte-12jesfv:disabled,.cancel-button.svelte-12jesfv:disabled{opacity:.5;cursor:not-allowed}
@@ -1 +0,0 @@
1
- import{M as e,N as t,c as n,w as r}from"./index-DyvDC6pS.js";t(n,{assets:e});export{r as geoLibrary};
@@ -1 +0,0 @@
1
- import{N as e,s as t}from"./index-DyvDC6pS.js";e(t,{});
@@ -1 +0,0 @@
1
- import{C as e,N as t,S as n}from"./index-DyvDC6pS.js";t(e,{});export{n as popLibrary};