@field/cascade 0.10.0 → 0.12.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 (229) hide show
  1. package/AGENTS.md +25 -5
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +382 -0
  4. package/README.md +1 -2
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/Graph-OB6EWSGL.js +2975 -0
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/chunk-2JEU3MGB.js +1125 -0
  73. package/dist/cli/chunk-2KXB47FP.js +115 -0
  74. package/dist/cli/chunk-3RXL5GHB.js +816 -0
  75. package/dist/cli/chunk-53LGFPWX.js +138 -0
  76. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  77. package/dist/cli/chunk-73S4JZXK.js +336 -0
  78. package/dist/cli/chunk-7QE2ZNSN.js +121 -0
  79. package/dist/cli/chunk-BP7UKF36.js +376 -0
  80. package/dist/cli/chunk-BPODRH52.js +201 -0
  81. package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
  82. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  83. package/dist/cli/chunk-CO22XHNG.js +593 -0
  84. package/dist/cli/chunk-CTPLRSWF.js +2197 -0
  85. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  86. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  87. package/dist/cli/chunk-EZRWJ4G2.js +104 -0
  88. package/dist/cli/chunk-FWMSGKEH.js +71 -0
  89. package/dist/cli/chunk-G23A2BZZ.js +705 -0
  90. package/dist/cli/chunk-GRIN2DDP.js +1465 -0
  91. package/dist/cli/chunk-HFHWYL55.js +139 -0
  92. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  93. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  94. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  95. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  96. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  97. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  98. package/dist/cli/chunk-L6B6FUQP.js +330 -0
  99. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  100. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  101. package/dist/cli/chunk-MP653A5O.js +3766 -0
  102. package/dist/cli/chunk-NJFMDXHR.js +320 -0
  103. package/dist/cli/chunk-OM6BEME6.js +267 -0
  104. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  105. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  106. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  107. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  108. package/dist/cli/chunk-ST6LROUG.js +267 -0
  109. package/dist/cli/chunk-TJW67QSI.js +225 -0
  110. package/dist/cli/chunk-WD62JIP6.js +118 -0
  111. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  112. package/dist/cli/chunk-X4WKAOQP.js +634 -0
  113. package/dist/cli/chunk-X566FWBV.js +156 -0
  114. package/dist/cli/chunk-YH637PDL.js +317 -0
  115. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  116. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  117. package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
  118. package/dist/cli/commands-PHYFYA3F.js +1374 -0
  119. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  120. package/dist/cli/definitions-BPOPW52L.js +32 -0
  121. package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
  122. package/dist/cli/edit-S5KEQ5RA.js +28 -0
  123. package/dist/cli/editorCommands-NFR32JOY.js +41 -0
  124. package/dist/cli/encode-ICPEPMH5.js +17 -0
  125. package/dist/cli/file-3HTQHPYQ.js +41 -0
  126. package/dist/cli/filters-6ODFWYK4.js +379 -0
  127. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  128. package/dist/cli/generators-YU7GB4MD.js +210 -0
  129. package/dist/cli/geo-6ZPGKZRU.js +43 -0
  130. package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
  131. package/dist/cli/graphFile-YTGJB5F5.js +292 -0
  132. package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
  133. package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
  134. package/dist/cli/image-TXELZZK5.js +55 -0
  135. package/dist/cli/index.js +100 -30756
  136. package/dist/cli/math-RSYRQ3C6.js +16 -0
  137. package/dist/cli/mcp-JVZFQOEX.js +164 -0
  138. package/dist/cli/parity-X6DEH2DV.js +334 -0
  139. package/dist/cli/pop-B7SIIYKN.js +39 -0
  140. package/dist/cli/project-VYGIA7JX.js +12 -0
  141. package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
  142. package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
  143. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  144. package/dist/cli/render-PT4MWI6U.js +438 -0
  145. package/dist/cli/runCommand-HNPQVZMS.js +198 -0
  146. package/dist/cli/runner-5XPXVW7J.js +43 -0
  147. package/dist/cli/runtime/image.ts +302 -0
  148. package/dist/cli/runtime/io.ts +78 -0
  149. package/dist/cli/runtime/stage.ts +4 -1
  150. package/dist/cli/share-MHF6TNRK.js +166 -0
  151. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  152. package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
  153. package/dist/cli/text-MB3EKR4H.js +126 -0
  154. package/dist/cli/verbs-PI2JH5FI.js +19 -0
  155. package/dist/cli/webBuild-WPPMGGW7.js +44 -0
  156. package/dist/contracts/capabilities.d.ts +18 -1
  157. package/dist/contracts/color.d.ts +8 -0
  158. package/dist/contracts/color.js +23 -0
  159. package/dist/contracts/definition.d.ts +10 -0
  160. package/dist/contracts/documentFormat.d.ts +18 -0
  161. package/dist/contracts/documentFormat.js +195 -0
  162. package/dist/contracts/editor.d.ts +17 -0
  163. package/dist/contracts/index.d.ts +1 -0
  164. package/dist/contracts/index.js +1 -0
  165. package/dist/contracts/schema.d.ts +1 -1
  166. package/dist/contracts/schema.js +1 -0
  167. package/dist/contracts/validate.js +11 -1
  168. package/dist/contracts/values.d.ts +24 -0
  169. package/dist/image.d.ts +156 -0
  170. package/dist/image.js +161 -0
  171. package/dist/index.html +8 -2
  172. package/dist/io.d.ts +29 -0
  173. package/dist/io.js +40 -0
  174. package/dist/player/app.js +3 -2
  175. package/dist/player/editor.js +35 -9
  176. package/dist/player/editorEngine.js +3 -2
  177. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  178. package/dist/runtime/builtins/assert/checks.js +207 -0
  179. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  180. package/dist/runtime/builtins/assert/definitions.js +54 -0
  181. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  182. package/dist/runtime/builtins/assert/index.js +36 -0
  183. package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
  184. package/dist/runtime/builtins/image/contactSheet.js +150 -0
  185. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  186. package/dist/runtime/builtins/image/definitions.js +343 -0
  187. package/dist/runtime/builtins/image/file.d.ts +8 -0
  188. package/dist/runtime/builtins/image/file.js +37 -0
  189. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  190. package/dist/runtime/builtins/image/filters.js +394 -0
  191. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  192. package/dist/runtime/builtins/image/generators.js +144 -0
  193. package/dist/runtime/builtins/image/index.d.ts +952 -0
  194. package/dist/runtime/builtins/image/index.js +44 -0
  195. package/dist/runtime/builtins/image/io.d.ts +100 -0
  196. package/dist/runtime/builtins/image/io.js +142 -0
  197. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  198. package/dist/runtime/builtins/image/planar.js +274 -0
  199. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  200. package/dist/runtime/builtins/image/simplex.js +92 -0
  201. package/dist/runtime/builtins/image/text.d.ts +4 -0
  202. package/dist/runtime/builtins/image/text.js +141 -0
  203. package/dist/runtime/builtins/index.d.ts +3 -0
  204. package/dist/runtime/builtins/index.js +7 -0
  205. package/dist/runtime/gpu.d.ts +25 -0
  206. package/dist/runtime/gpu.js +40 -0
  207. package/dist/runtime/runtime.d.ts +2 -0
  208. package/dist/runtime/runtime.js +268 -44
  209. package/dist/runtime/types.d.ts +30 -0
  210. package/dist/stage.js +4 -1
  211. package/doc/CLI_REFERENCE.md +91 -2
  212. package/doc/EDITOR_EXPORT.md +37 -2
  213. package/doc/MCP.md +18 -1
  214. package/doc/NODE_REFERENCE.md +424 -10
  215. package/doc/PROJECT_AUTHORING.md +222 -7
  216. package/package.json +19 -4
  217. package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
  218. package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
  219. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  220. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  221. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
  222. package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
  223. package/dist/assets/definitions-DUww2Rt-.js +0 -1
  224. package/dist/assets/geo-C2RHOVHC.js +0 -1
  225. package/dist/assets/image-DR4oeyJB.js +0 -2062
  226. package/dist/assets/index-7vae6YFC.css +0 -1
  227. package/dist/assets/index-DyvDC6pS.js +0 -219
  228. package/dist/assets/math-DaDEgWKt.js +0 -1
  229. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -0,0 +1,1374 @@
