@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
@@ -0,0 +1,199 @@
1
+ /**
2
+ * `cascade.image.ContactSheet` — several images as one labelled grid.
3
+ *
4
+ * The selection loop is a ladder of three to five options and a pick, and the
5
+ * pick is made by looking at them side by side. So the sheet is a node rather
6
+ * than a script beside the graph: a graph can end in one, and `cascade ladder`
7
+ * lays out its variants with the same layout and drawing code below.
8
+ *
9
+ * `portable` with `assets`, like `cascade.geo.Render`: it reads each image
10
+ * through the asset capability, decodes it with `createImageBitmap` and draws
11
+ * on an `OffscreenCanvas`, which both hosts supply (the headless one through
12
+ * @napi-rs/canvas). It lives in `cascade.image.*` beside Studio's image
13
+ * library, as the first definition-v1 node there; Studio does not register it
14
+ * yet, so today it runs headless and is listed in `../headless.ts` rather
15
+ * than in the built-ins Studio bundles.
16
+ */
17
+ export const contactSheetDefinition = {
18
+ apiVersion: 1,
19
+ label: "Contact Sheet",
20
+ description: "Lay images out as one labelled grid.",
21
+ icon: "LayoutGrid",
22
+ runsOn: "portable",
23
+ capabilities: ["assets"],
24
+ inputs: {
25
+ images: { kind: "data", type: "image", variadic: true },
26
+ /** More images after the wired ones: image values or project-relative paths. */
27
+ list: { kind: "data", type: "array" },
28
+ },
29
+ outputs: {
30
+ image: { kind: "data", type: "image" },
31
+ asset: { kind: "data", type: "asset" },
32
+ },
33
+ props: {
34
+ columns: { type: "int", default: 0, min: 0, max: 64, description: "0: as square as the count allows." },
35
+ gap: { type: "int", default: 16, min: 0, max: 512 },
36
+ background: { type: "color", default: [1, 1, 1, 1], label: "Background" },
37
+ cellWidth: { type: "int", default: 0, min: 0, max: 8192, description: "0: images at their own size. Otherwise each is scaled to fit this width." },
38
+ labels: { type: "string", default: "", description: "One label per line, in image order." },
39
+ labelSize: { type: "int", default: 14, min: 6, max: 128 },
40
+ labelColor: { type: "color", default: [0.1, 0.1, 0.1, 1], label: "Label Colour" },
41
+ filename: { type: "string", default: "contact-sheet.png", label: "Filename" },
42
+ },
43
+ };
44
+ /**
45
+ * Where each image goes. By default every cell is as wide as the widest image
46
+ * and as tall as the tallest, and images keep their size. With `cellWidth`,
47
+ * cells are that wide and as tall as the tallest image needs at that width,
48
+ * and each image is scaled to fit. Either way an image is centred in its cell
49
+ * and never cropped, in whole pixels, so a sheet is the same bytes every run.
50
+ */
51
+ export function contactSheetLayout(sizes, options = {}) {
52
+ const count = sizes.length;
53
+ if (!count)
54
+ throw new Error("cascade.image.ContactSheet needs at least one image");
55
+ for (const [width, height] of sizes) {
56
+ if (!(width > 0 && height > 0))
57
+ throw new Error(`cascade.image.ContactSheet: an image is ${width}x${height}`);
58
+ }
59
+ const gap = Math.max(0, Math.round(options.gap ?? 16));
60
+ const columns = Math.min(count, options.columns && options.columns > 0 ? Math.round(options.columns) : Math.ceil(Math.sqrt(count)));
61
+ const rows = Math.ceil(count / columns);
62
+ const scaled = Boolean(options.cellWidth && options.cellWidth > 0);
63
+ const cellWidth = scaled ? Math.round(options.cellWidth) : Math.max(...sizes.map(([width]) => width));
64
+ const cellHeight = scaled
65
+ ? Math.max(...sizes.map(([width, height]) => Math.round((height * cellWidth) / width)))
66
+ : Math.max(...sizes.map(([, height]) => height));
67
+ const labelHeight = Math.max(0, Math.round(options.labelHeight ?? 0));
68
+ const cells = sizes.map(([width, height], index) => {
69
+ const column = index % columns;
70
+ const row = Math.floor(index / columns);
71
+ // Native size unless a cell width was asked for; never cropped.
72
+ const fit = Math.min(cellWidth / width, cellHeight / height);
73
+ const scale = scaled ? fit : Math.min(1, fit);
74
+ const drawnWidth = Math.max(1, Math.round(width * scale));
75
+ const drawnHeight = Math.max(1, Math.round(height * scale));
76
+ const left = gap + column * (cellWidth + gap);
77
+ const top = gap + row * (cellHeight + labelHeight + gap);
78
+ return {
79
+ x: left + Math.floor((cellWidth - drawnWidth) / 2),
80
+ y: top + Math.floor((cellHeight - drawnHeight) / 2),
81
+ width: drawnWidth,
82
+ height: drawnHeight,
83
+ labelX: left,
84
+ labelY: top + cellHeight,
85
+ };
86
+ });
87
+ return {
88
+ width: gap + columns * (cellWidth + gap),
89
+ height: gap + rows * (cellHeight + labelHeight + gap),
90
+ columns,
91
+ rows,
92
+ cell: [cellWidth, cellHeight],
93
+ labelHeight,
94
+ cells,
95
+ };
96
+ }
97
+ /** The host's canvas, read here and nowhere else (see `cascade.geo.Render`). */
98
+ function host() {
99
+ const globals = globalThis;
100
+ const Canvas = globals.OffscreenCanvas;
101
+ const createBitmap = globals.createImageBitmap;
102
+ if (!Canvas || !createBitmap || !globals.Blob) {
103
+ throw new Error("cascade.image.ContactSheet needs OffscreenCanvas and createImageBitmap, which this host does not provide");
104
+ }
105
+ const BlobType = globals.Blob;
106
+ return {
107
+ surface: (width, height) => new Canvas(width, height),
108
+ decode: (bytes) => createBitmap(new BlobType([bytes])),
109
+ };
110
+ }
111
+ function css(colour, fallback) {
112
+ const [r = 0, g = 0, b = 0, a = 1] = colour ?? fallback;
113
+ return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`;
114
+ }
115
+ /** Encoded images (PNG, JPEG, WebP) in, one PNG sheet out. */
116
+ export async function composeContactSheet(images, options = {}) {
117
+ const canvas = host();
118
+ const bitmaps = [];
119
+ try {
120
+ for (const bytes of images)
121
+ bitmaps.push(await canvas.decode(bytes));
122
+ const labels = options.labels ?? [];
123
+ const labelSize = Math.round(options.labelSize ?? 14);
124
+ const labelled = labels.some((label) => label.trim().length > 0);
125
+ const layout = contactSheetLayout(bitmaps.map((bitmap) => [bitmap.width, bitmap.height]), {
126
+ columns: options.columns,
127
+ gap: options.gap,
128
+ cellWidth: options.cellWidth,
129
+ labelHeight: labelled ? Math.round(labelSize * 1.8) : 0,
130
+ });
131
+ const surface = canvas.surface(layout.width, layout.height);
132
+ const context = surface.getContext("2d");
133
+ if (!context)
134
+ throw new Error("cascade.image.ContactSheet: no 2D context");
135
+ context.fillStyle = css(options.background, [1, 1, 1, 1]);
136
+ context.fillRect(0, 0, layout.width, layout.height);
137
+ context.imageSmoothingEnabled = true;
138
+ context.imageSmoothingQuality = "high";
139
+ layout.cells.forEach((cell, index) => context.drawImage(bitmaps[index], cell.x, cell.y, cell.width, cell.height));
140
+ if (labelled) {
141
+ context.fillStyle = css(options.labelColor, [0.1, 0.1, 0.1, 1]);
142
+ context.font = `${labelSize}px sans-serif`;
143
+ context.textBaseline = "top";
144
+ layout.cells.forEach((cell, index) => {
145
+ const label = labels[index];
146
+ if (label)
147
+ context.fillText(label, cell.labelX, cell.labelY + Math.round((layout.labelHeight - labelSize) / 2), layout.cell[0]);
148
+ });
149
+ }
150
+ const blob = await surface.convertToBlob({ type: "image/png" });
151
+ return { bytes: new Uint8Array(await blob.arrayBuffer()), layout };
152
+ }
153
+ finally {
154
+ for (const bitmap of bitmaps)
155
+ bitmap.close?.();
156
+ }
157
+ }
158
+ /** An image value or a path, as the asset reference to read it by. */
159
+ function imageReference(value) {
160
+ if (typeof value === "string" && value)
161
+ return { path: value };
162
+ const candidate = value?.path;
163
+ return typeof candidate === "string" && candidate ? { path: candidate } : undefined;
164
+ }
165
+ export async function executeContactSheet(context) {
166
+ const { props } = context;
167
+ const references = [...context.inputs.images, ...(context.inputs.list ?? [])]
168
+ .map(imageReference)
169
+ .filter((reference) => reference !== undefined);
170
+ const bytes = [];
171
+ for (const reference of references) {
172
+ bytes.push(await context.capabilities.assets.read(reference, { signal: context.signal }));
173
+ }
174
+ const { bytes: sheet, layout } = await composeContactSheet(bytes, {
175
+ columns: props.columns,
176
+ gap: props.gap,
177
+ cellWidth: props.cellWidth,
178
+ labels: props.labels ? props.labels.split("\n") : [],
179
+ labelSize: props.labelSize,
180
+ background: props.background,
181
+ labelColor: props.labelColor,
182
+ });
183
+ const asset = await context.capabilities.assets.write(sheet, { mediaType: "image/png", suggestedName: props.filename }, { signal: context.signal });
184
+ context.outputs.asset.set(asset);
185
+ context.outputs.image.set({
186
+ path: asset.path ?? props.filename,
187
+ size: [layout.width, layout.height],
188
+ channels: "rgba",
189
+ depth: "u8",
190
+ space: "srgb",
191
+ });
192
+ }
193
+ export const contactSheetRegistration = {
194
+ kind: "definition-v1",
195
+ moduleId: "cascade.image.ContactSheet",
196
+ definition: contactSheetDefinition,
197
+ loadExecute: async () => executeContactSheet,
198
+ };
199
+ //# sourceMappingURL=contactSheet.js.map
@@ -0,0 +1,9 @@
1
+ import type { DefinitionNodeRegistration } from "../../types.js";
2
+ /**
3
+ * The definition-v1 image nodes. One so far: the contact sheet a ladder of
4
+ * variants is judged on. Studio's `cascade.image.*` library (src/nodes/image)
5
+ * is the compatibility engine's and is not listed here.
6
+ */
7
+ export { composeContactSheet, contactSheetDefinition, contactSheetLayout, contactSheetRegistration, executeContactSheet, type ContactSheetOptions, type SheetLayout, } from "./contactSheet.js";
8
+ export declare const imageNodeRegistrations: readonly DefinitionNodeRegistration[];
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { contactSheetRegistration } from "./contactSheet.js";
2
+ /**
3
+ * The definition-v1 image nodes. One so far: the contact sheet a ladder of
4
+ * variants is judged on. Studio's `cascade.image.*` library (src/nodes/image)
5
+ * is the compatibility engine's and is not listed here.
6
+ */
7
+ export { composeContactSheet, contactSheetDefinition, contactSheetLayout, contactSheetRegistration, executeContactSheet, } from "./contactSheet.js";
8
+ export const imageNodeRegistrations = Object.freeze([contactSheetRegistration]);
9
+ //# sourceMappingURL=index.js.map
package/dist/stage.js CHANGED
@@ -53,7 +53,10 @@ async function overHttp(stage, args, signal) {
53
53
  const postStage = async () => fetch("/api/exec", {
54
54
  method: "POST",
55
55
  headers: { "Content-Type": "application/json", "X-Cascade-Exec-Capability": await capability() },
56
- body: JSON.stringify({ stage, args }),
56
+ // `cook`: a node's cook is a pure function of its inputs, so the server's
57
+ // default stage cache may serve it (see StageCachePolicy). A panel posts
58
+ // through its own api.runStage, without this, and is never cached unasked.
59
+ body: JSON.stringify({ stage, args, caller: "cook" }),
57
60
  signal
58
61
  });
59
62
  let response;
@@ -1,6 +1,6 @@
1
1
  # CLI reference: editing graphs
2
2
 
3
- Everything an agent needs to build or change a `.cascade` graph without hand-editing JSON. Each command is one graph concept (node, wire, parameter, subnet), checks the edit with the same code as `cascade check`, and writes the file atomically. For running, rendering and exporting, see `cascade --help`, `doc/EDITOR_EXPORT.md` and `doc/WEB_PLAYER.md`.
3
+ Everything an agent needs to build or change a `.cascade` graph without hand-editing JSON. Each command is one graph concept (node, wire, parameter, subnet), checks the edit with the same code as `cascade check`, and writes the file atomically. Running, rendering, exporting and locks are listed under [The other commands](#the-other-commands); see also `cascade --help`, `doc/EDITOR_EXPORT.md` and `doc/WEB_PLAYER.md`.
4
4
 
5
5
  ## The guarantees
6
6
 
@@ -15,7 +15,7 @@ Everything an agent needs to build or change a `.cascade` graph without hand-edi
15
15
  | --- | --- |
16
16
  | `cascade graph create <graph> [--name <title>]` | A new empty graph. Never overwrites. |
17
17
  | `cascade graph read <graph> [--in <subnet>]` | Nodes with stored parameters, subnets with their ports, wires as `a.out -> b.in`. |
18
- | `cascade graph list [dir]` | Every `.cascade` under a directory, with node and wire counts. |
18
+ | `cascade graph list [dir]` | Every `.cascade` under a directory, with node and wire counts. Skips `renders/`, `out/`, `dist/` and `node_modules/` below it. |
19
19
  | `cascade node add <graph> <type> [--name <id>] [--in <subnet>] [--set k=v]... [--at x,y] [--comment <text>]` | Add a node. `<type>` is a module id or a short name: `Circle` is `cascade.geo.Circle`, `InkVocabulary` is `project.InkVocabulary`. The id defaults to the type in kebab case (`copy-to-points`, then `copy-to-points-2`). |
20
20
  | `cascade node rm <graph> <node> [--recursive]` | Remove a node and its wires. A subnet with children needs `--recursive`. |
21
21
  | `cascade connect <graph> <node.output> <node.input> [--no-layout]` | Wire two ports. A port can be left off when the node has one on that side. An input that was fed is rewired, and the old wire is reported. |
@@ -31,6 +31,40 @@ Everything an agent needs to build or change a `.cascade` graph without hand-edi
31
31
 
32
32
  Every edit takes `--dry-run` (check, write nothing) and `--json`.
33
33
 
34
+ ## The other commands
35
+
36
+ Running, sharing, locks and Studio. `cascade --help` has their options; `cascade render --help` and `cascade lock --help` have their own.
37
+
38
+ | Command | What it does |
39
+ | --- | --- |
40
+ | `cascade [project-directory \| graph-file]` | Open Studio on a project or graph. |
41
+ | `cascade new <name>` | Create a Cascade project. |
42
+ | `cascade node <Name> [project-directory]` | Create a deterministic custom node. |
43
+ | `cascade projects [directory]` | Show or set the default projects directory. |
44
+ | `cascade serve start\|stop\|restart\|status\|logs\|install` | Run Studio as a managed background service. |
45
+ | `cascade run <graph-file> [options]` | Cook a graph headlessly; `--frames` renders a sequence. |
46
+ | `cascade render <graph-file>` | Render an animation across worker processes, then a preview video. |
47
+ | `cascade validate <graph-file>` | Validate a graph file without executing. |
48
+ | `cascade check <graph-file>` | Statically check node definitions, types and graph structure. |
49
+ | `cascade inspect <graph-file>` | Every node with its classification and full definition. |
50
+ | `cascade parity <graph-file>` | Cook headless twice and through Studio's path; report per-node differences. |
51
+ | `cascade build <graph-file> --out <new-directory>` | A standalone browser player ([WEB_PLAYER.md](WEB_PLAYER.md)). |
52
+ | `cascade export <graph-file> (--plan \| --out <dir>)` | The graph's published editor as a static site ([EDITOR_EXPORT.md](EDITOR_EXPORT.md)). |
53
+ | `cascade publish <graph-file> <node>.<prop>` | Publish a node prop as an editor parameter. |
54
+ | `cascade freeze <graph-file> <node> --fields <a,b>` | Snapshot chosen fields of a live data source for export. |
55
+ | `cascade scene <graph-file> <scene.json> --out <new-graph-file>` | Apply a scene file to a copy of the graph. |
56
+ | `cascade mcp [project-directory]` | Serve these commands as MCP tools over stdio ([MCP.md](MCP.md)). |
57
+ | `cascade lock <graph-file> <node> --name <name>` | Record a render (graph, params, seeds, inputs, code) in `locks/<id>.json`. |
58
+ | `cascade lock verify [<lock-id>...]` | Replay locks and report which still match. |
59
+ | `cascade replay <lock-id>` | Re-render a lock from its stored code and compare fingerprints. |
60
+ | `cascade locks` | List the project's locks. |
61
+ | `cascade ladder <graph-file> <node> --name <name> --vary <node.prop>=<a,b,c> \| --seeds <a,b,c>` | Cook 3 to 5 variants (values zipped across params, or graph seeds) from the stored code; write `ladders/<id>/<n>.png`, a labelled contact sheet `ladders/<id>/sheet.png` and the record `ladders/<id>.json` (per variant: graph, params, seed, inputs, fingerprint; the code and git commit once). |
62
+ | `cascade pick <ladder-id> <n> [--name <lock name>]` | At approval, write `locks/<id>.json` for variant `n` from the ladder's record, without cooking again; `cascade replay` reproduces that image pixel for pixel. |
63
+ | `cascade help node [<type>] [--project <dir>]` | A node type's inputs, props and outputs (type, default, range, description), where it runs and its capabilities, read from the installed package or the project's `nodes/`; without a type, every node type. No repository checkout needed. |
64
+ | `cascade doctor [project-directory] [--fix]` | What a headless run here will find: the declared Python environment, the stage interpreter, ffmpeg, the canvas. `--fix` builds the environment. See [Python for stages](#python-for-stages-cascadejson-python). |
65
+
66
+ Every verb is one row in `src/cli/verbs.ts`: its usage and summary for `--help`, the handler that runs it, how it treats `--json`, and the MCP tools that call it. A new verb is a row there, its handler, and its line here (`tests/cli-verbs.test.ts` checks both this page and [MCP.md](MCP.md)).
67
+
34
68
  ## `--json`, on every command
35
69
 
36
70
  Every command, not only the editing ones, prints one envelope on stdout and nothing else:
@@ -98,3 +132,40 @@ cascade check index.cascade && cascade run index.cascade --frames 1 --json
98
132
  ```
99
133
 
100
134
  Opened in Studio, the top level reads `motif → transform → render`, with the camera beside `transform`, and `motif` holds the circle and rectangle side by side above `copy-to-points` and its output.
135
+
136
+ ## Outputs as files (`run --out`)
137
+
138
+ `cascade run <graph> --out <dir>` copies each image output to `<dir>/<node>.<ext>`, or `<dir>/<node>.<port>.<ext>` for a node with more than one image output. With `--frames` the same names carry the frame: `<node>[.<port>].<frame>.<ext>`. Which outputs: every image output nothing downstream consumes (a `cascade.core.Null` passing an image counts), or those of `--node`; `--out-node <id>` (repeatable) adds a mid-graph node's. `--out` may be project-relative or absolute; `files` in the result is spelled the same way. `run --json` lists every image output as `data.images: [{ node, port, source, file? }]`, with or without `--out`. The MCP `cook` and `render` tools take `out` and `outNodes`.
139
+
140
+ ## Checks that fail a run (`cascade.assert.*`)
141
+
142
+ Put the check in the graph instead of a script beside it:
143
+
144
+ ```sh
145
+ cascade node add index.cascade NotBlank --name poster-not-blank --set 'message="the poster rendered empty"'
146
+ cascade connect index.cascade render.image poster-not-blank.image
147
+ cascade node add index.cascade cascade.assert.Range --name density-ok --set min=0.2 --set max=0.8
148
+ cascade connect index.cascade stats.density density-ok.value
149
+ cascade run index.cascade --json # exit 1: { "ok": false, "error": { "message": "Assertion failed in \"density-ok\" (cascade.assert.Range): value 0.93 is outside [0.2, 0.8]." } }
150
+ ```
151
+
152
+ `Range` (a finite number in `[min, max]`), `Equals` (JSON-equal to `expected`, numbers within `tolerance`), `True`, and `NotBlank` (a PNG or PPM that is not one flat colour within `tolerance`, nor fully transparent). Each passes its input through and runs when the graph runs whole, or when the `--node` you name is downstream of it. An image wired only into assertions is still saved by `--out`. Headless only for now; see `doc/NODE_REFERENCE.md`.
153
+
154
+ ## Debug output and environment (`.cascade/runs/`, `--env`)
155
+
156
+ Every run's trace in `.cascade/runs/<time>-<graph>.json` holds, per node, what it printed while it cooked: `console.log/info/warn/error/debug` from the node, and its Python stages' stderr, as `logs: [{ level, text }]` (500 lines per node, the rest counted in `logsDropped`). `run --json` gives the same lines as `data.logs: [{ node, level, text }]`. A failed run writes its trace too, and the error ends with `Run trace: <path>`.
157
+
158
+ `--env NAME=value` (repeatable) sets a variable for the run: Python stages, the warm stage worker and `cascade render`'s worker processes see it, so `CLOUD_ANALYST_ONLINE=1` no longer needs a shell prefix. The trace records the names under `env`, never the values. The MCP `cook` and `render` tools take `env: { NAME: "value" }`.
159
+
160
+ ## Python for stages (`cascade.json` `python`)
161
+
162
+ A project whose stages import numpy or Pillow declares them, and Cascade keeps a virtual environment for it in `.cascade-cache/python/venv` (inside the project, never system-wide):
163
+
164
+ ```json
165
+ {
166
+ "exec": { "stages": { "entrypoint": "runtime/node_cli.py" } },
167
+ "python": { "requirements": ["numpy", "Pillow>=10"] }
168
+ }
169
+ ```
170
+
171
+ `requirements` is a list of pip specifiers or a project-relative requirements file; `interpreter` (default `python3`) is what creates the venv. `cascade doctor` reports it as `current`, `missing` or `stale` (cascade.json changed since it was built); `cascade doctor --fix` creates or rebuilds it, moving an old one to `.cascade-cache/python/_archive/`. While one is declared, stages run under its interpreter instead of a bare `python`/`python3` in `exec.stages.python` (a project-relative executable there still wins), and a stage call with the environment missing or stale fails with the command that fixes it, rather than later on an `ImportError`. MCP: `doctor { "fix": true }`.
@@ -92,7 +92,8 @@ prints the same manifest without building anything.
92
92
  directory, so no `.env.local` can land in the site. It deploys to
93
93
  production, then checks that an anonymous request to
94
94
  `https://<project>.vercel.app` is refused. If it is not, it says to turn on
95
- Vercel Authentication.
95
+ Vercel Authentication. Add `--public` when the editor is meant to be open to
96
+ anyone: a readable site is then reported as intended instead of failing.
96
97
 
97
98
  ## The `editor` section
98
99
 
@@ -121,12 +122,18 @@ Studio keeps it verbatim when it saves. The types are in
121
122
  { "id": "soft", "label": "Soft", "values": { "strength": 1 }, "keep": ["colour", "font"] }
122
123
  ],
