@nodetool-ai/node-sdk 0.7.0-rc.23 → 0.8.2

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 (158) hide show
  1. package/README.md +34 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/base-node.d.ts +285 -14
  4. package/dist/base-node.d.ts.map +1 -1
  5. package/dist/base-node.js +359 -49
  6. package/dist/base-node.js.map +1 -1
  7. package/dist/class-name-to-title.d.ts +26 -0
  8. package/dist/class-name-to-title.d.ts.map +1 -0
  9. package/dist/class-name-to-title.js +59 -0
  10. package/dist/class-name-to-title.js.map +1 -0
  11. package/dist/code-analysis.d.ts +234 -0
  12. package/dist/code-analysis.d.ts.map +1 -0
  13. package/dist/code-analysis.js +919 -0
  14. package/dist/code-analysis.js.map +1 -0
  15. package/dist/code-body.d.ts +61 -0
  16. package/dist/code-body.d.ts.map +1 -0
  17. package/dist/code-body.js +288 -0
  18. package/dist/code-body.js.map +1 -0
  19. package/dist/code-node-validation.d.ts +67 -0
  20. package/dist/code-node-validation.d.ts.map +1 -0
  21. package/dist/code-node-validation.js +540 -0
  22. package/dist/code-node-validation.js.map +1 -0
  23. package/dist/content-card.d.ts +30 -0
  24. package/dist/content-card.d.ts.map +1 -0
  25. package/dist/content-card.js +68 -0
  26. package/dist/content-card.js.map +1 -0
  27. package/dist/correlation-validation.d.ts +19 -0
  28. package/dist/correlation-validation.d.ts.map +1 -0
  29. package/dist/correlation-validation.js +101 -0
  30. package/dist/correlation-validation.js.map +1 -0
  31. package/dist/cost-estimate.d.ts +244 -0
  32. package/dist/cost-estimate.d.ts.map +1 -0
  33. package/dist/cost-estimate.js +637 -0
  34. package/dist/cost-estimate.js.map +1 -0
  35. package/dist/decorators.d.ts.map +1 -1
  36. package/dist/decorators.js +4 -1
  37. package/dist/decorators.js.map +1 -1
  38. package/dist/docs/nodes.d.ts.map +1 -1
  39. package/dist/docs/nodes.js +3 -2
  40. package/dist/docs/nodes.js.map +1 -1
  41. package/dist/docs/overview.d.ts.map +1 -1
  42. package/dist/docs/overview.js +4 -3
  43. package/dist/docs/overview.js.map +1 -1
  44. package/dist/docs/workflows.d.ts.map +1 -1
  45. package/dist/docs/workflows.js +10 -9
  46. package/dist/docs/workflows.js.map +1 -1
  47. package/dist/field-classification.d.ts +31 -0
  48. package/dist/field-classification.d.ts.map +1 -0
  49. package/dist/field-classification.js +58 -0
  50. package/dist/field-classification.js.map +1 -0
  51. package/dist/graph-validation.d.ts +239 -0
  52. package/dist/graph-validation.d.ts.map +1 -0
  53. package/dist/graph-validation.js +1174 -0
  54. package/dist/graph-validation.js.map +1 -0
  55. package/dist/index.d.ts +27 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +29 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/js-script-link.d.ts +53 -0
  60. package/dist/js-script-link.d.ts.map +1 -0
  61. package/dist/js-script-link.js +58 -0
  62. package/dist/js-script-link.js.map +1 -0
  63. package/dist/js-script-materialize.d.ts +52 -0
  64. package/dist/js-script-materialize.d.ts.map +1 -0
  65. package/dist/js-script-materialize.js +24 -0
  66. package/dist/js-script-materialize.js.map +1 -0
  67. package/dist/manifest-node-values.d.ts +106 -0
  68. package/dist/manifest-node-values.d.ts.map +1 -0
  69. package/dist/manifest-node-values.js +195 -0
  70. package/dist/manifest-node-values.js.map +1 -0
  71. package/dist/metadata.d.ts +75 -3
  72. package/dist/metadata.d.ts.map +1 -1
  73. package/dist/metadata.js +132 -49
  74. package/dist/metadata.js.map +1 -1
  75. package/dist/node-metadata.d.ts.map +1 -1
  76. package/dist/node-metadata.js +61 -56
  77. package/dist/node-metadata.js.map +1 -1
  78. package/dist/node-type-inventory.d.ts +51 -0
  79. package/dist/node-type-inventory.d.ts.map +1 -0
  80. package/dist/node-type-inventory.js +121 -0
  81. package/dist/node-type-inventory.js.map +1 -0
  82. package/dist/nodes/test-nodes.d.ts +77 -30
  83. package/dist/nodes/test-nodes.d.ts.map +1 -1
  84. package/dist/nodes/test-nodes.js +2 -34
  85. package/dist/nodes/test-nodes.js.map +1 -1
  86. package/dist/pack-loader.d.ts +202 -0
  87. package/dist/pack-loader.d.ts.map +1 -0
  88. package/dist/pack-loader.js +529 -0
  89. package/dist/pack-loader.js.map +1 -0
  90. package/dist/port-types.d.ts +15 -0
  91. package/dist/port-types.d.ts.map +1 -0
  92. package/dist/port-types.js +72 -0
  93. package/dist/port-types.js.map +1 -0
  94. package/dist/pricing-bundle.d.ts +83 -0
  95. package/dist/pricing-bundle.d.ts.map +1 -0
  96. package/dist/pricing-bundle.js +82 -0
  97. package/dist/pricing-bundle.js.map +1 -0
  98. package/dist/pricing-params.d.ts +59 -0
  99. package/dist/pricing-params.d.ts.map +1 -0
  100. package/dist/pricing-params.js +282 -0
  101. package/dist/pricing-params.js.map +1 -0
  102. package/dist/python-package-scan.d.ts +8 -0
  103. package/dist/python-package-scan.d.ts.map +1 -1
  104. package/dist/python-package-scan.js +21 -8
  105. package/dist/python-package-scan.js.map +1 -1
  106. package/dist/registry.d.ts +105 -2
  107. package/dist/registry.d.ts.map +1 -1
  108. package/dist/registry.js +403 -30
  109. package/dist/registry.js.map +1 -1
  110. package/dist/sandbox-bridge-packs.d.ts +39 -0
  111. package/dist/sandbox-bridge-packs.d.ts.map +1 -0
  112. package/dist/sandbox-bridge-packs.js +249 -0
  113. package/dist/sandbox-bridge-packs.js.map +1 -0
  114. package/dist/sandbox-catalog-host.d.ts +31 -0
  115. package/dist/sandbox-catalog-host.d.ts.map +1 -0
  116. package/dist/sandbox-catalog-host.js +78 -0
  117. package/dist/sandbox-catalog-host.js.map +1 -0
  118. package/dist/sandbox-module-catalog.d.ts +14 -0
  119. package/dist/sandbox-module-catalog.d.ts.map +1 -0
  120. package/dist/sandbox-module-catalog.js +402 -0
  121. package/dist/sandbox-module-catalog.js.map +1 -0
  122. package/dist/sandbox-npm-artifacts.d.ts +56 -0
  123. package/dist/sandbox-npm-artifacts.d.ts.map +1 -0
  124. package/dist/sandbox-npm-artifacts.js +17 -0
  125. package/dist/sandbox-npm-artifacts.js.map +1 -0
  126. package/dist/sandbox-pack-discovery.d.ts +93 -0
  127. package/dist/sandbox-pack-discovery.d.ts.map +1 -0
  128. package/dist/sandbox-pack-discovery.js +807 -0
  129. package/dist/sandbox-pack-discovery.js.map +1 -0
  130. package/dist/search.d.ts +29 -1
  131. package/dist/search.d.ts.map +1 -1
  132. package/dist/search.js +136 -9
  133. package/dist/search.js.map +1 -1
  134. package/dist/type-compat.d.ts +36 -0
  135. package/dist/type-compat.d.ts.map +1 -0
  136. package/dist/type-compat.js +98 -0
  137. package/dist/type-compat.js.map +1 -0
  138. package/dist/type-predicates.d.ts +25 -0
  139. package/dist/type-predicates.d.ts.map +1 -0
  140. package/dist/type-predicates.js +31 -0
  141. package/dist/type-predicates.js.map +1 -0
  142. package/dist/validation.d.ts +18 -0
  143. package/dist/validation.d.ts.map +1 -1
  144. package/dist/validation.js +70 -11
  145. package/dist/validation.js.map +1 -1
  146. package/dist/workflow-document-tools.d.ts +35 -0
  147. package/dist/workflow-document-tools.d.ts.map +1 -0
  148. package/dist/workflow-document-tools.js +402 -0
  149. package/dist/workflow-document-tools.js.map +1 -0
  150. package/dist/workflow-interface.d.ts +62 -0
  151. package/dist/workflow-interface.d.ts.map +1 -0
  152. package/dist/workflow-interface.js +351 -0
  153. package/dist/workflow-interface.js.map +1 -0
  154. package/package.json +67 -10
  155. package/dist/package-registry-client.d.ts +0 -12
  156. package/dist/package-registry-client.d.ts.map +0 -1
  157. package/dist/package-registry-client.js +0 -40
  158. package/dist/package-registry-client.js.map +0 -1