1
+ import {
2
+ sceneDocument
3
+ } from "./chunk-73S4JZXK.js";
4
+ import {
5
+ setEmbeddedCompiler,
6
+ setProjectModuleCompiler
7
+ } from "./chunk-2KXB47FP.js";
8
+ import {
9
+ installHeadlessCanvas
10
+ } from "./chunk-EZRWJ4G2.js";
11
+ import "./chunk-3RXL5GHB.js";
12
+ import "./chunk-OM6BEME6.js";
13
+ import {
14
+ composeContactSheet
15
+ } from "./chunk-53LGFPWX.js";
16
+ import {
17
+ RunTrace,
18
+ cookProjectGraph,
19
+ installProjectIo,
20
+ installProjectStages,
21
+ moduleId,
22
+ projectModuleFile
23
+ } from "./chunk-MP653A5O.js";
24
+ import "./chunk-MI5JLQYA.js";
25
+ import "./chunk-GRIN2DDP.js";
26
+ import "./chunk-X4WKAOQP.js";
27
+ import "./chunk-DEQSEFFH.js";
28
+ import "./chunk-RXDIMXN5.js";
29
+ import "./chunk-QQPA2ZHH.js";
30
+ import "./chunk-KH4MUBKF.js";
31
+ import "./chunk-HSNOBIVT.js";
32
+ import {
33
+ graphSeed
34
+ } from "./chunk-L6B6FUQP.js";
35
+ import "./chunk-HFHWYL55.js";
36
+ import "./chunk-KBGUO27Q.js";
37
+ import {
38
+ imagePath
39
+ } from "./chunk-ICH7N5PN.js";
40
+ import "./chunk-CO22XHNG.js";
41
+ import "./chunk-LTXWBXOM.js";
42
+ import "./chunk-BZ63XQ3I.js";
43
+ import {
44
+ compileEmbedded,
45
+ compileProjectModule
46
+ } from "./chunk-EU4ZFZ7Y.js";
47
+ import "./chunk-YH637PDL.js";
48
+ import "./chunk-X566FWBV.js";
49
+ import "./chunk-WD62JIP6.js";
50
+ import {
51
+ cascadeVersion
52
+ } from "./chunk-5ORUQ6ES.js";
53
+ import {
54
+ ProjectRoot
55
+ } from "./chunk-2JEU3MGB.js";
56
+ import "./chunk-NJFMDXHR.js";
57
+ import "./chunk-J7VK7MNF.js";
58
+ import "./chunk-FWMSGKEH.js";
59
+ import "./chunk-JVOCK5W4.js";
60
+
61
+ // src/cli/locks/commands.ts
62
+ import path8 from "node:path";
63
+
64
+ // src/cli/locks/lock.ts
65
+ import { execFile as execFile2 } from "node:child_process";
66
+ import fssync3 from "node:fs";
67
+ import fs4 from "node:fs/promises";
68
+ import os from "node:os";
69
+ import path5 from "node:path";
70
+ import { fileURLToPath } from "node:url";
71
+ import { promisify as promisify2 } from "node:util";
72
+
73
+ // src/cli/locks/cook.ts
74
+ import path from "node:path";
75
+ async function cookNode(graphFile, document, nodeId, options = {}) {
76
+ const project = new ProjectRoot(path.dirname(path.resolve(graphFile)));
77
+ const trace = new RunTrace(options.traceGraph ?? path.resolve(graphFile), cascadeVersion(), graphSeed(document));
78
+ setProjectModuleCompiler(async (folder) => (await compileProjectModule(project, folder)).code);
79
+ setEmbeddedCompiler(async (code) => (await compileEmbedded(project, code)).code);
80
+ const removeStages = installProjectStages(project, { verbose: options.verbose, onTrace: (stage) => trace.recordStage(stage) });
81
+ const removeIo = installProjectIo(project);
82
+ const canvas = await installHeadlessCanvas().catch(() => void 0);
83
+ const started = performance.now();
84
+ try {
85
+ const { outputs } = await cookProjectGraph(graphFile, document, nodeId, (graph) => trace.attach(graph));
86
+ const node = outputs.get(nodeId);
87
+ if (!node) throw new Error(`No node "${nodeId}" in the graph`);
88
+ let written;
89
+ try {
90
+ written = trace.write(options.traceRoot ?? project.root);
91
+ } catch (error) {
92
+ console.warn(`Could not write the run trace: ${error instanceof Error ? error.message : error}`);
93
+ }
94
+ return { outputs: node, durationMs: Math.round(performance.now() - started), ...written ? { trace: written } : {} };
95
+ } finally {
96
+ removeStages();
97
+ removeIo();
98
+ canvas?.dispose();
99
+ setProjectModuleCompiler(null);
100
+ setEmbeddedCompiler(null);
101
+ }
102
+ }
103
+
104
+ // src/cli/locks/fingerprint.ts
105
+ import fssync from "node:fs";
106
+ import fs2 from "node:fs/promises";
107
+ import path3 from "node:path";
108
+
109
+ // src/cli/locks/store.ts
110
+ import crypto from "node:crypto";
111
+ import fs from "node:fs/promises";
112
+ import path2 from "node:path";
113
+ var OBJECTS_DIR = path2.join(".cascade", "objects");
114
+ function digest(bytes) {
115
+ return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
116
+ }
117
+ async function digestFile(file) {
118
+ return digest(await fs.readFile(file));
119
+ }
120
+ function objectPath(projectRoot, id) {
121
+ const hex = id.slice("sha256:".length);
122
+ if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error(`Not an object id: ${id}`);
123
+ return path2.join(projectRoot, OBJECTS_DIR, hex.slice(0, 2), hex.slice(2));
124
+ }
125
+ async function putObject(projectRoot, bytes) {
126
+ const id = digest(bytes);
127
+ const file = objectPath(projectRoot, id);
128
+ try {
129
+ await fs.access(file);
130
+ return id;
131
+ } catch {
132
+ }
133
+ await fs.mkdir(path2.dirname(file), { recursive: true });
134
+ const partial = `${file}.${process.pid}.partial`;
135
+ await fs.writeFile(partial, bytes);
136
+ await fs.rename(partial, file);
137
+ return id;
138
+ }
139
+ async function getObject(projectRoot, id) {
140
+ let bytes;
141
+ try {
142
+ bytes = await fs.readFile(objectPath(projectRoot, id));
143
+ } catch {
144
+ throw new Error(`Object ${short(id)} is missing from ${OBJECTS_DIR}/ \u2014 was .cascade/objects committed with the lock?`);
145
+ }
146
+ if (digest(bytes) !== id) throw new Error(`Object ${short(id)} is corrupt: its bytes no longer hash to its name`);
147
+ return bytes;
148
+ }
149
+ function short(id) {
150
+ return id.replace(/^sha256:/, "").slice(0, 12);
151
+ }
152
+ function stableJson(value, leading = []) {
153
+ const sorted = sortKeys(value);
154
+ if (!leading.length || !sorted || typeof sorted !== "object" || Array.isArray(sorted)) {
155
+ return `${JSON.stringify(sorted, null, 2)}
156
+ `;
157
+ }
158
+ const ordered = {};
159
+ for (const key of leading) if (key in sorted) ordered[key] = sorted[key];
160
+ for (const [key, entry] of Object.entries(sorted)) if (!(key in ordered)) ordered[key] = entry;
161
+ return `${JSON.stringify(ordered, null, 2)}
162
+ `;
163
+ }
164
+ function sortKeys(value) {
165
+ if (Array.isArray(value)) return value.map(sortKeys);
166
+ if (value && typeof value === "object" && !ArrayBuffer.isView(value)) {
167
+ const sorted = {};
168
+ for (const key of Object.keys(value).sort()) {
169
+ const entry = value[key];
170
+ if (entry !== void 0) sorted[key] = sortKeys(entry);
171
+ }
172
+ return sorted;
173
+ }
174
+ return value;
175
+ }
176
+
177
+ // src/cli/locks/fingerprint.ts
178
+ var RASTER = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".tif", ".tiff"]);
179
+ var TEXT = /* @__PURE__ */ new Set([".svg", ".json", ".txt", ".csv", ".xml"]);
180
+ async function fingerprintOutputs(context, outputs, ports) {
181
+ const result = {};
182
+ const names = ports?.length ? ports : [...outputs.keys()].sort();
183
+ for (const port of names) {
184
+ if (!outputs.has(port)) throw new Error(`The node has no output named "${port}" (it has: ${[...outputs.keys()].join(", ")})`);
185
+ result[port] = await fingerprintValue(context, outputs.get(port));
186
+ }
187
+ return result;
188
+ }
189
+ async function fingerprintValue(context, value) {
190
+ const file = await existingFile(context, value);
191
+ if (file) return fingerprintFile(context, file.absolute, file.relative);
192
+ const json = normalize(context.projectRoot, JSON.stringify(plain(value)) ?? "undefined");
193
+ return { kind: "value", sha256: digest(json), ...json.length <= 200 ? { value: JSON.parse(json === "undefined" ? "null" : json) } : {} };
194
+ }
195
+ async function existingFile(context, value) {
196
+ const named = imagePath(value);
197
+ if (!named || named.length > 1024 || /^[a-z]+:/i.test(named) || named.includes("\n")) return null;
198
+ const relative = named.replace(/^\.?\//, "");
199
+ try {
200
+ const absolute = path3.isAbsolute(named) ? named : context.resolve(relative);
201
+ if ((await fs2.stat(absolute)).isFile()) return { absolute, relative };
202
+ } catch {
203
+ }
204
+ return null;
205
+ }
206
+ async function fingerprintFile(context, absolute, relative) {
207
+ const extension = path3.extname(absolute).toLowerCase();
208
+ let bytes = await fs2.readFile(absolute);
209
+ if (TEXT.has(extension)) bytes = Buffer.from(normalize(context.projectRoot, bytes.toString("utf8")));
210
+ const base = { kind: "file", path: relative, sha256: digest(bytes), bytes: bytes.length };
211
+ if (!RASTER.has(extension)) return base;
212
+ const { default: sharp } = await import("sharp");
213
+ const { data, info } = await sharp(absolute, { limitInputPixels: false }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
214
+ return { ...base, pixels: digest(data), width: info.width, height: info.height };
215
+ }
216
+ function plain(value) {
217
+ if (ArrayBuffer.isView(value)) return Array.from(value);
218
+ if (value instanceof Map) return Object.fromEntries([...value].map(([key, entry]) => [String(key), plain(entry)]));
219
+ if (Array.isArray(value)) return value.map(plain);
220
+ if (value && typeof value === "object") {
221
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, plain(entry)]));
222
+ }
223
+ return value;
224
+ }
225
+ function normalize(projectRoot, text) {
226
+ let out = text;
227
+ for (const root of /* @__PURE__ */ new Set([projectRoot, safeRealpath(projectRoot)])) out = out.split(root).join("<project>");
228
+ return out;
229
+ }
230
+ function safeRealpath(file) {
231
+ try {
232
+ return fssync.realpathSync(file);
233
+ } catch {
234
+ return file;
235
+ }
236
+ }
237
+ function compareFingerprints(expected, actual) {
238
+ const differences = [];
239
+ for (const port of Object.keys(expected).sort()) {
240
+ const want = expected[port];
241
+ const got = actual[port];
242
+ if (!got) {
243
+ differences.push({ port, expected: describe(want), actual: "nothing" });
244
+ continue;
245
+ }
246
+ const same = want.kind === "file" && got.kind === "file" && want.pixels && got.pixels ? want.pixels === got.pixels : want.sha256 === got.sha256;
247
+ if (!same) differences.push({ port, expected: describe(want), actual: describe(got) });
248
+ }
249
+ return differences;
250
+ }
251
+ function describe(fingerprint) {
252
+ if (fingerprint.kind === "value") {
253
+ return fingerprint.value !== void 0 ? `${JSON.stringify(fingerprint.value)} (${short(fingerprint.sha256)})` : `value ${short(fingerprint.sha256)}`;
254
+ }
255
+ const size = fingerprint.width ? ` ${fingerprint.width}x${fingerprint.height}` : "";
256
+ const pixels = fingerprint.pixels ? `, pixels ${short(fingerprint.pixels)}` : "";
257
+ return `file ${short(fingerprint.sha256)}${size}${pixels}, ${fingerprint.bytes} bytes`;
258
+ }
259
+
260
+ // src/cli/locks/snapshot.ts
261
+ import { execFile } from "node:child_process";
262
+ import fssync2 from "node:fs";
263
+ import fs3 from "node:fs/promises";
264
+ import path4 from "node:path";
265
+ import { promisify } from "node:util";
266
+ var run = promisify(execFile);
267
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
268
+ ".ts",
269
+ ".tsx",
270
+ ".mts",
271
+ ".cts",
272
+ ".js",
273
+ ".mjs",
274
+ ".cjs",
275
+ ".svelte",
276
+ ".py",
277
+ ".wgsl",
278
+ ".glsl",
279
+ ".vert",
280
+ ".frag",
281
+ ".json"
282
+ ]);
283
+ var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
284
+ var NEVER = /* @__PURE__ */ new Set([".git", ".cascade", ".cascade-cache", "locks", "ladders", "node_modules", "__pycache__"]);
285
+ async function collectSources(projectRoot) {
286
+ const files = /* @__PURE__ */ new Map();
287
+ const skipped = [];
288
+ const visited = /* @__PURE__ */ new Set();
289
+ const visit = async (directory, prefix) => {
290
+ const real = await fs3.realpath(directory);
291
+ if (visited.has(real)) return;
292
+ visited.add(real);
293
+ for (const relative of await listCandidates(real)) {
294
+ const name = path4.posix.join(prefix, relative);
295
+ if (name.split("/").some((part) => NEVER.has(part))) continue;
296
+ const absolute = path4.join(real, relative);
297
+ let stat;
298
+ try {
299
+ stat = await fs3.stat(absolute);
300
+ } catch {
301
+ continue;
302
+ }
303
+ if (stat.isDirectory()) {
304
+ await visit(absolute, name);
305
+ continue;
306
+ }
307
+ if (!stat.isFile() || !isSource(name)) continue;
308
+ if (stat.size > MAX_SOURCE_BYTES) skipped.push(name);
309
+ else files.set(name, absolute);
310
+ }
311
+ };
312
+ await visit(projectRoot, "");
313
+ return { files, skipped: skipped.sort() };
314
+ }
315
+ function isSource(name) {
316
+ if (name.endsWith(".cascade")) return false;
317
+ return SOURCE_EXTENSIONS.has(path4.posix.extname(name).toLowerCase());
318
+ }
319
+ async function listCandidates(directory) {
320
+ try {
321
+ const { stdout } = await run("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
322
+ cwd: directory,
323
+ maxBuffer: 64 * 1024 * 1024
324
+ });
325
+ return [...new Set(stdout.split("\0").filter(Boolean))];
326
+ } catch {
327
+ const found = [];
328
+ const walk = async (relative) => {
329
+ for (const entry of await fs3.readdir(path4.join(directory, relative), { withFileTypes: true })) {
330
+ if (entry.name.startsWith(".") || NEVER.has(entry.name) || entry.name === "venv") continue;
331
+ const name = relative ? `${relative}/${entry.name}` : entry.name;
332
+ if (entry.isDirectory()) await walk(name);
333
+ else found.push(name);
334
+ }
335
+ };
336
+ await walk("");
337
+ return found;
338
+ }
339
+ }
340
+ async function hashSources(sources) {
341
+ const tree = {};
342
+ for (const name of [...sources.files.keys()].sort()) tree[name] = await digestFile(sources.files.get(name));
343
+ return tree;
344
+ }
345
+ async function storeSources(projectRoot, sources) {
346
+ const tree = {};
347
+ for (const name of [...sources.files.keys()].sort()) {
348
+ tree[name] = await putObject(projectRoot, await fs3.readFile(sources.files.get(name)));
349
+ }
350
+ const id = await putObject(projectRoot, stableJson({ format: "cascade.tree", version: 1, files: tree }, ["format", "version"]));
351
+ return { id, tree };
352
+ }
353
+ async function readTree(projectRoot, id) {
354
+ const parsed = JSON.parse((await getObject(projectRoot, id)).toString("utf8"));
355
+ if (parsed?.format !== "cascade.tree") throw new Error(`Object ${id} is not a source tree`);
356
+ return parsed.files;
357
+ }
358
+ function diffTrees(locked, other) {
359
+ const changed = [];
360
+ const removed = [];
361
+ for (const [name, id] of Object.entries(locked)) {
362
+ if (!(name in other)) removed.push(name);
363
+ else if (other[name] !== id) changed.push(name);
364
+ }
365
+ const added = Object.keys(other).filter((name) => !(name in locked));
366
+ return { changed: changed.sort(), added: added.sort(), removed: removed.sort() };
367
+ }
368
+ async function materialize(projectRoot, tree, into) {
369
+ await fs3.mkdir(into, { recursive: true });
370
+ const target = await fs3.realpath(into);
371
+ const tops = new Set(Object.keys(tree).map((name) => name.split("/")[0]));
372
+ for (const entry of await fs3.readdir(projectRoot, { withFileTypes: true })) {
373
+ if (NEVER.has(entry.name) && entry.name !== "node_modules") continue;
374
+ const source = path4.join(projectRoot, entry.name);
375
+ const destination = path4.join(target, entry.name);
376
+ const environment = (entry.name.startsWith(".") || entry.name === "node_modules") && !tops.has(entry.name);
377
+ if (environment) await fs3.symlink(source, destination);
378
+ else await cloneInto(await fs3.realpath(source), destination);
379
+ }
380
+ const live = await collectSources(projectRoot);
381
+ for (const name of live.files.keys()) {
382
+ if (!(name in tree)) await fs3.rm(path4.join(target, name), { force: true });
383
+ }
384
+ for (const [name, id] of Object.entries(tree)) {
385
+ const file = path4.join(target, name);
386
+ await fs3.mkdir(path4.dirname(file), { recursive: true });
387
+ const parent = await fs3.realpath(path4.dirname(file));
388
+ if (parent !== target && !parent.startsWith(target + path4.sep)) {
389
+ throw new Error(`Refusing to restore ${name}: its folder resolves outside the replay copy (${parent})`);
390
+ }
391
+ await fs3.rm(file, { force: true });
392
+ await fs3.writeFile(file, await getObject(projectRoot, id));
393
+ }
394
+ }
395
+ async function cloneInto(source, destination) {
396
+ if (process.platform === "darwin") {
397
+ await run("cp", ["-c", "-R", source, destination]);
398
+ return;
399
+ }
400
+ await fs3.cp(source, destination, { recursive: true, verbatimSymlinks: true, mode: fssync2.constants.COPYFILE_FICLONE });
401
+ }
402
+
403
+ // src/cli/locks/lock.ts
404
+ var run2 = promisify2(execFile2);
405
+ var LOCKS_DIR = "locks";
406
+ var PREVIEW_SIZE = 800;
407
+ var LOCK_KEY_ORDER = ["format", "version", "id", "name", "status", "note", "supersedes", "created", "graph"];
408
+ async function createLock(options) {
409
+ const graphFile = path5.resolve(options.graph);
410
+ const root = path5.dirname(graphFile);
411
+ const project = new ProjectRoot(root);
412
+ const id = lockId(options.name);
413
+ await checkLockId(root, id, options.supersedes);
414
+ const graphBytes = await fs4.readFile(graphFile);
415
+ let document = JSON.parse(graphBytes.toString("utf8"));
416
+ let scene;
417
+ if (options.scene) {
418
+ const applied = await sceneDocument(graphFile, options.scene);
419
+ for (const warning of applied.warnings) console.error(`Warning: ${warning}`);
420
+ document = applied.document;
421
+ scene = { file: displayPath(root, path5.resolve(options.scene)), object: await putObject(root, await fs4.readFile(options.scene)) };
422
+ }
423
+ if (options.seed !== void 0) document.seed = options.seed;
424
+ const graphObject = options.scene || options.seed !== void 0 ? await putObject(root, `${JSON.stringify(document, null, 2)}
425
+ `) : await putObject(root, graphBytes);
426
+ if (!document.nodes?.some((node) => node.id === options.node)) {
427
+ throw new Error(`No node "${options.node}" in ${path5.basename(graphFile)}`);
428
+ }
429
+ const sources = await collectSources(root);
430
+ const { id: treeId, tree } = await storeSources(root, sources);
431
+ const inputs = await hashInputs(project, document, options.inputs ?? []);
432
+ const cooked = options.live ? await cookIn(root, graphFile, document, options) : await cookInCopy(root, tree, path5.relative(root, graphFile), document, options, `lock-${id}`);
433
+ const preview = await writePreview(root, id, options.preview ? path5.resolve(options.preview) : firstRaster(cooked.root, cooked.ports));
434
+ if (cooked.dispose) await cooked.dispose();
435
+ const lock = {
436
+ format: "cascade.lock",
437
+ version: 1,
438
+ id,
439
+ name: options.name,
440
+ status: options.status ?? "approved",
441
+ ...options.note ? { note: options.note } : {},
442
+ ...options.supersedes ? { supersedes: options.supersedes } : {},
443
+ created: (/* @__PURE__ */ new Date()).toISOString(),
444
+ graph: { file: relativeTo(root, graphFile), node: options.node, object: graphObject, ...scene ? { scene } : {} },
445
+ params: graphParams(document),
446
+ ...publishedParams(document) ? { published: publishedParams(document) } : {},
447
+ seeds: graphSeeds(document),
448
+ inputs,
449
+ code: {
450
+ tree: treeId,
451
+ files: Object.keys(tree).length,
452
+ ...sources.skipped.length ? { skipped: sources.skipped } : {},
453
+ modules: await moduleHashes(root, document, tree),
454
+ ...await stageHashes(root, tree)
455
+ },
456
+ versions: {
457
+ cascade: await cascadeVersion2(),
458
+ project: await projectVersion(root),
459
+ node: process.version
460
+ },
461
+ host: { kind: "headless", platform: `${process.platform}-${process.arch}`, cooked: options.live ? "live" : "snapshot" },
462
+ output: { ports: cooked.ports, durationMs: cooked.durationMs, ...cooked.trace ? { trace: cooked.trace } : {} },
463
+ ...preview ? { preview } : {}
464
+ };
465
+ return { lock, file: await writeLock(root, lock) };
466
+ }
467
+ async function writeLock(root, lock) {
468
+ const lockFile = path5.join(root, LOCKS_DIR, `${lock.id}.json`);
469
+ await fs4.mkdir(path5.dirname(lockFile), { recursive: true });
470
+ try {
471
+ await fs4.writeFile(lockFile, stableJson(lock, LOCK_KEY_ORDER), { flag: "wx" });
472
+ } catch (error) {
473
+ if (error.code === "EEXIST") {
474
+ throw new Error(`${path5.relative(process.cwd(), lockFile)} exists. A lock is never rewritten; make a new one with --supersedes ${lock.id}`);
475
+ }
476
+ throw error;
477
+ }
478
+ return lockFile;
479
+ }
480
+ async function checkLockId(root, id, supersedes) {
481
+ const lockFile = path5.join(root, LOCKS_DIR, `${id}.json`);
482
+ if (await exists(lockFile)) {
483
+ throw new Error(`${path5.relative(process.cwd(), lockFile)} exists. A lock is never rewritten; make a new one with --supersedes ${id}`);
484
+ }
485
+ if (supersedes && !await exists(path5.join(root, LOCKS_DIR, `${supersedes}.json`))) {
486
+ throw new Error(`--supersedes ${supersedes}: no such lock in ${LOCKS_DIR}/`);
487
+ }
488
+ }
489
+ async function replayLock(projectRoot, lock, options = {}) {
490
+ const root = path5.resolve(projectRoot);
491
+ const project = new ProjectRoot(root);
492
+ const document = JSON.parse((await getObject(root, lock.graph.object)).toString("utf8"));
493
+ const tree = await readTree(root, lock.code.tree);
494
+ const inputsChanged = await changedInputs(project, lock.inputs);
495
+ if (inputsChanged.length && !options.allowInputChange) {
496
+ throw new Error([
497
+ `${lock.id}: ${inputsChanged.length} input ${inputsChanged.length === 1 ? "file has" : "files have"} changed since the lock, so the replay could not reproduce it:`,
498
+ ...inputsChanged.map((change) => ` ${change.path}: locked ${short(change.expected)}, now ${change.actual === "missing" ? "missing" : short(change.actual)}`),
499
+ "Restore the inputs, or pass --allow-input-change to replay anyway."
500
+ ].join("\n"));
501
+ }
502
+ const warnings = versionWarnings(lock, await cascadeVersion2());
503
+ const live = await hashSources(await collectSources(root));
504
+ const codeSinceLock = diffTrees(tree, live);
505
+ const graphFile = path5.join(root, lock.graph.file);
506
+ const ports = Object.keys(lock.output.ports);
507
+ const cooked = options.live ? await cookIn(root, graphFile, document, { node: lock.graph.node, ports, verbose: options.verbose }) : await cookInCopy(root, tree, lock.graph.file, document, { node: lock.graph.node, ports, verbose: options.verbose }, `replay-${lock.id}`);
508
+ try {
509
+ const differences = compareFingerprints(lock.output.ports, cooked.ports);
510
+ const files = [];
511
+ if (options.out) {
512
+ const out = path5.resolve(options.out);
513
+ await fs4.mkdir(out, { recursive: true });
514
+ for (const [port, fingerprint] of Object.entries(cooked.ports)) {
515
+ if (fingerprint.kind !== "file") continue;
516
+ const target = path5.join(out, `${lock.id}.${port}${path5.extname(fingerprint.path)}`);
517
+ await fs4.copyFile(cooked.resolve(fingerprint.path), target);
518
+ files.push(target);
519
+ }
520
+ }
521
+ return {
522
+ id: lock.id,
523
+ name: lock.name,
524
+ status: differences.length ? "mismatch" : "match",
525
+ cooked: options.live ? "live" : "snapshot",
526
+ differences,
527
+ ports: cooked.ports,
528
+ inputsChanged,
529
+ codeSinceLock,
530
+ warnings,
531
+ durationMs: cooked.durationMs,
532
+ ...cooked.trace ? { trace: cooked.trace } : {},
533
+ files,
534
+ ...options.keep && !options.live ? { kept: cooked.root } : {}
535
+ };
536
+ } finally {
537
+ if (cooked.dispose && !options.keep) await cooked.dispose();
538
+ }
539
+ }
540
+ async function readLock(projectRoot, idOrFile) {
541
+ const asFile = path5.resolve(idOrFile);
542
+ const file = idOrFile.endsWith(".json") && fssync3.existsSync(asFile) ? asFile : path5.join(path5.resolve(projectRoot), LOCKS_DIR, `${idOrFile}.json`);
543
+ let parsed;
544
+ try {
545
+ parsed = JSON.parse(await fs4.readFile(file, "utf8"));
546
+ } catch {
547
+ throw new Error(`No lock "${idOrFile}" (looked for ${path5.relative(process.cwd(), file) || file})`);
548
+ }
549
+ if (parsed?.format !== "cascade.lock") throw new Error(`${file} is not a Cascade lock`);
550
+ return { lock: parsed, root: path5.dirname(path5.dirname(file)) };
551
+ }
552
+ async function listLocks(projectRoot) {
553
+ const directory = path5.join(path5.resolve(projectRoot), LOCKS_DIR);
554
+ let names;
555
+ try {
556
+ names = await fs4.readdir(directory);
557
+ } catch {
558
+ return [];
559
+ }
560
+ const locks = [];
561
+ for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
562
+ try {
563
+ const parsed = JSON.parse(await fs4.readFile(path5.join(directory, name), "utf8"));
564
+ if (parsed?.format === "cascade.lock") locks.push(parsed);
565
+ } catch {
566
+ }
567
+ }
568
+ return locks.sort((a, b) => a.created.localeCompare(b.created) || a.id.localeCompare(b.id));
569
+ }
570
+ async function cookIn(root, graphFile, document, options) {
571
+ const project = new ProjectRoot(root);
572
+ const { outputs, durationMs, trace } = await cookNode(graphFile, document, options.node, options);
573
+ const resolve = (relative) => project.resolveMedia(relative);
574
+ const ports = await fingerprintOutputs({ projectRoot: root, resolve }, outputs, options.ports);
575
+ return { root, ports, durationMs, resolve, ...trace ? { trace } : {} };
576
+ }
577
+ async function cookInCopy(root, tree, graphRelative, document, options, label) {
578
+ const copy = await openCopy(root, tree, label);
579
+ try {
580
+ return { ...await copy.cook(graphRelative, document, options), dispose: copy.dispose };
581
+ } catch (error) {
582
+ await copy.dispose();
583
+ throw error;
584
+ }
585
+ }
586
+ async function openCopy(root, tree, label) {
587
+ const copy = await fs4.mkdtemp(path5.join(os.tmpdir(), `cascade-${label.replace(/[^A-Za-z0-9._-]/g, "_")}-`));
588
+ const dispose = () => fs4.rm(copy, { recursive: true, force: true });
589
+ try {
590
+ await materialize(root, tree, copy);
591
+ } catch (error) {
592
+ await dispose();
593
+ throw error;
594
+ }
595
+ const real = await fs4.realpath(copy);
596
+ return {
597
+ root: real,
598
+ dispose,
599
+ async cook(graphRelative, document, options) {
600
+ const graphFile = path5.join(real, graphRelative);
601
+ await fs4.rm(graphFile, { force: true });
602
+ await fs4.writeFile(graphFile, `${JSON.stringify(document, null, 2)}
603
+ `);
604
+ return cookIn(real, graphFile, document, {
605
+ ...options,
606
+ traceRoot: root,
607
+ traceGraph: path5.join(root, graphRelative)
608
+ });
609
+ }
610
+ };
611
+ }
612
+ function lockId(name) {
613
+ const id = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
614
+ if (!id || id.startsWith(".")) throw new Error(`"${name}" does not make a usable lock id`);
615
+ return id;
616
+ }
617
+ function compact(value) {
618
+ const json = JSON.stringify(value);
619
+ return json !== void 0 && json.length > 200 ? digest(stableJson(value)) : value;
620
+ }
621
+ function graphParams(document) {
622
+ const params = {};
623
+ for (const node of document.nodes ?? []) {
624
+ for (const [prop, value] of Object.entries(node.props ?? {})) params[`${node.id}.${prop}`] = compact(value);
625
+ }
626
+ return params;
627
+ }
628
+ function publishedParams(document) {
629
+ const published = document.editor?.parameters;
630
+ if (!published?.length) return void 0;
631
+ const nodes = new Map((document.nodes ?? []).map((node) => [node.id, node]));
632
+ const values = {};
633
+ for (const parameter of published) values[parameter.id] = compact(nodes.get(parameter.node)?.props?.[parameter.prop] ?? null);
634
+ return values;
635
+ }
636
+ function graphSeeds(document) {
637
+ const seeds = { graph: graphSeed(document) };
638
+ for (const node of document.nodes ?? []) {
639
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
640
+ if (/seed/i.test(prop)) seeds[`${node.id}.${prop}`] = value;
641
+ }
642
+ }
643
+ for (const parameter of document.editor?.parameters ?? []) {
644
+ if (parameter.control === "seed") {
645
+ seeds[`${parameter.node}.${parameter.prop}`] = document.nodes?.find((node) => node.id === parameter.node)?.props?.[parameter.prop] ?? null;
646
+ }
647
+ }
648
+ return seeds;
649
+ }
650
+ async function hashInputs(project, document, extra) {
651
+ const candidates = new Set(extra.map((file) => relativeTo(project.root, path5.resolve(file))));
652
+ const walk = (value) => {
653
+ if (typeof value === "string") {
654
+ if (value.length < 1024 && !value.includes("\n") && !/^[a-z]+:\/\//i.test(value) && /\.[A-Za-z0-9]{1,6}$/.test(value)) {
655
+ candidates.add(value.replace(/^\.?\//, ""));
656
+ }
657
+ } else if (Array.isArray(value)) value.forEach(walk);
658
+ else if (value && typeof value === "object") Object.values(value).forEach(walk);
659
+ };
660
+ for (const node of document.nodes ?? []) walk(node.props);
661
+ const inputs = {};
662
+ for (const candidate of [...candidates].sort()) {
663
+ const file = resolveInput(project, candidate);
664
+ if (!file) continue;
665
+ inputs[candidate] = await digestFile(file);
666
+ }
667
+ return inputs;
668
+ }
669
+ function resolveInput(project, relative) {
670
+ try {
671
+ const file = project.resolveMedia(relative);
672
+ return fssync3.statSync(file).isFile() ? file : null;
673
+ } catch {
674
+ return null;
675
+ }
676
+ }
677
+ async function changedInputs(project, inputs) {
678
+ const changes = [];
679
+ for (const [name, expected] of Object.entries(inputs)) {
680
+ const file = resolveInput(project, name);
681
+ const actual = file ? await digestFile(file) : "missing";
682
+ if (actual !== expected) changes.push({ path: name, expected, actual });
683
+ }
684
+ return changes;
685
+ }
686
+ async function moduleHashes(root, document, tree) {
687
+ const modules = {};
688
+ for (const node of document.nodes ?? []) {
689
+ const id = moduleId(node);
690
+ if (!id || id in modules) continue;
691
+ const { file } = await projectModuleFile(root, node, id);
692
+ if (!file) continue;
693
+ const name = relativeTo(root, file);
694
+ modules[id] = tree[name] ?? await digestFile(file);
695
+ }
696
+ return modules;
697
+ }
698
+ async function stageHashes(root, tree) {
699
+ let manifest;
700
+ try {
701
+ manifest = JSON.parse(await fs4.readFile(path5.join(root, "cascade.json"), "utf8"));
702
+ } catch {
703
+ return {};
704
+ }
705
+ const declared = manifest?.exec?.stages;
706
+ if (!declared) return {};
707
+ const stages = {};
708
+ for (const key of ["entrypoint", "worker"]) {
709
+ const name = typeof declared[key] === "string" ? declared[key].replace(/^\.\//, "") : void 0;
710
+ if (name && tree[name]) stages[name] = tree[name];
711
+ }
712
+ return Object.keys(stages).length ? { stages } : {};
713
+ }
714
+ function firstRaster(root, ports) {
715
+ for (const fingerprint of Object.values(ports)) {
716
+ if (fingerprint.kind === "file" && fingerprint.pixels) {
717
+ try {
718
+ return new ProjectRoot(root).resolveMedia(fingerprint.path);
719
+ } catch {
720
+ }
721
+ }
722
+ }
723
+ return void 0;
724
+ }
725
+ async function writePreview(root, id, source) {
726
+ if (!source) return void 0;
727
+ const { default: sharp } = await import("sharp");
728
+ const file = path5.join(LOCKS_DIR, `${id}.preview.png`);
729
+ const bytes = await sharp(source, { limitInputPixels: false }).resize(PREVIEW_SIZE, PREVIEW_SIZE, { fit: "inside", withoutEnlargement: true }).png({ compressionLevel: 9, palette: true }).toBuffer();
730
+ await fs4.mkdir(path5.join(root, LOCKS_DIR), { recursive: true });
731
+ await fs4.writeFile(path5.join(root, file), bytes);
732
+ return { file: file.split(path5.sep).join("/"), sha256: digest(bytes) };
733
+ }
734
+ async function git(cwd) {
735
+ try {
736
+ const { stdout: commit } = await run2("git", ["rev-parse", "HEAD"], { cwd });
737
+ const { stdout: status } = await run2("git", ["status", "--porcelain", "--untracked-files=no"], { cwd, maxBuffer: 16 * 1024 * 1024 });
738
+ return { commit: commit.trim(), dirty: status.trim().length > 0 };
739
+ } catch {
740
+ return {};
741
+ }
742
+ }
743
+ async function cascadeVersion2() {
744
+ let directory = path5.dirname(fileURLToPath(import.meta.url));
745
+ for (; ; ) {
746
+ try {
747
+ const manifest = JSON.parse(await fs4.readFile(path5.join(directory, "package.json"), "utf8"));
748
+ if (manifest.name === "@field/cascade") return { version: manifest.version, ...await git(directory) };
749
+ } catch {
750
+ }
751
+ const parent = path5.dirname(directory);
752
+ if (parent === directory) return {};
753
+ directory = parent;
754
+ }
755
+ }
756
+ async function projectVersion(root) {
757
+ let version;
758
+ try {
759
+ version = JSON.parse(await fs4.readFile(path5.join(root, "package.json"), "utf8")).version;
760
+ } catch {
761
+ }
762
+ return { ...version ? { version } : {}, ...await git(root) };
763
+ }
764
+ function versionWarnings(lock, current) {
765
+ const locked = lock.versions.cascade;
766
+ const warnings = [];
767
+ if (locked.version && current.version && locked.version !== current.version) {
768
+ warnings.push(`Locked with Cascade ${locked.version}, replaying with ${current.version}`);
769
+ } else if (locked.commit && current.commit && locked.commit !== current.commit) {
770
+ warnings.push(`Locked with Cascade at ${locked.commit.slice(0, 7)}, replaying at ${current.commit.slice(0, 7)}`);
771
+ }
772
+ if (lock.versions.node !== process.version) warnings.push(`Locked under Node ${lock.versions.node}, replaying under ${process.version}`);
773
+ return warnings;
774
+ }
775
+ function relativeTo(root, file) {
776
+ const relative = path5.relative(root, file);
777
+ if (relative.startsWith("..") || path5.isAbsolute(relative)) throw new Error(`${file} is outside the project (${root})`);
778
+ return relative.split(path5.sep).join("/");
779
+ }
780
+ function displayPath(root, file) {
781
+ const relative = path5.relative(root, file);
782
+ return relative.startsWith("..") || path5.isAbsolute(relative) ? file : relative.split(path5.sep).join("/");
783
+ }
784
+ async function exists(file) {
785
+ try {
786
+ await fs4.access(file);
787
+ return true;
788
+ } catch {
789
+ return false;
790
+ }
791
+ }
792
+
793
+ // src/cli/locks/ladder.ts
794
+ import fs5 from "node:fs/promises";
795
+ import path6 from "node:path";
796
+ var LADDERS_DIR = "ladders";
797
+ var MAX_VARIANTS = 12;
798
+ var LADDER_KEY_ORDER = ["format", "version", "id", "name", "note", "created", "graph", "vary", "seeds"];
799
+ async function createLadder(options) {
800
+ const graphFile = path6.resolve(options.graph);
801
+ const root = path6.dirname(graphFile);
802
+ const project = new ProjectRoot(root);
803
+ const id = lockId(options.name);
804
+ const ladderFile = path6.join(root, LADDERS_DIR, `${id}.json`);
805
+ if (await exists2(ladderFile) || await exists2(path6.join(root, LADDERS_DIR, id))) {
806
+ throw new Error(`${LADDERS_DIR}/${id} exists. A ladder is never rewritten; give this one another --name`);
807
+ }
808
+ const base = JSON.parse(await fs5.readFile(graphFile, "utf8"));
809
+ if (!base.nodes?.some((node) => node.id === options.node)) {
810
+ throw new Error(`No node "${options.node}" in ${path6.basename(graphFile)}`);
811
+ }
812
+ const plan = variantPlan(base, options.vary ?? {}, options.seeds);
813
+ const graphRelative = relativeTo(root, graphFile);
814
+ const sources = await collectSources(root);
815
+ const { id: treeId, tree } = await storeSources(root, sources);
816
+ const directory = path6.join(root, LADDERS_DIR, id);
817
+ await fs5.mkdir(directory, { recursive: true });
818
+ const copy = options.live ? void 0 : await openCopy(root, tree, `ladder-${id}`);
819
+ const variants = [];
820
+ try {
821
+ for (const [offset, step] of plan.entries()) {
822
+ const index = offset + 1;
823
+ const document = withVariant(base, step.set, step.seed);
824
+ const cookOptions = { node: options.node, ports: options.ports, verbose: options.verbose };
825
+ const cooked = copy ? await copy.cook(graphRelative, document, cookOptions) : await cookIn(root, graphFile, document, cookOptions);
826
+ const source = firstRaster(cooked.root, cooked.ports);
827
+ if (!source) throw new Error(`Variant ${index}: node "${options.node}" made no raster image to put on a sheet (ports: ${Object.keys(cooked.ports).join(", ")})`);
828
+ const image = path6.join(directory, `${String(index).padStart(2, "0")}${path6.extname(source).toLowerCase()}`);
829
+ await fs5.copyFile(source, image);
830
+ variants.push({
831
+ index,
832
+ label: variantLabel(index, step.set, options.seeds ? step.seed : void 0),
833
+ set: step.set,
834
+ seed: graphSeed(document),
835
+ graph: await putObject(root, `${JSON.stringify(document, null, 2)}
836
+ `),
837
+ params: graphParams(document),
838
+ seeds: graphSeeds(document),
839
+ inputs: await hashInputs(project, document, options.inputs ?? []),
840
+ image: relativeTo(root, image),
841
+ output: { ports: cooked.ports, durationMs: cooked.durationMs, ...cooked.trace ? { trace: cooked.trace } : {} }
842
+ });
843
+ }
844
+ } finally {
845
+ await copy?.dispose();
846
+ }
847
+ const sheet = await writeSheet(root, path6.join(directory, "sheet.png"), variants, options.columns);
848
+ const ladder = {
849
+ format: "cascade.ladder",
850
+ version: 1,
851
+ id,
852
+ name: options.name,
853
+ ...options.note ? { note: options.note } : {},
854
+ created: (/* @__PURE__ */ new Date()).toISOString(),
855
+ graph: { file: graphRelative, node: options.node, object: await putObject(root, await fs5.readFile(graphFile)) },
856
+ vary: Object.fromEntries(Object.keys(options.vary ?? {}).map((key) => [key, plan.map((step) => step.set[key])])),
857
+ ...options.seeds ? { seeds: [...options.seeds] } : {},
858
+ code: {
859
+ tree: treeId,
860
+ files: Object.keys(tree).length,
861
+ ...sources.skipped.length ? { skipped: sources.skipped } : {},
862
+ modules: await moduleHashes(root, base, tree),
863
+ ...await stageHashes(root, tree)
864
+ },
865
+ versions: { cascade: await cascadeVersion2(), project: await projectVersion(root), node: process.version },
866
+ host: { kind: "headless", platform: `${process.platform}-${process.arch}`, cooked: options.live ? "live" : "snapshot" },
867
+ sheet,
868
+ variants
869
+ };
870
+ await fs5.writeFile(ladderFile, stableJson(ladder, LADDER_KEY_ORDER), { flag: "wx" });
871
+ return { ladder, file: ladderFile };
872
+ }
873
+ function variantPlan(document, vary, seeds) {
874
+ const lengths = [...Object.values(vary).map((values) => values.length), ...seeds ? [seeds.length] : []];
875
+ if (!lengths.length) throw new Error("A ladder needs --vary <node.prop>=<a,b,c> or --seeds <a,b,c>");
876
+ const count = Math.max(...lengths);
877
+ if (count < 2) throw new Error("A ladder needs at least two variants");
878
+ if (count > MAX_VARIANTS) throw new Error(`A ladder has at most ${MAX_VARIANTS} variants (got ${count}); three to five is the usual`);
879
+ for (const [key, values] of Object.entries(vary)) {
880
+ if (values.length !== 1 && values.length !== count) throw new Error(`--vary ${key} has ${values.length} values; every list needs ${count} (or one)`);
881
+ const [nodeId, prop] = splitTarget(key);
882
+ if (!document.nodes?.some((node) => node.id === nodeId)) throw new Error(`--vary ${key}: no node "${nodeId}"`);
883
+ if (!prop) throw new Error(`--vary ${key}: name the parameter as node.prop`);
884
+ }
885
+ if (seeds && seeds.length !== 1 && seeds.length !== count) throw new Error(`--seeds has ${seeds.length} values; it needs ${count} (or one)`);
886
+ return Array.from({ length: count }, (_, index) => ({
887
+ set: Object.fromEntries(Object.entries(vary).map(([key, values]) => [key, values[values.length === 1 ? 0 : index]])),
888
+ seed: seeds ? seeds[seeds.length === 1 ? 0 : index] : graphSeed(document)
889
+ }));
890
+ }
891
+ function withVariant(document, set, seed) {
892
+ const copy = structuredClone(document);
893
+ for (const [key, value] of Object.entries(set)) {
894
+ const [nodeId, prop] = splitTarget(key);
895
+ const node = copy.nodes.find((candidate) => candidate.id === nodeId);
896
+ node.props = { ...node.props ?? {}, [prop]: value };
897
+ }
898
+ if (seed !== graphSeed(document) || document.seed !== void 0) copy.seed = seed;
899
+ return copy;
900
+ }
901
+ function splitTarget(key) {
902
+ const at = key.lastIndexOf(".");
903
+ return at <= 0 ? [key, ""] : [key.slice(0, at), key.slice(at + 1)];
904
+ }
905
+ function variantLabel(index, set, seed) {
906
+ const parts = Object.entries(set).map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
907
+ if (seed !== void 0) parts.push(`seed ${seed}`);
908
+ return [String(index), ...parts].join(" ");
909
+ }
910
+ async function writeSheet(root, file, variants, columns) {
911
+ const canvas = await installHeadlessCanvas();
912
+ try {
913
+ const images = await Promise.all(variants.map((variant) => fs5.readFile(path6.join(root, variant.image))));
914
+ const { bytes, layout } = await composeContactSheet(images.map((bytes2) => new Uint8Array(bytes2)), {
915
+ columns: columns ?? Math.min(variants.length, 5),
916
+ labels: variants.map((variant) => variant.label),
917
+ labelSize: 16
918
+ });
919
+ await fs5.writeFile(file, bytes);
920
+ return { file: relativeTo(root, file), sha256: digest(bytes), columns: layout.columns };
921
+ } finally {
922
+ canvas.dispose();
923
+ }
924
+ }
925
+ async function readLadder(projectRoot, idOrFile) {
926
+ const asFile = path6.resolve(idOrFile);
927
+ const file = idOrFile.endsWith(".json") && await exists2(asFile) ? asFile : path6.join(path6.resolve(projectRoot), LADDERS_DIR, `${idOrFile}.json`);
928
+ let parsed;
929
+ try {
930
+ parsed = JSON.parse(await fs5.readFile(file, "utf8"));
931
+ } catch {
932
+ throw new Error(`No ladder "${idOrFile}" (looked for ${path6.relative(process.cwd(), file) || file})`);
933
+ }
934
+ if (parsed?.format !== "cascade.ladder") throw new Error(`${file} is not a Cascade ladder`);
935
+ return { ladder: parsed, root: path6.dirname(path6.dirname(file)), file };
936
+ }
937
+ async function pickVariant(projectRoot, ladderIdOrFile, index, options = {}) {
938
+ const { ladder, root, file: ladderFile } = await readLadder(projectRoot, ladderIdOrFile);
939
+ const variant = ladder.variants.find((candidate) => candidate.index === index);
940
+ if (!variant) throw new Error(`${ladder.id} has variants 1 to ${ladder.variants.length}; there is no ${index}`);
941
+ const id = lockId(options.name ?? `${ladder.name} ${index}`);
942
+ await checkLockId(root, id, options.supersedes);
943
+ const image = path6.join(root, variant.image);
944
+ const recorded = Object.values(variant.output.ports).find((port) => port.kind === "file" && port.pixels);
945
+ const current = await fingerprintFile({ projectRoot: root, resolve: (relative) => path6.join(root, relative) }, image, variant.image).catch(() => void 0);
946
+ if (!recorded || recorded.kind !== "file" || current?.kind !== "file" || current.pixels !== recorded.pixels) {
947
+ throw new Error(`${variant.image} is missing or no longer the image the ladder recorded; make a new ladder rather than lock a different picture`);
948
+ }
949
+ const document = JSON.parse((await getObject(root, variant.graph)).toString("utf8"));
950
+ const warnings = [];
951
+ const changed = await changedInputs(new ProjectRoot(root), variant.inputs);
952
+ if (changed.length) {
953
+ warnings.push(`${changed.length} input ${changed.length === 1 ? "file has" : "files have"} changed since the ladder (${changed.map((change) => change.path).join(", ")}); the lock records the ladder's, so a replay needs them restored`);
954
+ }
955
+ const preview = await writePreview(root, id, image);
956
+ const published = publishedParams(document);
957
+ const lock = {
958
+ format: "cascade.lock",
959
+ version: 1,
960
+ id,
961
+ name: options.name ?? `${ladder.name} ${index}`,
962
+ status: options.status ?? "approved",
963
+ ...options.note ? { note: options.note } : {},
964
+ ...options.supersedes ? { supersedes: options.supersedes } : {},
965
+ created: (/* @__PURE__ */ new Date()).toISOString(),
966
+ graph: { file: ladder.graph.file, node: ladder.graph.node, object: variant.graph },
967
+ params: variant.params,
968
+ ...published ? { published } : {},
969
+ seeds: variant.seeds,
970
+ inputs: variant.inputs,
971
+ code: ladder.code,
972
+ versions: ladder.versions,
973
+ host: ladder.host,
974
+ output: variant.output,
975
+ ...preview ? { preview } : {},
976
+ ladder: {
977
+ id: ladder.id,
978
+ file: relativeTo(root, ladderFile),
979
+ variant: index,
980
+ label: variant.label,
981
+ image: variant.image,
982
+ sheet: ladder.sheet.file
983
+ }
984
+ };
985
+ const file = await writeLock(root, lock);
986
+ const picks = [...ladder.picks ?? [], { variant: index, lock: id, at: lock.created }];
987
+ await fs5.writeFile(ladderFile, stableJson({ ...ladder, picks }, LADDER_KEY_ORDER));
988
+ return { lock, file, ladder: ladder.id, variant, warnings };
989
+ }
990
+ function parseValues(text) {
991
+ try {
992
+ const parsed = JSON.parse(`[${text}]`);
993
+ if (Array.isArray(parsed)) return parsed;
994
+ } catch {
995
+ }
996
+ return text.split(",").map((word) => word.trim());
997
+ }
998
+ async function exists2(file) {
999
+ try {
1000
+ await fs5.access(file);
1001
+ return true;
1002
+ } catch {
1003
+ return false;
1004
+ }
1005
+ }
1006
+
1007
+ // src/cli/locks/ladderCard.ts
1008
+ import fs6 from "node:fs/promises";
1009
+ import path7 from "node:path";
1010
+ var MAX_CHOICE_OPTIONS = 8;
1011
+ function checkCardSize(count) {
1012
+ if (count > MAX_CHOICE_OPTIONS) {
1013
+ throw new Error(`--cairn shows at most ${MAX_CHOICE_OPTIONS} options on a choice card (this ladder has ${count}); ladder fewer variants`);
1014
+ }
1015
+ }
1016
+ function pickValue(ladderId, index) {
1017
+ return `pick:${ladderId}:${index}`;
1018
+ }
1019
+ function choiceLabel(variant, seedVaried) {
1020
+ const parts = Object.entries(variant.set).map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
1021
+ if (seedVaried) parts.push(`seed ${variant.seed}`);
1022
+ return parts.join(", ");
1023
+ }
1024
+ async function writeLadderCard(root, ladder, directory, vault) {
1025
+ checkCardSize(ladder.variants.length);
1026
+ const target = path7.resolve(directory);
1027
+ const vaultRoot = vault ? path7.resolve(vault) : await findVault(target);
1028
+ const relative = path7.relative(vaultRoot, target);
1029
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) throw new Error(`--cairn ${target} is not inside the vault ${vaultRoot}`);
1030
+ await fs6.mkdir(target, { recursive: true });
1031
+ const seedVaried = Boolean(ladder.seeds);
1032
+ const files = [];
1033
+ const options = [];
1034
+ for (const variant of ladder.variants) {
1035
+ const file = path7.join(target, `${ladder.id}-${String(variant.index).padStart(2, "0")}${path7.extname(variant.image).toLowerCase()}`);
1036
+ await fs6.copyFile(path7.join(root, variant.image), file);
1037
+ files.push(file);
1038
+ options.push({
1039
+ label: choiceLabel(variant, seedVaried) || `variant ${variant.index}`,
1040
+ value: pickValue(ladder.id, variant.index),
1041
+ image: path7.relative(vaultRoot, file).split(path7.sep).join("/")
1042
+ });
1043
+ }
1044
+ const choice = { prompt: `${ladder.name}: which one? (ladder ${ladder.id}, ${ladder.graph.file} ${ladder.graph.node})`, options };
1045
+ return { choice, fence: choiceFence(choice), vault: vaultRoot, files };
1046
+ }
1047
+ function choiceFence(choice) {
1048
+ return `\`\`\`cairn:choice
1049
+ ${JSON.stringify(choice)}
1050
+ \`\`\``;
1051
+ }
1052
+ async function findVault(start) {
1053
+ for (const marker of [".obsidian", ".git"]) {
1054
+ for (let directory = start; ; directory = path7.dirname(directory)) {
1055
+ if (await exists3(path7.join(directory, marker))) return directory;
1056
+ if (path7.dirname(directory) === directory) break;
1057
+ }
1058
+ }
1059
+ throw new Error(`--cairn ${start}: no vault above it (no .obsidian or .git); name it with --vault <dir>`);
1060
+ }
1061
+ async function exists3(file) {
1062
+ try {
1063
+ await fs6.access(file);
1064
+ return true;
1065
+ } catch {
1066
+ return false;
1067
+ }
1068
+ }
1069
+
1070
+ // src/cli/locks/commands.ts
1071
+ var LOCK_COMMANDS = ["lock", "locks", "replay", "ladder", "pick"];
1072
+ var LOCK_HELP = `
1073
+ Locks: this exact render, from this code, these parameters and this seed.
1074
+
1075
+ cascade lock <graph> <node> --name <name> [--note <text>] [--status approved]
1076
+ [--supersedes <id>] [--scene <scene.json>] [--seed <n>] [--input <file>]...
1077
+ [--port <output>]... [--preview <image>] [--live] [--json]
1078
+ Cook <node> and write locks/<id>.json: the graph, parameters, seeds,
1079
+ input hashes, versions, the project's source code (stored in
1080
+ .cascade/objects/) and a fingerprint of every output. --seed sets the
1081
+ graph seed for this render. Cooks from the stored code in a fresh copy
1082
+ of the project unless --live.
1083
+
1084
+ cascade replay <id | locks/<id>.json> [--out <dir>] [--live] [--keep]
1085
+ [--allow-input-change] [--project <dir>] [--json]
1086
+ Rebuild the lock's code in a throwaway copy, cook the stored graph and
1087
+ compare fingerprints. --live cooks with the project's code as it is now.
1088
+ Exits 1 when the output differs.
1089
+
1090
+ cascade lock verify [<id>...] [--live] [--project <dir>] [--json]
1091
+ Replay every lock (or the named ones) and report which still match.
1092
+
1093
+ cascade locks [<project-dir>] [--json]
1094
+ List the project's locks.
1095
+
1096
+ Ladders: three to five options side by side, and a lock at approval.
1097
+
1098
+ cascade ladder <graph> <node> --name <name> [--vary <node.prop>=<a,b,c>]...
1099
+ [--seeds <a,b,c>] [--columns N] [--port <output>]... [--input <file>]...
1100
+ [--note <text>] [--live] [--cairn <vault dir> [--vault <dir>]] [--json]
1101
+ Cook <node> once per variant and write ladders/<id>/<n>.png, a labelled
1102
+ contact sheet ladders/<id>/sheet.png, and the record ladders/<id>.json
1103
+ (per variant: graph, params, seed, inputs, output fingerprint; the
1104
+ stored code and git commit once). Values are a JSON list without its
1105
+ brackets (0.5,1,2 or [1,2],[3,4]); lists are zipped, one value applies
1106
+ to every variant. Cooks from the stored code in a fresh copy unless --live.
1107
+ --cairn copies each variant to <dir>/<id>-<nn>.png (a space's
1108
+ _attachments/ladders/<id>/) and prints a cairn:choice card (--json:
1109
+ data.cairnChoice) whose images are relative to the vault (the nearest
1110
+ .obsidian or .git above, or --vault); at most 8 variants. A tap answers
1111
+ "Picked: <label> (pick:<id>:<n>)": run cascade pick <id> <n>.
1112
+
1113
+ cascade pick <ladder-id | ladders/<id>.json> <n> [--name <lock name>] [--note <text>]
1114
+ [--status approved] [--supersedes <id>] [--project <dir>] [--json]
1115
+ Write locks/<id>.json for variant <n> from the ladder's record, without
1116
+ cooking again: its fingerprint is the image that was approved, so
1117
+ cascade replay reproduces that image. Default name: "<ladder> <n>".
1118
+ `;
1119
+ var SWITCHES = /* @__PURE__ */ new Set(["--json", "--live", "--keep", "--allow-input-change", "--verify", "--verbose", "-v"]);
1120
+ function parse(args) {
1121
+ const positional = [];
1122
+ const values = /* @__PURE__ */ new Map();
1123
+ const switches = /* @__PURE__ */ new Set();
1124
+ for (let index = 0; index < args.length; index += 1) {
1125
+ const arg = args[index];
1126
+ if (SWITCHES.has(arg)) switches.add(arg === "-v" ? "--verbose" : arg);
1127
+ else if (arg.startsWith("--")) {
1128
+ const value = args[index + 1];
1129
+ if (value === void 0 || value.startsWith("--")) throw new Error(`${arg} needs a value`);
1130
+ values.set(arg, [...values.get(arg) ?? [], value]);
1131
+ index += 1;
1132
+ } else positional.push(arg);
1133
+ }
1134
+ return { positional, values, switches };
1135
+ }
1136
+ var one = (parsed, flag) => parsed.values.get(flag)?.slice(-1)[0];
1137
+ async function runLockCommand(args) {
1138
+ const json = args.includes("--json");
1139
+ const log = console.log;
1140
+ if (json) console.log = console.error;
1141
+ const print = (value) => process.stdout.write(`${JSON.stringify(value, null, 2)}
1142
+ `);
1143
+ try {
1144
+ const [command, ...rest] = args;
1145
+ if (command === "locks") await locksCommand(parse(rest), json, print);
1146
+ else if (command === "replay") await replayCommand(parse(rest), json, print);
1147
+ else if (command === "lock" && rest[0] === "verify") await verifyCommand(parse(rest.slice(1)), json, print);
1148
+ else if (command === "lock") await lockCommand(parse(rest), json, print);
1149
+ else if (command === "ladder") await ladderCommand(parse(rest), json, print);
1150
+ else if (command === "pick") await pickCommand(parse(rest), json, print);
1151
+ else throw new Error(`Unknown lock command: ${command}`);
1152
+ } catch (error) {
1153
+ if (json) throw error;
1154
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
1155
+ process.exitCode = 1;
1156
+ } finally {
1157
+ console.log = log;
1158
+ }
1159
+ }
1160
+ async function lockCommand(parsed, json, print) {
1161
+ const [graph, node] = parsed.positional;
1162
+ const name = one(parsed, "--name");
1163
+ if (!graph || !node || !name) throw new Error("cascade lock needs <graph> <node> --name <name>");
1164
+ const { lock, file } = await createLock({
1165
+ graph,
1166
+ node,
1167
+ name,
1168
+ note: one(parsed, "--note"),
1169
+ status: one(parsed, "--status"),
1170
+ supersedes: one(parsed, "--supersedes"),
1171
+ scene: one(parsed, "--scene"),
1172
+ seed: seedValue(one(parsed, "--seed")),
1173
+ inputs: parsed.values.get("--input"),
1174
+ ports: parsed.values.get("--port"),
1175
+ preview: one(parsed, "--preview"),
1176
+ live: parsed.switches.has("--live"),
1177
+ verbose: parsed.switches.has("--verbose")
1178
+ });
1179
+ if (json) return print({ status: "locked", file, lock });
1180
+ console.log(`Locked ${lock.id} -> ${shown(file)}`);
1181
+ console.log(` ${lock.graph.file} node ${lock.graph.node}, graph ${short(lock.graph.object)}, code ${short(lock.code.tree)} (${lock.code.files} files)`);
1182
+ console.log(` ${Object.keys(lock.inputs).length} inputs, ${Object.keys(lock.seeds).length} seeds, cooked from the ${lock.host.cooked} in ${seconds(lock.output.durationMs)}`);
1183
+ for (const [port, fingerprint] of Object.entries(lock.output.ports)) console.log(` ${port}: ${describePort(fingerprint)}`);
1184
+ if (lock.code.skipped?.length) console.log(` not stored (over 2 MB): ${lock.code.skipped.join(", ")}`);
1185
+ if (lock.output.trace) console.log(` trace ${lock.output.trace}`);
1186
+ }
1187
+ async function replayCommand(parsed, json, print) {
1188
+ const target = parsed.positional[0];
1189
+ if (!target) throw new Error("cascade replay needs a lock id or file");
1190
+ const { lock, root } = await readLock(one(parsed, "--project") ?? process.cwd(), target);
1191
+ const result = await replayLock(root, lock, {
1192
+ out: one(parsed, "--out"),
1193
+ live: parsed.switches.has("--live"),
1194
+ keep: parsed.switches.has("--keep"),
1195
+ allowInputChange: parsed.switches.has("--allow-input-change"),
1196
+ verbose: parsed.switches.has("--verbose")
1197
+ });
1198
+ if (json) return print(result);
1199
+ printReplay(result);
1200
+ if (lock.ladder) console.log(` picked from ladder ${lock.ladder.id}, variant ${lock.ladder.label}: ${result.status === "match" ? "same pixels as" : "differs from"} ${lock.ladder.image}`);
1201
+ if (result.status !== "match") process.exitCode = 1;
1202
+ }
1203
+ async function verifyCommand(parsed, json, print) {
1204
+ const root = path8.resolve(one(parsed, "--project") ?? process.cwd());
1205
+ const locks = parsed.positional.length ? await Promise.all(parsed.positional.map(async (id) => (await readLock(root, id)).lock)) : await listLocks(root);
1206
+ if (!locks.length) throw new Error(`No locks in ${path8.join(root, "locks")}`);
1207
+ const results = [];
1208
+ for (const lock of locks) {
1209
+ try {
1210
+ const result = await replayLock(root, lock, {
1211
+ live: parsed.switches.has("--live"),
1212
+ allowInputChange: parsed.switches.has("--allow-input-change"),
1213
+ verbose: parsed.switches.has("--verbose")
1214
+ });
1215
+ results.push(result);
1216
+ if (!json) printReplay(result);
1217
+ } catch (error) {
1218
+ const message = error instanceof Error ? error.message : String(error);
1219
+ results.push({ id: lock.id, status: "error", error: message });
1220
+ if (!json) console.log(`${lock.id}: ERROR
1221
+ ${message.split("\n").join("\n ")}`);
1222
+ }
1223
+ }
1224
+ const failed = results.filter((result) => result.status !== "match").length;
1225
+ if (json) return print({ status: failed ? "failed" : "ok", verified: results.length, failed, results });
1226
+ console.log(`
1227
+ ${results.length - failed} of ${results.length} locks reproduce${failed ? `; ${failed} do not` : ""}.`);
1228
+ if (failed) process.exitCode = 1;
1229
+ }
1230
+ async function locksCommand(parsed, json, print) {
1231
+ const root = path8.resolve(parsed.positional[0] ?? one(parsed, "--project") ?? process.cwd());
1232
+ const locks = await listLocks(root);
1233
+ if (json) {
1234
+ return print({
1235
+ project: root,
1236
+ locks: locks.map((lock) => ({
1237
+ id: lock.id,
1238
+ name: lock.name,
1239
+ status: lock.status,
1240
+ created: lock.created,
1241
+ graph: lock.graph.file,
1242
+ node: lock.graph.node,
1243
+ ...lock.note ? { note: lock.note } : {},
1244
+ ...lock.supersedes ? { supersedes: lock.supersedes } : {},
1245
+ commit: lock.versions.project.commit ?? null,
1246
+ preview: lock.preview?.file ?? null
1247
+ }))
1248
+ });
1249
+ }
1250
+ if (!locks.length) return console.log(`No locks in ${path8.join(root, "locks")}`);
1251
+ const width = Math.max(...locks.map((lock) => lock.id.length));
1252
+ for (const lock of locks) {
1253
+ const commit = lock.versions.project.commit ? ` @${lock.versions.project.commit.slice(0, 7)}${lock.versions.project.dirty ? "+" : ""}` : "";
1254
+ const supersedes = lock.supersedes ? `, supersedes ${lock.supersedes}` : "";
1255
+ console.log(`${lock.id.padEnd(width)} ${lock.status.padEnd(9)} ${lock.created.slice(0, 16).replace("T", " ")} ${lock.graph.file} ${lock.graph.node}${commit}${supersedes}`);
1256
+ }
1257
+ }
1258
+ async function ladderCommand(parsed, json, print) {
1259
+ const [graph, node] = parsed.positional;
1260
+ const name = one(parsed, "--name");
1261
+ if (!graph || !node || !name) throw new Error("cascade ladder needs <graph> <node> --name <name> and --vary or --seeds");
1262
+ const vary = {};
1263
+ for (const pair of parsed.values.get("--vary") ?? []) {
1264
+ const at = pair.indexOf("=");
1265
+ if (at <= 0) throw new Error(`--vary takes node.prop=a,b,c (got "${pair}")`);
1266
+ vary[pair.slice(0, at)] = parseValues(pair.slice(at + 1));
1267
+ }
1268
+ const seedsText = one(parsed, "--seeds");
1269
+ const seeds = seedsText === void 0 ? void 0 : seedsText.split(",").map((text) => seedValue(text.trim()));
1270
+ const cairn = one(parsed, "--cairn");
1271
+ if (cairn) checkCardSize(Math.max(0, ...Object.values(vary).map((values) => values.length), seeds?.length ?? 0));
1272
+ const columns = one(parsed, "--columns");
1273
+ const { ladder, file } = await createLadder({
1274
+ graph,
1275
+ node,
1276
+ name,
1277
+ note: one(parsed, "--note"),
1278
+ vary,
1279
+ seeds,
1280
+ columns: columns === void 0 ? void 0 : Number(columns),
1281
+ ports: parsed.values.get("--port"),
1282
+ inputs: parsed.values.get("--input"),
1283
+ live: parsed.switches.has("--live"),
1284
+ verbose: parsed.switches.has("--verbose")
1285
+ });
1286
+ const root = path8.dirname(path8.dirname(file));
1287
+ const card = cairn ? await writeLadderCard(root, ladder, cairn, one(parsed, "--vault")) : void 0;
1288
+ if (json) {
1289
+ return print({
1290
+ status: "laddered",
1291
+ file,
1292
+ sheet: path8.join(root, ladder.sheet.file),
1293
+ variants: ladder.variants.map((variant) => ({ index: variant.index, label: variant.label, image: path8.join(root, variant.image), set: variant.set, seed: variant.seed })),
1294
+ ...card ? { cairnChoice: card.fence, cairn: { vault: card.vault, files: card.files, choice: card.choice } } : {},
1295
+ ladder
1296
+ });
1297
+ }
1298
+ console.log(`Ladder ${ladder.id} -> ${shown(file)}, cooked from the ${ladder.host.cooked}`);
1299
+ console.log(` sheet ${shown(path8.join(root, ladder.sheet.file))}`);
1300
+ for (const variant of ladder.variants) console.log(` ${variant.label} ${variant.image} (${seconds(variant.output.durationMs)})`);
1301
+ console.log(`Pick one with: cascade pick ${ladder.id} <n> --project ${root}`);
1302
+ if (card) {
1303
+ console.log(`
1304
+ Cairn choice card (images in ${shown(path8.dirname(card.files[0]))}); post it as is, and on "Picked: \u2026 (pick:${ladder.id}:<n>)" run cascade pick ${ladder.id} <n> --project ${root}:
1305
+ `);
1306
+ console.log(card.fence);
1307
+ }
1308
+ }
1309
+ async function pickCommand(parsed, json, print) {
1310
+ const [ladder, variant] = parsed.positional;
1311
+ const index = Number(variant);
1312
+ if (!ladder || !Number.isInteger(index)) throw new Error("cascade pick needs <ladder> <variant number>");
1313
+ const result = await pickVariant(one(parsed, "--project") ?? process.cwd(), ladder, index, {
1314
+ name: one(parsed, "--name"),
1315
+ note: one(parsed, "--note"),
1316
+ status: one(parsed, "--status"),
1317
+ supersedes: one(parsed, "--supersedes")
1318
+ });
1319
+ for (const warning of result.warnings) console.error(`Warning: ${warning}`);
1320
+ if (json) return print({ status: "locked", file: result.file, ladder: result.ladder, variant: result.variant.index, warnings: result.warnings, lock: result.lock });
1321
+ const commit = result.lock.versions.project.commit;
1322
+ console.log(`Locked ${result.lock.id} -> ${shown(result.file)}: ${result.ladder} variant ${result.variant.label}`);
1323
+ console.log(` graph seed ${result.lock.seeds.graph}, code ${short(result.lock.code.tree)}${commit ? `, commit ${commit.slice(0, 7)}${result.lock.versions.project.dirty ? "+" : ""}` : ""}`);
1324
+ console.log(` image ${result.variant.image}${result.lock.preview ? `, preview ${result.lock.preview.file}` : ""}`);
1325
+ console.log(`Reproduce it with: cascade replay ${result.lock.id}`);
1326
+ }
1327
+ function printReplay(result) {
1328
+ const verdict = result.status === "match" ? "match" : "MISMATCH";
1329
+ const from = result.cooked === "live" ? "the project's current code" : "the lock's code";
1330
+ console.log(`${result.id}: ${verdict}, replayed from ${from} in ${seconds(result.durationMs)}`);
1331
+ for (const difference of result.differences) {
1332
+ console.log(` ${difference.port}: locked ${difference.expected}`);
1333
+ console.log(` ${" ".repeat(difference.port.length)} replay ${difference.actual}`);
1334
+ }
1335
+ if (result.status === "match") {
1336
+ for (const [port, fingerprint] of Object.entries(result.ports)) console.log(` ${port}: ${describePort(fingerprint)}`);
1337
+ }
1338
+ const code = result.codeSinceLock;
1339
+ const drift = code.changed.length + code.added.length + code.removed.length;
1340
+ if (drift) {
1341
+ console.log(` project code since the lock: ${code.changed.length} changed, ${code.added.length} added, ${code.removed.length} removed`);
1342
+ for (const name of code.changed.slice(0, 10)) console.log(` changed ${name}`);
1343
+ if (code.changed.length > 10) console.log(` ... and ${code.changed.length - 10} more`);
1344
+ }
1345
+ for (const change of result.inputsChanged) console.log(` input changed: ${change.path}`);
1346
+ for (const warning of result.warnings) console.log(` note: ${warning}`);
1347
+ for (const file of result.files) console.log(` wrote ${shown(file)}`);
1348
+ if (result.trace) console.log(` trace ${result.trace}`);
1349
+ if (result.kept) console.log(` replay copy kept at ${result.kept}`);
1350
+ }
1351
+ function describePort(fingerprint) {
1352
+ if (fingerprint.kind === "value") return `value ${short(fingerprint.sha256)}`;
1353
+ const size = fingerprint.width ? ` ${fingerprint.width}x${fingerprint.height}` : "";
1354
+ return `${fingerprint.path}${size} ${short(fingerprint.pixels ?? fingerprint.sha256)}`;
1355
+ }
1356
+ function shown(file) {
1357
+ const relative = path8.relative(process.cwd(), file);
1358
+ return relative.startsWith("..") ? file : relative;
1359
+ }
1360
+ function seedValue(text) {
1361
+ if (text === void 0) return void 0;
1362
+ const value = Number(text);
1363
+ if (!Number.isInteger(value) || value < 0 || value > 4294967295) throw new Error(`--seed must be an integer from 0 to 4294967295 (got "${text}")`);
1364
+ return value;
1365
+ }
1366
+ function seconds(ms) {
1367
+ return ms < 1e3 ? `${ms} ms` : `${(ms / 1e3).toFixed(1)} s`;
1368
+ }
1369
+ export {
1370
+ LOCK_COMMANDS,
1371
+ LOCK_HELP,
1372
+ runLockCommand
1373
+ };
1374
+ //# sourceMappingURL=commands-PHYFYA3F.js.map