@loomwork/compiler 0.0.0-stage → 0.1.1

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 (59) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +62 -2
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +81 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/hash.d.ts +3 -0
  9. package/dist/hash.d.ts.map +1 -0
  10. package/dist/hash.js +16 -0
  11. package/dist/hash.js.map +1 -0
  12. package/dist/importResolve.d.ts +5 -0
  13. package/dist/importResolve.d.ts.map +1 -0
  14. package/dist/importResolve.js +130 -0
  15. package/dist/importResolve.js.map +1 -0
  16. package/dist/index.d.ts +14 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +26 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/loopIdentity.d.ts +18 -0
  21. package/dist/loopIdentity.d.ts.map +1 -0
  22. package/dist/loopIdentity.js +179 -0
  23. package/dist/loopIdentity.js.map +1 -0
  24. package/dist/manifest.d.ts +98 -0
  25. package/dist/manifest.d.ts.map +1 -0
  26. package/dist/manifest.js +123 -0
  27. package/dist/manifest.js.map +1 -0
  28. package/dist/nondeterminism.d.ts +25 -0
  29. package/dist/nondeterminism.d.ts.map +1 -0
  30. package/dist/nondeterminism.js +88 -0
  31. package/dist/nondeterminism.js.map +1 -0
  32. package/dist/plugin.d.ts +3 -0
  33. package/dist/plugin.d.ts.map +1 -0
  34. package/dist/plugin.js +71 -0
  35. package/dist/plugin.js.map +1 -0
  36. package/dist/runnerReach.d.ts +136 -0
  37. package/dist/runnerReach.d.ts.map +1 -0
  38. package/dist/runnerReach.js +1809 -0
  39. package/dist/runnerReach.js.map +1 -0
  40. package/dist/transform.d.ts +6 -0
  41. package/dist/transform.d.ts.map +1 -0
  42. package/dist/transform.js +416 -0
  43. package/dist/transform.js.map +1 -0
  44. package/dist/types.d.ts +73 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +3 -0
  47. package/dist/types.js.map +1 -0
  48. package/package.json +64 -3
  49. package/src/cli.ts +90 -0
  50. package/src/hash.ts +12 -0
  51. package/src/importResolve.ts +133 -0
  52. package/src/index.ts +23 -0
  53. package/src/loopIdentity.ts +210 -0
  54. package/src/manifest.ts +185 -0
  55. package/src/nondeterminism.ts +133 -0
  56. package/src/plugin.ts +74 -0
  57. package/src/runnerReach.ts +1964 -0
  58. package/src/transform.ts +523 -0
  59. package/src/types.ts +79 -0