123
124
  "assets": ["assets/fonts/InterDisplay-Medium.otf"],
125
+ "fonts": [{ "family": "DM Mono", "file": "assets/fonts/DMMono-Regular.woff2", "license": "OFL-1.1", "weight": 400 }],
124
126
  "freeze": [{ "node": "moment", "output": "moment", "snapshot": "frozen/moment.json" }],
125
127
  "panel": { "module": "editor/panel.ts", "placement": "extend" },
126
128
  "inspect": true
127
129
  }
128
130
  ```
129
131
 
132
+ `fonts` ships open-licensed fonts with the site and loads them in the page
133
+ (`@font-face`) and in the engine worker (`FontFace`), so a panel or a canvas
134
+ that names the family draws with it. Each entry must say `"license": "OFL-1.1"`;
135
+ anything else, or a file under `licensed/`, is refused.
136
+
130
137
  - **Parameter `id`** is the key a scene uses. It survives renaming the node
131
138
  or the prop, so keep it stable.
132
139
  - **`control`** is one of `number`, `slider`, `int`, `bool`, `select`
@@ -216,7 +223,9 @@ The same scene gives byte-identical output in the editor and under `cascade run`
216
223
 
217
224
  - Only browser-runnable definition-v1 nodes ship. Python, shell and dynamic
218
225
  nodes are listed together as "not exportable". Port them to TypeScript, or
219
- freeze their output.
226
+ freeze their output. A node that draws ships when it uses `cascade/image`
227
+ (PROJECT_AUTHORING.md, "Draw an image in any host"); fonts and images it
228
+ reads must be shipped files (`editor.assets`, `editor.fonts`, frozen files).
220
229
  - The editor cooks in a module worker. Unchanged stages are reused
221
230
  (`run({ reuse: true })`), so only the stages after an edit re-cook, and the
222
231
  page stays responsive while they do. Values cross to the page by structured
package/doc/MCP.md CHANGED
@@ -37,8 +37,13 @@ With a global install (`npm i -g @field/cascade`), use `"command": "cascade", "a
37
37
  | `render` | `cascade run <graph> --frames … [--fps] [--out] [--node]… [--scene]` |