@@ -0,0 +1,919 @@
1
+ /**
2
+ * AST primitives for the body of a Code node (`nodetool.code.Code`).
3
+ *
4
+ * A Code node's `code` property is the body of one async function: declared
5
+ * dynamic inputs arrive on an `inputs` object, and outputs leave through
6
+ * `emit(name, value)` / `output(name, value)` — or, for a body that calls
7
+ * neither, through the legacy returned object's keys. Everything a checker
8
+ * needs to say about such a body —
9
+ * does it parse, what does it return, which names does it invent — is derived
10
+ * here, so the graph validator, the code-generation planner and the editor all
11
+ * read the same AST rather than three approximations of it.
12
+ *
13
+ * Pure functions, no I/O: the parser is the only dependency.
14
+ */
15
+ import * as acorn from "acorn";
16
+ import { isNumber, isObjectLike, isString } from "./type-predicates.js";
17
+ /**
18
+ * The body is parsed as a module so an `import` reads as an import rather than
19
+ * as an unexplained syntax error — but the runtime splices the same text into
20
+ * an async function, where a module declaration is invalid syntax. Callers
21
+ * reject them explicitly via {@link moduleDeclarationKinds}.
22
+ */
23
+ const PARSE_OPTIONS = {
24
+ ecmaVersion: "latest",
25
+ sourceType: "module",
26
+ allowReturnOutsideFunction: true,
27
+ allowAwaitOutsideFunction: true,
28
+ locations: true
29
+ };
30
+ const MODULE_DECLARATION_TYPES = new Set([
31
+ "ImportDeclaration",
32
+ "ExportNamedDeclaration",
33
+ "ExportDefaultDeclaration",
34
+ "ExportAllDeclaration"
35
+ ]);
36
+ /**
37
+ * Parse a Code node body into its top-level statement list.
38
+ *
39
+ * The body is a bare statement list with `return` at the top level, which most
40
+ * of the time parses directly. Anything the direct parse rejects gets a second
41
+ * pass wrapped in an async function, so `await` in odd positions doesn't read
42
+ * as a syntax error.
43
+ */
44
+ export function parseCodeBody(code) {
45
+ try {
46
+ return { statements: acorn.parse(code, PARSE_OPTIONS).body };
47
+ }
48
+ catch (directError) {
49
+ try {
50
+ const wrapped = acorn.parse(`(async function(){\n${code}\n})()`, {
51
+ ...PARSE_OPTIONS,
52
+ allowReturnOutsideFunction: false
53
+ });
54
+ const statement = wrapped.body[0];
55
+ if (statement?.type === "ExpressionStatement") {
56
+ const call = statement.expression;
57
+ if (call.type === "CallExpression" &&
58
+ call.callee.type === "FunctionExpression") {
59
+ return { statements: call.callee.body.body };
60
+ }
61
+ }
62
+ return { error: "Could not read the code as a function body." };
63
+ }
64
+ catch {
65
+ const message = directError instanceof Error ? directError.message : String(directError);
66
+ const line = directError.loc?.line;
67
+ return isNumber(line) ? { error: message, line } : { error: message };
68
+ }
69
+ }
70
+ }
71
+ /** `"import"` / `"export"` for every module declaration at the top level. */
72
+ export function moduleDeclarationKinds(statements) {
73
+ const kinds = new Set();
74
+ for (const statement of statements) {
75
+ if (MODULE_DECLARATION_TYPES.has(statement.type)) {
76
+ kinds.add(statement.type.startsWith("Import") ? "import" : "export");
77
+ }
78
+ }
79
+ return [...kinds];
80
+ }
81
+ /**
82
+ * Specifiers of the top-level static `import` declarations, in source order.
83
+ *
84
+ * These are the only imports the guest loader can serve: it resolves a
85
+ * specifier the run declared, and nothing else.
86
+ */
87
+ export function staticImportSpecifiers(statements) {
88
+ const specifiers = [];
89
+ for (const statement of statements) {
90
+ if (statement.type !== "ImportDeclaration")
91
+ continue;
92
+ const source = statement.source;
93
+ if (isString(source.value))
94
+ specifiers.push(source.value);
95
+ }
96
+ return specifiers;
97
+ }
98
+ /**
99
+ * Specifier plus imported names for every top-level static `import`, in source
100
+ * order.
101
+ *
102
+ * {@link staticImportSpecifiers} answers which modules a body wants;
103
+ * this answers which of their exports it wants, so a loader can refuse a
104
+ * misspelled name with the module's own export list instead of leaving the
105
+ * guest to report "Could not find export".
106
+ */
107
+ export function staticImportBindings(statements) {
108
+ const bindings = [];
109
+ for (const statement of statements) {
110
+ if (statement.type !== "ImportDeclaration")
111
+ continue;
112
+ const source = statement.source;
113
+ if (!isString(source.value))
114
+ continue;
115
+ const named = [];
116
+ for (const specifier of statement.specifiers) {
117
+ if (specifier.type !== "ImportSpecifier")
118
+ continue;
119
+ const imported = specifier.imported;
120
+ if (imported.type === "Identifier") {
121
+ named.push(imported.name);
122
+ }
123
+ else if (isString(imported.value)) {
124
+ named.push(imported.value);
125
+ }
126
+ }
127
+ bindings.push({ specifier: source.value, named });
128
+ }
129
+ return bindings;
130
+ }
131
+ /**
132
+ * The names a module's source exports, or `null` when they cannot be decided.
133
+ *
134
+ * `null` means "do not check": the source did not parse, or it carries an
135
+ * `export * from …` whose names live in another file. A caller that refuses an
136
+ * import on this list must treat `null` as permission, never as an empty
137
+ * module.
138
+ *
139
+ * Used to turn QuickJS's "Could not find export 'websearch'" — which names
140
+ * neither the module's exports nor the near miss — into a refusal made before
141
+ * the guest starts, with both.
142
+ */
143
+ export function exportedNames(source) {
144
+ let program;
145
+ try {
146
+ program = acorn.parse(source, PARSE_OPTIONS);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ const names = new Set();
152
+ for (const statement of program.body) {
153
+ if (statement.type === "ExportAllDeclaration") {
154
+ // `export * as ns from …` names one export; a bare `export * from …`
155
+ // forwards an unknowable set.
156
+ if (statement.exported === null || statement.exported === undefined) {
157
+ return null;
158
+ }
159
+ const exported = statement.exported;
160
+ if (exported.type === "Identifier")
161
+ names.add(exported.name);
162
+ else if (isString(exported.value))
163
+ names.add(exported.value);
164
+ continue;
165
+ }
166
+ if (statement.type === "ExportDefaultDeclaration") {
167
+ names.add("default");
168
+ continue;
169
+ }
170
+ if (statement.type !== "ExportNamedDeclaration")
171
+ continue;
172
+ for (const specifier of statement.specifiers) {
173
+ const exported = specifier.exported;
174
+ if (exported.type === "Identifier")
175
+ names.add(exported.name);
176
+ else if (isString(exported.value))
177
+ names.add(exported.value);
178
+ }
179
+ const declaration = statement.declaration;
180
+ if (declaration === null || declaration === undefined)
181
+ continue;
182
+ if (declaration.type === "FunctionDeclaration" ||
183
+ declaration.type === "ClassDeclaration") {
184
+ if (declaration.id)
185
+ names.add(declaration.id.name);
186
+ continue;
187
+ }
188
+ if (declaration.type === "VariableDeclaration") {
189
+ for (const declarator of declaration.declarations) {
190
+ patternNames(declarator.id, names);
191
+ }
192
+ }
193
+ }
194
+ return [...names];
195
+ }
196
+ export function dynamicModuleAccess(statements) {
197
+ let dynamicImport = false;
198
+ let require = false;
199
+ walk(statements, (node) => {
200
+ if (node.type === "ImportExpression") {
201
+ dynamicImport = true;
202
+ return;
203
+ }
204
+ if (node.type === "CallExpression" &&
205
+ node.callee.type === "Identifier" &&
206
+ node.callee.name === "require") {
207
+ require = true;
208
+ }
209
+ });
210
+ return { dynamicImport, require };
211
+ }
212
+ function objectKeys(expression) {
213
+ const keys = new Set();
214
+ let opaque = false;
215
+ for (const property of expression.properties) {
216
+ if (property.type === "SpreadElement") {
217
+ opaque = true;
218
+ continue;
219
+ }
220
+ if (property.key.type === "Identifier" && !property.computed) {
221
+ keys.add(property.key.name);
222
+ }
223
+ else if (property.key.type === "Literal" &&
224
+ isString(property.key.value)) {
225
+ keys.add(property.key.value);
226
+ }
227
+ else {
228
+ // A computed key can be anything at runtime.
229
+ opaque = true;
230
+ }
231
+ }
232
+ return { keys, opaque, notAnObject: false };
233
+ }
234
+ /**
235
+ * Shapes a return argument can take. A ternary splits into one shape per
236
+ * branch, so `return cond ? {a, b} : {a}` is caught on the branch that drops
237
+ * `b` instead of passing on the union.
238
+ */
239
+ export function returnShapes(argument) {
240
+ if (!argument) {
241
+ return [{ keys: new Set(), opaque: false, notAnObject: true }];
242
+ }
243
+ switch (argument.type) {
244
+ case "ObjectExpression":
245
+ return [objectKeys(argument)];
246
+ case "ConditionalExpression":
247
+ return [
248
+ ...returnShapes(argument.consequent),
249
+ ...returnShapes(argument.alternate)
250
+ ];
251
+ case "ArrayExpression":
252
+ case "TemplateLiteral":
253
+ case "FunctionExpression":
254
+ case "ArrowFunctionExpression":
255
+ return [{ keys: new Set(), opaque: false, notAnObject: true }];
256
+ case "Literal":
257
+ // `return null` is as wrong as `return 5`; both drop every output.
258
+ return [{ keys: new Set(), opaque: false, notAnObject: true }];
259
+ default:
260
+ // A variable, a call, `await …` — the shape is only known at runtime.
261
+ return [{ keys: new Set(), opaque: true, notAnObject: false }];
262
+ }
263
+ }
264
+ /** Node types that own a `return` — the body's own returns stop here. */
265
+ function isFunctionLike(node) {
266
+ return (node.type === "FunctionDeclaration" ||
267
+ node.type === "FunctionExpression" ||
268
+ node.type === "ArrowFunctionExpression");
269
+ }
270
+ function collectReturns(node, out) {
271
+ if (!isObjectLike(node))
272
+ return;
273
+ if (Array.isArray(node)) {
274
+ for (const item of node)
275
+ collectReturns(item, out);
276
+ return;
277
+ }
278
+ if (!isString(node.type))
279
+ return;
280
+ const astNode = node;
281
+ // A `return` inside a helper function belongs to that helper, not the body.
282
+ if (isFunctionLike(astNode))
283
+ return;
284
+ if (astNode.type === "ReturnStatement")
285
+ out.push(astNode);
286
+ for (const [key, value] of Object.entries(astNode)) {
287
+ if (key === "type" || key === "start" || key === "end" || key === "loc") {
288
+ continue;
289
+ }
290
+ collectReturns(value, out);
291
+ }
292
+ }
293
+ /**
294
+ * Whether a statement can complete normally — i.e. control flows past it.
295
+ * `return`/`throw` cannot; an `if` without an `else` always can.
296
+ */
297
+ function completesNormally(statement) {
298
+ switch (statement.type) {
299
+ case "ReturnStatement":
300
+ case "ThrowStatement":
301
+ return false;
302
+ case "BlockStatement":
303
+ return statementsComplete(statement.body);
304
+ case "IfStatement":
305
+ if (!statement.alternate)
306
+ return true;
307
+ return (completesNormally(statement.consequent) ||
308
+ completesNormally(statement.alternate));
309
+ case "TryStatement": {
310
+ if (statement.finalizer && !statementsComplete(statement.finalizer.body)) {
311
+ return false;
312
+ }
313
+ const blockCompletes = statementsComplete(statement.block.body);
314
+ if (!statement.handler)
315
+ return blockCompletes;
316
+ return blockCompletes || statementsComplete(statement.handler.body.body);
317
+ }
318
+ case "SwitchStatement": {
319
+ // Without a default, an unmatched value skips the whole switch.
320
+ if (!statement.cases.some((c) => c.test === null))
321
+ return true;
322
+ // A `break` lands after the switch, so control continues there.
323
+ if (statement.cases.some((c) => c.consequent.some((s) => s.type === "BreakStatement"))) {
324
+ return true;
325
+ }
326
+ // Every other case falls through to the next, so only the last one
327
+ // decides whether control leaves the switch normally.
328
+ const last = statement.cases[statement.cases.length - 1];
329
+ return !last || statementsComplete(last.consequent);
330
+ }
331
+ case "LabeledStatement":
332
+ return completesNormally(statement.body);
333
+ default:
334
+ // Loops are assumed to terminate: `while (true) { return … }` is rare
335
+ // enough that treating it as exhaustive would hide real fall-throughs.
336
+ return true;
337
+ }
338
+ }
339
+ function statementsComplete(statements) {
340
+ for (const statement of statements) {
341
+ if (!completesNormally(statement))
342
+ return false;
343
+ }
344
+ return true;
345
+ }
346
+ /** The body's own `return` statements, and whether control can fall past them. */
347
+ export function analyzeCodeBody(statements) {
348
+ const returns = [];
349
+ collectReturns(statements, returns);
350
+ return { returns, fallsThrough: statementsComplete(statements) };
351
+ }
352
+ /** The two host bridges a body delivers outputs through. */
353
+ export const CODE_OUTPUT_CALLEES = ["emit", "output"];
354
+ /**
355
+ * Handles the body delivers to via `emit(name, …)` / `output(name, …)`.
356
+ *
357
+ * Presence in the AST is the whole rule: a call inside a branch, a loop or a
358
+ * helper function counts, because the contract is per-handle existence rather
359
+ * than per-path coverage. `x.output(…)` is a method on something else and is
360
+ * not counted.
361
+ */
362
+ export function outputCallNames(statements) {
363
+ const names = new Set();
364
+ let nonLiteral = false;
365
+ walk(statements, (node) => {
366
+ if (node.type !== "CallExpression")
367
+ return;
368
+ if (node.callee.type !== "Identifier")
369
+ return;
370
+ if (!CODE_OUTPUT_CALLEES.includes(node.callee.name))
371
+ return;
372
+ const first = node.arguments[0];
373
+ if (first?.type === "Literal" && isString(first.value)) {
374
+ names.add(first.value);
375
+ return;
376
+ }
377
+ nonLiteral = true;
378
+ });
379
+ return { names: [...names], nonLiteral };
380
+ }
381
+ /** The global a body reads its input streams through. */
382
+ export const CODE_STREAM_GLOBAL = "stream";
383
+ /** Members of `stream` that take an input handle name as first argument. */
384
+ const STREAM_NAMED_MEMBERS = ["first", "open"];
385
+ /**
386
+ * Handles the body takes from via `stream(name)`, `stream.first(name)` and
387
+ * `stream.open(name)`, plus whether it also uses `stream.any()`.
388
+ *
389
+ * Presence in the AST is the whole rule, as for {@link outputCallNames}: a call
390
+ * inside a branch or a loop counts. `x.stream(…)` is a method on something else,
391
+ * and a body that binds `stream` itself reports nothing — the calls are that
392
+ * binding's, not the sandbox's.
393
+ */
394
+ export function streamCallNames(statements) {
395
+ if (collectBoundNamesFrom(statements).has(CODE_STREAM_GLOBAL)) {
396
+ return { names: [], nonLiteral: false, usesAny: false };
397
+ }
398
+ const names = new Set();
399
+ let nonLiteral = false;
400
+ let usesAny = false;
401
+ walk(statements, (node) => {
402
+ if (node.type !== "CallExpression")
403
+ return;
404
+ const callee = node.callee;
405
+ if (callee.type === "Identifier" && callee.name === CODE_STREAM_GLOBAL) {
406
+ addStreamName(node.arguments[0]);
407
+ return;
408
+ }
409
+ if (callee.type !== "MemberExpression" ||
410
+ callee.computed ||
411
+ callee.object.type !== "Identifier" ||
412
+ callee.object.name !== CODE_STREAM_GLOBAL ||
413
+ callee.property.type !== "Identifier") {
414
+ return;
415
+ }
416
+ if (callee.property.name === "any") {
417
+ usesAny = true;
418
+ return;
419
+ }
420
+ if (STREAM_NAMED_MEMBERS.includes(callee.property.name)) {
421
+ addStreamName(node.arguments[0]);
422
+ }
423
+ });
424
+ function addStreamName(first) {
425
+ if (first?.type === "Literal" && isString(first.value)) {
426
+ names.add(first.value);
427
+ return;
428
+ }
429
+ nonLiteral = true;
430
+ }
431
+ return { names: [...names], nonLiteral, usesAny };
432
+ }
433
+ /** Names a binding pattern introduces (`const { a, b: [c] } = x`). */
434
+ function patternNames(pattern, out) {
435
+ if (pattern == null)
436
+ return;
437
+ const node = pattern;
438
+ switch (node.type) {
439
+ case "Identifier":
440
+ out.add(node.name);
441
+ return;
442
+ case "ObjectPattern":
443
+ for (const property of node.properties) {
444
+ patternNames(property.type === "RestElement" ? property.argument : property.value, out);
445
+ }
446
+ return;
447
+ case "ArrayPattern":
448
+ for (const element of node.elements)
449
+ patternNames(element, out);
450
+ return;
451
+ case "AssignmentPattern":
452
+ patternNames(node.left, out);
453
+ return;
454
+ case "RestElement":
455
+ patternNames(node.argument, out);
456
+ return;
457
+ default:
458
+ return;
459
+ }
460
+ }
461
+ const isAstNode = (value) => typeof value === "object" &&
462
+ value !== null &&
463
+ typeof value.type === "string";
464
+ /** Depth-first walk, passing each node with its ancestor chain (root first). */
465
+ function walk(root, visit, ancestors = []) {
466
+ if (Array.isArray(root)) {
467
+ for (const item of root)
468
+ walk(item, visit, ancestors);
469
+ return;
470
+ }
471
+ if (!isAstNode(root))
472
+ return;
473
+ visit(root, [...ancestors, root]);
474
+ ancestors.push(root);
475
+ for (const [key, value] of Object.entries(root)) {
476
+ if (key === "type" || key === "start" || key === "end" || key === "loc") {
477
+ continue;
478
+ }
479
+ walk(value, visit, ancestors);
480
+ }
481
+ ancestors.pop();
482
+ }
483
+ function collectBoundNamesFrom(statements) {
484
+ const names = new Set();
485
+ walk(statements, (node) => {
486
+ switch (node.type) {
487
+ case "VariableDeclarator":
488
+ patternNames(node.id, names);
489
+ break;
490
+ case "FunctionDeclaration":
491
+ case "FunctionExpression":
492
+ case "ArrowFunctionExpression":
493
+ if ("id" in node && node.id)
494
+ names.add(node.id.name);
495
+ for (const param of node.params)
496
+ patternNames(param, names);
497
+ break;
498
+ case "ClassDeclaration":
499
+ case "ClassExpression":
500
+ if (node.id)
501
+ names.add(node.id.name);
502
+ break;
503
+ case "CatchClause":
504
+ if (node.param)
505
+ patternNames(node.param, names);
506
+ break;
507
+ case "ImportDeclaration":
508
+ for (const specifier of node.specifiers) {
509
+ names.add(specifier.local.name);
510
+ }
511
+ break;
512
+ case "AssignmentExpression":
513
+ // The body runs non-strict, so `x = 1` on an undeclared name creates a
514
+ // global instead of throwing. Reading it afterwards is legal.
515
+ patternNames(node.left, names);
516
+ break;
517
+ default:
518
+ break;
519
+ }
520
+ });
521
+ return names;
522
+ }
523
+ /**
524
+ * Every name the code itself binds — variables, functions, classes, parameters,
525
+ * catch clauses, and undeclared assignment targets. A reference to anything
526
+ * else has to come from the sandbox or from a dynamic input, so this is the
527
+ * allowlist a caller subtracts before deciding a name was invented.
528
+ */
529
+ export function collectBoundNames(code) {
530
+ const parsed = parseCodeBody(code);
531
+ if ("error" in parsed)
532
+ return [];
533
+ return [...collectBoundNamesFrom(parsed.statements)];
534
+ }
535
+ /**
536
+ * Names the code tests with `typeof`. `typeof x` is the one read that never
537
+ * throws on a missing binding, so a name guarded anywhere in the body is one
538
+ * the author already treats as optional — a caller deciding whether a name is
539
+ * undefined has to subtract these.
540
+ */
541
+ export function typeofGuardedNames(statements) {
542
+ const guarded = new Set();
543
+ walk(statements, (node) => {
544
+ if (node.type === "UnaryExpression" &&
545
+ node.operator === "typeof" &&
546
+ node.argument.type === "Identifier") {
547
+ guarded.add(node.argument.name);
548
+ }
549
+ });
550
+ return guarded;
551
+ }
552
+ /** The global every declared input of a Code node arrives on. */
553
+ export const CODE_INPUTS_GLOBAL = "inputs";
554
+ /**
555
+ * Input names the body reads off the `inputs` global, and whether it also
556
+ * reaches for the object in a way this analysis cannot resolve to names.
557
+ *
558
+ * `inputs.text` and `inputs["text"]` both count. A dynamic read
559
+ * (`inputs[key]`), a spread, or passing `inputs` somewhere sets `opaque`:
560
+ * the body uses inputs the reader cannot enumerate, so a caller must not
561
+ * conclude an undeclared slot is unused. A body that shadows `inputs` with
562
+ * its own binding reports nothing — the reads are that binding's, not the
563
+ * node's.
564
+ */
565
+ export function inputsMemberReads(statements) {
566
+ if (collectBoundNamesFrom(statements).has(CODE_INPUTS_GLOBAL)) {
567
+ return { names: [], opaque: false };
568
+ }
569
+ const names = new Set();
570
+ let opaque = false;
571
+ walk(statements, (node, ancestors) => {
572
+ if (node.type !== "Identifier" || node.name !== CODE_INPUTS_GLOBAL)
573
+ return;
574
+ const parent = ancestors[ancestors.length - 2];
575
+ // A property *named* `inputs` (`opts.inputs`, `{inputs: 1}`) is not the
576
+ // global. `inputs.x` is, and there `inputs` is the object, not the
577
+ // property.
578
+ if (parent?.type === "MemberExpression") {
579
+ if (parent.property === node && !parent.computed)
580
+ return;
581
+ if (parent.object === node) {
582
+ if (!parent.computed && parent.property.type === "Identifier") {
583
+ names.add(parent.property.name);
584
+ return;
585
+ }
586
+ if (parent.computed &&
587
+ parent.property.type === "Literal" &&
588
+ isString(parent.property.value)) {
589
+ names.add(parent.property.value);
590
+ return;
591
+ }
592
+ // `inputs[key]` — a real read of a name only the runtime knows.
593
+ opaque = true;
594
+ return;
595
+ }
596
+ }
597
+ if (parent?.type === "Property" && parent.key === node && !parent.computed) {
598
+ return;
599
+ }
600
+ // `{...inputs}`, `f(inputs)`, `Object.keys(inputs)` — the whole bag leaves.
601
+ opaque = true;
602
+ });
603
+ return { names: [...names], opaque };
604
+ }
605
+ /**
606
+ * Identifier *reads* — occurrences that resolve against the scope chain.
607
+ * Property names, object keys and labels are excluded: none of them resolve,
608
+ * so none of them can throw a ReferenceError.
609
+ *
610
+ * Positions come along because the migration rewrites these occurrences in
611
+ * place; sharing one collector keeps "what counts as a read" from drifting
612
+ * between the check and the rewrite.
613
+ */
614
+ function identifierReads(statements) {
615
+ const reads = [];
616
+ walk(statements, (node, ancestors) => {
617
+ if (node.type !== "Identifier")
618
+ return;
619
+ const parent = ancestors[ancestors.length - 2];
620
+ if (!parent)
621
+ return;
622
+ switch (parent.type) {
623
+ case "MemberExpression":
624
+ if (parent.property === node && !parent.computed)
625
+ return;
626
+ break;
627
+ case "Property":
628
+ if (parent.key === node && !parent.computed && !parent.shorthand) {
629
+ return;
630
+ }
631
+ break;
632
+ case "MethodDefinition":
633
+ case "PropertyDefinition":
634
+ if (parent.key === node && !parent.computed)
635
+ return;
636
+ break;
637
+ case "LabeledStatement":
638
+ if (parent.label === node)
639
+ return;
640
+ break;
641
+ case "BreakStatement":
642
+ case "ContinueStatement":
643
+ if (parent.label === node)
644
+ return;
645
+ break;
646
+ case "MetaProperty":
647
+ return;
648
+ case "ImportSpecifier":
649
+ case "ImportDefaultSpecifier":
650
+ case "ImportNamespaceSpecifier":
651
+ case "ExportSpecifier":
652
+ return;
653
+ case "VariableDeclarator":
654
+ if (parent.id === node)
655
+ return;
656
+ break;
657
+ case "UnaryExpression":
658
+ // `typeof maybeMissing` is a read, but never a ReferenceError — see
659
+ // {@link typeofGuardedNames}.
660
+ break;
661
+ case "FunctionDeclaration":
662
+ case "FunctionExpression":
663
+ case "ClassDeclaration":
664
+ case "ClassExpression":
665
+ if (parent.id === node)
666
+ return;
667
+ break;
668
+ default:
669
+ break;
670
+ }
671
+ reads.push({
672
+ name: node.name,
673
+ start: node.start,
674
+ end: node.end,
675
+ shorthand: parent.type === "Property" && parent.shorthand === true
676
+ });
677
+ });
678
+ // A shorthand property's `key` and `value` are the same node, so the walk
679
+ // reaches the occurrence twice. One position is one read.
680
+ const seen = new Set();
681
+ return reads.filter((read) => {
682
+ if (seen.has(read.start))
683
+ return false;
684
+ seen.add(read.start);
685
+ return true;
686
+ });
687
+ }
688
+ /**
689
+ * Identifiers the code reads without binding them. Property names, object
690
+ * keys and labels are excluded: none of them resolve against the scope chain,
691
+ * so none of them can throw a ReferenceError.
692
+ */
693
+ export function freeIdentifiers(statements) {
694
+ const bound = collectBoundNamesFrom(statements);
695
+ const names = new Set(identifierReads(statements).map((read) => read.name));
696
+ return [...names].filter((name) => !bound.has(name));
697
+ }
698
+ /**
699
+ * Rewrite a pre-`inputs` Code node body: every free read of a declared input
700
+ * becomes `inputs.<name>`.
701
+ *
702
+ * Inputs used to arrive as globals of their own name. Bodies written that way
703
+ * throw a ReferenceError now, so this is the mechanical half of that move —
704
+ * offsets come from the AST, so a name inside a string or a comment, an object
705
+ * key, or a locally shadowed binding is left alone.
706
+ *
707
+ * Returns the rewritten source and the names it touched. `changed` is false
708
+ * when there was nothing to do, when the body does not parse, or when it binds
709
+ * `inputs` itself — rewriting into a shadowed name would silently change which
710
+ * object is read.
711
+ */
712
+ export function migrateCodeBodyToInputs(code, inputNames) {
713
+ const unchanged = { code, changed: false, rewritten: [] };
714
+ const targets = new Set(inputNames);
715
+ if (targets.size === 0)
716
+ return unchanged;
717
+ const parsed = parseCodeBody(code);
718
+ if ("error" in parsed)
719
+ return unchanged;
720
+ const bound = collectBoundNamesFrom(parsed.statements);
721
+ if (bound.has(CODE_INPUTS_GLOBAL))
722
+ return unchanged;
723
+ const edits = identifierReads(parsed.statements).filter((read) => targets.has(read.name) && !bound.has(read.name));
724
+ if (edits.length === 0)
725
+ return unchanged;
726
+ // Apply back to front so earlier offsets stay valid.
727
+ let out = code;
728
+ for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
729
+ const replacement = edit.shorthand
730
+ ? `${edit.name}: ${CODE_INPUTS_GLOBAL}.${edit.name}`
731
+ : `${CODE_INPUTS_GLOBAL}.${edit.name}`;
732
+ out = out.slice(0, edit.start) + replacement + out.slice(edit.end);
733
+ }
734
+ return {
735
+ code: out,
736
+ changed: true,
737
+ rewritten: [...new Set(edits.map((e) => e.name))].sort()
738
+ };
739
+ }
740
+ /**
741
+ * Property names the Code node itself owns. A body that reads `inputs.code`
742
+ * is not naming a user handle — it is colliding with the editor field.
743
+ */
744
+ export const CODE_NODE_OWN_PROPERTIES = new Set([
745
+ "code",
746
+ "script",
747
+ "packages",
748
+ "secrets",
749
+ "timeout",
750
+ "max_response_mb",
751
+ "allow_local_network",
752
+ "allow_host_filesystem"
753
+ ]);
754
+ function isUserHandleName(name) {
755
+ return name !== "" && !CODE_NODE_OWN_PROPERTIES.has(name);
756
+ }
757
+ /**
758
+ * Input handle names the body itself names: `inputs.foo` / `inputs["foo"]`
759
+ * and `stream("foo")` / `stream.first("foo")` / `stream.open("foo")`.
760
+ *
761
+ * A named read is a handle. The editor shows it and an agent can connect to
762
+ * it without a separate "add dynamic input" step.
763
+ */
764
+ export function inferredCodeInputNames(code) {
765
+ const parsed = parseCodeBody(code);
766
+ if ("error" in parsed)
767
+ return [];
768
+ const names = new Set();
769
+ for (const name of inputsMemberReads(parsed.statements).names) {
770
+ if (isUserHandleName(name))
771
+ names.add(name);
772
+ }
773
+ for (const name of streamCallNames(parsed.statements).names) {
774
+ if (isUserHandleName(name))
775
+ names.add(name);
776
+ }
777
+ return [...names];
778
+ }
779
+ /**
780
+ * Output handle names the body itself names: `emit("foo")` / `output("foo")`,
781
+ * or — when the body uses neither — the keys of the last `return { … }`.
782
+ */
783
+ export function inferredCodeOutputNames(code) {
784
+ const parsed = parseCodeBody(code);
785
+ if ("error" in parsed)
786
+ return [];
787
+ const emitted = outputCallNames(parsed.statements);
788
+ if (emitted.names.length > 0) {
789
+ return emitted.names.filter(isUserHandleName);
790
+ }
791
+ const { returns } = analyzeCodeBody(parsed.statements);
792
+ for (let i = returns.length - 1; i >= 0; i--) {
793
+ const shapes = returnShapes(returns[i].argument);
794
+ if (shapes.length !== 1)
795
+ continue;
796
+ const shape = shapes[0];
797
+ if (shape.opaque || shape.notAnObject || shape.keys.size === 0)
798
+ continue;
799
+ return [...shape.keys].filter(isUserHandleName);
800
+ }
801
+ return [];
802
+ }
803
+ /** Hosts of model APIs a planned Code step must not call itself. */
804
+ const MODEL_API_HOSTS = [
805
+ "api.openai.com",
806
+ "api.anthropic.com",
807
+ "generativelanguage.googleapis.com",
808
+ "openrouter.ai",
809
+ "api.mistral.ai",
810
+ "api.groq.com",
811
+ "api.together.xyz",
812
+ "api.deepseek.com"
813
+ ];
814
+ /** Host calls that return a promise; a body that does not await one holds a Promise. */
815
+ const ASYNC_HOST_GLOBALS = new Set(["getSecret", "fetch", "sleep"]);
816
+ const ASYNC_HOST_OBJECTS = new Set(["workspace", "media"]);
817
+ /** The host call a `const x = <call>` initializer makes without `await`, if any. */
818
+ function unawaitedHostCall(init) {
819
+ if (init?.type !== "CallExpression")
820
+ return null;
821
+ const callee = init.callee;
822
+ if (callee.type === "Identifier" && ASYNC_HOST_GLOBALS.has(callee.name)) {
823
+ return callee.name;
824
+ }
825
+ if (callee.type === "MemberExpression" &&
826
+ callee.object.type === "Identifier" &&
827
+ ASYNC_HOST_OBJECTS.has(callee.object.name) &&
828
+ callee.property.type === "Identifier") {
829
+ return `${callee.object.name}.${callee.property.name}`;
830
+ }
831
+ return null;
832
+ }
833
+ const LOOP_TYPES = new Set([
834
+ "ForStatement",
835
+ "ForInStatement",
836
+ "ForOfStatement",
837
+ "WhileStatement",
838
+ "DoWhileStatement"
839
+ ]);
840
+ /** A function handed to a call — `rows.forEach(row => …)` runs per item. */
841
+ function isCallbackArgument(node, parent) {
842
+ return ((node.type === "ArrowFunctionExpression" ||
843
+ node.type === "FunctionExpression") &&
844
+ parent?.type === "CallExpression" &&
845
+ parent.arguments.includes(node));
846
+ }
847
+ /**
848
+ * What would make a Code step the workflow planner wrote fail or pass nothing
849
+ * on, checked before anything runs.
850
+ *
851
+ * `inputs` are the handles the build connects, `output` the handle the next
852
+ * step reads. Each problem is a sentence the planner can act on: the repair
853
+ * round sends them back verbatim.
854
+ */
855
+ export function plannedCodeStepProblems(code, handles) {
856
+ const parsed = parseCodeBody(code);
857
+ if ("error" in parsed) {
858
+ return [`its code does not parse: ${parsed.error}`];
859
+ }
860
+ const problems = [];
861
+ let outputInLoop = false;
862
+ walk(parsed.statements, (node, ancestors) => {
863
+ if (outputInLoop || node.type !== "CallExpression")
864
+ return;
865
+ if (node.callee.type !== "Identifier" || node.callee.name !== "output") {
866
+ return;
867
+ }
868
+ outputInLoop = ancestors.some((ancestor, index) => LOOP_TYPES.has(ancestor.type) ||
869
+ isCallbackArgument(ancestor, ancestors[index - 1]));
870
+ });
871
+ if (outputInLoop) {
872
+ problems.push("it calls output() once per item, but output() can be set only once per run — " +
873
+ "call await emit(name, item) per item, or collect the items and call output() once after the loop");
874
+ }
875
+ const wired = new Set(handles.inputs);
876
+ const memberReads = inputsMemberReads(parsed.statements);
877
+ const streamReads = streamCallNames(parsed.statements);
878
+ const unwired = memberReads.names
879
+ .filter(isUserHandleName)
880
+ .filter((name) => !wired.has(name));
881
+ if (unwired.length > 0) {
882
+ problems.push(`it reads ${unwired.map((name) => `inputs.${name}`).join(", ")}, but the only connected ` +
883
+ `input${handles.inputs.length === 1 ? " is" : "s are"} ${handles.inputs.length > 0
884
+ ? handles.inputs.map((name) => `inputs.${name}`).join(", ")
885
+ : "none"}`);
886
+ }
887
+ // A connected input the body never names carries a value nowhere. Skipped
888
+ // when the body reads `inputs` as a whole or by a computed key.
889
+ if (!memberReads.opaque && !streamReads.nonLiteral && !streamReads.usesAny) {
890
+ const read = new Set([...memberReads.names, ...streamReads.names]);
891
+ const unread = handles.inputs.filter((name) => !read.has(name));
892
+ if (unread.length > 0) {
893
+ problems.push(`${unread.map((name) => `inputs.${name}`).join(", ")} ${unread.length === 1 ? "is" : "are"} connected, but the body never reads ${unread.length === 1 ? "it" : "them"}`);
894
+ }
895
+ }
896
+ const unawaited = new Set();
897
+ walk(parsed.statements, (node) => {
898
+ if (node.type !== "VariableDeclarator")
899
+ return;
900
+ const call = unawaitedHostCall(node.init);
901
+ if (call !== null)
902
+ unawaited.add(call);
903
+ });
904
+ if (unawaited.size > 0) {
905
+ problems.push(`it assigns ${[...unawaited].map((name) => `${name}()`).join(", ")} without await, so the variable holds a Promise`);
906
+ }
907
+ const modelHost = MODEL_API_HOSTS.find((host) => code.includes(host));
908
+ if (modelHost !== undefined) {
909
+ problems.push(`it calls the model API at ${modelHost} itself; put the model call in its own step with a model_role`);
910
+ }
911
+ const outputs = inferredCodeOutputNames(code);
912
+ if (!outputs.includes(handles.output)) {
913
+ problems.push(outputs.length === 0
914
+ ? `it never sets "${handles.output}", so the next step receives nothing`
915
+ : `it sets ${outputs.map((name) => `"${name}"`).join(", ")}, but the next step reads "${handles.output}"`);
916
+ }
917
+ return problems;
918
+ }
919
+ //# sourceMappingURL=code-analysis.js.map