@@ -0,0 +1,1809 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.KNOWN_STEP_FACTORIES = void 0;
7
+ exports.stringLiteralValue = stringLiteralValue;
8
+ exports.inferStepFactory = inferStepFactory;
9
+ exports.analyzeReachNode = analyzeReachNode;
10
+ exports.analyzeReachRegion = analyzeReachRegion;
11
+ exports.analyzeRunnerReach = analyzeRunnerReach;
12
+ const typescript_1 = __importDefault(require("typescript"));
13
+ /**
14
+ * First-party factories whose effect id is fixed and whose source is in a package (never followed
15
+ * through an import). `mcp.invoke` is `MCP_INVOKE_TOOL_ID` in `@loomwork/plugin-mcp` and `sandbox.eval` is
16
+ * `SANDBOX_TOOL_ID` in `@loomwork/sandbox` (re-exported by `@loomwork/plugin-sandbox`, ADR 0144); a test pins each to its package.
17
+ */
18
+ exports.KNOWN_STEP_FACTORIES = {
19
+ mcpInvoke$: { fixed: "mcp.invoke" },
20
+ // `sandboxEval$(id, toolId = SANDBOX_TOOL_ID)` in `@loomwork/sandbox` (ADR 0144).
21
+ sandboxEval$: { argIndex: 1, default: "sandbox.eval" },
22
+ };
23
+ /** The string value of a property on an object literal, when it is a literal. */
24
+ function stringProp(obj, name) {
25
+ for (const p of obj.properties) {
26
+ if (!typescript_1.default.isPropertyAssignment(p))
27
+ continue;
28
+ const key = typescript_1.default.isIdentifier(p.name) || typescript_1.default.isStringLiteral(p.name) ? p.name.text : undefined;
29
+ if (key !== name)
30
+ continue;
31
+ if (typescript_1.default.isStringLiteral(p.initializer) || typescript_1.default.isNoSubstitutionTemplateLiteral(p.initializer)) {
32
+ return p.initializer.text;
33
+ }
34
+ return undefined;
35
+ }
36
+ return undefined;
37
+ }
38
+ /** Whether a property is present at all, regardless of whether its value is a literal. */
39
+ function hasProp(obj, name) {
40
+ return obj.properties.some((p) => {
41
+ if (!typescript_1.default.isPropertyAssignment(p) && !typescript_1.default.isShorthandPropertyAssignment(p))
42
+ return false;
43
+ return (typescript_1.default.isIdentifier(p.name) || typescript_1.default.isStringLiteral(p.name)) && p.name.text === name;
44
+ });
45
+ }
46
+ /** A method call `<anything>.<name>(…)` — receiver-agnostic, like the determinism analyzer. */
47
+ function methodCall(node, name) {
48
+ if (!typescript_1.default.isCallExpression(node))
49
+ return undefined;
50
+ const e = node.expression;
51
+ return typescript_1.default.isPropertyAccessExpression(e) && typescript_1.default.isIdentifier(e.name) && e.name.text === name
52
+ ? node
53
+ : undefined;
54
+ }
55
+ /**
56
+ * Collect `const NAME = anything$({ … toolId: "…" … })` bindings in this region.
57
+ *
58
+ * Shape-based rather than keyed to `step$` by name, so a project wrapper
59
+ * (`myStep$`, a re-export, a locally curried factory) resolves too. The
60
+ * discriminator is a string-literal `toolId`, which is the only thing that makes
61
+ * a binding useful here.
62
+ */
63
+ function collectBindings(root) {
64
+ const bindings = new Map();
65
+ function visit(node) {
66
+ if (typescript_1.default.isVariableDeclaration(node) && typescript_1.default.isIdentifier(node.name)) {
67
+ const init = node.initializer;
68
+ if (init !== undefined && typescript_1.default.isCallExpression(init) && init.arguments.length > 0) {
69
+ const arg = init.arguments[0];
70
+ if (arg !== undefined && typescript_1.default.isObjectLiteralExpression(arg)) {
71
+ const toolId = stringProp(arg, "toolId");
72
+ if (toolId !== undefined) {
73
+ const stepId = stringProp(arg, "id");
74
+ bindings.set(node.name.text, { toolId, ...(stepId !== undefined && { stepId }) });
75
+ }
76
+ }
77
+ }
78
+ }
79
+ typescript_1.default.forEachChild(node, visit);
80
+ }
81
+ visit(root);
82
+ return bindings;
83
+ }
84
+ /** The name a call's callee ends in: `ctx.run` / `ctx["run"]` / `run`, through `.call/.apply/.bind`. */
85
+ function calleeMethodName(call) {
86
+ let e = unwrap(call.expression);
87
+ for (;;) {
88
+ if (typescript_1.default.isPropertyAccessExpression(e)) {
89
+ if (["call", "apply", "bind"].includes(e.name.text) &&
90
+ typescript_1.default.isPropertyAccessExpression(unwrap(e.expression)))
91
+ e = unwrap(e.expression);
92
+ else
93
+ return e.name.text;
94
+ }
95
+ else if (typescript_1.default.isElementAccessExpression(e)) {
96
+ const a = unwrap(e.argumentExpression);
97
+ return typescript_1.default.isStringLiteralLike(a) ? a.text : undefined;
98
+ }
99
+ else
100
+ return typescript_1.default.isIdentifier(e) ? e.text : undefined;
101
+ }
102
+ }
103
+ /**
104
+ * Whether a step object bound to `name` can change between its declaration and its use (ADR 0086,
105
+ * 2026-10-10 amendment). The only uses that cannot change it are: the declaration, an argument of a
106
+ * ctx method call (`ctx.run(s)`) or a ctx hand-off (`lib(ctx, s)`, L34), an element of an array literal (a `ctx.parallel` tuple), a spread
107
+ * copy, and a property READ. Anything else - a property assignment / `delete` / `++`, being handed to
108
+ * `Object.assign(s, ...)` or any other function, aliased, returned, stored in another object -
109
+ * makes the binding "not followed" (unresolved): a guess at `toolId` would name the wrong effect.
110
+ */
111
+ function stepBindingEscapes(sourceFile, name, ctxNames, isLoomFlowBuilder = () => false) {
112
+ let escapes = false;
113
+ const strip = (n) => {
114
+ let top = n;
115
+ while (top.parent !== undefined &&
116
+ (typescript_1.default.isParenthesizedExpression(top.parent) ||
117
+ typescript_1.default.isAsExpression(top.parent) ||
118
+ typescript_1.default.isNonNullExpression(top.parent) ||
119
+ typescript_1.default.isSatisfiesExpression(top.parent)))
120
+ top = top.parent;
121
+ return top;
122
+ };
123
+ const isWriteTarget = (member) => {
124
+ let top = strip(member);
125
+ // Walk out of destructuring-assignment patterns: `[s.toolId] = …`, `({ a: s.toolId } = …)`.
126
+ while (top.parent !== undefined &&
127
+ (typescript_1.default.isArrayLiteralExpression(top.parent) ||
128
+ typescript_1.default.isPropertyAssignment(top.parent) ||
129
+ typescript_1.default.isShorthandPropertyAssignment(top.parent) ||
130
+ typescript_1.default.isSpreadElement(top.parent) ||
131
+ typescript_1.default.isSpreadAssignment(top.parent) ||
132
+ typescript_1.default.isObjectLiteralExpression(top.parent)))
133
+ top = strip(top.parent);
134
+ const p = top.parent;
135
+ if (p === undefined)
136
+ return false;
137
+ if (typescript_1.default.isBinaryExpression(p))
138
+ return (p.left === top &&
139
+ p.operatorToken.kind >= typescript_1.default.SyntaxKind.FirstAssignment &&
140
+ p.operatorToken.kind <= typescript_1.default.SyntaxKind.LastAssignment);
141
+ if (typescript_1.default.isDeleteExpression(p))
142
+ return true;
143
+ if (typescript_1.default.isPrefixUnaryExpression(p) || typescript_1.default.isPostfixUnaryExpression(p))
144
+ return (p.operator === typescript_1.default.SyntaxKind.PlusPlusToken || p.operator === typescript_1.default.SyntaxKind.MinusMinusToken);
145
+ // `for (s.toolId of xs)` / `for (s.toolId in o)`.
146
+ return (typescript_1.default.isForOfStatement(p) || typescript_1.default.isForInStatement(p)) && p.initializer === top;
147
+ };
148
+ const walk = (n) => {
149
+ if (escapes)
150
+ return;
151
+ if (typescript_1.default.isIdentifier(n) && n.text === name) {
152
+ const p = n.parent;
153
+ const top = strip(n);
154
+ const q = top.parent;
155
+ const isDecl = typescript_1.default.isVariableDeclaration(p) && p.name === n;
156
+ // `{ s: 1 }` key, `x.s` member name, `{ s }` shorthand is a VALUE use (handled by falling through).
157
+ const isNotAReference = (typescript_1.default.isPropertyAssignment(p) && p.name === n) ||
158
+ (typescript_1.default.isPropertyAccessExpression(p) && p.name === n) ||
159
+ (typescript_1.default.isPropertySignature(p) && p.name === n);
160
+ if (!isDecl && !isNotAReference) {
161
+ let ok = false;
162
+ // A ctx method call or a ctx hand-off (`lib(ctx, s)`, the declared-reach seam, L34) may take
163
+ // an argument: its step arguments are recorded for the callee's declaration.
164
+ const isCtxCall = (c) => {
165
+ const m = calleeMethodName(c);
166
+ return ((m !== undefined && CTX_METHODS.has(m)) ||
167
+ c.arguments.some((a) => {
168
+ const u = unwrap(a);
169
+ return typescript_1.default.isIdentifier(u) && ctxNames.has(u.text);
170
+ }));
171
+ };
172
+ if (q !== undefined &&
173
+ typescript_1.default.isCallExpression(q) &&
174
+ q.arguments.includes(top)) {
175
+ ok = isCtxCall(q);
176
+ }
177
+ else if (q !== undefined && typescript_1.default.isArrayLiteralExpression(q)) {
178
+ // A `ctx.parallel([[s, input]])` tuple: safe ONLY when the OUTERMOST enclosing array
179
+ // literal is itself directly an argument of a ctx call. Bound, iterated, destructured or
180
+ // spread (`const a = [s]`, `[s].forEach`, `f(...[s])`) it is an alias: an escape.
181
+ let outer = q;
182
+ for (;;) {
183
+ const o = strip(outer);
184
+ if (o.parent !== undefined && typescript_1.default.isArrayLiteralExpression(o.parent))
185
+ outer = o.parent;
186
+ else {
187
+ outer = o;
188
+ break;
189
+ }
190
+ }
191
+ const host = outer.parent;
192
+ if (host !== undefined &&
193
+ typescript_1.default.isVariableDeclaration(host) &&
194
+ host.initializer === outer &&
195
+ typescript_1.default.isIdentifier(host.name) &&
196
+ typescript_1.default.isVariableDeclarationList(host.parent) &&
197
+ (host.parent.flags & typescript_1.default.NodeFlags.Const) !== 0) {
198
+ // `const pairs = [[s, 1]]; ctx.parallel(pairs)`: the list is followed (rule 5) only if
199
+ // the binding is used nowhere but as a direct ctx-call argument.
200
+ const listName = host.name.text;
201
+ let clean = true;
202
+ const scan = (n) => {
203
+ if (!clean)
204
+ return;
205
+ if (typescript_1.default.isIdentifier(n) && n.text === listName && n !== host.name) {
206
+ const isKey = (typescript_1.default.isPropertyAssignment(n.parent) && n.parent.name === n) ||
207
+ (typescript_1.default.isPropertyAccessExpression(n.parent) && n.parent.name === n);
208
+ const t = strip(n);
209
+ const c = t.parent;
210
+ if (!isKey &&
211
+ !(c !== undefined &&
212
+ typescript_1.default.isCallExpression(c) &&
213
+ c.arguments.includes(t) &&
214
+ isCtxCall(c)))
215
+ clean = false;
216
+ }
217
+ typescript_1.default.forEachChild(n, scan);
218
+ };
219
+ scan(sourceFile);
220
+ ok = clean;
221
+ }
222
+ else if (host !== undefined &&
223
+ typescript_1.default.isPropertyAssignment(host) &&
224
+ host.initializer === outer &&
225
+ propName(host.name) === "steps" &&
226
+ typescript_1.default.isObjectLiteralExpression(strip(host.parent)) &&
227
+ (() => {
228
+ const lit = strip(host.parent);
229
+ const call = lit.parent;
230
+ return (call !== undefined &&
231
+ typescript_1.default.isCallExpression(call) &&
232
+ call.arguments[0] === lit &&
233
+ isLoomFlowBuilder(call.expression));
234
+ })()) {
235
+ // `flow$({ steps: [s] })` DECLARES the step: loom's own `flow$` (imported unshadowed from
236
+ // @loomwork/core / @loomwork/sdk) keeps the list and the runtime only reads it.
237
+ ok = true;
238
+ }
239
+ else {
240
+ ok =
241
+ host !== undefined &&
242
+ typescript_1.default.isCallExpression(host) &&
243
+ host.arguments.includes(outer) &&
244
+ isCtxCall(host);
245
+ }
246
+ }
247
+ else if (q !== undefined && typescript_1.default.isSpreadAssignment(q))
248
+ ok = true;
249
+ else if (q !== undefined &&
250
+ (typescript_1.default.isPropertyAccessExpression(q) || typescript_1.default.isElementAccessExpression(q)) &&
251
+ q.expression === top) {
252
+ // A property READ is fine; a write through it, or a method call on it, is not.
253
+ let chain = q;
254
+ while (chain.parent !== undefined &&
255
+ (typescript_1.default.isPropertyAccessExpression(chain.parent) ||
256
+ typescript_1.default.isElementAccessExpression(chain.parent)) &&
257
+ chain.parent.expression === chain)
258
+ chain = chain.parent;
259
+ const callee = strip(chain).parent;
260
+ const isMethodCall = callee !== undefined &&
261
+ typescript_1.default.isCallExpression(callee) &&
262
+ callee.expression === strip(chain);
263
+ ok = !isWriteTarget(chain) && !isMethodCall;
264
+ }
265
+ if (!ok)
266
+ escapes = true;
267
+ }
268
+ }
269
+ typescript_1.default.forEachChild(n, walk);
270
+ };
271
+ walk(sourceFile);
272
+ return escapes;
273
+ }
274
+ /** Short, stable source text for an unresolved expression (bounded for log safety). */
275
+ function exprText(node) {
276
+ if (typescript_1.default.isTaggedTemplateExpression(node))
277
+ return `${exprText(node.tag)}\`…\``;
278
+ const text = node.getText().replace(/\s+/g, " ").trim();
279
+ return text.length > 80 ? `${text.slice(0, 77)}…` : text;
280
+ }
281
+ function isFn(node) {
282
+ return (typescript_1.default.isArrowFunction(node) || typescript_1.default.isFunctionExpression(node) || typescript_1.default.isFunctionDeclaration(node));
283
+ }
284
+ /**
285
+ * Collect functions declared in this region by name (`function f(){}`,
286
+ * `const f = (s) => …`), so `ctx.decide("x", f)` can be followed when `f` is local.
287
+ *
288
+ * Resolution is by TEXTUAL name in one flat map, so a name is only followed when it is
289
+ * unambiguous: declared exactly once in the region (as a function, a variable, a
290
+ * parameter or a binding), with `const`/`function`, and never reassigned. Anything else
291
+ * is left out, which makes `ctx.decide("x", name)` unresolved rather than silently
292
+ * binding to whichever declaration came last.
293
+ */
294
+ function collectFunctions(root) {
295
+ return scanDeclarations(root).fns;
296
+ }
297
+ /** Same unambiguity rule as {@link collectFunctions}, for any `const NAME = <initializer>`. */
298
+ function collectConstInits(root) {
299
+ return scanDeclarations(root).inits;
300
+ }
301
+ function scanDeclarations(root) {
302
+ const fns = new Map();
303
+ const inits = new Map();
304
+ const declared = new Map();
305
+ const ambiguous = new Set();
306
+ const declare = (name) => {
307
+ declared.set(name, (declared.get(name) ?? 0) + 1);
308
+ };
309
+ function visit(node) {
310
+ if (typescript_1.default.isFunctionDeclaration(node) && node.name !== undefined) {
311
+ declare(node.name.text);
312
+ fns.set(node.name.text, node);
313
+ }
314
+ else if ((typescript_1.default.isClassDeclaration(node) ||
315
+ typescript_1.default.isClassExpression(node) ||
316
+ typescript_1.default.isEnumDeclaration(node) ||
317
+ typescript_1.default.isFunctionExpression(node)) &&
318
+ node.name !== undefined) {
319
+ declare(node.name.text);
320
+ ambiguous.add(node.name.text);
321
+ }
322
+ else if (typescript_1.default.isModuleDeclaration(node) && typescript_1.default.isIdentifier(node.name)) {
323
+ declare(node.name.text);
324
+ ambiguous.add(node.name.text);
325
+ }
326
+ else if (typescript_1.default.isVariableDeclaration(node) && typescript_1.default.isIdentifier(node.name)) {
327
+ declare(node.name.text);
328
+ const isConst = (typescript_1.default.getCombinedNodeFlags(node) & typescript_1.default.NodeFlags.Const) !== 0;
329
+ if (!isConst)
330
+ ambiguous.add(node.name.text);
331
+ if (node.initializer !== undefined &&
332
+ (typescript_1.default.isArrowFunction(node.initializer) || typescript_1.default.isFunctionExpression(node.initializer))) {
333
+ fns.set(node.name.text, node.initializer);
334
+ }
335
+ if (node.initializer !== undefined)
336
+ inits.set(node.name.text, node.initializer);
337
+ }
338
+ else if (typescript_1.default.isParameter(node) && typescript_1.default.isIdentifier(node.name)) {
339
+ declare(node.name.text);
340
+ }
341
+ else if (typescript_1.default.isBindingElement(node) && typescript_1.default.isIdentifier(node.name)) {
342
+ declare(node.name.text);
343
+ }
344
+ else if (typescript_1.default.isBinaryExpression(node) &&
345
+ node.operatorToken.kind >= typescript_1.default.SyntaxKind.FirstAssignment &&
346
+ node.operatorToken.kind <= typescript_1.default.SyntaxKind.LastAssignment &&
347
+ typescript_1.default.isIdentifier(node.left)) {
348
+ ambiguous.add(node.left.text);
349
+ }
350
+ typescript_1.default.forEachChild(node, visit);
351
+ }
352
+ visit(root);
353
+ for (const name of [...fns.keys()]) {
354
+ if (ambiguous.has(name) || (declared.get(name) ?? 0) > 1)
355
+ fns.delete(name);
356
+ }
357
+ for (const name of [...inits.keys()]) {
358
+ if (ambiguous.has(name) || (declared.get(name) ?? 0) > 1)
359
+ inits.delete(name);
360
+ }
361
+ return { fns, inits, declared, ambiguous };
362
+ }
363
+ /** Every name bound by a binding name (identifier or destructuring pattern). */
364
+ function bindingNames(name, out) {
365
+ if (typescript_1.default.isIdentifier(name))
366
+ out.add(name.text);
367
+ else
368
+ for (const el of name.elements)
369
+ if (!typescript_1.default.isOmittedExpression(el))
370
+ bindingNames(el.name, out);
371
+ }
372
+ /** Every name declared anywhere inside `node` (parameters, variables, patterns, functions, classes). */
373
+ function namesDeclaredWithin(node) {
374
+ const out = new Set();
375
+ const visit = (n) => {
376
+ if ((typescript_1.default.isVariableDeclaration(n) || typescript_1.default.isParameter(n) || typescript_1.default.isBindingElement(n)) && n.name)
377
+ bindingNames(n.name, out);
378
+ else if ((typescript_1.default.isFunctionDeclaration(n) ||
379
+ typescript_1.default.isFunctionExpression(n) ||
380
+ typescript_1.default.isClassDeclaration(n) ||
381
+ typescript_1.default.isClassExpression(n) ||
382
+ typescript_1.default.isEnumDeclaration(n)) &&
383
+ n.name !== undefined)
384
+ out.add(n.name.text);
385
+ typescript_1.default.forEachChild(n, visit);
386
+ };
387
+ visit(node);
388
+ return out;
389
+ }
390
+ /** Whether any identifier inside `e` is in `names`. */
391
+ function mentionsAny(e, names) {
392
+ if (typescript_1.default.isIdentifier(e) && names.has(e.text))
393
+ return true;
394
+ return typescript_1.default.forEachChild(e, (c) => (mentionsAny(c, names) ? true : undefined)) === true;
395
+ }
396
+ /** Import bindings of a module: local name -> module specifier and the imported (exported) name. */
397
+ /** Every local name an import binds (named, default and namespace; type-only included, harmlessly). */
398
+ function allImportBindings(sf) {
399
+ const out = new Set();
400
+ for (const st of sf.statements) {
401
+ if (!typescript_1.default.isImportDeclaration(st))
402
+ continue;
403
+ const clause = st.importClause;
404
+ if (clause === undefined)
405
+ continue;
406
+ if (clause.name !== undefined)
407
+ out.add(clause.name.text);
408
+ const nb = clause.namedBindings;
409
+ if (nb === undefined)
410
+ continue;
411
+ if (typescript_1.default.isNamespaceImport(nb))
412
+ out.add(nb.name.text);
413
+ else
414
+ for (const el of nb.elements)
415
+ out.add(el.name.text);
416
+ }
417
+ return out;
418
+ }
419
+ function importedNames(sf) {
420
+ const out = new Map();
421
+ for (const st of sf.statements) {
422
+ if (!typescript_1.default.isImportDeclaration(st) || !typescript_1.default.isStringLiteral(st.moduleSpecifier))
423
+ continue;
424
+ const clause = st.importClause;
425
+ if (clause === undefined || clause.isTypeOnly)
426
+ continue;
427
+ const nb = clause.namedBindings;
428
+ if (nb !== undefined && typescript_1.default.isNamedImports(nb)) {
429
+ for (const el of nb.elements) {
430
+ if (el.isTypeOnly)
431
+ continue;
432
+ out.set(el.name.text, {
433
+ spec: st.moduleSpecifier.text,
434
+ imported: (el.propertyName ?? el.name).text,
435
+ });
436
+ }
437
+ }
438
+ }
439
+ return out;
440
+ }
441
+ /**
442
+ * Module specifiers a {@link KNOWN_STEP_FACTORIES} entry may be imported from: its package, and
443
+ * `@loomwork/sdk` where the SDK re-exports it (`export * from "@loomwork/plugin-mcp"`; the sandbox plugin
444
+ * is only at the `@loomwork/sdk/sandbox` subpath).
445
+ */
446
+ const KNOWN_FACTORY_SOURCES = {
447
+ mcpInvoke$: ["@loomwork/plugin-mcp", "@loomwork/sdk"],
448
+ sandboxEval$: ["@loomwork/plugin-sandbox", "@loomwork/sandbox", "@loomwork/sdk/sandbox"],
449
+ };
450
+ /** The string a literal-ish expression denotes (`"x"`, an expression-free template, `"x" as const`). */
451
+ function stringLiteralValue(e) {
452
+ const u = unwrap(e);
453
+ return typescript_1.default.isStringLiteral(u) || typescript_1.default.isNoSubstitutionTemplateLiteral(u) ? u.text : undefined;
454
+ }
455
+ /**
456
+ * L37 - infer how a function yields a step's effect id, by SHAPE (like {@link collectBindings}):
457
+ * its single returned expression is a call whose first argument is an object literal with a
458
+ * `toolId` that is a constant (per `str`) or one of the function's plain parameters. Anything else
459
+ * (several returns, a spread, a computed toolId) is `undefined`, never a guess.
460
+ */
461
+ function inferStepFactory(fn, rawStr) {
462
+ if (!isFn(fn) || fn.body === undefined)
463
+ return undefined;
464
+ // Names declared anywhere in the factory (parameters, body locals, nested scopes) shadow a
465
+ // module constant of the same name, so an expression that mentions one is never constant-folded.
466
+ const inner = namesDeclaredWithin(fn);
467
+ const str = (e) => mentionsAny(e, inner) ? undefined : rawStr(e);
468
+ let returned;
469
+ if (!typescript_1.default.isBlock(fn.body))
470
+ returned = fn.body;
471
+ else {
472
+ let count = 0;
473
+ const scan = (n) => {
474
+ if (isFn(n) || typescript_1.default.isMethodDeclaration(n) || typescript_1.default.isClassLike(n))
475
+ return;
476
+ if (typescript_1.default.isReturnStatement(n)) {
477
+ count++;
478
+ returned = n.expression;
479
+ }
480
+ typescript_1.default.forEachChild(n, scan);
481
+ };
482
+ typescript_1.default.forEachChild(fn.body, scan);
483
+ if (count !== 1)
484
+ return undefined;
485
+ }
486
+ if (returned === undefined)
487
+ return undefined;
488
+ const call = unwrap(returned);
489
+ if (!typescript_1.default.isCallExpression(call))
490
+ return undefined;
491
+ const arg = call.arguments[0];
492
+ if (arg === undefined || !typescript_1.default.isObjectLiteralExpression(arg))
493
+ return undefined;
494
+ const paramIndex = (name) => {
495
+ const i = fn.parameters.findIndex((p) => typescript_1.default.isIdentifier(p.name) && p.name.text === name);
496
+ const p = fn.parameters[i];
497
+ return i >= 0 && p !== undefined && p.dotDotDotToken === undefined ? i : undefined;
498
+ };
499
+ const viaParam = (i) => {
500
+ const init = fn.parameters[i]?.initializer;
501
+ const dflt = init === undefined ? undefined : str(init);
502
+ return { argIndex: i, ...(dflt !== undefined && { default: dflt }) };
503
+ };
504
+ for (const prop of arg.properties) {
505
+ if (typescript_1.default.isShorthandPropertyAssignment(prop) && prop.name.text === "toolId") {
506
+ const i = paramIndex("toolId");
507
+ return i === undefined ? undefined : viaParam(i);
508
+ }
509
+ if (typescript_1.default.isPropertyAssignment(prop) && propName(prop.name) === "toolId") {
510
+ const v = unwrap(prop.initializer);
511
+ if (typescript_1.default.isIdentifier(v)) {
512
+ const i = paramIndex(v.text);
513
+ if (i !== undefined)
514
+ return viaParam(i);
515
+ }
516
+ const fixed = str(v);
517
+ return fixed !== undefined ? { fixed } : undefined;
518
+ }
519
+ }
520
+ return undefined;
521
+ }
522
+ /** ExecutionContext members whose VALUE, handed out, lets the receiver run or suspend the flow. */
523
+ const CTX_METHODS = new Set(["run", "execute", "parallel", "suspend", "sleep", "sleepUntil"]);
524
+ /** Strip parentheses and type-only wrappers. */
525
+ function unwrap(e) {
526
+ let cur = e;
527
+ while (typescript_1.default.isParenthesizedExpression(cur) ||
528
+ typescript_1.default.isAsExpression(cur) ||
529
+ typescript_1.default.isNonNullExpression(cur) ||
530
+ typescript_1.default.isSatisfiesExpression(cur) ||
531
+ typescript_1.default.isTypeAssertionExpression(cur) ||
532
+ (typescript_1.default.isBinaryExpression(cur) && cur.operatorToken.kind === typescript_1.default.SyntaxKind.CommaToken)) {
533
+ cur = typescript_1.default.isBinaryExpression(cur) ? cur.right : cur.expression;
534
+ }
535
+ return cur;
536
+ }
537
+ /** A runner expression that is plainly not a function value (no refusal needed). */
538
+ function isInertValue(e) {
539
+ return (e.kind === typescript_1.default.SyntaxKind.NullKeyword ||
540
+ e.kind === typescript_1.default.SyntaxKind.TrueKeyword ||
541
+ e.kind === typescript_1.default.SyntaxKind.FalseKeyword ||
542
+ typescript_1.default.isStringLiteralLike(e) ||
543
+ typescript_1.default.isNumericLiteral(e) ||
544
+ (typescript_1.default.isIdentifier(e) && e.text === "undefined"));
545
+ }
546
+ /**
547
+ * Runner functions declared in this region: a `runner:` property or `runner = …` class field
548
+ * (inline or a local identifier / shorthand), a `runner(ctx, …) {}` method, the second argument
549
+ * of `registerFlow(def, runner)`, or a function bound to a `…FlowRunner`-typed variable. Their
550
+ * FIRST parameter is the flow's ctx whatever it is called.
551
+ *
552
+ * A runner VALUE that is not a followable function is never silently skipped (round 2):
553
+ * - a call (`withRetry(async (c) => …)`, `makeRunner(lib)`): every function-literal argument
554
+ * (or local function passed by name) is treated as a runner, because a wrapper hands its ctx
555
+ * to the callback; and the call itself is reported `runner not analysable` since the wrapper
556
+ * may do anything else with ctx;
557
+ * - an identifier is followed to a local function or a `const` initializer, else refused
558
+ * (imported, ambiguous);
559
+ * - anything else (member access, conditional branches that are not functions) is refused.
560
+ */
561
+ function collectRunners(root, functions) {
562
+ const out = [];
563
+ /** Followed runners tied to a `flow$` definition (see {@link boundContext}). */
564
+ const bound = new Set();
565
+ let boundNow = false;
566
+ const isFlowCall = (e) => {
567
+ if (e === undefined)
568
+ return false;
569
+ const u = unwrap(e);
570
+ if (!typescript_1.default.isCallExpression(u))
571
+ return false;
572
+ const c = unwrap(u.expression);
573
+ return ((typescript_1.default.isIdentifier(c) ? c.text : typescript_1.default.isPropertyAccessExpression(c) ? c.name.text : "") ===
574
+ "flow$");
575
+ };
576
+ /** An expression that IS a `flow$(...)` call, or an identifier bound once to a `const` holding one. */
577
+ const isFlowDefinition = (e) => {
578
+ if (e === undefined)
579
+ return false;
580
+ const u = unwrap(e);
581
+ return isFlowCall(u) || (typescript_1.default.isIdentifier(u) && isFlowCall(inits.get(u.text)));
582
+ };
583
+ /** Does this object literal / class body also hold a flow definition (`definition: flow$(...)`)? */
584
+ const holdsDefinition = (container) => {
585
+ if (container === undefined)
586
+ return false;
587
+ if (typescript_1.default.isObjectLiteralExpression(container)) {
588
+ return container.properties.some((m) => (typescript_1.default.isPropertyAssignment(m) && isFlowDefinition(m.initializer)) ||
589
+ (typescript_1.default.isShorthandPropertyAssignment(m) && isFlowDefinition(m.name)));
590
+ }
591
+ if (typescript_1.default.isClassLike(container)) {
592
+ return container.members.some((m) => typescript_1.default.isPropertyDeclaration(m) && isFlowDefinition(m.initializer));
593
+ }
594
+ return false;
595
+ };
596
+ const markBound = (f) => {
597
+ if (boundNow)
598
+ bound.add(f);
599
+ };
600
+ const unresolved = [];
601
+ const inits = collectConstInits(root);
602
+ const seenValues = new Set();
603
+ const refuse = (e) => {
604
+ const text = `runner not analysable: ${exprText(e)}`;
605
+ if (!unresolved.includes(text))
606
+ unresolved.push(text);
607
+ };
608
+ const argFn = (a) => {
609
+ const u = unwrap(a);
610
+ if (isFn(u))
611
+ return u;
612
+ return typescript_1.default.isIdentifier(u) ? functions.get(u.text) : undefined;
613
+ };
614
+ /** The functions a factory returns, or undefined unless EVERY return is a followable function. */
615
+ function returnedFns(factory) {
616
+ const body = factory.body;
617
+ if (body === undefined)
618
+ return undefined;
619
+ const found = [];
620
+ let ok = true;
621
+ const take = (e) => {
622
+ const f = e === undefined ? undefined : argFn(e);
623
+ if (f === undefined)
624
+ ok = false;
625
+ else
626
+ found.push(f);
627
+ };
628
+ if (!typescript_1.default.isBlock(body))
629
+ take(body);
630
+ else {
631
+ const scan = (n) => {
632
+ if (isFn(n) || typescript_1.default.isMethodDeclaration(n) || typescript_1.default.isClassLike(n))
633
+ return;
634
+ if (typescript_1.default.isReturnStatement(n))
635
+ take(n.expression);
636
+ typescript_1.default.forEachChild(n, scan);
637
+ };
638
+ typescript_1.default.forEachChild(body, scan);
639
+ }
640
+ return ok && found.length > 0 ? found : undefined;
641
+ }
642
+ function runnerValue(e) {
643
+ if (e === undefined)
644
+ return;
645
+ const u = unwrap(e);
646
+ if (seenValues.has(u)) {
647
+ if (isFn(u))
648
+ markBound(u);
649
+ return;
650
+ }
651
+ seenValues.add(u);
652
+ if (isFn(u)) {
653
+ out.push(u);
654
+ markBound(u);
655
+ }
656
+ else if (isInertValue(u)) {
657
+ return;
658
+ }
659
+ else if (typescript_1.default.isIdentifier(u)) {
660
+ const f = functions.get(u.text);
661
+ const init = inits.get(u.text);
662
+ if (f !== undefined) {
663
+ out.push(f);
664
+ markBound(f);
665
+ }
666
+ else if (init !== undefined)
667
+ runnerValue(init);
668
+ else
669
+ refuse(u);
670
+ }
671
+ else if (typescript_1.default.isCallExpression(u) || typescript_1.default.isNewExpression(u)) {
672
+ for (const a of u.arguments ?? []) {
673
+ const f = argFn(a);
674
+ if (f !== undefined) {
675
+ out.push(f);
676
+ markBound(f);
677
+ }
678
+ }
679
+ // A LOCAL factory (`makeRunner(lib)` where `function makeRunner` is in this region) whose
680
+ // every return is a function: those returned functions are the runners, nothing hidden.
681
+ const callee = typescript_1.default.isCallExpression(u) ? unwrap(u.expression) : undefined;
682
+ const factory = callee !== undefined && typescript_1.default.isIdentifier(callee) ? functions.get(callee.text) : undefined;
683
+ const returned = factory === undefined ? undefined : returnedFns(factory);
684
+ if (returned !== undefined) {
685
+ out.push(...returned);
686
+ for (const r of returned)
687
+ markBound(r);
688
+ }
689
+ else
690
+ refuse(u);
691
+ }
692
+ else if (typescript_1.default.isConditionalExpression(u)) {
693
+ runnerValue(u.whenTrue);
694
+ runnerValue(u.whenFalse);
695
+ }
696
+ else {
697
+ refuse(u);
698
+ }
699
+ }
700
+ function visit(node) {
701
+ const prevBound = boundNow;
702
+ boundNow = false;
703
+ if (typescript_1.default.isPropertyAssignment(node) && propName(node.name) === "runner") {
704
+ boundNow = holdsDefinition(node.parent);
705
+ runnerValue(node.initializer);
706
+ }
707
+ else if (typescript_1.default.isShorthandPropertyAssignment(node) && node.name.text === "runner") {
708
+ boundNow = holdsDefinition(node.parent);
709
+ runnerValue(node.name);
710
+ }
711
+ else if (typescript_1.default.isMethodDeclaration(node) && propName(node.name) === "runner") {
712
+ out.push(node);
713
+ boundNow = holdsDefinition(node.parent);
714
+ markBound(node);
715
+ }
716
+ else if (typescript_1.default.isPropertyDeclaration(node) && propName(node.name) === "runner") {
717
+ boundNow = holdsDefinition(node.parent);
718
+ runnerValue(node.initializer);
719
+ }
720
+ else if (typescript_1.default.isGetAccessorDeclaration(node) && propName(node.name) === "runner") {
721
+ // `get runner() { return async (c) => … }`: the returned function(s) are the runner, like a
722
+ // local factory; a getter returning anything else is refused, never silently skipped.
723
+ const returned = returnedFns(node);
724
+ if (returned !== undefined) {
725
+ out.push(...returned);
726
+ if (holdsDefinition(node.parent))
727
+ for (const r of returned)
728
+ bound.add(r);
729
+ }
730
+ else {
731
+ const text = "runner not analysable: get runner()";
732
+ if (!unresolved.includes(text))
733
+ unresolved.push(text);
734
+ }
735
+ }
736
+ else if (typescript_1.default.isCallExpression(node) &&
737
+ (typescript_1.default.isIdentifier(node.expression)
738
+ ? node.expression.text
739
+ : typescript_1.default.isPropertyAccessExpression(node.expression)
740
+ ? node.expression.name.text
741
+ : "") === "registerFlow") {
742
+ // Bound only when the definition argument IS (or names) this module's `flow$` call.
743
+ boundNow = isFlowDefinition(node.arguments[0]);
744
+ runnerValue(node.arguments[1]);
745
+ }
746
+ else if (typescript_1.default.isVariableDeclaration(node) &&
747
+ node.type !== undefined &&
748
+ /FlowRunner\b/.test(node.type.getText())) {
749
+ runnerValue(node.initializer);
750
+ }
751
+ boundNow = false;
752
+ typescript_1.default.forEachChild(node, visit);
753
+ boundNow = prevBound;
754
+ }
755
+ visit(root);
756
+ return { runners: out, bound, unresolved };
757
+ }
758
+ function propName(n) {
759
+ return typescript_1.default.isIdentifier(n) || typescript_1.default.isStringLiteralLike(n) ? n.text : undefined;
760
+ }
761
+ /**
762
+ * L32 — names that denote a flow's execution context in this region. Textual and module-wide:
763
+ * - `ctx`, a parameter typed `…ExecutionContext` (or a local alias of that type), and the
764
+ * receiver of a `.decide/.suspend/.sleepUntil` call;
765
+ * - the FIRST parameter of every runner (`collectRunners`), whatever it is called, with an
766
+ * object pattern splitting into method values and a rest name (which IS the ctx);
767
+ * - propagation to a fixpoint: a ctx name handed as argument i to a same-module function adds
768
+ * that function's i-th parameter, and `const { …rest } = <ctx name>` adds the rest name.
769
+ * Over-reports by design (a textual name shadowed elsewhere is still treated as ctx).
770
+ */
771
+ function collectCtx(root, functions) {
772
+ const names = new Set(["ctx"]);
773
+ const members = new Map();
774
+ const unresolved = new Set();
775
+ const methods = new Set(["decide", "suspend", "sleepUntil"]);
776
+ // Local type aliases that resolve to an ExecutionContext.
777
+ const ctxTypes = new Set();
778
+ const isCtxType = (t) => t !== undefined &&
779
+ typescript_1.default.isTypeReferenceNode(t) &&
780
+ (/ExecutionContext$/.test(t.typeName.getText()) || ctxTypes.has(t.typeName.getText()));
781
+ for (let grew = true; grew;) {
782
+ grew = false;
783
+ const scanTypes = (n) => {
784
+ if (typescript_1.default.isTypeAliasDeclaration(n) && !ctxTypes.has(n.name.text) && isCtxType(n.type)) {
785
+ ctxTypes.add(n.name.text);
786
+ grew = true;
787
+ }
788
+ typescript_1.default.forEachChild(n, scanTypes);
789
+ };
790
+ scanTypes(root);
791
+ }
792
+ // Worklist: every name is processed once per set it joins, against indexes built by ONE walk.
793
+ const queue = [];
794
+ const addName = (n) => {
795
+ if (!names.has(n)) {
796
+ names.add(n);
797
+ queue.push(n);
798
+ }
799
+ };
800
+ const addMember = (n, m) => {
801
+ if (!members.has(n)) {
802
+ members.set(n, m);
803
+ queue.push(n);
804
+ }
805
+ };
806
+ const bindPattern = (pattern) => {
807
+ for (const el of pattern.elements) {
808
+ if (el.dotDotDotToken !== undefined) {
809
+ if (typescript_1.default.isIdentifier(el.name))
810
+ addName(el.name.text);
811
+ else
812
+ unresolved.add(`ctx pattern ${exprText(el)}`);
813
+ continue;
814
+ }
815
+ const key = el.propertyName ?? el.name;
816
+ const keyText = typescript_1.default.isIdentifier(key) || typescript_1.default.isStringLiteralLike(key) ? key.text : undefined;
817
+ if (keyText === "decide")
818
+ continue; // L31 reports every decide alias
819
+ if (keyText === undefined || !typescript_1.default.isIdentifier(el.name)) {
820
+ unresolved.add(`ctx pattern ${exprText(el)}`);
821
+ continue;
822
+ }
823
+ addMember(el.name.text, keyText);
824
+ }
825
+ };
826
+ const bindParam = (p) => {
827
+ if (typescript_1.default.isIdentifier(p.name))
828
+ addName(p.name.text);
829
+ else if (typescript_1.default.isObjectBindingPattern(p.name))
830
+ bindPattern(p.name);
831
+ else
832
+ unresolved.add(`ctx parameter ${exprText(p.name)}`);
833
+ };
834
+ const found = collectRunners(root, functions);
835
+ for (const u of found.unresolved)
836
+ unresolved.add(u);
837
+ for (const r of found.runners) {
838
+ const first = r.parameters[0];
839
+ if (first !== undefined)
840
+ bindParam(first);
841
+ }
842
+ // One walk: seed names, and index the places a name can flow onward from.
843
+ /** identifier argument -> the same-module parameter it is handed to. */
844
+ const argUses = new Map();
845
+ /** identifier -> declarations initialised from it (`const { … } = n`, `const k = n`). */
846
+ const declUses = new Map();
847
+ const push = (m, k, v) => {
848
+ const list = m.get(k);
849
+ if (list === undefined)
850
+ m.set(k, [v]);
851
+ else
852
+ list.push(v);
853
+ };
854
+ function index(node) {
855
+ if (typescript_1.default.isCallExpression(node) &&
856
+ typescript_1.default.isPropertyAccessExpression(node.expression) &&
857
+ methods.has(node.expression.name.text) &&
858
+ typescript_1.default.isIdentifier(node.expression.expression)) {
859
+ addName(node.expression.expression.text);
860
+ }
861
+ else if (typescript_1.default.isParameter(node) && typescript_1.default.isIdentifier(node.name) && isCtxType(node.type)) {
862
+ addName(node.name.text);
863
+ }
864
+ if (typescript_1.default.isCallExpression(node) || typescript_1.default.isNewExpression(node)) {
865
+ const callee = unwrap(node.expression);
866
+ const fn = typescript_1.default.isIdentifier(callee) ? functions.get(callee.text) : undefined;
867
+ if (fn !== undefined) {
868
+ (node.arguments ?? []).forEach((arg, i) => {
869
+ const a = unwrap(arg);
870
+ const param = fn.parameters[i];
871
+ if (typescript_1.default.isIdentifier(a) &&
872
+ param !== undefined &&
873
+ param.dotDotDotToken === undefined &&
874
+ typescript_1.default.isIdentifier(param.name)) {
875
+ push(argUses, a.text, param);
876
+ }
877
+ });
878
+ }
879
+ }
880
+ else if (typescript_1.default.isVariableDeclaration(node) && node.initializer !== undefined) {
881
+ const init = unwrap(node.initializer);
882
+ const isConst = (typescript_1.default.getCombinedNodeFlags(node) & typescript_1.default.NodeFlags.Const) !== 0;
883
+ if (typescript_1.default.isIdentifier(init) &&
884
+ (typescript_1.default.isObjectBindingPattern(node.name) || (isConst && typescript_1.default.isIdentifier(node.name)))) {
885
+ push(declUses, init.text, node);
886
+ }
887
+ }
888
+ typescript_1.default.forEachChild(node, index);
889
+ }
890
+ index(root);
891
+ for (const n of [...names])
892
+ queue.push(n);
893
+ for (const n of members.keys())
894
+ queue.push(n);
895
+ while (queue.length > 0) {
896
+ const n = queue.pop();
897
+ const asName = names.has(n);
898
+ const m = members.get(n);
899
+ for (const param of argUses.get(n) ?? []) {
900
+ const pn = param.name.text;
901
+ if (asName)
902
+ addName(pn);
903
+ if (m !== undefined)
904
+ addMember(pn, m);
905
+ }
906
+ for (const decl of declUses.get(n) ?? []) {
907
+ if (typescript_1.default.isObjectBindingPattern(decl.name)) {
908
+ if (asName)
909
+ bindPattern(decl.name);
910
+ }
911
+ else if (typescript_1.default.isIdentifier(decl.name)) {
912
+ if (asName)
913
+ addName(decl.name.text);
914
+ if (m !== undefined)
915
+ addMember(decl.name.text, m);
916
+ }
917
+ }
918
+ }
919
+ return {
920
+ names,
921
+ members,
922
+ unresolved,
923
+ runners: found.runners.length,
924
+ boundRunners: found.bound.size,
925
+ };
926
+ }
927
+ /** The leftmost identifier of a member/call chain (`planner` for `planner.a.b(c).d`). */
928
+ function chainRoot(e) {
929
+ let cur = e;
930
+ for (;;) {
931
+ if (typescript_1.default.isIdentifier(cur))
932
+ return cur.text;
933
+ if (typescript_1.default.isPropertyAccessExpression(cur) || typescript_1.default.isElementAccessExpression(cur))
934
+ cur = cur.expression;
935
+ else if (typescript_1.default.isCallExpression(cur) || typescript_1.default.isNonNullExpression(cur))
936
+ cur = cur.expression;
937
+ else if (typescript_1.default.isParenthesizedExpression(cur))
938
+ cur = cur.expression;
939
+ else
940
+ return undefined;
941
+ }
942
+ }
943
+ /**
944
+ * Where does a value go? Climb through literals/spreads/wrappers to the call or `new` that
945
+ * receives it as an argument; `undefined` when the value is stored, returned or otherwise
946
+ * leaves through something other than an argument. `direct` is true when the value IS the
947
+ * argument (not wrapped in a literal/spread), with its index.
948
+ */
949
+ function receivingCall(node) {
950
+ let child = node;
951
+ let direct = true;
952
+ let p = node.parent;
953
+ while (p !== undefined) {
954
+ if (typescript_1.default.isParenthesizedExpression(p) ||
955
+ typescript_1.default.isAsExpression(p) ||
956
+ typescript_1.default.isNonNullExpression(p) ||
957
+ typescript_1.default.isSatisfiesExpression(p) ||
958
+ typescript_1.default.isTypeAssertionExpression(p)) {
959
+ child = p;
960
+ p = p.parent;
961
+ continue;
962
+ }
963
+ if (typescript_1.default.isObjectLiteralExpression(p) ||
964
+ typescript_1.default.isArrayLiteralExpression(p) ||
965
+ typescript_1.default.isSpreadAssignment(p) ||
966
+ typescript_1.default.isSpreadElement(p) ||
967
+ typescript_1.default.isShorthandPropertyAssignment(p) ||
968
+ (typescript_1.default.isPropertyAssignment(p) && p.initializer === child)) {
969
+ direct = false;
970
+ child = p;
971
+ p = p.parent;
972
+ continue;
973
+ }
974
+ if (typescript_1.default.isCallExpression(p) || typescript_1.default.isNewExpression(p)) {
975
+ const index = p.arguments?.indexOf(child) ?? -1;
976
+ if (index >= 0)
977
+ return { call: p, direct, index };
978
+ }
979
+ return undefined;
980
+ }
981
+ return undefined;
982
+ }
983
+ /** Callees that run their first argument (or forward the rest) rather than being the real target. */
984
+ const PASS_THROUGH = new Set([
985
+ "Reflect.apply",
986
+ "setTimeout",
987
+ "setInterval",
988
+ "setImmediate",
989
+ "queueMicrotask",
990
+ "process.nextTick",
991
+ ]);
992
+ /** Analyze a parsed node (a flow runner body) for reachable steps and suspend points. */
993
+ function analyzeReachNode(root, location, options = {}) {
994
+ return analyzeReachRegion(root, location, options).reach;
995
+ }
996
+ /**
997
+ * {@link analyzeReachNode} plus how many runner functions the region declares and the analysis
998
+ * followed, and how many of them are BOUND to a `flow$` definition: declared in the same object
999
+ * literal / class as a `flow$(...)` (or a const holding one), or passed to `registerFlow(def, …)`
1000
+ * whose `def` is a `flow$` call or names one. An EMPTY reach is ambiguous on its own: "the runner
1001
+ * reaches nothing" and "no runner of THIS flow is here" (an unrelated runner elsewhere in the
1002
+ * module, or one registered from another module) look the same. A runner that is NOT followable is
1003
+ * never silent (`unresolved`), so an empty reach with `boundRunners > 0` means the flow's own
1004
+ * runner was read and reaches no step, suspend or ctx hand-off: a pure runner, analysed.
1005
+ */
1006
+ function analyzeReachRegion(root, location, options = {}) {
1007
+ const scan = scanDeclarations(root);
1008
+ const functions = scan.fns;
1009
+ const constInits = scan.inits;
1010
+ const sourceFile = typescript_1.default.isSourceFile(root) ? root : root.getSourceFile();
1011
+ const declaredAnywhere = scan.declared;
1012
+ const imports = importedNames(sourceFile);
1013
+ // An imported name that is ALSO declared somewhere in the module (a same-named local in another
1014
+ // function) is ambiguous in BOTH directions: the use may be the import or the local, so neither
1015
+ // the local function/const/binding nor the import may answer for it (-> unresolved).
1016
+ const importBindings = allImportBindings(sourceFile);
1017
+ for (const name of importBindings) {
1018
+ functions.delete(name);
1019
+ constInits.delete(name);
1020
+ }
1021
+ // A step binding is followed only when its name is declared exactly once, never reassigned: a
1022
+ // shadowing parameter or a second same-named binding in another function makes it ambiguous.
1023
+ const bindings = collectBindings(root);
1024
+ for (const name of [...bindings.keys()]) {
1025
+ if ((declaredAnywhere.get(name) ?? 0) !== 1 ||
1026
+ scan.ambiguous.has(name) ||
1027
+ importBindings.has(name))
1028
+ bindings.delete(name);
1029
+ }
1030
+ /** A name with no declaration anywhere in the module: only then can it be the import binding. */
1031
+ const unshadowed = (name) => (declaredAnywhere.get(name) ?? 0) === 0;
1032
+ const importer = options.importResolver;
1033
+ const steps = [];
1034
+ const unresolved = [];
1035
+ const suspends = [];
1036
+ const escapes = [];
1037
+ const escapeSeen = new Set();
1038
+ const ctx = collectCtx(root, functions);
1039
+ const ctxNames = ctx.names;
1040
+ const at = location !== undefined ? { location } : {};
1041
+ for (const expr of ctx.unresolved)
1042
+ unresolved.push({ expr, kind: "step", ...at });
1043
+ /**
1044
+ * L37 - the string a constant expression denotes: a literal, an expression-free or
1045
+ * all-constant template, `a + b`, or an identifier bound once to such a `const` in this module
1046
+ * (or IMPORTED from a relative module, when an `importResolver` is supplied). `undefined` for
1047
+ * anything else - never a guess.
1048
+ */
1049
+ const constString = (e, depth = 0) => {
1050
+ if (depth > 8)
1051
+ return undefined;
1052
+ const u = unwrap(e);
1053
+ if (typescript_1.default.isStringLiteral(u) || typescript_1.default.isNoSubstitutionTemplateLiteral(u))
1054
+ return u.text;
1055
+ if (typescript_1.default.isTemplateExpression(u)) {
1056
+ let out = u.head.text;
1057
+ for (const span of u.templateSpans) {
1058
+ const v = constString(span.expression, depth + 1);
1059
+ if (v === undefined)
1060
+ return undefined;
1061
+ out += v + span.literal.text;
1062
+ }
1063
+ return out;
1064
+ }
1065
+ if (typescript_1.default.isBinaryExpression(u) && u.operatorToken.kind === typescript_1.default.SyntaxKind.PlusToken) {
1066
+ const l = constString(u.left, depth + 1);
1067
+ const r = l === undefined ? undefined : constString(u.right, depth + 1);
1068
+ return l !== undefined && r !== undefined ? l + r : undefined;
1069
+ }
1070
+ if (typescript_1.default.isIdentifier(u)) {
1071
+ // Also declared somewhere in this module, yet imported: which one the use means is unknown.
1072
+ if (imports.has(u.text) && !unshadowed(u.text))
1073
+ return undefined;
1074
+ const init = constInits.get(u.text);
1075
+ if (init !== undefined)
1076
+ return constString(init, depth + 1);
1077
+ return unshadowed(u.text) ? importer?.(u.text)?.value : undefined;
1078
+ }
1079
+ return undefined;
1080
+ };
1081
+ /** A string property of an object literal, constant-resolved (`toolId: LLM_X`). */
1082
+ const constProp = (obj, name) => {
1083
+ // JS takes the LAST duplicate key, and a spread AFTER the key may override it: neither is
1084
+ // authoritative, so the property is unresolved (a spread BEFORE the key is fine).
1085
+ let found;
1086
+ let count = 0;
1087
+ let spreadAfter = false;
1088
+ for (const p of obj.properties) {
1089
+ if (typescript_1.default.isSpreadAssignment(p)) {
1090
+ if (count > 0)
1091
+ spreadAfter = true;
1092
+ continue;
1093
+ }
1094
+ if (typescript_1.default.isPropertyAssignment(p) && propName(p.name) === name) {
1095
+ count++;
1096
+ found = constString(p.initializer);
1097
+ }
1098
+ else if (typescript_1.default.isShorthandPropertyAssignment(p) && p.name.text === name) {
1099
+ count++;
1100
+ found = constString(p.name);
1101
+ }
1102
+ }
1103
+ return count === 1 && !spreadAfter ? found : undefined;
1104
+ };
1105
+ /**
1106
+ * L37 - a step-producing call: the shape-based `anything$({ toolId })` first, then a factory
1107
+ * (same-module function, imported function, or a first-party one in {@link KNOWN_STEP_FACTORIES})
1108
+ * whose effect id is fixed or one of its arguments. The step ID may be templated (`tool:${i}`):
1109
+ * verify needs the EFFECT id.
1110
+ */
1111
+ const resolveCall = (call) => {
1112
+ const first = call.arguments[0];
1113
+ if (first !== undefined && typescript_1.default.isObjectLiteralExpression(first)) {
1114
+ const toolId = constProp(first, "toolId");
1115
+ if (toolId !== undefined) {
1116
+ const stepId = constProp(first, "id");
1117
+ return { toolId, ...(stepId !== undefined && { stepId }) };
1118
+ }
1119
+ }
1120
+ const callee = unwrap(call.expression);
1121
+ // A step with NO `toolId` dispatches its own id: the runtime resolves `step.toolId ?? step.id`
1122
+ // (WorkflowRuntime). Followed only for `step$` itself (an unshadowed import from @loomwork/core or
1123
+ // @loomwork/sdk, any alias) whose object has no `toolId` key and no spread: a wrapper might add one.
1124
+ if (first !== undefined &&
1125
+ typescript_1.default.isObjectLiteralExpression(first) &&
1126
+ typescript_1.default.isIdentifier(callee) &&
1127
+ unshadowed(callee.text) &&
1128
+ first.properties.every((p) => !typescript_1.default.isSpreadAssignment(p) &&
1129
+ !typescript_1.default.isMethodDeclaration(p) &&
1130
+ !(p.name !== undefined && propName(p.name) === "toolId") &&
1131
+ !(p.name !== undefined && typescript_1.default.isComputedPropertyName(p.name)))) {
1132
+ const imp = imports.get(callee.text);
1133
+ if (imp?.imported === "step$" &&
1134
+ (imp.spec === "@loomwork/core" || imp.spec === "@loomwork/sdk")) {
1135
+ const id = constProp(first, "id");
1136
+ return id === undefined ? undefined : { toolId: id, stepId: id };
1137
+ }
1138
+ }
1139
+ const name = typescript_1.default.isIdentifier(callee)
1140
+ ? callee.text
1141
+ : typescript_1.default.isPropertyAccessExpression(callee)
1142
+ ? callee.name.text
1143
+ : undefined;
1144
+ if (name === undefined || call.arguments.some((a) => typescript_1.default.isSpreadElement(a)))
1145
+ return undefined;
1146
+ let shape;
1147
+ const local = typescript_1.default.isIdentifier(callee) ? functions.get(name) : undefined;
1148
+ if (local !== undefined)
1149
+ shape = inferStepFactory(local, constString);
1150
+ else if (!typescript_1.default.isIdentifier(callee) || !unshadowed(name))
1151
+ shape = undefined;
1152
+ else {
1153
+ // A bare, undeclared name: the import binding. A relative wrapper is followed through the
1154
+ // importer; a first-party factory only when imported from its own package (or the SDK).
1155
+ const imp = imports.get(name);
1156
+ shape = importer?.(name)?.factory;
1157
+ if (shape === undefined &&
1158
+ imp !== undefined &&
1159
+ Object.hasOwn(exports.KNOWN_STEP_FACTORIES, imp.imported) &&
1160
+ KNOWN_FACTORY_SOURCES[imp.imported]?.includes(imp.spec) === true) {
1161
+ shape = exports.KNOWN_STEP_FACTORIES[imp.imported];
1162
+ }
1163
+ }
1164
+ if (shape === undefined)
1165
+ return undefined;
1166
+ if ("fixed" in shape)
1167
+ return { toolId: shape.fixed };
1168
+ const arg = call.arguments[shape.argIndex];
1169
+ const toolId = arg === undefined ? shape.default : constString(arg);
1170
+ return toolId === undefined ? undefined : { toolId };
1171
+ };
1172
+ /**
1173
+ * Resolve a `ctx.run` / `ctx.parallel` / hand-off step target to a tool, or `undefined` when it
1174
+ * cannot be (reported unresolved by the caller - never silently dropped).
1175
+ */
1176
+ const isLoomFlowBuilder = (callee) => {
1177
+ const c = unwrap(callee);
1178
+ if (!typescript_1.default.isIdentifier(c) || !unshadowed(c.text))
1179
+ return false;
1180
+ const imp = imports.get(c.text);
1181
+ return (imp?.imported === "flow$" && (imp.spec === "@loomwork/core" || imp.spec === "@loomwork/sdk"));
1182
+ };
1183
+ const escapeCache = new Map();
1184
+ const resolveTarget = (target, depth = 0) => {
1185
+ const t = unwrap(target);
1186
+ if (typescript_1.default.isIdentifier(t)) {
1187
+ // A step object that is mutated, aliased or handed to another function may no longer be what
1188
+ // its declaration says (ADR 0086 amendment 2026-10-10): not followed, never guessed.
1189
+ let escapes = escapeCache.get(t.text);
1190
+ if (escapes === undefined) {
1191
+ escapes = stepBindingEscapes(sourceFile, t.text, ctxNames, isLoomFlowBuilder);
1192
+ escapeCache.set(t.text, escapes);
1193
+ }
1194
+ if (escapes)
1195
+ return undefined;
1196
+ const bound = bindings.get(t.text);
1197
+ if (bound !== undefined)
1198
+ return bound;
1199
+ const init = constInits.get(t.text);
1200
+ return init !== undefined && depth < 4 ? resolveTarget(init, depth + 1) : undefined;
1201
+ }
1202
+ if (typescript_1.default.isObjectLiteralExpression(t)) {
1203
+ const toolId = constProp(t, "toolId");
1204
+ if (toolId === undefined)
1205
+ return undefined;
1206
+ const stepId = constProp(t, "id");
1207
+ return { toolId, ...(stepId !== undefined && { stepId }) };
1208
+ }
1209
+ if (typescript_1.default.isCallExpression(t))
1210
+ return resolveCall(t);
1211
+ return undefined;
1212
+ };
1213
+ const stripCallish = (c) => {
1214
+ let cur = unwrap(c);
1215
+ while (typescript_1.default.isPropertyAccessExpression(cur) &&
1216
+ ["call", "apply", "bind"].includes(cur.name.text)) {
1217
+ cur = unwrap(cur.expression);
1218
+ }
1219
+ return cur;
1220
+ };
1221
+ const isPlainChain = (e) => typescript_1.default.isIdentifier(e) || (typescript_1.default.isPropertyAccessExpression(e) && isPlainChain(e.expression));
1222
+ /**
1223
+ * L34 - the hand-off's arguments (at most the first 8) that resolve to a step.
1224
+ */
1225
+ function escapeArgs(site) {
1226
+ const out = [];
1227
+ (site.arguments ?? []).slice(0, 8).forEach((a, index) => {
1228
+ const step = resolveTarget(a);
1229
+ if (step !== undefined)
1230
+ out.push({ index, step });
1231
+ });
1232
+ return out;
1233
+ }
1234
+ /**
1235
+ * Record a hand-off to whatever `site` calls. `.call/.apply/.bind` on the callee is stripped
1236
+ * and a pass-through (`Reflect.apply(lib, …)`, `setTimeout(lib, …)`) is unwrapped to its real
1237
+ * target when that is a plain name; otherwise the hand-off is reported unresolved. A
1238
+ * same-module callee is already analysed for a `decide` alias; for ctx or a ctx method it is
1239
+ * reported (the value is wrapped or the parameter cannot be followed).
1240
+ */
1241
+ function recordEscape(site, via, member) {
1242
+ const handed = via === "decide" ? "ctx.decide" : member !== undefined ? `ctx.${member}` : "ctx";
1243
+ let callee = unwrap(site.expression);
1244
+ const direct = exprText(callee);
1245
+ if (!PASS_THROUGH.has(direct))
1246
+ callee = stripCallish(callee);
1247
+ if (PASS_THROUGH.has(direct)) {
1248
+ const target = site.arguments?.[0] === undefined ? undefined : stripCallish(site.arguments[0]);
1249
+ if (target === undefined || !isPlainChain(target)) {
1250
+ unresolved.push({
1251
+ expr: `${handed} handed through ${direct} (target not static)`,
1252
+ kind: "step",
1253
+ ...at,
1254
+ });
1255
+ return;
1256
+ }
1257
+ callee = target;
1258
+ }
1259
+ else if (/^Promise\./.test(direct)) {
1260
+ unresolved.push({ expr: `${handed} handed to ${direct}`, kind: "step", ...at });
1261
+ return;
1262
+ }
1263
+ if (typescript_1.default.isIdentifier(callee) && functions.has(callee.text)) {
1264
+ if (via === "decide")
1265
+ return;
1266
+ unresolved.push({
1267
+ expr: `${handed} handed to same-module ${callee.text} in a shape that is not followed`,
1268
+ kind: "step",
1269
+ ...at,
1270
+ });
1271
+ return;
1272
+ }
1273
+ const text = exprText(callee);
1274
+ const rootName = chainRoot(callee);
1275
+ // The receiving callee is itself the ctx (`ctx.emit("x", ctx)`, `setTimeout(ctx.run, 0)`,
1276
+ // `Reflect.apply(ctx.run, ctx, [step])`): there is no outside code to look up a declaration
1277
+ // for, and `verify.reach["ctx"]` would be nonsense. The call is a ctx-method call whose
1278
+ // argument this walk does not resolve, so refuse it (once per callee).
1279
+ if (rootName !== undefined && (ctxNames.has(rootName) || ctx.members.has(rootName))) {
1280
+ const key = `ctx-rooted:${text}`;
1281
+ if (escapeSeen.has(key))
1282
+ return;
1283
+ escapeSeen.add(key);
1284
+ unresolved.push({
1285
+ expr: `ctx value handed to ctx-rooted ${text} (not resolved)`,
1286
+ kind: "step",
1287
+ ...at,
1288
+ });
1289
+ return;
1290
+ }
1291
+ // L34 - the other arguments, for a whole-ctx hand-off made directly (not through a
1292
+ // pass-through, whose arguments are not the callee's).
1293
+ const args = via === "ctx" && member === undefined && !PASS_THROUGH.has(direct)
1294
+ ? escapeArgs(site)
1295
+ : undefined;
1296
+ const key = `${via}:${member ?? ""}:${text}:${args === undefined ? "" : JSON.stringify(args)}`;
1297
+ if (escapeSeen.has(key))
1298
+ return;
1299
+ escapeSeen.add(key);
1300
+ escapes.push({
1301
+ callee: text,
1302
+ ...(rootName !== undefined && { root: rootName }),
1303
+ via,
1304
+ ...(member !== undefined && { member }),
1305
+ ...(args !== undefined && args.length > 0 && { args }),
1306
+ ...at,
1307
+ });
1308
+ }
1309
+ /** Route a ctx (or ctx-member) VALUE at `node` to its receiving callee, or report it stored. */
1310
+ function routeValue(node, member) {
1311
+ const recv = receivingCall(node);
1312
+ if (recv === undefined) {
1313
+ const what = member !== undefined ? `ctx.${member}` : "ctx";
1314
+ // A value interpolated into a tagged template reads as the whole tagged expression.
1315
+ let where = node.parent;
1316
+ for (let p = node.parent; p !== undefined; p = p.parent) {
1317
+ if (typescript_1.default.isTaggedTemplateExpression(p)) {
1318
+ where = p;
1319
+ break;
1320
+ }
1321
+ if (!typescript_1.default.isTemplateSpan(p) && !typescript_1.default.isTemplateExpression(p))
1322
+ break;
1323
+ }
1324
+ unresolved.push({ expr: `${what} stored as ${exprText(where)}`, kind: "step", ...at });
1325
+ return;
1326
+ }
1327
+ // Direct hand-off to a same-module function: its i-th parameter was added to the ctx
1328
+ // names (propagation) and its body is walked here, so it is followed — but only for the
1329
+ // ctx itself, and only when the parameter is a plain identifier.
1330
+ const callee = unwrap(recv.call.expression);
1331
+ if (member === undefined && typescript_1.default.isIdentifier(callee) && functions.has(callee.text)) {
1332
+ const fn = functions.get(callee.text);
1333
+ const param = fn?.parameters[recv.index];
1334
+ if (recv.direct &&
1335
+ param !== undefined &&
1336
+ param.dotDotDotToken === undefined &&
1337
+ typescript_1.default.isIdentifier(param.name)) {
1338
+ return;
1339
+ }
1340
+ unresolved.push({
1341
+ expr: `ctx handed to same-module ${callee.text} where the parameter cannot be followed`,
1342
+ kind: "step",
1343
+ ...at,
1344
+ });
1345
+ return;
1346
+ }
1347
+ recordEscape(recv.call, "ctx", member);
1348
+ }
1349
+ /**
1350
+ * `ctx.<m>` / `ctx["m"]`. A direct call is the normal path (the walk analyses it). A method
1351
+ * taken as a VALUE (`lib(ctx.run)`, `ctx.run.bind(ctx)`, `ctx.run.call(ctx, step)`) hands the
1352
+ * receiver the ability to run or suspend the flow: an escape like `ctx.decide`.
1353
+ */
1354
+ function flagCtxMember(access) {
1355
+ let member;
1356
+ if (typescript_1.default.isPropertyAccessExpression(access))
1357
+ member = access.name.text;
1358
+ else if (typescript_1.default.isStringLiteralLike(access.argumentExpression))
1359
+ member = access.argumentExpression.text;
1360
+ else {
1361
+ unresolved.push({ expr: `ctx dynamic member ${exprText(access)}`, kind: "step", ...at });
1362
+ return;
1363
+ }
1364
+ if (member === "decide" || !CTX_METHODS.has(member))
1365
+ return; // L31 / data properties
1366
+ // The expression the member value flows out of, skipping wrappers.
1367
+ let top = access;
1368
+ while (top.parent !== undefined &&
1369
+ (typescript_1.default.isParenthesizedExpression(top.parent) ||
1370
+ typescript_1.default.isNonNullExpression(top.parent) ||
1371
+ typescript_1.default.isAsExpression(top.parent))) {
1372
+ top = top.parent;
1373
+ }
1374
+ const parent = top.parent;
1375
+ if (parent !== undefined && typescript_1.default.isCallExpression(parent) && parent.expression === top) {
1376
+ // Direct call. Property form is analysed by the receiver-agnostic walk; the
1377
+ // element-access form is not, so do it here.
1378
+ if (typescript_1.default.isElementAccessExpression(access)) {
1379
+ if (member === "run")
1380
+ visitRun(parent);
1381
+ else if (member === "parallel")
1382
+ visitParallel(parent);
1383
+ else if (member === "suspend")
1384
+ visitSuspend(parent);
1385
+ }
1386
+ return;
1387
+ }
1388
+ if (parent !== undefined &&
1389
+ typescript_1.default.isPropertyAccessExpression(parent) &&
1390
+ parent.expression === top) {
1391
+ const op = parent.name.text;
1392
+ const gp = parent.parent;
1393
+ const called = gp !== undefined && typescript_1.default.isCallExpression(gp) && gp.expression === parent;
1394
+ if (called && (op === "call" || op === "apply")) {
1395
+ if (op === "call" && member === "run")
1396
+ visitRun(gp, 1);
1397
+ else if (op === "call" && member === "parallel")
1398
+ visitParallel(gp, 1);
1399
+ else if (op === "call" && member === "suspend")
1400
+ visitSuspend(gp, 1);
1401
+ else if (op === "apply" &&
1402
+ (member === "run" || member === "suspend" || member === "parallel")) {
1403
+ unresolved.push({
1404
+ expr: `ctx.${member}.apply(…)`,
1405
+ kind: member === "suspend" ? "suspend" : "step",
1406
+ ...at,
1407
+ });
1408
+ }
1409
+ return;
1410
+ }
1411
+ if (called && op === "bind") {
1412
+ routeValue(gp, member);
1413
+ return;
1414
+ }
1415
+ return; // `ctx.run.length` and the like
1416
+ }
1417
+ routeValue(top, member);
1418
+ }
1419
+ /** A reference to the execution context (or a method taken off it) in a value position. */
1420
+ function flagCtxReference(node, kind) {
1421
+ const parent = node.parent;
1422
+ if ((typescript_1.default.isPropertyAccessExpression(parent) && parent.name === node) ||
1423
+ (typescript_1.default.isPropertyAssignment(parent) && parent.name === node) ||
1424
+ (typescript_1.default.isParameter(parent) && parent.name === node) ||
1425
+ (typescript_1.default.isVariableDeclaration(parent) && parent.name === node) ||
1426
+ (typescript_1.default.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
1427
+ typescript_1.default.isTypeOfExpression(parent) ||
1428
+ (typescript_1.default.isBinaryExpression(parent) &&
1429
+ [
1430
+ typescript_1.default.SyntaxKind.EqualsEqualsToken,
1431
+ typescript_1.default.SyntaxKind.EqualsEqualsEqualsToken,
1432
+ typescript_1.default.SyntaxKind.ExclamationEqualsToken,
1433
+ typescript_1.default.SyntaxKind.ExclamationEqualsEqualsToken,
1434
+ ].includes(parent.operatorToken.kind))) {
1435
+ return;
1436
+ }
1437
+ // `const k = ctx` — a const alias is itself a ctx name (collectCtx), so it is followed
1438
+ // rather than reported stored. `let`/reassigned stays stored-unresolved.
1439
+ if (typescript_1.default.isVariableDeclaration(parent) &&
1440
+ parent.initializer !== undefined &&
1441
+ unwrap(parent.initializer) === node &&
1442
+ typescript_1.default.isIdentifier(parent.name) &&
1443
+ (typescript_1.default.getCombinedNodeFlags(parent) & typescript_1.default.NodeFlags.Const) !== 0) {
1444
+ return;
1445
+ }
1446
+ if (kind === "method") {
1447
+ const member = ctx.members.get(node.text);
1448
+ if (typescript_1.default.isCallExpression(parent) && parent.expression === node) {
1449
+ if (member === "run")
1450
+ visitRun(parent);
1451
+ else if (member === "parallel")
1452
+ visitParallel(parent);
1453
+ else if (member === "suspend")
1454
+ visitSuspend(parent);
1455
+ return;
1456
+ }
1457
+ routeValue(node, member);
1458
+ return;
1459
+ }
1460
+ if ((typescript_1.default.isPropertyAccessExpression(parent) || typescript_1.default.isElementAccessExpression(parent)) &&
1461
+ parent.expression === node) {
1462
+ flagCtxMember(parent);
1463
+ return;
1464
+ }
1465
+ // `const { decide, …rest } = ctx` — `decide` is L31's, the rest names were bound as ctx.
1466
+ if (typescript_1.default.isVariableDeclaration(parent) &&
1467
+ parent.initializer === node &&
1468
+ typescript_1.default.isObjectBindingPattern(parent.name)) {
1469
+ return;
1470
+ }
1471
+ // `ctx.run.bind(ctx)` / `.call(ctx, …)`: the ctx argument belongs to the member call,
1472
+ // which flagCtxMember already routes.
1473
+ const recv = receivingCall(node);
1474
+ if (recv !== undefined) {
1475
+ const e = unwrap(recv.call.expression);
1476
+ if (typescript_1.default.isPropertyAccessExpression(e) && ["bind", "call", "apply"].includes(e.name.text)) {
1477
+ const m = unwrap(e.expression);
1478
+ const mm = typescript_1.default.isPropertyAccessExpression(m) && typescript_1.default.isIdentifier(unwrap(m.expression))
1479
+ ? unwrap(m.expression).text
1480
+ : undefined;
1481
+ if (mm !== undefined &&
1482
+ ctxNames.has(mm) &&
1483
+ CTX_METHODS.has(m.name.text))
1484
+ return;
1485
+ }
1486
+ }
1487
+ routeValue(node);
1488
+ }
1489
+ function visitRun(call, argOffset = 0) {
1490
+ const target = call.arguments[argOffset];
1491
+ if (target === undefined)
1492
+ return;
1493
+ const ref = resolveTarget(target);
1494
+ if (ref !== undefined) {
1495
+ steps.push(ref);
1496
+ return;
1497
+ }
1498
+ // An element access, a parameter, a factory whose effect id is not constant - all real and
1499
+ // all unresolvable from this region alone.
1500
+ unresolved.push({ expr: exprText(target), kind: "step", ...at });
1501
+ }
1502
+ /**
1503
+ * L35 - `ctx.parallel([[step, input], …])`. Each tuple's first element is a `ctx.run` target,
1504
+ * resolved the same way. A branch written as a callback is already walked (its `b.run(...)` is
1505
+ * found like any `run`). A pair list that is not an array literal (or a const bound to one), a
1506
+ * spread, or a branch that is neither a tuple nor a function is reported unresolved: the steps
1507
+ * it holds cannot be seen, and an empty answer would read as "reaches nothing".
1508
+ */
1509
+ function listUsedBeyondParallel(name) {
1510
+ let used = false;
1511
+ const walk = (n) => {
1512
+ if (used)
1513
+ return;
1514
+ if (typescript_1.default.isIdentifier(n) && n.text === name) {
1515
+ let top = n;
1516
+ while (top.parent !== undefined &&
1517
+ (typescript_1.default.isParenthesizedExpression(top.parent) ||
1518
+ typescript_1.default.isAsExpression(top.parent) ||
1519
+ typescript_1.default.isNonNullExpression(top.parent) ||
1520
+ typescript_1.default.isSatisfiesExpression(top.parent)))
1521
+ top = top.parent;
1522
+ const p = n.parent;
1523
+ const isDecl = typescript_1.default.isVariableDeclaration(p) && p.name === n;
1524
+ const isMemberName = typescript_1.default.isPropertyAccessExpression(p) && p.name === n;
1525
+ const isKey = (typescript_1.default.isPropertyAssignment(p) || typescript_1.default.isPropertySignature(p)) && p.name === n;
1526
+ const isParallelArg = top.parent !== undefined &&
1527
+ typescript_1.default.isCallExpression(top.parent) &&
1528
+ methodCall(top.parent, "parallel") !== undefined &&
1529
+ top.parent.arguments.includes(top);
1530
+ if (!isDecl && !isMemberName && !isKey && !isParallelArg)
1531
+ used = true;
1532
+ }
1533
+ typescript_1.default.forEachChild(n, walk);
1534
+ };
1535
+ walk(sourceFile);
1536
+ return used;
1537
+ }
1538
+ function visitParallel(call, argOffset = 0) {
1539
+ const arg = call.arguments[argOffset];
1540
+ if (arg === undefined)
1541
+ return;
1542
+ let list = unwrap(arg);
1543
+ if (typescript_1.default.isIdentifier(list)) {
1544
+ const init = constInits.get(list.text);
1545
+ if (init !== undefined) {
1546
+ // A list that is also pushed to / passed on / iterated is not the literal it was bound to.
1547
+ if (listUsedBeyondParallel(list.text)) {
1548
+ unresolved.push({ expr: `parallel(${exprText(arg)})`, kind: "step", ...at });
1549
+ return;
1550
+ }
1551
+ list = unwrap(init);
1552
+ }
1553
+ }
1554
+ if (!typescript_1.default.isArrayLiteralExpression(list)) {
1555
+ unresolved.push({ expr: `parallel(${exprText(arg)})`, kind: "step", ...at });
1556
+ return;
1557
+ }
1558
+ for (const el of list.elements) {
1559
+ if (typescript_1.default.isSpreadElement(el)) {
1560
+ unresolved.push({ expr: `parallel(...${exprText(el.expression)})`, kind: "step", ...at });
1561
+ continue;
1562
+ }
1563
+ const u = unwrap(el);
1564
+ if (typescript_1.default.isArrayLiteralExpression(u)) {
1565
+ const first = u.elements[0];
1566
+ if (first === undefined || typescript_1.default.isSpreadElement(first)) {
1567
+ unresolved.push({ expr: `parallel branch ${exprText(u)}`, kind: "step", ...at });
1568
+ continue;
1569
+ }
1570
+ const ref = resolveTarget(first);
1571
+ if (ref !== undefined)
1572
+ steps.push(ref);
1573
+ else
1574
+ unresolved.push({ expr: exprText(first), kind: "step", ...at });
1575
+ }
1576
+ else if (isFn(u) || (typescript_1.default.isIdentifier(u) && functions.has(u.text))) {
1577
+ // A callback branch: its body is part of this walk.
1578
+ }
1579
+ else {
1580
+ unresolved.push({ expr: `parallel branch ${exprText(u)}`, kind: "step", ...at });
1581
+ }
1582
+ }
1583
+ }
1584
+ function visitSuspend(call, argOffset = 0) {
1585
+ const condition = call.arguments[argOffset];
1586
+ if (condition === undefined || !typescript_1.default.isObjectLiteralExpression(condition)) {
1587
+ // A condition built elsewhere. Recording it as unresolved keeps a flow that
1588
+ // suspends from looking like a flow that does not.
1589
+ unresolved.push({
1590
+ expr: condition === undefined ? "suspend()" : exprText(condition),
1591
+ kind: "suspend",
1592
+ ...at,
1593
+ });
1594
+ return;
1595
+ }
1596
+ const on = stringProp(condition, "on");
1597
+ const correlationKey = stringProp(condition, "correlationKey");
1598
+ suspends.push({
1599
+ ...(on !== undefined && { id: on }),
1600
+ ...(correlationKey !== undefined && { correlationKey }),
1601
+ ...(hasProp(condition, "timeout") && { timed: true }),
1602
+ ...(hasProp(condition, "wakeAt") && { wakeAt: true }),
1603
+ });
1604
+ }
1605
+ /**
1606
+ * L31 — a `ctx.decide(id, decider)` decider receives a scope whose
1607
+ * `invokeTransient(toolId, …)` runs an effect as the deciding execution. Those calls
1608
+ * are reached effects exactly like `ctx.run` targets, but live in a callback the
1609
+ * `ctx.run` walk never classified.
1610
+ *
1611
+ * The scope is tracked by BINDING: the first parameter, either an identifier
1612
+ * (`(scope) =>`) whose only permitted use is as the receiver of a called
1613
+ * `.invokeTransient`, or an object pattern (`({ invokeTransient })`, with or without a
1614
+ * rename) whose bound name may only be called. ANY other use — passing the scope on,
1615
+ * aliasing it, `scope["invokeTransient"]`, a rest element — is reported as unresolved,
1616
+ * because the call it hides cannot be seen. Name matching is textual: a nested
1617
+ * function that shadows the scope's name is treated as the scope (over-reports, never
1618
+ * under-reports).
1619
+ */
1620
+ function visitDecide(call) {
1621
+ if (call.arguments.some((a) => typescript_1.default.isSpreadElement(a))) {
1622
+ unresolved.push({
1623
+ expr: `decide(${exprText(call.arguments[0] ?? call)})`,
1624
+ kind: "step",
1625
+ ...at,
1626
+ });
1627
+ return;
1628
+ }
1629
+ const decider = call.arguments[1];
1630
+ if (decider === undefined)
1631
+ return;
1632
+ let fn;
1633
+ if (isFn(decider))
1634
+ fn = decider;
1635
+ else if (typescript_1.default.isIdentifier(decider))
1636
+ fn = functions.get(decider.text);
1637
+ if (fn === undefined) {
1638
+ // A decider whose body is not in this region (imported, returned from a factory,
1639
+ // a member). It may take a scope and call anything; same treatment as an
1640
+ // unresolvable `ctx.run` target.
1641
+ unresolved.push({ expr: `decide(${exprText(decider)})`, kind: "step", ...at });
1642
+ return;
1643
+ }
1644
+ const param = fn.parameters[0];
1645
+ if (param === undefined || fn.body === undefined)
1646
+ return; // never receives a scope
1647
+ const scopeNames = new Set();
1648
+ const callNames = new Set();
1649
+ if (typescript_1.default.isIdentifier(param.name)) {
1650
+ scopeNames.add(param.name.text);
1651
+ }
1652
+ else if (typescript_1.default.isObjectBindingPattern(param.name)) {
1653
+ for (const el of param.name.elements) {
1654
+ if (el.dotDotDotToken !== undefined) {
1655
+ unresolved.push({ expr: `decide scope rest ${exprText(el)}`, kind: "step", ...at });
1656
+ continue;
1657
+ }
1658
+ const key = el.propertyName ?? el.name;
1659
+ const keyText = typescript_1.default.isIdentifier(key) || typescript_1.default.isStringLiteral(key) || typescript_1.default.isNoSubstitutionTemplateLiteral(key)
1660
+ ? key.text
1661
+ : undefined;
1662
+ // A computed key could be anything; a nested pattern or non-identifier name
1663
+ // could bind `invokeTransient` where the call cannot be followed.
1664
+ const couldBind = keyText === "invokeTransient" || typescript_1.default.isComputedPropertyName(key);
1665
+ if (!couldBind)
1666
+ continue;
1667
+ if (keyText === "invokeTransient" && typescript_1.default.isIdentifier(el.name)) {
1668
+ callNames.add(el.name.text);
1669
+ }
1670
+ else {
1671
+ unresolved.push({ expr: `decide scope ${exprText(el)}`, kind: "step", ...at });
1672
+ }
1673
+ }
1674
+ }
1675
+ else {
1676
+ unresolved.push({ expr: `decide(${exprText(param)})`, kind: "step", ...at });
1677
+ return;
1678
+ }
1679
+ if (scopeNames.size === 0 && callNames.size === 0)
1680
+ return;
1681
+ const record = (c) => {
1682
+ const first = c.arguments[0];
1683
+ const id = first === undefined ? undefined : constString(first);
1684
+ if (id !== undefined)
1685
+ steps.push({ toolId: id, viaDecider: true });
1686
+ else {
1687
+ const arg = c.arguments[0];
1688
+ unresolved.push({
1689
+ expr: `invokeTransient(${arg === undefined ? "" : exprText(arg)})`,
1690
+ kind: "step",
1691
+ ...at,
1692
+ });
1693
+ }
1694
+ };
1695
+ const escaped = (n) => {
1696
+ unresolved.push({ expr: `decide scope used as ${exprText(n.parent)}`, kind: "step", ...at });
1697
+ };
1698
+ function scan(node) {
1699
+ if (typescript_1.default.isIdentifier(node)) {
1700
+ const parent = node.parent;
1701
+ // Property names and declaration names are not references.
1702
+ const isName = (typescript_1.default.isPropertyAccessExpression(parent) && parent.name === node) ||
1703
+ (typescript_1.default.isPropertyAssignment(parent) && parent.name === node) ||
1704
+ (typescript_1.default.isBindingElement(parent) && parent.propertyName === node);
1705
+ if (!isName) {
1706
+ if (scopeNames.has(node.text)) {
1707
+ const grand = parent.parent;
1708
+ if (typescript_1.default.isPropertyAccessExpression(parent) &&
1709
+ parent.expression === node &&
1710
+ parent.name.text === "invokeTransient" &&
1711
+ grand !== undefined &&
1712
+ typescript_1.default.isCallExpression(grand) &&
1713
+ grand.expression === parent) {
1714
+ record(grand);
1715
+ }
1716
+ else {
1717
+ escaped(node);
1718
+ }
1719
+ }
1720
+ else if (callNames.has(node.text)) {
1721
+ if (typescript_1.default.isCallExpression(parent) && parent.expression === node)
1722
+ record(parent);
1723
+ else
1724
+ escaped(node);
1725
+ }
1726
+ }
1727
+ }
1728
+ typescript_1.default.forEachChild(node, scan);
1729
+ }
1730
+ scan(fn.body);
1731
+ }
1732
+ /**
1733
+ * `ctx.decide` reached by any route other than a direct `<x>.decide(…)` call hides a
1734
+ * decider this walk would have followed: `ctx.decide.bind(ctx)`, `const f = ctx.decide`,
1735
+ * `const { decide } = ctx`, a `({ decide }) =>` runner parameter, `ctx["decide"](…)`.
1736
+ * Receiver-agnostic like `methodCall`; reported as unresolved so the shape cannot pass
1737
+ * vacuously.
1738
+ */
1739
+ function flagIndirectDecide(node) {
1740
+ let hit = false;
1741
+ if (typescript_1.default.isPropertyAccessExpression(node) && node.name.text === "decide") {
1742
+ const p = node.parent;
1743
+ hit = !(typescript_1.default.isCallExpression(p) && p.expression === node);
1744
+ }
1745
+ else if (typescript_1.default.isElementAccessExpression(node) &&
1746
+ typescript_1.default.isStringLiteralLike(node.argumentExpression) &&
1747
+ node.argumentExpression.text === "decide") {
1748
+ hit = true;
1749
+ }
1750
+ else if (typescript_1.default.isBindingElement(node) && node.dotDotDotToken === undefined) {
1751
+ const key = node.propertyName ?? node.name;
1752
+ hit = (typescript_1.default.isIdentifier(key) || typescript_1.default.isStringLiteralLike(key)) && key.text === "decide";
1753
+ }
1754
+ if (!hit)
1755
+ return;
1756
+ // L32 — `lib({ decide: ctx.decide })` hands the decider seam to a callee a declaration may
1757
+ // cover; every other alias shape has no callee to look up.
1758
+ if (typescript_1.default.isPropertyAccessExpression(node)) {
1759
+ const recv = receivingCall(node);
1760
+ if (recv !== undefined) {
1761
+ recordEscape(recv.call, "decide");
1762
+ return;
1763
+ }
1764
+ }
1765
+ unresolved.push({ expr: `decide alias ${exprText(node)}`, kind: "step", ...at });
1766
+ }
1767
+ function visit(node) {
1768
+ const run = methodCall(node, "run");
1769
+ if (run !== undefined)
1770
+ visitRun(run);
1771
+ const parallel = methodCall(node, "parallel");
1772
+ if (parallel !== undefined)
1773
+ visitParallel(parallel);
1774
+ const decide = methodCall(node, "decide");
1775
+ if (decide !== undefined)
1776
+ visitDecide(decide);
1777
+ else
1778
+ flagIndirectDecide(node);
1779
+ if (typescript_1.default.isIdentifier(node)) {
1780
+ if (ctxNames.has(node.text))
1781
+ flagCtxReference(node, "ctx");
1782
+ else if (ctx.members.has(node.text))
1783
+ flagCtxReference(node, "method");
1784
+ }
1785
+ const suspend = methodCall(node, "suspend");
1786
+ if (suspend !== undefined)
1787
+ visitSuspend(suspend);
1788
+ // `ctx.sleep`/`sleepUntil` are deliberately NOT recorded. The kernel authors
1789
+ // their condition (`on: TIMER_FIRED`, `wakeAt` always set), so treating them
1790
+ // as user-authored suspend points would manufacture findings about code the
1791
+ // developer never wrote.
1792
+ typescript_1.default.forEachChild(node, visit);
1793
+ }
1794
+ visit(root);
1795
+ return {
1796
+ reach: { steps, unresolved, suspends, ...(escapes.length > 0 && { escapes }) },
1797
+ runners: ctx.runners,
1798
+ boundRunners: ctx.boundRunners,
1799
+ };
1800
+ }
1801
+ /**
1802
+ * Analyze a source string (a flow runner / orchestration body). Feed the result
1803
+ * to `@loomwork/verify` via `resolveVerifyInput({ runnerReachByFlow })`.
1804
+ */
1805
+ function analyzeRunnerReach(source, location) {
1806
+ const sf = typescript_1.default.createSourceFile("runner.ts", source, typescript_1.default.ScriptTarget.ESNext, true);
1807
+ return analyzeReachNode(sf, location);
1808
+ }
1809
+ //# sourceMappingURL=runnerReach.js.map