38
38
  | `export` | `cascade export <graph> --plan \| --out <dir>` |
39
39
  | `lock`, `replay`, `locks` | `cascade lock <graph> <node> --name`, `cascade replay <id> [--live]`, `cascade locks` |
40
+ | `ladder`, `pick` | `cascade ladder <graph> <node> --name --vary node.prop=a,b,c \| --seeds a,b,c` (`vary` is `{ "node.prop": [a, b, c] }`), `cascade pick <ladder> <n>` |
41
+ | `node_help` | `cascade help node [<type>]` (a node type's declaration; every type without one) |
42
+ | `doctor` | `cascade doctor [--fix]` (Python environment, stage interpreter, ffmpeg, canvas; `fix` builds the environment) |
40
43
 
41
- The editing tools take `dryRun`. Reads and edits run inside the server through the same functions the CLI commands call. Cooking, rendering, checking and exporting run `cascade <command> --json` as a child process, so a long render leaves nothing behind in the server and returns exactly what the CLI would print. Calls run one at a time, so two edits to one file never interleave.
44
+ The editing tools take `dryRun`. Reads and edits run inside the server through the same functions the CLI commands call. Cooking, rendering, checking, exporting and the lock tools run `cascade <command> --json` as one child process each, so a long render leaves nothing behind in the server and returns exactly what the CLI would print. Calls run one at a time, so two edits to one file never interleave.
45
+
46
+ Each tool is an `mcp` entry on its command's row in `src/cli/verbs.ts`, with its description and input schema, so a tool cannot name a command this build does not have (`tests/cli-verbs.test.ts` checks every tool against the table and this page).
42
47
 
43
48
  ## A session
44
49
 
@@ -51,3 +56,14 @@ render { "graph": "index.cascade", "frames": "1-48", "out": "renders/check
51
56
  ```
52
57
 
53
58
  See `doc/CLI_REFERENCE.md` for what each operation guarantees about diffs, placement and subnet boundaries.
59
+
60
+ ## Another project, per call
61
+
62
+ Every tool takes an optional `project`: the project directory for that call, absolute or relative to the directory the server was started in. Without it nothing changes. With it, the call's paths (`graph`, `out`, `scene`, `directory`) resolve against that project and may not leave it, and `cook`, `render`, `check` and the others run their CLI child there. So one server started in `commons` can edit and render a sketch next to it:
63
+
64
+ ```text
65
+ graph_read { "project": "/Users/me/Documents/Cascade/cloud-volumes", "graph": "index.cascade" }
66
+ cook { "project": "../cloud-volumes", "graph": "index.cascade", "out": "renders/check" }
67
+ ```
68
+
69
+ A `project` that is not a directory is refused.
@@ -589,6 +589,39 @@ Runs on `portable`.
589
589
  | --- | --- | --- | --- |
590
590
  | `geometry` | `geometry` | | |
591
591
 
592
+ ### `cascade.image.ContactSheet`
593
+
594
+ Contact Sheet — Lay images out as one labelled grid.
595
+
596
+ Runs on `portable`.
597
+
598
+ **Inputs**
599
+
600
+ | name | type | default | range |
601
+ | --- | --- | --- | --- |
602
+ | `images` | `image` | | |
603
+ | `list` | `array` | | |
604
+
605
+ **Props**
606
+
607
+ | name | type | default | range |
608
+ | --- | --- | --- | --- |
609
+ | `columns` | `int` | `0` | 0…64 |
610
+ | `gap` | `int` | `16` | 0…512 |
611
+ | `background` | `color` | `[1,1,1,1]` | |
612
+ | `cellWidth` | `int` | `0` | 0…8192 |
613
+ | `labels` | `string` | `""` | |
614
+ | `labelSize` | `int` | `14` | 6…128 |
615
+ | `labelColor` | `color` | `[0.1,0.1,0.1,1]` | |
616
+ | `filename` | `string` | `"contact-sheet.png"` | |
617
+
618
+ **Outputs**
619
+
620
+ | name | type | default | range |
621
+ | --- | --- | --- | --- |
622
+ | `image` | `image` | | |
623
+ | `asset` | `asset` | | |
624
+
592
625
  ### `cascade.math.Binary`
593
626
 
594
627
  Binary Math — Two values, one operation. Component-wise, and a scalar broadcasts against a vector.
@@ -973,6 +1006,115 @@ Runs on `portable`.
973
1006
  | --- | --- | --- | --- |
974
1007
  | `scene` | `scene` | | |
975
1008
 
1009
+ ## Assertions, headless only
1010
+
1011
+ `cascade.assert.*` fail a `cascade run` (non-zero exit, the message in
1012
+ `run --json`'s `error.message`) when their check does not hold, and pass
1013
+ their input through. The CLI registers them; Studio does not yet, and shows
1014
+ them as unresolved. An image wired only into assertions still counts as the
1015
+ graph's final image for `run --out`.
1016
+
1017
+ ### `cascade.assert.Equals`
1018
+
1019
+ Assert Equals — Fails the run unless value equals expected (JSON-equal; numbers within tolerance); passes value through
1020
+
1021
+ Runs on `portable`.
1022
+
1023
+ **Inputs**
1024
+
1025
+ | name | type | default | range |
1026
+ | --- | --- | --- | --- |
1027
+ | `value` | `any` | `null` | |
1028
+
1029
+ **Props**
1030
+
1031
+ | name | type | default | range |
1032
+ | --- | --- | --- | --- |
1033
+ | `expected` | `any` | `null` | |
1034
+ | `tolerance` | `float` | `0` | |
1035
+ | `message` | `string` | `""` | |
1036
+
1037
+ **Outputs**
1038
+
1039
+ | name | type | default | range |
1040
+ | --- | --- | --- | --- |
1041
+ | `value` | `any` | | |
1042
+
1043
+ ### `cascade.assert.NotBlank`
1044
+
1045
+ Assert Not Blank — Fails the run when an image (PNG or PPM) is one flat colour or fully transparent; passes the image through
1046
+
1047
+ Runs on `portable`.
1048
+
1049
+ **Inputs**
1050
+
1051
+ | name | type | default | range |
1052
+ | --- | --- | --- | --- |
1053
+ | `image` | `image` | | |
1054
+
1055
+ **Props**
1056
+
1057
+ | name | type | default | range |
1058
+ | --- | --- | --- | --- |
1059
+ | `tolerance` | `int` | `0` | 0…255 |
1060
+ | `message` | `string` | `""` | |
1061
+
1062
+ **Outputs**
1063
+
1064
+ | name | type | default | range |
1065
+ | --- | --- | --- | --- |
1066
+ | `image` | `image` | | |
1067
+
1068
+ ### `cascade.assert.Range`
1069
+
1070
+ Assert Range — Fails the run unless value is a finite number in [min, max]; passes value through
1071
+
1072
+ Runs on `portable`.
1073
+
1074
+ **Inputs**
1075
+
1076
+ | name | type | default | range |
1077
+ | --- | --- | --- | --- |
1078
+ | `value` | `float` | `0` | |
1079
+
1080
+ **Props**
1081
+
1082
+ | name | type | default | range |
1083
+ | --- | --- | --- | --- |
1084
+ | `min` | `float` | `0` | |
1085
+ | `max` | `float` | `1` | |
1086
+ | `message` | `string` | `""` | |
1087
+
1088
+ **Outputs**
1089
+
1090
+ | name | type | default | range |
1091
+ | --- | --- | --- | --- |
1092
+ | `value` | `float` | | |
1093
+
1094
+ ### `cascade.assert.True`
1095
+
1096
+ Assert True — Fails the run unless value is true (wire a condition, or give the prop an expression); passes value through
1097
+
1098
+ Runs on `portable`.
1099
+
1100
+ **Inputs**
1101
+
1102
+ | name | type | default | range |
1103
+ | --- | --- | --- | --- |
1104
+ | `value` | `bool` | `false` | |
1105
+
1106
+ **Props**
1107
+
1108
+ | name | type | default | range |
1109
+ | --- | --- | --- | --- |
1110
+ | `message` | `string` | `""` | |
1111
+
1112
+ **Outputs**
1113
+
1114
+ | name | type | default | range |
1115
+ | --- | --- | --- | --- |
1116
+ | `value` | `bool` | | |
1117
+
976
1118
  ## Studio node libraries
977
1119
 
978
1120
  Class-based nodes, available in Studio. Their parameters are declared in the