@venn-lang/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/index.d.ts +453 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3314 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +56 -0
  8. package/src/check/check-calls.ts +82 -0
  9. package/src/check/check-deco-body.ts +77 -0
  10. package/src/check/check-document.ts +106 -0
  11. package/src/check/check-env.ts +96 -0
  12. package/src/check/check-fragment-call.ts +27 -0
  13. package/src/check/check-imports.ts +82 -0
  14. package/src/check/check-interpolation.ts +58 -0
  15. package/src/check/check-options.ts +18 -0
  16. package/src/check/check-uncalled.ts +42 -0
  17. package/src/check/check.types.ts +30 -0
  18. package/src/check/index.ts +4 -0
  19. package/src/context/action-context.ts +20 -0
  20. package/src/context/index.ts +1 -0
  21. package/src/decorators/builtin-decorators.ts +87 -0
  22. package/src/decorators/create-decorator-source.ts +25 -0
  23. package/src/decorators/index.ts +2 -0
  24. package/src/emit/create-emitter.ts +16 -0
  25. package/src/emit/emitter.types.ts +6 -0
  26. package/src/emit/index.ts +3 -0
  27. package/src/emit/run-id.ts +11 -0
  28. package/src/eventsink/event-sink.port.ts +13 -0
  29. package/src/eventsink/event-sink.types.ts +11 -0
  30. package/src/eventsink/index.ts +4 -0
  31. package/src/eventsink/memory-sink.ts +17 -0
  32. package/src/eventsink/ndjson-sink.ts +14 -0
  33. package/src/index.ts +14 -0
  34. package/src/ports/create-port-resolver.ts +39 -0
  35. package/src/ports/index.ts +2 -0
  36. package/src/ports/port-resolver.types.ts +12 -0
  37. package/src/registry/build-registry.ts +76 -0
  38. package/src/registry/index.ts +2 -0
  39. package/src/registry/registry.types.ts +24 -0
  40. package/src/run/create-runner.ts +103 -0
  41. package/src/run/index.ts +4 -0
  42. package/src/run/resolve-imports.ts +166 -0
  43. package/src/run/runner.types.ts +67 -0
  44. package/src/scheduler/absorb-exit.ts +19 -0
  45. package/src/scheduler/aliases.ts +44 -0
  46. package/src/scheduler/annotations.ts +48 -0
  47. package/src/scheduler/base-scope.ts +25 -0
  48. package/src/scheduler/bind-globals.ts +55 -0
  49. package/src/scheduler/bind-imports.ts +130 -0
  50. package/src/scheduler/bind-namespaces.ts +61 -0
  51. package/src/scheduler/bind-prelude.ts +11 -0
  52. package/src/scheduler/block-plan.ts +66 -0
  53. package/src/scheduler/branch-engine.ts +13 -0
  54. package/src/scheduler/call-params.ts +140 -0
  55. package/src/scheduler/cleanup.types.ts +18 -0
  56. package/src/scheduler/collect.ts +60 -0
  57. package/src/scheduler/concurrency.ts +16 -0
  58. package/src/scheduler/create-cleanup-list.ts +17 -0
  59. package/src/scheduler/declared-arity.ts +13 -0
  60. package/src/scheduler/engine.types.ts +50 -0
  61. package/src/scheduler/filter.ts +5 -0
  62. package/src/scheduler/filter.types.ts +9 -0
  63. package/src/scheduler/flaky.ts +32 -0
  64. package/src/scheduler/flaky.types.ts +7 -0
  65. package/src/scheduler/index.ts +16 -0
  66. package/src/scheduler/invocation.ts +103 -0
  67. package/src/scheduler/local-call.ts +28 -0
  68. package/src/scheduler/node-span.ts +29 -0
  69. package/src/scheduler/opts-text.ts +10 -0
  70. package/src/scheduler/opts.ts +14 -0
  71. package/src/scheduler/pending.types.ts +9 -0
  72. package/src/scheduler/run-action.ts +163 -0
  73. package/src/scheduler/run-around.ts +23 -0
  74. package/src/scheduler/run-attempts.ts +105 -0
  75. package/src/scheduler/run-bindings.ts +67 -0
  76. package/src/scheduler/run-block.ts +65 -0
  77. package/src/scheduler/run-document.ts +181 -0
  78. package/src/scheduler/run-expect.ts +48 -0
  79. package/src/scheduler/run-flow.ts +69 -0
  80. package/src/scheduler/run-foreach.ts +148 -0
  81. package/src/scheduler/run-group.ts +9 -0
  82. package/src/scheduler/run-if.ts +21 -0
  83. package/src/scheduler/run-lifecycle.ts +86 -0
  84. package/src/scheduler/run-matcher.ts +99 -0
  85. package/src/scheduler/run-parallel.ts +68 -0
  86. package/src/scheduler/run-prologue.ts +59 -0
  87. package/src/scheduler/run-race.ts +20 -0
  88. package/src/scheduler/run-repeat.ts +54 -0
  89. package/src/scheduler/run-run.ts +41 -0
  90. package/src/scheduler/run-script.ts +48 -0
  91. package/src/scheduler/run-statements.ts +107 -0
  92. package/src/scheduler/run-step.ts +76 -0
  93. package/src/scheduler/run-try.ts +34 -0
  94. package/src/scheduler/run-while.ts +50 -0
  95. package/src/scheduler/script-lifecycle.ts +47 -0
  96. package/src/scheduler/settled.ts +20 -0
  97. package/src/scheduler/signals.ts +33 -0
  98. package/src/scheduler/target.ts +30 -0
  99. package/src/scheduler/unknown-option.ts +87 -0
  100. package/src/scope/create-scope.ts +67 -0
  101. package/src/scope/index.ts +2 -0
  102. package/src/scope/scope.types.ts +12 -0
  103. package/src/types/create-type-catalog.ts +70 -0
  104. package/src/types/index.ts +1 -0
package/dist/index.js ADDED
@@ -0,0 +1,3314 @@
1
+ import { CODES, PRELUDE_VALUES, ProblemError, buildDiff, buildProblem, callArgs, closureOfDecl, compileExpr, decoCannotCall, decorateCallable, display, evaluate, expand, invoke, isActionCall, isBlock, isCall, isCallable, isCaptureStmt, isConfigDecl, isDecoDecl, isFlowDecl, isFnDecl, isFragmentDecl, isIfStmt, isLetStmt, isLifecycleDecl, isMatcherClause, isMatrixDecl, isMember, isNamespaceValue, isPackageSpecifier, isRef, isRunStmt, isRunnable, isStepDecl, isStringLit, isUseDecl, isValueImport, memberValue, namespaceValue, nativeFn, parse, parseExpression, prune, readDecorations, readMeta, scanInterpolations, specToType, splitCall, truthy, typeName, walkAst } from "@venn-lang/core";
2
+ import { ConsolePort, VennError, bindPort, missingCapabilities } from "@venn-lang/contracts";
3
+ import { paramSpecs } from "@venn-lang/sdk";
4
+ //#region src/scheduler/signals.ts
5
+ var BreakSignal = class {};
6
+ var ContinueSignal = class {};
7
+ var ReturnSignal = class {
8
+ value;
9
+ constructor(value) {
10
+ this.value = value;
11
+ }
12
+ };
13
+ /** Thrown at a statement boundary when the branch's race has already been won. */
14
+ var CancelSignal = class {};
15
+ /**
16
+ * Thrown by `exit`, carrying the code the host should leave with.
17
+ *
18
+ * A signal and not an error: a run that exits has not failed. The code it
19
+ * carries is the whole verdict, and `exit 0` is a clean ending, which an error
20
+ * unwinding as a failure could not express.
21
+ */
22
+ var ExitSignal = class {
23
+ code;
24
+ constructor(code) {
25
+ this.code = code;
26
+ }
27
+ };
28
+ function isControlSignal(value) {
29
+ return value instanceof BreakSignal || value instanceof ContinueSignal || value instanceof ReturnSignal || value instanceof CancelSignal || value instanceof ExitSignal;
30
+ }
31
+ //#endregion
32
+ //#region src/scheduler/absorb-exit.ts
33
+ /**
34
+ * Run `body`, letting an `exit` end it.
35
+ *
36
+ * The exit code travels on the engine rather than on the throw, so whatever
37
+ * wraps this still finishes the way it always does: teardowns run, `run.finished`
38
+ * is emitted, and the host reads one number off the result. Any other error keeps
39
+ * unwinding untouched.
40
+ */
41
+ async function absorbExit(engine, body) {
42
+ try {
43
+ await body();
44
+ } catch (error) {
45
+ if (!(error instanceof ExitSignal)) throw error;
46
+ engine.exit = error.code;
47
+ }
48
+ }
49
+ //#endregion
50
+ //#region src/scheduler/aliases.ts
51
+ /**
52
+ * The namespaces this file actually brought in with `use`: the alias when one
53
+ * was given, otherwise the namespace the package contributes. A name outside
54
+ * this set was never imported, however well the registry knows it.
55
+ */
56
+ function collectNamespaces(document, registry) {
57
+ const names = /* @__PURE__ */ new Set();
58
+ for (const decl of document.imports) {
59
+ if (!isUseDecl(decl)) continue;
60
+ const bound = decl.alias ?? registry.namespaceOf(decl.pkg);
61
+ if (bound) names.add(bound);
62
+ }
63
+ return names;
64
+ }
65
+ /**
66
+ * The names this file binds at the top level.
67
+ *
68
+ * `page.click()` reads like a namespace and a verb, and is neither: it is a
69
+ * method on something the file holds. The registry has no schema for it, so it
70
+ * is known here and verified nowhere, which beats reporting it as an unknown
71
+ * action it plainly is not.
72
+ */
73
+ function collectBoundNames(document) {
74
+ const names = /* @__PURE__ */ new Set();
75
+ for (const decl of document.decls) if (isLetStmt(decl)) names.add(decl.name);
76
+ return names;
77
+ }
78
+ /** `use "venn/http" as h` → `{ h → "http" }`, resolved through the registry. */
79
+ function collectAliases(document, registry) {
80
+ const aliases = /* @__PURE__ */ new Map();
81
+ for (const decl of document.imports) {
82
+ if (!isUseDecl(decl) || !decl.alias) continue;
83
+ const namespace = registry.namespaceOf(decl.pkg);
84
+ if (namespace) aliases.set(decl.alias, namespace);
85
+ }
86
+ return aliases;
87
+ }
88
+ //#endregion
89
+ //#region src/scheduler/bind-globals.ts
90
+ /**
91
+ * Bind every top-level `fn` as a callable closure. Hoisted so a function can be
92
+ * called before its textual position, and so functions may reference each other
93
+ * regardless of order.
94
+ *
95
+ * A `deco` that asked to `wrap` one is honoured here, at the only moment the
96
+ * function becomes a value: the name a caller reaches is already the decorated
97
+ * one, so a middleware cannot be stepped around by calling from somewhere else.
98
+ */
99
+ function bindFunctions(doc, scope) {
100
+ for (const decl of doc.decls) if (isFnDecl(decl)) scope.set(decl.name, decorateCallable(decl, closureOfDecl(decl, scope)));
101
+ }
102
+ /**
103
+ * Bind top-level functions and the plain `const`/`let` globals every flow can
104
+ * read without anything having run yet.
105
+ *
106
+ * Only pure values. One that calls a verb is a statement, and statements run in
107
+ * the prologue, in order and once, before the flows: opening a database is
108
+ * something a program *does*, not something a name quietly is.
109
+ */
110
+ function bindGlobals(doc, scope) {
111
+ bindFunctions(doc, scope);
112
+ bindPlainValues(doc, scope);
113
+ }
114
+ /**
115
+ * The `const`/`let` globals, evaluated where they stand.
116
+ *
117
+ * Separate from the functions because the order matters across files: a function
118
+ * is hoisted and reads its scope when it is *called*, while a value is read now,
119
+ * so anything a value might depend on has to be in place first.
120
+ */
121
+ function bindPlainValues(doc, scope) {
122
+ for (const decl of doc.decls) if (isLetStmt(decl) && isPlainValue(decl)) scope.set(decl.name, evaluate(decl.value, scope));
123
+ }
124
+ function isPlainValue(decl) {
125
+ return decl.args.length === 0 && !decl.opts;
126
+ }
127
+ //#endregion
128
+ //#region src/scheduler/bind-imports.ts
129
+ /**
130
+ * Bind the names a document imported, each to the value its own module made.
131
+ *
132
+ * A `pub fn` is a closure over the file it was written in (it calls that file's
133
+ * private helpers and reads that file's globals) so it cannot simply be lifted
134
+ * into the importer's scope. Each module gets a scope of its own, built the same
135
+ * way the entry document's is, and the importer takes only the names it asked
136
+ * for out of it.
137
+ *
138
+ * Three passes over every module at once, not one walk per import: two files
139
+ * that call each other are ordinary, and no single order resolves them. Filling
140
+ * every scope first and wiring afterwards has no order to get wrong, because a
141
+ * function captures the scope object and wiring mutates that same object.
142
+ *
143
+ * Call this before the document's own globals are bound, so a local name of the
144
+ * same spelling wins. That is the rule fragments already follow.
145
+ */
146
+ function bindImports(args) {
147
+ const built = /* @__PURE__ */ new Map();
148
+ for (const [uri, module] of args.graph.modules) {
149
+ const scope = args.base();
150
+ built.set(uri, scope);
151
+ bindFunctions(module, scope);
152
+ }
153
+ for (const [uri, module] of args.graph.modules) wire({
154
+ document: module,
155
+ uri,
156
+ into: scopeAt(built, uri),
157
+ graph: args.graph,
158
+ built
159
+ });
160
+ for (const [uri, module] of args.graph.modules) bindPlainValues(module, scopeAt(built, uri));
161
+ wire({
162
+ document: args.document,
163
+ uri: args.uri,
164
+ into: args.scope,
165
+ graph: args.graph,
166
+ built
167
+ });
168
+ }
169
+ function scopeAt(built, uri) {
170
+ return built.get(uri);
171
+ }
172
+ function wire(args) {
173
+ for (const decl of args.document.imports) {
174
+ if (!isValueImport(decl)) continue;
175
+ if (isPackageSpecifier(decl.path)) {
176
+ takePackage({
177
+ decl,
178
+ graph: args.graph,
179
+ into: args.into
180
+ });
181
+ continue;
182
+ }
183
+ const from = args.graph.resolve(args.uri, decl.path);
184
+ const module = args.graph.modules.get(from);
185
+ const source = args.built.get(from);
186
+ if (module && source) take({
187
+ decl,
188
+ module,
189
+ source,
190
+ into: args.into
191
+ });
192
+ }
193
+ }
194
+ /**
195
+ * The names an installed package published.
196
+ *
197
+ * Everything it exports is fair game: a package has no `pub`, its exports *are*
198
+ * what it made public, so unlike a `.vn` module there is nothing to filter
199
+ * against. `default` is bound too, under whichever name the import gave it,
200
+ * because that is the only name it has here.
201
+ */
202
+ function takePackage(args) {
203
+ const module = args.graph.npm?.get(args.decl.path);
204
+ if (!module) return;
205
+ if (args.decl.wildcard) return void args.into.set(args.decl.wildcard, { ...module });
206
+ if (args.decl.default) return void args.into.set(args.decl.default, module.default);
207
+ for (const name of args.decl.names) if (name in module) args.into.set(name, module[name]);
208
+ }
209
+ function take(args) {
210
+ const exported = exportedFunctions(args.module);
211
+ if (args.decl.wildcard) {
212
+ args.into.set(args.decl.wildcard, gathered(exported, args.source));
213
+ return;
214
+ }
215
+ for (const name of args.decl.names) if (exported.has(name)) args.into.set(name, args.source.lookup(name));
216
+ }
217
+ /** `import * as u`: one value holding everything the module published. */
218
+ function gathered(names, source) {
219
+ const out = {};
220
+ for (const name of names) out[name] = source.lookup(name);
221
+ return out;
222
+ }
223
+ /** The names a module made `pub` that are values a caller can hold. */
224
+ function exportedFunctions(document) {
225
+ const names = /* @__PURE__ */ new Set();
226
+ for (const decl of document.decls) if (isFnDecl(decl) && decl.export) names.add(decl.name);
227
+ return names;
228
+ }
229
+ //#endregion
230
+ //#region src/scope/create-scope.ts
231
+ /**
232
+ * A lexical scope.
233
+ *
234
+ * A class, not an object literal: a `forEach` over 50k items builds 50k scopes,
235
+ * and a literal allocates a closure per method for each of them. One prototype
236
+ * does for all.
237
+ *
238
+ * Bindings live in cells rather than as bare values, so a compiled function can
239
+ * address one once and read it by index on every call after. The cell exists as
240
+ * soon as anyone asks for it, which is what lets a recursive `fn` work: the
241
+ * closure asks for its own name while being built, before the binding fills it.
242
+ */
243
+ var MapScope = class MapScope {
244
+ parent;
245
+ vars = /* @__PURE__ */ new Map();
246
+ constructor(parent) {
247
+ this.parent = parent;
248
+ }
249
+ lookup(name) {
250
+ const found = this.vars.get(name);
251
+ return found === void 0 ? this.parent?.lookup(name) : found.value;
252
+ }
253
+ set(name, value) {
254
+ const own = this.vars.get(name);
255
+ if (own) {
256
+ own.value = value;
257
+ return;
258
+ }
259
+ this.vars.set(name, { value });
260
+ }
261
+ /**
262
+ * The cell holding `name`, taken from wherever in the chain already has one.
263
+ *
264
+ * A function reading a global must get the global's own cell, not a fresh
265
+ * local one that the binding would never fill. Only a name nobody has bound
266
+ * anywhere gets its cell here, where a later `set` will land.
267
+ */
268
+ cell(name) {
269
+ const own = this.vars.get(name);
270
+ if (own) return own;
271
+ const inherited = this.parent?.cell(name);
272
+ if (inherited) return inherited;
273
+ const made = { value: void 0 };
274
+ this.vars.set(name, made);
275
+ return made;
276
+ }
277
+ child() {
278
+ return new MapScope(this);
279
+ }
280
+ };
281
+ /**
282
+ * Create a scope; `lookup` falls back to the parent, `set` writes locally.
283
+ *
284
+ * @param parent The enclosing scope, or nothing for a root scope.
285
+ */
286
+ function createScope(parent) {
287
+ return new MapScope(parent);
288
+ }
289
+ //#endregion
290
+ //#region src/scheduler/collect.ts
291
+ /** Index the document's fragments by name for `run`. */
292
+ function collectFragments(doc) {
293
+ const map = /* @__PURE__ */ new Map();
294
+ for (const decl of doc.decls) if (isFragmentDecl(decl)) map.set(decl.name, decl);
295
+ return map;
296
+ }
297
+ /** Evaluate the top-level `config { … }` block with `env` bound, for the action context. */
298
+ function collectConfig(doc, env) {
299
+ const decl = doc.decls.find(isConfigDecl);
300
+ if (!decl) return {};
301
+ const scope = createScope();
302
+ scope.set("env", env);
303
+ return evaluate(decl.body, scope);
304
+ }
305
+ /** Gather top-level setup/teardown/beforeEach/afterEach blocks. */
306
+ function collectHooks(doc) {
307
+ const hooks = {
308
+ setup: [],
309
+ teardown: [],
310
+ beforeEach: [],
311
+ afterEach: []
312
+ };
313
+ for (const decl of doc.decls) if (isLifecycleDecl(decl)) addHook(hooks, decl);
314
+ return hooks;
315
+ }
316
+ function addHook(hooks, decl) {
317
+ const bucket = hooks[decl.hook];
318
+ if (bucket) bucket.push(decl);
319
+ }
320
+ //#endregion
321
+ //#region src/scheduler/create-cleanup-list.ts
322
+ /**
323
+ * The runtime's own sink, for a host that does not bring one.
324
+ *
325
+ * Closing runs newest first and survives a cleanup that throws: a program on its
326
+ * way out still has to hand back everything else it was holding.
327
+ */
328
+ function createCleanupList() {
329
+ const pending = [];
330
+ return {
331
+ add: (cleanup) => pending.push(cleanup),
332
+ close: async () => {
333
+ for (const cleanup of pending.splice(0).reverse()) await cleanup().catch(() => {});
334
+ }
335
+ };
336
+ }
337
+ //#endregion
338
+ //#region src/scheduler/filter.ts
339
+ /** A title matches when the needle is absent, or contained case-insensitively. */
340
+ function matchesTitle(title, needle) {
341
+ if (!needle) return true;
342
+ return title.toLowerCase().includes(needle.toLowerCase());
343
+ }
344
+ //#endregion
345
+ //#region src/scheduler/invocation.ts
346
+ /**
347
+ * The dotted path an expression spells, such as `http.post`, or undefined when
348
+ * it is not a plain path. The parser cannot tell a namespace from a field; only
349
+ * the registry can, so the path is carried this far as an expression.
350
+ */
351
+ function actionTarget(expr) {
352
+ if (!expr) return void 0;
353
+ if (isRef(expr)) return expr.name;
354
+ if (!isMember(expr) || expr.optional) return void 0;
355
+ const receiver = actionTarget(expr.receiver);
356
+ return receiver === void 0 ? void 0 : `${receiver}.${expr.member}`;
357
+ }
358
+ /**
359
+ * The bareword action a `let` spells: `let auth = http.post url { … }`. The
360
+ * trailing arguments or options map are what mark it as a call. A parenthesised
361
+ * call (`f(x)`) is an ordinary expression the evaluator runs, since it may be a
362
+ * `fn` or a method rather than an action; only {@link actionCall} rescues the
363
+ * case where it turns out to name a plugin action.
364
+ */
365
+ function invocationOf(stmt) {
366
+ return remember(invocations, stmt, readInvocation);
367
+ }
368
+ function readInvocation(stmt) {
369
+ if (stmt.args.length === 0 && !stmt.opts) return void 0;
370
+ const target = actionTarget(stmt.value);
371
+ if (target === void 0) return void 0;
372
+ return {
373
+ target,
374
+ args: stmt.args,
375
+ opts: stmt.opts,
376
+ node: stmt
377
+ };
378
+ }
379
+ /**
380
+ * An action written in expression position: the bare path `data.faker.uuid`, or
381
+ * the parenthesised `data.faker.uuid()`. Returns the target and its arguments,
382
+ * or undefined when it is not a dotted path at all.
383
+ */
384
+ function actionCall(value) {
385
+ return remember(calls, value, readActionCall);
386
+ }
387
+ function readActionCall(value) {
388
+ const bare = actionTarget(value);
389
+ if (bare !== void 0) return {
390
+ target: bare,
391
+ args: []
392
+ };
393
+ if (!isCall(value)) return void 0;
394
+ const target = actionTarget(value.callee);
395
+ return target === void 0 ? void 0 : {
396
+ target,
397
+ args: argValues(value)
398
+ };
399
+ }
400
+ /**
401
+ * What a node spells, worked out once.
402
+ *
403
+ * The shape of the tree decides whether `let y = x * 2` names an action, and the
404
+ * tree does not change, so the answer is cached per node rather than rewalked
405
+ * through the type guards on every execution. Whether the *name* resolves to an
406
+ * action depends on the scope and the registry, so that part is not cached.
407
+ */
408
+ const invocations = /* @__PURE__ */ new WeakMap();
409
+ const calls = /* @__PURE__ */ new WeakMap();
410
+ function remember(cache, key, read) {
411
+ const known = cache.get(key);
412
+ if (known !== void 0) return known ?? void 0;
413
+ const found = read(key) ?? null;
414
+ cache.set(key, found);
415
+ return found ?? void 0;
416
+ }
417
+ function argValues(call) {
418
+ return (call.args?.args ?? []).map((arg) => arg.value);
419
+ }
420
+ //#endregion
421
+ //#region src/scheduler/node-span.ts
422
+ /** The exact source text of an AST node, for the event stream. */
423
+ function nodeSource(node) {
424
+ return node.$cstNode?.text ?? "";
425
+ }
426
+ /** A Span for an AST node, for a runtime Problem. */
427
+ function nodeSpan(node, uri) {
428
+ const cst = node.$cstNode;
429
+ return {
430
+ uri,
431
+ offset: cst?.offset ?? 0,
432
+ length: cst?.length ?? 0,
433
+ line: (cst?.range?.start?.line ?? 0) + 1,
434
+ column: (cst?.range?.start?.character ?? 0) + 1
435
+ };
436
+ }
437
+ //#endregion
438
+ //#region src/scheduler/annotations.ts
439
+ /**
440
+ * Whether a decorator set this flag on the node.
441
+ *
442
+ * The scheduler reads what expansion concluded, never the annotation list
443
+ * itself, so a decorator is understood in exactly one place.
444
+ */
445
+ function hasAnnotation(node, name) {
446
+ return readMeta(node, name) === true;
447
+ }
448
+ /** `@timeout(90s)` → milliseconds, or undefined. */
449
+ function readTimeout(node) {
450
+ return readMeta(node, "timeout");
451
+ }
452
+ /** `@retry(2, { backoff: 500ms, factor: 2 })` → a retry spec, or undefined. */
453
+ function readRetry(node) {
454
+ return readMeta(node, "retry");
455
+ }
456
+ /** `@lock("orders")` → the lock name, or undefined. */
457
+ function readLock(node) {
458
+ return readMeta(node, "lock");
459
+ }
460
+ /** `@flaky(0.05)` → the tolerated failure ratio; bare `@flaky` tolerates all. */
461
+ function readFlaky(node) {
462
+ return readMeta(node, "flaky");
463
+ }
464
+ /** `@tags(smoke, critical)` → the tag names. */
465
+ function readTags(node) {
466
+ return readMeta(node, "tags") ?? [];
467
+ }
468
+ //#endregion
469
+ //#region src/scheduler/bind-namespaces.ts
470
+ /**
471
+ * Put every plugin namespace in the root scope as a value, so its verbs work
472
+ * inside any expression (`print(fmt.table(rows))`, `"${crypto.hash(x)}"`) and
473
+ * not only as a bare statement.
474
+ *
475
+ * A verb reached this way runs synchronously: it is the pure corner of the
476
+ * stdlib (`fmt`, `data`, `crypto`). An action that does I/O returns its promise,
477
+ * so those stay written as statements, where the runtime awaits them.
478
+ */
479
+ function bindNamespaces(args) {
480
+ const byNamespace = /* @__PURE__ */ new Map();
481
+ for (const entry of args.registry.actions()) {
482
+ const members = byNamespace.get(entry.namespace) ?? {};
483
+ place({
484
+ members,
485
+ path: entry.name,
486
+ action: entry.action,
487
+ ctx: args.ctx
488
+ });
489
+ byNamespace.set(entry.namespace, members);
490
+ }
491
+ for (const [namespace, members] of byNamespace) args.scope.set(namespace, namespaceValue(members));
492
+ }
493
+ /** `jwt.decode` nests: `{ jwt: { decode: fn } }`, so the dotted call reads naturally. */
494
+ function place(args) {
495
+ const parts = args.path.split(".");
496
+ const leaf = parts.pop();
497
+ if (!leaf) return;
498
+ let level = args.members;
499
+ for (const part of parts) {
500
+ level[part] = level[part] ?? {};
501
+ level = level[part];
502
+ }
503
+ level[leaf] = verb(args.action, args.ctx);
504
+ }
505
+ function verb(action, ctx) {
506
+ return nativeFn((values) => action.run(ctx, {
507
+ args: values,
508
+ params: params(action)
509
+ }));
510
+ }
511
+ /** No options map in expression position, so let the schema supply its defaults. */
512
+ function params(action) {
513
+ if (!action.params) return {};
514
+ try {
515
+ return action.params.parse({});
516
+ } catch {
517
+ return {};
518
+ }
519
+ }
520
+ //#endregion
521
+ //#region src/scheduler/bind-prelude.ts
522
+ /**
523
+ * Put the pure prelude values (`len`, `range`, `pretty`, `str`, `min`…) in the
524
+ * root scope. Being values rather than verbs is what lets them appear in any
525
+ * expression: `xs.take(len(ys))`, `"${pretty(user)}"`.
526
+ */
527
+ function bindPrelude(scope) {
528
+ for (const [name, value] of Object.entries(PRELUDE_VALUES)) scope.set(name, value);
529
+ }
530
+ //#endregion
531
+ //#region src/scheduler/base-scope.ts
532
+ /**
533
+ * What every scope in a run starts as: the prelude, the plugin namespaces and
534
+ * `env`, plus the matrix variant when there is one.
535
+ *
536
+ * One place, shared by test runs and program runs, so `venn test` and `venn run`
537
+ * cannot drift apart. Every scope a module is read in starts here too, which is
538
+ * what makes an imported function see the same world its own file did.
539
+ */
540
+ function createBaseScope(args) {
541
+ const scope = createScope();
542
+ bindPrelude(scope);
543
+ bindNamespaces({
544
+ registry: args.engine.registry,
545
+ ctx: args.engine.ctx,
546
+ scope
547
+ });
548
+ scope.set("env", args.engine.env);
549
+ if (args.variant) scope.set("matrix", args.variant);
550
+ return scope;
551
+ }
552
+ //#endregion
553
+ //#region src/scheduler/flaky.ts
554
+ /** Record one execution of a `@flaky(ratio)` node, and how much it failed by. */
555
+ function recordFlaky(engine, node, before) {
556
+ const ratio = readFlaky(node);
557
+ if (ratio === void 0) return;
558
+ const tally = engine.flaky.get(node) ?? {
559
+ ratio,
560
+ runs: 0,
561
+ failedRuns: 0,
562
+ failedUnits: 0
563
+ };
564
+ const delta = engine.result.failed - before;
565
+ tally.runs += 1;
566
+ tally.failedRuns += delta > 0 ? 1 : 0;
567
+ tally.failedUnits += delta > 0 ? delta : 0;
568
+ engine.flaky.set(node, tally);
569
+ }
570
+ /**
571
+ * Forgive the failures of every `@flaky` node whose observed failure ratio
572
+ * stayed within its declared tolerance. Settled once, so the verdict does not
573
+ * depend on the order iterations happened to fail in.
574
+ */
575
+ function settleFlaky(engine) {
576
+ for (const tally of engine.flaky.values()) {
577
+ if (tally.failedRuns === 0 || tally.failedRuns / tally.runs > tally.ratio) continue;
578
+ engine.result.failed = Math.max(0, engine.result.failed - tally.failedUnits);
579
+ engine.emitter.emit({
580
+ kind: "log",
581
+ data: {
582
+ level: "warn",
583
+ message: message(tally)
584
+ }
585
+ });
586
+ }
587
+ }
588
+ function message(tally) {
589
+ return `flaky tolerated: ${tally.failedRuns}/${tally.runs} runs failed, within ratio ${tally.ratio}`;
590
+ }
591
+ //#endregion
592
+ //#region src/scheduler/run-around.ts
593
+ /**
594
+ * Run the `.before` and `.after` closures a `deco` left on a flow or a step.
595
+ *
596
+ * The language has no syntax for "around this body", so the verbs leave a fact
597
+ * behind and this is where it is honoured. `.after` runs from a `finally`,
598
+ * because a hook that only runs when nothing went wrong is the one case it was
599
+ * written for.
600
+ */
601
+ async function runAround(node, body) {
602
+ const around = readDecorations(node);
603
+ if (!around) {
604
+ await body();
605
+ return;
606
+ }
607
+ for (const fn of around.before) invoke(fn, []);
608
+ try {
609
+ await body();
610
+ } finally {
611
+ for (const fn of around.after) invoke(fn, []);
612
+ }
613
+ }
614
+ //#endregion
615
+ //#region src/scheduler/run-attempts.ts
616
+ /** Run a step/flow body applying its `@timeout` and `@retry` annotations. */
617
+ async function runWithAnnotations(args) {
618
+ const timeout = readTimeout(args.node);
619
+ const retry = readRetry(args.node);
620
+ const attempt = () => withTimeout(timeout, args.run);
621
+ if (!retry || retry.attempts === 0) return attempt();
622
+ await withRetry({
623
+ engine: args.engine,
624
+ retry,
625
+ title: args.title,
626
+ run: attempt
627
+ });
628
+ }
629
+ /** Hold the named mutex (`@lock`/`@serial`) around `run`, releasing on exit. */
630
+ async function withLock(engine, name, run) {
631
+ if (!name) return run();
632
+ const release = await engine.lock.acquire(name);
633
+ try {
634
+ await run();
635
+ } finally {
636
+ release();
637
+ }
638
+ }
639
+ async function withTimeout(timeoutMs, run) {
640
+ if (timeoutMs === void 0) return run();
641
+ let timer;
642
+ const guard = new Promise((_resolve, reject) => {
643
+ timer = setTimeout(() => reject(timeoutError(timeoutMs)), timeoutMs);
644
+ });
645
+ try {
646
+ await Promise.race([run(), guard]);
647
+ } finally {
648
+ if (timer) clearTimeout(timer);
649
+ }
650
+ }
651
+ async function withRetry(args) {
652
+ const total = args.retry.attempts + 1;
653
+ for (let i = 1; i <= total; i++) {
654
+ const snapshot = { ...args.engine.result };
655
+ const outcome = await attempt(args.engine, args.run);
656
+ if (outcome.ok) return;
657
+ if (i >= total) {
658
+ if (outcome.error) throw outcome.error;
659
+ return;
660
+ }
661
+ Object.assign(args.engine.result, snapshot);
662
+ emitRetrying({
663
+ engine: args.engine,
664
+ title: args.title,
665
+ attempt: i
666
+ });
667
+ await args.engine.clock.sleep(backoff(args.retry, i));
668
+ }
669
+ }
670
+ async function attempt(engine, run) {
671
+ const before = engine.result.failed;
672
+ try {
673
+ await run();
674
+ return { ok: engine.result.failed === before };
675
+ } catch (error) {
676
+ if (isControlSignal(error)) throw error;
677
+ return {
678
+ ok: false,
679
+ error
680
+ };
681
+ }
682
+ }
683
+ function emitRetrying(args) {
684
+ args.engine.emitter.emit({
685
+ kind: "flow.retrying",
686
+ data: {
687
+ title: args.title,
688
+ attempt: args.attempt,
689
+ reason: "previous attempt failed"
690
+ }
691
+ });
692
+ }
693
+ function backoff(retry, attempt) {
694
+ return retry.backoffMs * retry.factor ** (attempt - 1);
695
+ }
696
+ function timeoutError(ms) {
697
+ return new VennError({
698
+ code: "VN8001",
699
+ message: `Timed out after ${ms}ms.`
700
+ });
701
+ }
702
+ //#endregion
703
+ //#region src/scheduler/unknown-option.ts
704
+ /**
705
+ * The keys an options map spells that the schema behind it never declared,
706
+ * whether that schema is an action's or a matcher's.
707
+ *
708
+ * One list, two readers: the checker squiggles them before anything runs, the
709
+ * runtime refuses them when the line executes. Same code, same words, so a typo
710
+ * cannot read one way in the editor and another in the terminal.
711
+ *
712
+ * A schema that declares no keys accepts a free-form map, and nothing there is
713
+ * unknown: `grpc.request` takes a `z.record`, `db.seed` takes no schema at all
714
+ * and reads the whole map as table names.
715
+ */
716
+ function unknownOptions(args) {
717
+ const specs = paramSpecs(args.params);
718
+ if (!args.opts || specs.length === 0 || welcomesMore(args.params)) return [];
719
+ const known = new Set(specs.map((spec) => spec.name));
720
+ return args.opts.entries.filter((entry) => !known.has(entry.key)).map((entry) => unknownOption({
721
+ entry,
722
+ specs,
723
+ uri: args.uri
724
+ }));
725
+ }
726
+ /**
727
+ * A schema written with a catchall (`z.looseObject`, `.catchall(…)`) names some
728
+ * keys and takes the rest as well. A key it never named is still a key it asked
729
+ * to receive, so there is nothing to report. Read structurally, past the
730
+ * wrappers `.optional()` and `.default()` add, the way {@link paramSpecs} reads
731
+ * the shape.
732
+ */
733
+ function welcomesMore(params) {
734
+ const def = params?.def;
735
+ if (def?.catchall !== void 0) return true;
736
+ return def?.innerType !== void 0 && welcomesMore(def.innerType);
737
+ }
738
+ function unknownOption(args) {
739
+ const { key } = args.entry;
740
+ const hint = nearest$1(key, args.specs);
741
+ const accepted = args.specs.map((spec) => spec.name).join(", ");
742
+ const title = hint ? `"${key}" is not an option here — did you mean "${hint}"?` : `"${key}" is not an option here. Accepted: ${accepted}.`;
743
+ return buildProblem({
744
+ spec: CODES.VN3001_UNKNOWN_OPTION,
745
+ span: nodeSpan(args.entry, args.uri),
746
+ title
747
+ });
748
+ }
749
+ /** The closest accepted key, when one is close enough to be worth suggesting. */
750
+ function nearest$1(key, specs) {
751
+ const scored = specs.map((spec) => ({
752
+ name: spec.name,
753
+ distance: distance$1(key, spec.name)
754
+ })).sort((left, right) => left.distance - right.distance)[0];
755
+ return scored && scored.distance <= Math.max(2, Math.floor(key.length / 2)) ? scored.name : void 0;
756
+ }
757
+ /** Levenshtein, small enough to keep here and precise enough for a "did you mean". */
758
+ function distance$1(left, right) {
759
+ let previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
760
+ for (let i = 1; i <= left.length; i += 1) {
761
+ const current = [i];
762
+ for (let j = 1; j <= right.length; j += 1) {
763
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
764
+ current[j] = Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
765
+ }
766
+ previous = current;
767
+ }
768
+ return previous[right.length] ?? 0;
769
+ }
770
+ //#endregion
771
+ //#region src/scheduler/call-params.ts
772
+ /** Zod's word for a type, in the word the language uses for it. */
773
+ const WORDS = {
774
+ object: "map",
775
+ record: "map",
776
+ array: "list",
777
+ boolean: "bool",
778
+ int: "number",
779
+ bigint: "number"
780
+ };
781
+ /**
782
+ * The options map of a call, an action's or a matcher's, validated before the
783
+ * thing it belongs to ever sees it.
784
+ *
785
+ * Unknown keys are refused rather than stripped, because `z.object` drops them
786
+ * in silence and `crypto.hash "x" { algorithmm: "sha512" }` would then hash with
787
+ * the default. A rejected value becomes a `Problem` rather than escaping as a
788
+ * ZodError, whose `message` is a JSON dump of issues.
789
+ *
790
+ * @param args.site Where the call is written, for a failure with no smaller node
791
+ * to point at: a required option nobody typed leaves nothing to underline.
792
+ * @returns The parsed options.
793
+ * @throws ProblemError `VN3010` when a key is unknown or a value is rejected.
794
+ */
795
+ function callParams(args) {
796
+ const { schema, opts, raw, site, uri } = args;
797
+ if (!schema) return raw;
798
+ const unknown = unknownOptions({
799
+ opts,
800
+ params: schema,
801
+ uri
802
+ });
803
+ if (unknown[0]) throw new ProblemError(unknown[0]);
804
+ return parsed({
805
+ schema,
806
+ opts,
807
+ raw,
808
+ site,
809
+ uri
810
+ });
811
+ }
812
+ /** Parse, and turn a rejection into a sentence about the option that failed. */
813
+ function parsed(args) {
814
+ try {
815
+ return args.schema.parse(args.raw);
816
+ } catch (error) {
817
+ const issue = firstIssue(error);
818
+ if (!issue) throw error;
819
+ const { raw, opts, site, uri } = args;
820
+ throw new ProblemError(badOption({
821
+ issue,
822
+ raw,
823
+ opts,
824
+ site,
825
+ uri
826
+ }));
827
+ }
828
+ }
829
+ function badOption(args) {
830
+ const path = args.issue.path ?? [];
831
+ return buildProblem({
832
+ spec: CODES.VN3010_TYPE_MISMATCH,
833
+ span: nodeSpan(entryFor(args.opts, path) ?? args.opts ?? args.site, args.uri),
834
+ title: title({
835
+ key: path.join("."),
836
+ issue: args.issue,
837
+ value: valueAt(args.raw, path)
838
+ })
839
+ });
840
+ }
841
+ /**
842
+ * One line, naming the option and what it wanted. Only the shapes worth
843
+ * modelling get their own sentence; anything else says so plainly rather than
844
+ * dressing up an issue it does not understand.
845
+ */
846
+ function title(args) {
847
+ const { key, issue, value } = args;
848
+ if (!key) return "These options are not valid here.";
849
+ if (issue.code === "invalid_value" && issue.values) return oneOf(key, issue.values, value);
850
+ if (issue.code !== "invalid_type" || !issue.expected) return `"${key}" is not a valid option.`;
851
+ const needs = WORDS[issue.expected] ?? issue.expected;
852
+ if (value === void 0) return `"${key}" is required here, and it takes a ${needs}.`;
853
+ return `"${key}" needs a ${needs}, and ${shown(value)} is a ${typeName(value)}.`;
854
+ }
855
+ /** `{ algorithm: "sha5" }` against a schema that lists the digests it knows. */
856
+ function oneOf(key, values, value) {
857
+ return `"${key}" must be one of ${values.map((option) => shown(option)).join(", ")} — not ${shown(value)}.`;
858
+ }
859
+ /** A value as a message shows it: strings quoted, so `"soon"` reads as text. */
860
+ function shown(value) {
861
+ return typeof value === "string" ? JSON.stringify(value) : display(value);
862
+ }
863
+ function valueAt(raw, path) {
864
+ let value = raw;
865
+ for (const key of path) value = value?.[key];
866
+ return value;
867
+ }
868
+ /** The written entry to underline, when the failure names a key this map spells. */
869
+ function entryFor(opts, path) {
870
+ const key = String(path[0] ?? "");
871
+ return opts?.entries.find((entry) => entry.key === key);
872
+ }
873
+ /** Zod's failure, read structurally: the runtime does not depend on zod itself. */
874
+ function firstIssue(error) {
875
+ const issues = error?.issues;
876
+ return Array.isArray(issues) ? issues[0] : void 0;
877
+ }
878
+ //#endregion
879
+ //#region src/scheduler/declared-arity.ts
880
+ /**
881
+ * How many positional arguments a verb declared.
882
+ *
883
+ * Used to tell an argument that happens to be a map from the options map that
884
+ * configures the call. A variadic verb such as `data.oneOf a b c` takes every
885
+ * argument it is given, so nothing is left over to mistake for configuration.
886
+ */
887
+ function takes(action) {
888
+ if (action.args?.some((each) => each.rest)) return Number.POSITIVE_INFINITY;
889
+ return action.args?.length ?? action.signature?.params.length ?? 0;
890
+ }
891
+ //#endregion
892
+ //#region src/scheduler/local-call.ts
893
+ /**
894
+ * What a dotted statement target names, when it names something in scope.
895
+ *
896
+ * `conn.close()` and `http.get "url"` are written alike and mean different
897
+ * things: one is a method on a value the program holds, the other a verb a
898
+ * plugin contributes. Only the name can tell them apart, and a name the program
899
+ * bound wins. That is the rule the evaluator already follows for `auth` the
900
+ * variable against `auth` the namespace.
901
+ *
902
+ * Returns undefined whenever this is not that: no such name, a namespace, or a
903
+ * path that leads somewhere uncallable. The caller then treats it as a verb,
904
+ * and an unknown verb reports itself as it always did.
905
+ */
906
+ function localCallee(target, scope) {
907
+ const segments = target.split(".");
908
+ const head = segments[0];
909
+ if (!head) return void 0;
910
+ const root = scope.lookup(head);
911
+ if (root === void 0 || isNamespaceValue(root)) return void 0;
912
+ let value = root;
913
+ for (let at = 1; at < segments.length; at += 1) value = memberValue(value, segments[at]);
914
+ return isCallable(value) ? value : void 0;
915
+ }
916
+ //#endregion
917
+ //#region src/scheduler/settled.ts
918
+ /**
919
+ * Whether evaluating an expression produced something still running.
920
+ *
921
+ * Expressions compile synchronously, so a plugin verb called from inside one
922
+ * hands back the promise it is running on rather than its result. Statement
923
+ * position is already asynchronous, so it can wait, and `let id = newId()` binds
924
+ * the id instead of `[object Promise]`.
925
+ *
926
+ * Callers branch on this rather than awaiting unconditionally: `await` on a
927
+ * value that is not a promise still costs a turn of the event loop, and a
928
+ * `forEach` over 50k items reads one expression per iteration.
929
+ */
930
+ function isPending(value) {
931
+ return value instanceof Promise;
932
+ }
933
+ /** Wait for a value only if there is something to wait for. */
934
+ async function settle(value) {
935
+ return isPending(value) ? await value : value;
936
+ }
937
+ //#endregion
938
+ //#region src/scheduler/target.ts
939
+ /**
940
+ * Prelude verbs, callable without `use` (§12). `print` writes to the console,
941
+ * `log` records into the event stream (what a reporter and a test see), and the
942
+ * rest steer the run. Pure prelude *values* (`len`, `range`, `pretty`…) are not
943
+ * here: they live in the root scope, so they work in any expression.
944
+ */
945
+ const PRELUDE = /* @__PURE__ */ new Set([
946
+ "print",
947
+ "log",
948
+ "wait",
949
+ "skip",
950
+ "fail",
951
+ "exit"
952
+ ]);
953
+ /** Split an action target `namespace.action` into parts (no dot ⇒ namespace only). */
954
+ function splitTarget(target) {
955
+ const dot = target.indexOf(".");
956
+ if (dot < 0) return {
957
+ namespace: target,
958
+ name: ""
959
+ };
960
+ return {
961
+ namespace: target.slice(0, dot),
962
+ name: target.slice(dot + 1)
963
+ };
964
+ }
965
+ /** Split a target, mapping a `use "…" as h` alias back to its real namespace. */
966
+ function resolveTarget(target, aliases) {
967
+ const { namespace, name } = splitTarget(target);
968
+ return {
969
+ namespace: aliases.get(namespace) ?? namespace,
970
+ name
971
+ };
972
+ }
973
+ //#endregion
974
+ //#region src/scheduler/run-action.ts
975
+ /**
976
+ * Resolve, invoke and time an action call.
977
+ *
978
+ * @returns Whatever the action produced. The caller names it: there is no
979
+ * implicit `res` for a reader to know about.
980
+ * @throws VennError `VN2003` when no plugin provides the target.
981
+ */
982
+ async function runAction(engine, call, scope) {
983
+ if (PRELUDE.has(call.target)) return runPrelude(engine, call, scope);
984
+ const callee = localCallee(call.target, scope);
985
+ if (callee !== void 0) return callMethod(callee, call, scope);
986
+ const { namespace, name } = resolveTarget(call.target, engine.aliases);
987
+ const resolved = engine.registry.action({
988
+ namespace,
989
+ name
990
+ });
991
+ if (!resolved) throw unknownAction(call.target);
992
+ engine.emitter.emit({
993
+ kind: "action.started",
994
+ data: {
995
+ namespace,
996
+ action: name
997
+ }
998
+ });
999
+ const start = engine.clock.now();
1000
+ const value = await resolved.action.run(engine.ctx, await buildInput({
1001
+ action: resolved.action,
1002
+ call,
1003
+ scope,
1004
+ uri: engine.uri
1005
+ }));
1006
+ const durationMs = engine.clock.now() - start;
1007
+ engine.emitter.emit({
1008
+ kind: "action.finished",
1009
+ data: {
1010
+ namespace,
1011
+ action: name,
1012
+ status: "passed",
1013
+ durationMs
1014
+ }
1015
+ });
1016
+ return value;
1017
+ }
1018
+ /** Call a value the program holds, and wait for it as any other call is waited for. */
1019
+ async function callMethod(callee, call, scope) {
1020
+ return settle(invoke(callee, await Promise.all(call.args.map((expr) => settle(evaluate(expr, scope))))));
1021
+ }
1022
+ /**
1023
+ * An action written as a bare statement: run it, drop what it returned.
1024
+ *
1025
+ * The arguments are normalised here because the two spellings, `conn.close()`
1026
+ * and `http.get "url"`, put them in different places on the node.
1027
+ */
1028
+ async function runActionStatement(engine, call, scope) {
1029
+ await runAction(engine, {
1030
+ ...call,
1031
+ args: callArgs(call)
1032
+ }, scope);
1033
+ }
1034
+ async function buildInput(args) {
1035
+ const split = splitCall(args.call.args, takes(args.action));
1036
+ const opts = args.call.opts ?? split.opts;
1037
+ const positional = await Promise.all(split.args.map((expr) => settle(evaluate(expr, args.scope))));
1038
+ const raw = await settle(opts ? evaluate(opts, args.scope) : {});
1039
+ const schema = args.action.params;
1040
+ return {
1041
+ args: positional,
1042
+ params: callParams({
1043
+ schema,
1044
+ opts,
1045
+ raw,
1046
+ site: siteOf(args.call),
1047
+ uri: args.uri
1048
+ })
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Where the call is written. A call spelled as a statement is its own node; one
1053
+ * rescued out of a `let` carries the statement that spells it, since the
1054
+ * invocation itself is a plain object with no place in the source.
1055
+ */
1056
+ function siteOf(call) {
1057
+ return call.node ?? call;
1058
+ }
1059
+ function unknownAction(target) {
1060
+ return new VennError({
1061
+ code: "VN2003",
1062
+ message: `Unknown action "${target}".`,
1063
+ detail: { target }
1064
+ });
1065
+ }
1066
+ /** Prelude verbs, available without `use` (§12): print, log, wait, skip, fail, exit. */
1067
+ async function runPrelude(engine, call, scope) {
1068
+ const args = await Promise.all(call.args.map((arg) => settle(evaluate(arg, scope))));
1069
+ if (call.target === "print") return printLine(engine, args);
1070
+ if (call.target === "log") return logLine(engine, args);
1071
+ const message = String(args[0] ?? "");
1072
+ if (call.target === "wait") await engine.clock.sleep(waitMs(args[0]));
1073
+ else if (call.target === "skip") skipLog(engine, message);
1074
+ else if (call.target === "exit") throw new ExitSignal(exitCode(args[0]));
1075
+ else if (call.target === "fail") throw failError(message);
1076
+ }
1077
+ /**
1078
+ * `print x y`: the program's own output, on standard output. Distinct from
1079
+ * `log`, which records into the event stream a reporter reads.
1080
+ */
1081
+ function printLine(engine, args) {
1082
+ engine.ctx.port(ConsolePort).write(`${line(args)}\n`);
1083
+ }
1084
+ /** `log a b c`: every argument, spaced, objects shown as JSON not `[object Object]`. */
1085
+ function logLine(engine, args) {
1086
+ engine.emitter.emit({
1087
+ kind: "log",
1088
+ data: {
1089
+ level: "info",
1090
+ message: line(args)
1091
+ }
1092
+ });
1093
+ }
1094
+ function line(args) {
1095
+ return args.map(display).join(" ");
1096
+ }
1097
+ /**
1098
+ * The code `exit` leaves with. Anything the machine cannot use as one still
1099
+ * ended badly, so `exit "boom"` leaves with 1 and never with 0, which would read
1100
+ * as success to whatever is waiting on this process.
1101
+ */
1102
+ function exitCode(value) {
1103
+ const code = Math.trunc(Number(value ?? 0));
1104
+ return Number.isFinite(code) ? code : 1;
1105
+ }
1106
+ function skipLog(engine, message) {
1107
+ engine.emitter.emit({
1108
+ kind: "log",
1109
+ data: {
1110
+ level: "warn",
1111
+ message: `skipped: ${message}`
1112
+ }
1113
+ });
1114
+ }
1115
+ function waitMs(value) {
1116
+ if (typeof value === "number") return value;
1117
+ const duration = value;
1118
+ return duration?.kind === "duration" ? duration.ms ?? 0 : 0;
1119
+ }
1120
+ function failError(message) {
1121
+ return new VennError({
1122
+ code: "VN6002",
1123
+ message: message || "fail"
1124
+ });
1125
+ }
1126
+ //#endregion
1127
+ //#region src/scheduler/run-bindings.ts
1128
+ /**
1129
+ * `let plan = "pro"` binds a value; `let auth = http.post url { … }` binds what
1130
+ * the action returned. Naming the result is what makes it readable: there is no
1131
+ * implicit variable to know about.
1132
+ */
1133
+ function runLet(engine, stmt, scope) {
1134
+ const call = invocationOf(stmt) ?? impliedCall(engine, stmt, scope);
1135
+ if (!call) return bind$1(stmt.name, evaluate(stmt.value, scope), scope);
1136
+ return runAction(engine, call, scope).then((value) => scope.set(stmt.name, value));
1137
+ }
1138
+ /**
1139
+ * `let id = data.faker.uuid` or `let id = data.faker.uuid()`: a dotted path
1140
+ * naming a plugin action is a call, exactly as it would be as a statement.
1141
+ *
1142
+ * A binding in scope always wins, so `let code = res.status` reads a field and
1143
+ * `let parts = name.split(" ")` calls a method. Neither turns into I/O, whatever
1144
+ * the registry happens to hold.
1145
+ */
1146
+ function impliedCall(engine, stmt, scope) {
1147
+ const call = actionCall(stmt.value);
1148
+ const dot = call ? call.target.indexOf(".") : -1;
1149
+ if (!call || dot < 0 || bound(scope, call.target.slice(0, dot))) return void 0;
1150
+ return engine.registry.action(resolveTarget(call.target, engine.aliases)) ? {
1151
+ target: call.target,
1152
+ args: call.args,
1153
+ node: stmt
1154
+ } : void 0;
1155
+ }
1156
+ /** A name the user bound. A plugin namespace in scope is not one, so the verb still calls. */
1157
+ function bound(scope, name) {
1158
+ const value = scope.lookup(name);
1159
+ return value !== void 0 && !isNamespaceValue(value);
1160
+ }
1161
+ /** Bind now, or once the value it is waiting on arrives. */
1162
+ function bind$1(name, value, scope) {
1163
+ if (isPending(value)) return value.then((settled) => scope.set(name, settled));
1164
+ scope.set(name, value);
1165
+ }
1166
+ /** `capture` is removed in favour of `let`; still run, so old files keep working. */
1167
+ function runCapture(stmt, scope) {
1168
+ scope.set(stmt.name, evaluate(stmt.value, scope));
1169
+ }
1170
+ /** `return expr`: unwind to the enclosing fragment with the value. */
1171
+ function runReturn(stmt, scope) {
1172
+ throw new ReturnSignal(stmt.value ? evaluate(stmt.value, scope) : void 0);
1173
+ }
1174
+ //#endregion
1175
+ //#region src/scheduler/run-matcher.ts
1176
+ /** Resolve a bareword matcher from the registry and run it against the subject. */
1177
+ async function evalMatcher(args) {
1178
+ const clause = args.stmt.matcher;
1179
+ const resolved = args.engine.registry.matcher(clause.name);
1180
+ if (!resolved) throw unknownMatcher(clause.name);
1181
+ const input = buildArgs({
1182
+ ...args,
1183
+ resolved,
1184
+ clause
1185
+ });
1186
+ const raw = await resolved.matcher.test(input);
1187
+ if (args.stmt.negate ? !raw : raw) return { passed: true };
1188
+ return failure({
1189
+ matcher: resolved.matcher,
1190
+ input,
1191
+ stmt: args.stmt
1192
+ });
1193
+ }
1194
+ /**
1195
+ * A failing matcher hands back the two sides it compared, labelled with how the
1196
+ * flow spelled the subject. A negated expect gets no diff on purpose: under
1197
+ * `not` the two sides matched, and "expected 200, actual 200" explains nothing.
1198
+ */
1199
+ function failure(args) {
1200
+ const message = args.matcher.message(args.input);
1201
+ const detail = args.stmt.negate ? void 0 : args.matcher.detail?.(args.input);
1202
+ if (!detail) return {
1203
+ passed: false,
1204
+ message
1205
+ };
1206
+ return {
1207
+ passed: false,
1208
+ message,
1209
+ diff: buildDiff({
1210
+ label: subjectLabel(args.stmt),
1211
+ ...detail
1212
+ })
1213
+ };
1214
+ }
1215
+ /** The diff's header: the subject as the flow spelled it, on one line. */
1216
+ function subjectLabel(stmt) {
1217
+ const source = stmt.subject ? nodeSource(stmt.subject).replace(/\s+/g, " ").trim() : "";
1218
+ return source === "" ? "value" : source;
1219
+ }
1220
+ function buildArgs(args) {
1221
+ const { stmt, clause, scope } = args;
1222
+ return {
1223
+ subject: stmt.subject ? evaluate(stmt.subject, scope) : void 0,
1224
+ args: clause.args.map((expr) => evaluate(expr, scope)),
1225
+ params: matcherOptions(args)
1226
+ };
1227
+ }
1228
+ /**
1229
+ * A matcher's options map goes through the same gate an action's does: a key it
1230
+ * never declared is refused rather than dropped, and a value it rejects reads as
1231
+ * one line. Nothing checks these before the run, since `checkOptions` only ever
1232
+ * sees calls, so this is the only place `{ withinn: 1 }` is ever noticed.
1233
+ */
1234
+ function matcherOptions(args) {
1235
+ const { resolved, stmt, clause, scope, engine } = args;
1236
+ const raw = clause.opts ? evaluate(clause.opts, scope) : {};
1237
+ const schema = resolved.matcher.params;
1238
+ return callParams({
1239
+ schema,
1240
+ opts: clause.opts,
1241
+ raw,
1242
+ site: stmt,
1243
+ uri: engine.uri
1244
+ });
1245
+ }
1246
+ function unknownMatcher(name) {
1247
+ return new VennError({
1248
+ code: "VN2004",
1249
+ message: `Unknown matcher "${name}".`,
1250
+ detail: { name }
1251
+ });
1252
+ }
1253
+ //#endregion
1254
+ //#region src/scheduler/run-expect.ts
1255
+ /** Evaluate an `expect` (matcher clause, `.all` checks, negated, or subject) and emit the outcome. */
1256
+ async function runExpect(engine, stmt, scope) {
1257
+ record({
1258
+ engine,
1259
+ stmt,
1260
+ outcome: stmt.matcher ? await evalMatcher({
1261
+ engine,
1262
+ stmt,
1263
+ scope
1264
+ }) : { passed: await evalBoolean({
1265
+ stmt,
1266
+ scope
1267
+ }) }
1268
+ });
1269
+ }
1270
+ async function evalBoolean(args) {
1271
+ return args.stmt.modifier === "all" ? evalChecks(args) : evalSubject(args);
1272
+ }
1273
+ async function evalSubject(args) {
1274
+ if (!args.stmt.subject) return true;
1275
+ const value = truthy(await settle(evaluate(args.stmt.subject, args.scope)));
1276
+ return args.stmt.negate ? !value : value;
1277
+ }
1278
+ async function evalChecks(args) {
1279
+ for (const check of args.stmt.checks) if (!truthy(await settle(evaluate(check, args.scope)))) return false;
1280
+ return true;
1281
+ }
1282
+ function record(args) {
1283
+ if (args.outcome.passed) {
1284
+ args.engine.result.passed += 1;
1285
+ args.engine.emitter.emit({
1286
+ kind: "expect.passed",
1287
+ data: { source: nodeSource(args.stmt) }
1288
+ });
1289
+ return;
1290
+ }
1291
+ args.engine.result.failed += 1;
1292
+ const problem = buildProblem({
1293
+ spec: CODES.VN6001_ASSERTION_FAILED,
1294
+ span: nodeSpan(args.stmt, args.engine.uri),
1295
+ title: args.outcome.message ?? `Expectation failed: ${nodeSource(args.stmt)}`,
1296
+ diff: args.outcome.diff
1297
+ });
1298
+ args.engine.emitter.emit({
1299
+ kind: "expect.failed",
1300
+ data: { problem }
1301
+ });
1302
+ }
1303
+ //#endregion
1304
+ //#region src/scheduler/concurrency.ts
1305
+ /** Run `task` over `items` with at most `limit` in flight at once. */
1306
+ async function runPool(items, limit, task) {
1307
+ const size = Math.max(1, limit);
1308
+ let cursor = 0;
1309
+ const worker = async () => {
1310
+ while (cursor < items.length) {
1311
+ const index = cursor++;
1312
+ await task(items[index], index);
1313
+ }
1314
+ };
1315
+ await Promise.all(Array.from({ length: Math.min(size, items.length) }, worker));
1316
+ }
1317
+ //#endregion
1318
+ //#region src/scheduler/opts.ts
1319
+ /** Read a numeric field from an options map literal (e.g. `{ concurrency: 4 }`). */
1320
+ function optsNumber(opts, key, scope) {
1321
+ const entry = opts?.entries.find((candidate) => candidate.key === key);
1322
+ if (!entry) return void 0;
1323
+ const value = evaluate(entry.value, scope);
1324
+ return typeof value === "number" ? value : void 0;
1325
+ }
1326
+ //#endregion
1327
+ //#region src/scheduler/run-foreach.ts
1328
+ /** `forEach x in list { concurrency: N } { … }`: iterate, up to N in flight. */
1329
+ async function runForEach(engine, stmt, scope) {
1330
+ const source = await settle(evaluate(stmt.source, scope));
1331
+ if (!Array.isArray(source)) throw notAList({
1332
+ engine,
1333
+ stmt,
1334
+ source
1335
+ });
1336
+ const concurrency = optsNumber(stmt.opts, "concurrency", scope) ?? 1;
1337
+ if (concurrency === 1) return sequential(engine, stmt, source, scope);
1338
+ await runPool(source, concurrency, (item) => runIteration({
1339
+ engine,
1340
+ stmt,
1341
+ item,
1342
+ scope
1343
+ }));
1344
+ }
1345
+ /**
1346
+ * Anything but a list is refused, because iterating it zero times would report
1347
+ * success: a test that checked nothing, dressed as one that passed. Built here,
1348
+ * off the iteration path, so the loop itself pays nothing for it.
1349
+ */
1350
+ function notAList(args) {
1351
+ return new ProblemError(buildProblem({
1352
+ spec: CODES.VN3015_NOT_A_LIST,
1353
+ span: nodeSpan(args.stmt.source, args.engine.uri),
1354
+ title: `forEach needs a list, and this is a ${typeName(args.source)}.`,
1355
+ help: helpFor(args.source)
1356
+ }));
1357
+ }
1358
+ /** The everyday cause: an endpoint answering `{ data: [...] }` rather than a list. */
1359
+ function helpFor(source) {
1360
+ if (typeName(source) !== "map") return void 0;
1361
+ return "Name the list inside it, as in `forEach item in res.data`.";
1362
+ }
1363
+ /**
1364
+ * Walk the items one at a time, staying synchronous for as long as the body
1365
+ * does. A body of pure work (a binding, a comparison, arithmetic) never
1366
+ * suspends, so 50k iterations cost no promises at all. The first iteration that
1367
+ * does suspend hands the remainder to {@link resume}, from where it left off.
1368
+ */
1369
+ function sequential(engine, stmt, items, scope) {
1370
+ const plan = planOf$1(stmt.body);
1371
+ const name = stmt.item;
1372
+ if (plan.defers) return slowSequential(engine, stmt, items, scope);
1373
+ const child = scope.child();
1374
+ for (let at = 0; at < items.length; at += 1) try {
1375
+ child.set(name, items[at]);
1376
+ const pending = runSteps(engine, plan.steps, child);
1377
+ if (pending) return resume$1(engine, stmt, items, at + 1, scope, pending);
1378
+ } catch (error) {
1379
+ if (error instanceof BreakSignal) return void 0;
1380
+ if (error instanceof ContinueSignal) continue;
1381
+ throw error;
1382
+ }
1383
+ }
1384
+ function slowSequential(engine, stmt, items, scope) {
1385
+ for (let at = 0; at < items.length; at += 1) try {
1386
+ const pending = iterate(engine, stmt, items[at], scope);
1387
+ if (pending) return resume$1(engine, stmt, items, at + 1, scope, pending);
1388
+ } catch (error) {
1389
+ if (error instanceof BreakSignal) return void 0;
1390
+ if (error instanceof ContinueSignal) continue;
1391
+ throw error;
1392
+ }
1393
+ }
1394
+ function iterate(engine, stmt, item, scope) {
1395
+ const child = scope.child();
1396
+ child.set(stmt.item, item);
1397
+ return runBlock(engine, stmt.body, child);
1398
+ }
1399
+ async function resume$1(engine, stmt, items, from, scope, pending) {
1400
+ try {
1401
+ await pending;
1402
+ } catch (error) {
1403
+ if (error instanceof BreakSignal) return;
1404
+ if (!(error instanceof ContinueSignal)) throw error;
1405
+ }
1406
+ for (let at = from; at < items.length; at += 1) try {
1407
+ await iterate(engine, stmt, items[at], scope);
1408
+ } catch (error) {
1409
+ if (error instanceof BreakSignal) return;
1410
+ if (error instanceof ContinueSignal) continue;
1411
+ throw error;
1412
+ }
1413
+ }
1414
+ async function runIteration(args) {
1415
+ const child = args.scope.child();
1416
+ child.set(args.stmt.item, args.item);
1417
+ try {
1418
+ await runBlock(args.engine, args.stmt.body, child);
1419
+ } catch (error) {
1420
+ if (error instanceof BreakSignal || error instanceof ContinueSignal) return;
1421
+ throw error;
1422
+ }
1423
+ }
1424
+ //#endregion
1425
+ //#region src/scheduler/run-group.ts
1426
+ /** A group only clusters steps in the graph; it shares the enclosing scope. */
1427
+ async function runGroup(engine, stmt, scope) {
1428
+ await runBlock(engine, stmt.body, scope);
1429
+ }
1430
+ //#endregion
1431
+ //#region src/scheduler/run-if.ts
1432
+ /** `if cond { … } else if … else { … }`: evaluate and run the taken branch. */
1433
+ function runIf(engine, stmt, scope) {
1434
+ const cond = evaluate(stmt.cond, scope);
1435
+ if (isPending(cond)) return cond.then((value) => void taken(engine, stmt, scope, truthy(value)));
1436
+ return taken(engine, stmt, scope, truthy(cond));
1437
+ }
1438
+ function taken(engine, stmt, scope, yes) {
1439
+ if (yes) return runBlock(engine, stmt.then, scope.child());
1440
+ const branch = stmt.otherwise;
1441
+ if (!branch) return void 0;
1442
+ if (isIfStmt(branch)) return runIf(engine, branch, scope);
1443
+ return runBlock(engine, branch, scope.child());
1444
+ }
1445
+ //#endregion
1446
+ //#region src/scheduler/branch-engine.ts
1447
+ /**
1448
+ * A per-branch view of the engine carrying the cancellation signal.
1449
+ *
1450
+ * Into `ctx` as well as onto the engine: the statement walker checks the signal
1451
+ * between statements, but a branch parked inside a long action is only reachable
1452
+ * through the context the action was handed. Without it, cancelling a branch
1453
+ * waits for whatever it is already doing to finish on its own.
1454
+ */
1455
+ function branchEngine(engine, signal) {
1456
+ return {
1457
+ ...engine,
1458
+ signal,
1459
+ ctx: {
1460
+ ...engine.ctx,
1461
+ signal
1462
+ }
1463
+ };
1464
+ }
1465
+ //#endregion
1466
+ //#region src/scheduler/opts-text.ts
1467
+ /** Read a string field from an options map literal (e.g. `{ onError: "cancel" }`). */
1468
+ function optsText(opts, key, scope) {
1469
+ const entry = opts?.entries.find((candidate) => candidate.key === key);
1470
+ if (!entry) return void 0;
1471
+ const value = evaluate(entry.value, scope);
1472
+ return typeof value === "string" ? value : void 0;
1473
+ }
1474
+ //#endregion
1475
+ //#region src/scheduler/run-parallel.ts
1476
+ /**
1477
+ * `parallel { concurrency: 2, onError: "collect" } { … }`: run each child
1478
+ * statement concurrently.
1479
+ *
1480
+ * The block does not outlive itself. However it ends, every branch has either
1481
+ * finished or been cancelled by the time this returns: `Promise.all` on its own
1482
+ * rejects at the first failure and leaves the rest running, which is how a
1483
+ * finished test carries on making requests.
1484
+ */
1485
+ async function runParallel(engine, stmt, scope) {
1486
+ const limit = optsNumber(stmt.opts, "concurrency", scope) ?? stmt.body.stmts.length;
1487
+ const onError = optsText(stmt.opts, "onError", scope) ?? "cancel";
1488
+ const controller = new AbortController();
1489
+ const failures = [];
1490
+ await runPool(stmt.body.stmts, Math.max(1, limit), (child) => branch({
1491
+ engine,
1492
+ child,
1493
+ scope,
1494
+ controller,
1495
+ failures,
1496
+ onError
1497
+ }));
1498
+ report(failures);
1499
+ }
1500
+ /**
1501
+ * One branch, and what its failure means for the others.
1502
+ *
1503
+ * `cancel`, the default, stops the siblings at their next statement boundary,
1504
+ * because a run that has already failed should stop working. `collect` lets them
1505
+ * all finish and reports every failure, which is what a set of independent
1506
+ * checks wants.
1507
+ */
1508
+ async function branch(args) {
1509
+ const engine = branchEngine(args.engine, args.controller.signal);
1510
+ try {
1511
+ await runStatement(engine, args.child, args.scope.child());
1512
+ } catch (error) {
1513
+ if (error instanceof CancelSignal) return;
1514
+ if (isControlSignal(error)) throw error;
1515
+ args.failures.push(error);
1516
+ if (args.onError === "cancel") args.controller.abort();
1517
+ }
1518
+ }
1519
+ /**
1520
+ * One failure is raised as itself, so the reader sees the message the branch
1521
+ * produced. Several are raised together rather than one being picked.
1522
+ */
1523
+ function report(failures) {
1524
+ if (failures.length === 0) return;
1525
+ if (failures.length === 1) throw failures[0];
1526
+ throw new AggregateError(failures, `${failures.length} parallel branches failed.`);
1527
+ }
1528
+ //#endregion
1529
+ //#region src/scheduler/run-race.ts
1530
+ /** `race { … }`: the first branch to settle wins, the losers are cancelled. */
1531
+ async function runRace(engine, stmt, scope) {
1532
+ const controller = new AbortController();
1533
+ const branches = stmt.body.stmts.map((child) => Promise.resolve(runStatement(branchEngine(engine, controller.signal), child, scope.child())));
1534
+ try {
1535
+ await Promise.race(branches);
1536
+ } finally {
1537
+ controller.abort();
1538
+ for (const branch of branches) branch.catch(() => {});
1539
+ }
1540
+ }
1541
+ //#endregion
1542
+ //#region src/scheduler/run-repeat.ts
1543
+ /** `repeat N as i { … }`: run the body N times, honouring break/continue. */
1544
+ async function runRepeat(engine, stmt, scope) {
1545
+ const count = toCount(engine, stmt, evaluate(stmt.count, scope));
1546
+ for (let i = 1; i <= count; i++) {
1547
+ const child = scope.child();
1548
+ if (stmt.index) child.set(stmt.index, i);
1549
+ try {
1550
+ const pending = runBlock(engine, stmt.body, child);
1551
+ if (pending) await pending;
1552
+ } catch (error) {
1553
+ if (error instanceof BreakSignal) break;
1554
+ if (error instanceof ContinueSignal) continue;
1555
+ throw error;
1556
+ }
1557
+ }
1558
+ }
1559
+ /**
1560
+ * A count the machine cannot read as one is refused, because running the body
1561
+ * zero times would report success: `repeat cfg.times` with nothing behind
1562
+ * `times` would check nothing and pass.
1563
+ *
1564
+ * Zero and below still mean "not at all": a bound that computes to none is a
1565
+ * program saying so, not a mistake.
1566
+ */
1567
+ function toCount(engine, stmt, value) {
1568
+ if (typeof value !== "number" || !Number.isFinite(value)) throw notANumber(engine, stmt, value);
1569
+ return value > 0 ? Math.floor(value) : 0;
1570
+ }
1571
+ function notANumber(engine, stmt, value) {
1572
+ return new ProblemError(buildProblem({
1573
+ spec: CODES.VN3016_NOT_A_NUMBER,
1574
+ span: nodeSpan(stmt.count, engine.uri),
1575
+ title: `repeat needs a number of times, and this is a ${typeName(value)}.`,
1576
+ help: "Give it a count, as in `repeat 3 { … }`."
1577
+ }));
1578
+ }
1579
+ //#endregion
1580
+ //#region src/scheduler/run-run.ts
1581
+ /** `run fragment(args) as x`: invoke a fragment and bind its return value. */
1582
+ async function runRun(engine, stmt, scope) {
1583
+ const fragment = engine.fragments.get(stmt.target);
1584
+ if (!fragment) throw unknownFragment(stmt.target);
1585
+ const args = (stmt.args?.args ?? []).map((arg) => evaluate(arg.value, scope));
1586
+ const fragScope = createScope();
1587
+ bindParams(fragScope, fragment.params, args);
1588
+ const value = await runFragment(engine, fragment, fragScope);
1589
+ if (stmt.bind) scope.set(stmt.bind, value);
1590
+ }
1591
+ async function runFragment(engine, fragment, scope) {
1592
+ try {
1593
+ await runBlock(engine, fragment.body, scope);
1594
+ return;
1595
+ } catch (error) {
1596
+ if (error instanceof ReturnSignal) return error.value;
1597
+ throw error;
1598
+ }
1599
+ }
1600
+ function bindParams(scope, params, args) {
1601
+ (params?.params ?? []).forEach((param, index) => {
1602
+ scope.set(param.name, args[index]);
1603
+ });
1604
+ }
1605
+ function unknownFragment(name) {
1606
+ return new VennError({
1607
+ code: "VN2005",
1608
+ message: `Unknown fragment "${name}".`,
1609
+ detail: { fragment: name }
1610
+ });
1611
+ }
1612
+ //#endregion
1613
+ //#region src/scheduler/run-step.ts
1614
+ /** Run a step honouring @skip/@only, @lock, @timeout and @retry; its bindings are step-local. */
1615
+ async function runStep(engine, step, parent) {
1616
+ if (!included(engine, step)) return;
1617
+ engine.emitter.emit({
1618
+ kind: "step.started",
1619
+ data: { title: step.title }
1620
+ });
1621
+ const before = engine.result.failed;
1622
+ const scope = parent.child();
1623
+ try {
1624
+ await execute(engine, step, {
1625
+ parent,
1626
+ scope
1627
+ });
1628
+ } catch (error) {
1629
+ if (!isControlSignal(error)) finished(engine, step.title, "failed");
1630
+ throw error;
1631
+ }
1632
+ recordFlaky(engine, step, before);
1633
+ finished(engine, step.title, engine.result.failed > before ? "failed" : "passed");
1634
+ }
1635
+ /**
1636
+ * The gate `selectFlows` puts on a flow, put on a step: `@only` focuses, `@skip`
1637
+ * drops, and only then the `--step` title filter.
1638
+ *
1639
+ * A step is focused among the steps it stands with, meaning the block it was
1640
+ * written in, the way a flow is focused among the flows of its document.
1641
+ */
1642
+ function included(engine, step) {
1643
+ if (hasAnnotation(step, "skip")) return false;
1644
+ if (focusing(step.$container) && !hasAnnotation(step, "only")) return false;
1645
+ return matchesTitle(step.title, engine.filter.step);
1646
+ }
1647
+ /** Whether any step in this block asked to be the only one that runs. */
1648
+ const focused = /* @__PURE__ */ new WeakMap();
1649
+ /**
1650
+ * Answered once per block: the source cannot change mid-run, and a step inside a
1651
+ * `forEach` would otherwise re-read its siblings on every pass.
1652
+ */
1653
+ function focusing(container) {
1654
+ if (!isBlock(container)) return false;
1655
+ const known = focused.get(container);
1656
+ if (known !== void 0) return known;
1657
+ const found = container.stmts.some((stmt) => isStepDecl(stmt) && hasAnnotation(stmt, "only"));
1658
+ focused.set(container, found);
1659
+ return found;
1660
+ }
1661
+ function execute(engine, step, scopes) {
1662
+ return withLock(engine, readLock(step), () => runWithAnnotations({
1663
+ engine,
1664
+ node: step,
1665
+ scope: scopes.parent,
1666
+ title: step.title,
1667
+ run: () => runAround(step, () => runBlock(engine, step.body, scopes.scope))
1668
+ }));
1669
+ }
1670
+ function finished(engine, title, status) {
1671
+ engine.emitter.emit({
1672
+ kind: "step.finished",
1673
+ data: {
1674
+ title,
1675
+ status
1676
+ }
1677
+ });
1678
+ }
1679
+ //#endregion
1680
+ //#region src/scheduler/run-try.ts
1681
+ /** `try { } catch e { } finally { }`: control signals pass through, errors go to catch. */
1682
+ async function runTry(engine, stmt, scope) {
1683
+ try {
1684
+ await runBlock(engine, stmt.body, scope.child());
1685
+ } catch (error) {
1686
+ if (isControlSignal(error)) throw error;
1687
+ await runCatch({
1688
+ engine,
1689
+ stmt,
1690
+ scope,
1691
+ error
1692
+ });
1693
+ } finally {
1694
+ if (stmt.finalizer) await runBlock(engine, stmt.finalizer, scope.child());
1695
+ }
1696
+ }
1697
+ async function runCatch(args) {
1698
+ if (!args.stmt.handler) return;
1699
+ const child = args.scope.child();
1700
+ if (args.stmt.error) child.set(args.stmt.error, toErrorValue(args.error));
1701
+ await runBlock(args.engine, args.stmt.handler, child);
1702
+ }
1703
+ function toErrorValue(error) {
1704
+ const e = error;
1705
+ return {
1706
+ message: e?.message ?? String(error),
1707
+ code: e?.code ?? "VN7000"
1708
+ };
1709
+ }
1710
+ //#endregion
1711
+ //#region src/scheduler/run-while.ts
1712
+ /**
1713
+ * Every `while` has an inherited timeout in the language; this is the hard cap
1714
+ * that stands in for it until timeouts are wired.
1715
+ */
1716
+ const MAX_ITERATIONS = 1e5;
1717
+ /** `while cond { … }`: loop while the condition is truthy. */
1718
+ async function runWhile(engine, stmt, scope) {
1719
+ let guard = 0;
1720
+ while (truthy(await settle(evaluate(stmt.cond, scope)))) {
1721
+ if (++guard > MAX_ITERATIONS) throw neverFinished(engine, stmt);
1722
+ try {
1723
+ await runBlock(engine, stmt.body, scope.child());
1724
+ } catch (error) {
1725
+ if (error instanceof BreakSignal) break;
1726
+ if (error instanceof ContinueSignal) continue;
1727
+ throw error;
1728
+ }
1729
+ }
1730
+ }
1731
+ /**
1732
+ * Reaching the cap is not the loop finishing, so it is raised rather than
1733
+ * returned: stopping at the limit in silence would report a pass.
1734
+ */
1735
+ function neverFinished(engine, stmt) {
1736
+ return new ProblemError(buildProblem({
1737
+ spec: CODES.VN8002_LOOP_LIMIT,
1738
+ span: nodeSpan(stmt, engine.uri),
1739
+ title: `This while loop ran ${MAX_ITERATIONS} times and its condition was still true.`,
1740
+ help: "Change something the condition reads inside the loop, or use `repeat n` to bound it."
1741
+ }));
1742
+ }
1743
+ //#endregion
1744
+ //#region src/scheduler/run-statements.ts
1745
+ /**
1746
+ * Dispatch one statement to its handler. This is the single boundary where a
1747
+ * cancelled `race` branch stops advancing.
1748
+ *
1749
+ * Switched on `$type`, not on the `isXxx` guards: each guard asks Langium's
1750
+ * reflection whether one type is a subtype of another, and a chain of them costs
1751
+ * a double-digit share of a large `forEach`. `$type` is a plain string every
1752
+ * node carries, so one switch answers what thirteen questions would.
1753
+ */
1754
+ function runStatement(engine, stmt, scope) {
1755
+ if (engine.signal?.aborted) throw new CancelSignal();
1756
+ switch (stmt.$type) {
1757
+ case "LetStmt": return runLet(engine, stmt, scope);
1758
+ case "ActionCall": return runActionStatement(engine, stmt, scope);
1759
+ case "ExpectStmt": return runExpect(engine, stmt, scope);
1760
+ case "IfStmt": return runIf(engine, stmt, scope);
1761
+ case "StepDecl": return runStep(engine, stmt, scope);
1762
+ case "GroupDecl": return runGroup(engine, stmt, scope);
1763
+ default: return control(engine, stmt, scope);
1764
+ }
1765
+ }
1766
+ /** Loops, concurrency and the control-flow signals: the rarer half. */
1767
+ function control(engine, stmt, scope) {
1768
+ switch (stmt.$type) {
1769
+ case "ForEachStmt": return runForEach(engine, stmt, scope);
1770
+ case "RepeatStmt": return runRepeat(engine, stmt, scope);
1771
+ case "WhileStmt": return runWhile(engine, stmt, scope);
1772
+ case "ParallelStmt": return runParallel(engine, stmt, scope);
1773
+ case "RaceStmt": return runRace(engine, stmt, scope);
1774
+ case "TryStmt": return runTry(engine, stmt, scope);
1775
+ case "RunStmt": return runRun(engine, stmt, scope);
1776
+ default: return leaf(stmt, scope);
1777
+ }
1778
+ }
1779
+ function leaf(stmt, scope) {
1780
+ if (stmt.$type === "CaptureStmt") return runCapture(stmt, scope);
1781
+ if (stmt.$type === "ReturnStmt") return runReturn(stmt, scope);
1782
+ if (stmt.$type === "BreakStmt") throw new BreakSignal();
1783
+ if (stmt.$type === "ContinueStmt") throw new ContinueSignal();
1784
+ }
1785
+ //#endregion
1786
+ //#region src/scheduler/block-plan.ts
1787
+ const plans = /* @__PURE__ */ new WeakMap();
1788
+ /** The plan for a block, built on first use and cached against the node itself. */
1789
+ function planOf$1(block) {
1790
+ const known = plans.get(block);
1791
+ if (known) return known;
1792
+ const built = {
1793
+ steps: block.stmts.map(stepOf),
1794
+ defers: block.stmts.some(isDefer$2)
1795
+ };
1796
+ plans.set(block, built);
1797
+ return built;
1798
+ }
1799
+ function isDefer$2(stmt) {
1800
+ return isLifecycleDecl(stmt) && stmt.hook === "defer";
1801
+ }
1802
+ function stepOf(stmt) {
1803
+ return plainLet(stmt) ?? ((engine, scope) => runStatement(engine, stmt, scope));
1804
+ }
1805
+ /**
1806
+ * `const y = x * 2`: no trailing args, no options map, and a value that is not a
1807
+ * dotted path, so no registry lookup can turn it into a verb. The name, the
1808
+ * compiled expression and the decision are all fixed by the source.
1809
+ */
1810
+ function plainLet(stmt) {
1811
+ if (!isLetStmt(stmt)) return void 0;
1812
+ const let_ = stmt;
1813
+ if (let_.args.length > 0 || let_.opts) return void 0;
1814
+ const call = actionCall(let_.value);
1815
+ if (call && call.target.indexOf(".") >= 0) return void 0;
1816
+ const name = let_.name;
1817
+ const thunk = compileExpr(let_.value);
1818
+ return (_engine, scope) => bind(name, thunk(scope), scope);
1819
+ }
1820
+ function bind(name, value, scope) {
1821
+ if (isPending(value)) return value.then((settled) => scope.set(name, settled));
1822
+ scope.set(name, value);
1823
+ }
1824
+ //#endregion
1825
+ //#region src/scheduler/run-block.ts
1826
+ /**
1827
+ * Run a block's statements in order, running any `defer { … }` bodies LIFO on
1828
+ * the way out, including on error or a control-flow signal.
1829
+ */
1830
+ function runBlock(engine, block, scope) {
1831
+ const plan = planOf$1(block);
1832
+ if (plan.defers) return withDefers(engine, block, scope);
1833
+ return runSteps(engine, plan.steps, scope);
1834
+ }
1835
+ /** The walk over a plan, reusable by a loop that hoists `planOf` out of it. */
1836
+ function runSteps(engine, steps, scope) {
1837
+ for (let at = 0; at < steps.length; at += 1) {
1838
+ if (engine.signal?.aborted) throw new CancelSignal();
1839
+ const pending = steps[at](engine, scope);
1840
+ if (pending) return resume(engine, steps, at + 1, scope, pending);
1841
+ }
1842
+ }
1843
+ async function resume(engine, steps, from, scope, pending) {
1844
+ await pending;
1845
+ for (let at = from; at < steps.length; at += 1) {
1846
+ if (engine.signal?.aborted) throw new CancelSignal();
1847
+ const next = steps[at](engine, scope);
1848
+ if (next) await next;
1849
+ }
1850
+ }
1851
+ async function withDefers(engine, block, scope) {
1852
+ const defers = [];
1853
+ try {
1854
+ for (const stmt of block.stmts) {
1855
+ if (isDefer$1(stmt)) {
1856
+ defers.unshift(stmt.body);
1857
+ continue;
1858
+ }
1859
+ const pending = runStatement(engine, stmt, scope);
1860
+ if (pending) await pending;
1861
+ }
1862
+ } finally {
1863
+ const cleanup = {
1864
+ ...engine,
1865
+ signal: void 0
1866
+ };
1867
+ for (const body of defers) await runBlock(cleanup, body, scope.child());
1868
+ }
1869
+ }
1870
+ function isDefer$1(stmt) {
1871
+ return isLifecycleDecl(stmt) && stmt.hook === "defer";
1872
+ }
1873
+ //#endregion
1874
+ //#region src/scheduler/run-lifecycle.ts
1875
+ /**
1876
+ * Run a set of lifecycle blocks (setup/teardown/beforeEach/afterEach) in order.
1877
+ *
1878
+ * The caller passes the scope the hooks belong to (the suite root for
1879
+ * setup/teardown, the flow's own scope for beforeEach/afterEach) and each body
1880
+ * runs in a child of it, the way the `on` handlers below do. In a parentless
1881
+ * scope `env`, the prelude, the top-level bindings and the open resources would
1882
+ * all read as undefined and the hook would run anyway: a teardown written as
1883
+ * `db.exec "DELETE FROM orders WHERE run = '${env.RUN_ID}'"` deletes the table
1884
+ * it was meant to tidy.
1885
+ */
1886
+ async function runHooks(args) {
1887
+ for (const hook of args.hooks) await runHook(args.engine, hook, args.scope);
1888
+ }
1889
+ /**
1890
+ * Run the `on <event>` handlers whose event matches (e.g. "failure"/"success").
1891
+ *
1892
+ * Through the same boundary as the hooks above: a handler that reacts to a
1893
+ * failure by failing itself is a second failure, not the end of the run.
1894
+ */
1895
+ async function runOnHandlers(args) {
1896
+ for (const handler of args.handlers) if (handler.event === args.event) await runHook(args.engine, handler, args.scope);
1897
+ }
1898
+ /**
1899
+ * A hook that throws fails the suite instead of ending the run: it is counted
1900
+ * and reported, and the walk carries on to `run.finished`, so the reporters
1901
+ * still close their files and the files after this one still run.
1902
+ */
1903
+ async function runHook(engine, hook, scope) {
1904
+ try {
1905
+ await runBlock(engine, hook.body, scope.child());
1906
+ } catch (error) {
1907
+ if (error instanceof ExitSignal) throw error;
1908
+ if (!isControlSignal(error)) recordFailure({
1909
+ engine,
1910
+ hook,
1911
+ error
1912
+ });
1913
+ }
1914
+ }
1915
+ /**
1916
+ * Counted where a failing flow is counted, and carried on the event that already
1917
+ * puts a Problem in front of every reporter. A hook failure the reporters never
1918
+ * hear about ends as a green artifact over a broken run.
1919
+ */
1920
+ function recordFailure(args) {
1921
+ args.engine.result.failed += 1;
1922
+ args.engine.emitter.emit({
1923
+ kind: "expect.failed",
1924
+ data: { problem: hookProblem(args) }
1925
+ });
1926
+ }
1927
+ function hookProblem(args) {
1928
+ return buildProblem({
1929
+ spec: CODES.VN7004_HOOK_FAILED,
1930
+ span: nodeSpan(args.hook, args.engine.uri),
1931
+ title: `${hookLabel(args.hook)} failed: ${messageOf(args.error)}`
1932
+ });
1933
+ }
1934
+ /** The block named the way the user wrote it: `setup`, or `on failure`. */
1935
+ function hookLabel(hook) {
1936
+ return hook.hook ?? (hook.event ? `on ${hook.event}` : "lifecycle");
1937
+ }
1938
+ function messageOf(error) {
1939
+ return error?.message ?? String(error);
1940
+ }
1941
+ //#endregion
1942
+ //#region src/scheduler/run-flow.ts
1943
+ /** Run a flow: body, then its `on failure`/`on success` handlers, then finish. */
1944
+ async function runFlow(engine, flow, scope) {
1945
+ engine.emitter.emit({
1946
+ kind: "flow.started",
1947
+ data: { title: flow.title }
1948
+ });
1949
+ const before = engine.result.failed;
1950
+ const handlers = flow.body.stmts.filter(isOnHandler);
1951
+ await runFlowBody(engine, flow, scope);
1952
+ recordFlaky(engine, flow, before);
1953
+ const status = engine.result.failed > before ? "failed" : "passed";
1954
+ await runOnHandlers({
1955
+ engine,
1956
+ handlers,
1957
+ event: status === "failed" ? "failure" : "success",
1958
+ scope
1959
+ });
1960
+ engine.emitter.emit({
1961
+ kind: "flow.finished",
1962
+ data: {
1963
+ title: flow.title,
1964
+ status
1965
+ }
1966
+ });
1967
+ }
1968
+ async function runFlowBody(engine, flow, scope) {
1969
+ try {
1970
+ await withLock(engine, flowLock(flow), () => runWithAnnotations({
1971
+ engine,
1972
+ node: flow,
1973
+ scope,
1974
+ title: flow.title,
1975
+ run: () => runAround(flow, () => runBlock(engine, flow.body, scope))
1976
+ }));
1977
+ } catch (error) {
1978
+ if (error instanceof ExitSignal) throw error;
1979
+ if (!isControlSignal(error)) recordError(engine, error);
1980
+ }
1981
+ }
1982
+ /** `@lock("x")` uses a named mutex; `@serial` serialises runs of the same flow. */
1983
+ function flowLock(flow) {
1984
+ return readLock(flow) ?? (hasAnnotation(flow, "serial") ? `serial:${flow.title}` : void 0);
1985
+ }
1986
+ function isOnHandler(stmt) {
1987
+ return isLifecycleDecl(stmt) && Boolean(stmt.event);
1988
+ }
1989
+ /**
1990
+ * A failure the flow itself could not handle.
1991
+ *
1992
+ * One that already knows what it is (a Problem, with its code, its span and its
1993
+ * help) travels on the event every reporter reads for failures. Flattening it to
1994
+ * a log line would strip the code and the location the raiser worked out.
1995
+ */
1996
+ function recordError(engine, error) {
1997
+ engine.result.failed += 1;
1998
+ if (error instanceof ProblemError) {
1999
+ engine.emitter.emit({
2000
+ kind: "expect.failed",
2001
+ data: { problem: error.problem }
2002
+ });
2003
+ return;
2004
+ }
2005
+ const message = error?.message ?? String(error);
2006
+ engine.emitter.emit({
2007
+ kind: "log",
2008
+ data: {
2009
+ level: "error",
2010
+ message
2011
+ }
2012
+ });
2013
+ }
2014
+ //#endregion
2015
+ //#region src/scheduler/run-prologue.ts
2016
+ /**
2017
+ * A document's top-level statements, run once, in the order they are written.
2018
+ *
2019
+ * The same lines mean the same thing in both modes: a script *is* its prologue,
2020
+ * and a test file runs one before its flows. So a `const` calling a verb at the
2021
+ * top of a file opens the thing either way, and the flows never find `null`.
2022
+ *
2023
+ * @param args.into Where deferred cleanups accumulate, in written order. A
2024
+ * program's ending is registered before its statements run, so it has to see
2025
+ * what they defer as they reach it rather than afterwards.
2026
+ * @returns The teardowns collected, which is `into` when one was supplied.
2027
+ */
2028
+ async function runPrologue(args) {
2029
+ const teardowns = args.into ?? [];
2030
+ for (const node of args.doc.decls) if (isDefer(node)) teardowns.push(deferred({
2031
+ ...args,
2032
+ body: node.body
2033
+ }));
2034
+ else if (isRunnable(node)) await runStatement(args.engine, node, args.scope);
2035
+ return teardowns;
2036
+ }
2037
+ /** `defer { … }` written at the top of a file: registered here, run on the way out. */
2038
+ function isDefer(node) {
2039
+ return isLifecycleDecl(node) && node.hook === "defer";
2040
+ }
2041
+ function deferred(args) {
2042
+ const engine = {
2043
+ ...args.engine,
2044
+ signal: void 0
2045
+ };
2046
+ return async () => {
2047
+ await runBlock(engine, args.body, args.scope.child());
2048
+ };
2049
+ }
2050
+ /** Undo in reverse: what opened last is given back first. */
2051
+ async function runTeardowns(teardowns) {
2052
+ for (const teardown of [...teardowns].reverse()) await teardown();
2053
+ }
2054
+ //#endregion
2055
+ //#region src/scheduler/run-document.ts
2056
+ /**
2057
+ * Walk a document: setup once, then for each `matrix` variant bind `env`/`matrix`
2058
+ * globals, open resources, run flows (with before/afterEach), close resources.
2059
+ */
2060
+ async function runDocument(engine, doc) {
2061
+ const flows = selectFlows(engine, doc.decls.filter(isFlowDecl));
2062
+ const hooks = collectHooks(doc);
2063
+ engine.emitter.emit({
2064
+ kind: "run.started",
2065
+ data: { plan: planOf(flows) }
2066
+ });
2067
+ const start = engine.clock.now();
2068
+ await runSuite({
2069
+ engine,
2070
+ doc,
2071
+ flows,
2072
+ hooks
2073
+ });
2074
+ settleFlaky(engine);
2075
+ const durationMs = engine.clock.now() - start;
2076
+ engine.emitter.emit({
2077
+ kind: "run.finished",
2078
+ data: {
2079
+ ...engine.result,
2080
+ durationMs
2081
+ }
2082
+ });
2083
+ }
2084
+ /**
2085
+ * Suite hooks run once, on either side of every variant, so they get a root of
2086
+ * their own: the document's bindings without any one variant's `matrix`.
2087
+ *
2088
+ * An `exit` in `setup` ends the run where it stands, so the variants never
2089
+ * start, and `teardown` still runs, because what `setup` opened is still open.
2090
+ * Both sides absorb their own, so the code reaches the host and `run.finished`
2091
+ * is still emitted above.
2092
+ */
2093
+ async function runSuite(args) {
2094
+ const { engine, hooks } = args;
2095
+ const suite = rootScope({
2096
+ engine,
2097
+ doc: args.doc,
2098
+ variant: {}
2099
+ });
2100
+ await absorbExit(engine, async () => {
2101
+ await runHooks({
2102
+ engine,
2103
+ hooks: hooks.setup,
2104
+ scope: suite
2105
+ });
2106
+ await runVariants(args);
2107
+ });
2108
+ await absorbExit(engine, () => runHooks({
2109
+ engine,
2110
+ hooks: hooks.teardown,
2111
+ scope: suite
2112
+ }));
2113
+ }
2114
+ /** Every `matrix` variant is a full pass over the flows, one after the other. */
2115
+ async function runVariants(args) {
2116
+ for (const variant of matrixVariants(args.engine, args.doc)) await runVariant({
2117
+ ...args,
2118
+ variant
2119
+ });
2120
+ }
2121
+ async function runVariant(args) {
2122
+ const root = rootScope({
2123
+ engine: args.engine,
2124
+ doc: args.doc,
2125
+ variant: args.variant
2126
+ });
2127
+ const teardowns = await runPrologue({
2128
+ engine: args.engine,
2129
+ doc: args.doc,
2130
+ scope: root
2131
+ });
2132
+ try {
2133
+ await runFlows({
2134
+ ...args,
2135
+ root
2136
+ });
2137
+ } finally {
2138
+ await runTeardowns(teardowns);
2139
+ }
2140
+ }
2141
+ async function runFlows(args) {
2142
+ for (const flow of args.flows) {
2143
+ await runFlowWithHooks({
2144
+ engine: args.engine,
2145
+ flow,
2146
+ hooks: args.hooks,
2147
+ root: args.root
2148
+ });
2149
+ if (args.engine.bail && args.engine.result.failed > 0) break;
2150
+ }
2151
+ }
2152
+ async function runFlowWithHooks(args) {
2153
+ const scope = args.root.child();
2154
+ const each = {
2155
+ engine: args.engine,
2156
+ scope
2157
+ };
2158
+ await runHooks({
2159
+ ...each,
2160
+ hooks: args.hooks.beforeEach
2161
+ });
2162
+ await runFlow(args.engine, args.flow, scope);
2163
+ await runHooks({
2164
+ ...each,
2165
+ hooks: args.hooks.afterEach
2166
+ });
2167
+ }
2168
+ /**
2169
+ * The scope a run reads from: the prelude, the plugin namespaces, `env`, the
2170
+ * matrix variant and the document's own globals. One per variant, so what one
2171
+ * variant binds is never visible to the next.
2172
+ */
2173
+ function rootScope(args) {
2174
+ const base = () => createBaseScope({
2175
+ engine: args.engine,
2176
+ variant: args.variant
2177
+ });
2178
+ const root = base();
2179
+ const graph = args.engine.imports;
2180
+ if (graph) bindImports({
2181
+ document: args.doc,
2182
+ uri: args.engine.uri,
2183
+ scope: root,
2184
+ graph,
2185
+ base
2186
+ });
2187
+ bindGlobals(args.doc, root);
2188
+ return root;
2189
+ }
2190
+ /** The cartesian product of the `matrix { … }` dimensions, else one empty variant. */
2191
+ function matrixVariants(engine, doc) {
2192
+ const matrix = doc.decls.find(isMatrixDecl);
2193
+ if (!matrix) return [{}];
2194
+ const scope = createScope();
2195
+ scope.set("env", engine.env);
2196
+ return product(evaluate(matrix.body, scope));
2197
+ }
2198
+ function product(dims) {
2199
+ let combos = [{}];
2200
+ for (const [key, values] of Object.entries(dims)) {
2201
+ const list = Array.isArray(values) ? values : [values];
2202
+ combos = combos.flatMap((combo) => list.map((value) => ({
2203
+ ...combo,
2204
+ [key]: value
2205
+ })));
2206
+ }
2207
+ return combos;
2208
+ }
2209
+ /** `@only` focuses, `@skip` drops, then the runner's own `--flow` / `--tags` filters. */
2210
+ function selectFlows(engine, flows) {
2211
+ const only = flows.filter((flow) => hasAnnotation(flow, "only"));
2212
+ return byTags((only.length > 0 ? only : flows).filter((flow) => !hasAnnotation(flow, "skip")).filter((flow) => matchesTitle(flow.title, engine.filter.flow)), engine.filter.tags);
2213
+ }
2214
+ function byTags(flows, tags) {
2215
+ if (!tags || tags.length === 0) return flows;
2216
+ return flows.filter((flow) => readTags(flow).some((tag) => tags.includes(tag)));
2217
+ }
2218
+ function planOf(flows) {
2219
+ return { flows: flows.map((flow) => ({
2220
+ title: flow.title,
2221
+ steps: stepTitles(flow)
2222
+ })) };
2223
+ }
2224
+ function stepTitles(flow) {
2225
+ return flow.body.stmts.filter(isStepDecl).map((step) => ({ title: step.title }));
2226
+ }
2227
+ //#endregion
2228
+ //#region src/scheduler/script-lifecycle.ts
2229
+ /**
2230
+ * The program's `setup`, run before its first statement.
2231
+ *
2232
+ * It reads what the file declared, its functions, rather than what its
2233
+ * statements bind, because none of them has run yet. A `setup` that wants a
2234
+ * connection opens one.
2235
+ */
2236
+ async function runSetup(args) {
2237
+ await runHooks({
2238
+ engine: args.engine,
2239
+ hooks: collectHooks(args.doc).setup,
2240
+ scope: args.scope
2241
+ });
2242
+ }
2243
+ /**
2244
+ * Say how the program ends, before it starts.
2245
+ *
2246
+ * Registered up front because the ending has to be in place for an interrupt
2247
+ * that arrives mid-statement, while what it runs is decided later, as the
2248
+ * statements defer things. One entry rather than one per `defer`, so the order
2249
+ * is stated here instead of falling out of a stack: `teardown` first, while the
2250
+ * connections it needs are still open, then the deferred work in reverse.
2251
+ */
2252
+ function registerEnding(args) {
2253
+ const ending = { deferred: [] };
2254
+ const teardown = collectHooks(args.doc).teardown;
2255
+ args.engine.cleanup.add(async () => {
2256
+ await runHooks({
2257
+ engine: args.engine,
2258
+ hooks: teardown,
2259
+ scope: args.scope
2260
+ });
2261
+ await runTeardowns(ending.deferred);
2262
+ });
2263
+ return ending;
2264
+ }
2265
+ //#endregion
2266
+ //#region src/scheduler/run-script.ts
2267
+ /**
2268
+ * Run a document as a program: its top-level statements, in order, top to
2269
+ * bottom, because the file itself is the entry point. Declarations (`flow`,
2270
+ * `fn`, `type`, …) are definitions and run only when something calls them.
2271
+ *
2272
+ * `setup`, `teardown` and `defer` are the program's own lifetime: `setup` runs
2273
+ * before the first statement and the rest are handed to the host to run on the
2274
+ * way out, because a program that serves outlives its last line.
2275
+ */
2276
+ async function runScript(engine, doc) {
2277
+ const base = () => createBaseScope({ engine });
2278
+ const root = base();
2279
+ const graph = engine.imports;
2280
+ if (graph) bindImports({
2281
+ document: doc,
2282
+ uri: engine.uri,
2283
+ scope: root,
2284
+ graph,
2285
+ base
2286
+ });
2287
+ bindFunctions(doc, root);
2288
+ engine.emitter.emit({
2289
+ kind: "run.started",
2290
+ data: { plan: { flows: [] } }
2291
+ });
2292
+ const start = engine.clock.now();
2293
+ await absorbExit(engine, () => runProgram(engine, doc, root));
2294
+ const durationMs = engine.clock.now() - start;
2295
+ engine.emitter.emit({
2296
+ kind: "run.finished",
2297
+ data: {
2298
+ ...engine.result,
2299
+ durationMs
2300
+ }
2301
+ });
2302
+ }
2303
+ /**
2304
+ * The program's lifetime: its `setup`, the ending it declared, then its
2305
+ * statements, all inside the one absorb, because `setup` is as much the program
2306
+ * as its first line. Outside it, an `exit` in `setup` would escape past
2307
+ * `run.finished` and leave the stream open.
2308
+ *
2309
+ * The ending is registered whatever happens to `setup`: a setup that exited or
2310
+ * failed still opened what it opened, and `teardown` is how it gives it back.
2311
+ */
2312
+ async function runProgram(engine, doc, scope) {
2313
+ const ending = registerEnding({
2314
+ engine,
2315
+ doc,
2316
+ scope
2317
+ });
2318
+ await runSetup({
2319
+ engine,
2320
+ doc,
2321
+ scope
2322
+ });
2323
+ await runPrologue({
2324
+ engine,
2325
+ doc,
2326
+ scope,
2327
+ into: ending.deferred
2328
+ });
2329
+ }
2330
+ //#endregion
2331
+ //#region src/check/check-options.ts
2332
+ /**
2333
+ * Check the keys of an options map against the action's own schema.
2334
+ *
2335
+ * The runtime refuses an unknown key too, but only when the flow reaches that
2336
+ * line. Both read the same list and say the same sentence, so the editor can
2337
+ * mark the word before anything runs.
2338
+ */
2339
+ function checkOptions(args) {
2340
+ return unknownOptions({
2341
+ opts: args.opts,
2342
+ params: args.params,
2343
+ uri: args.ctx.uri
2344
+ });
2345
+ }
2346
+ //#endregion
2347
+ //#region src/check/check-calls.ts
2348
+ /** `http.get "…"` written as a statement. */
2349
+ function checkAction(call, ctx) {
2350
+ return checkTarget({
2351
+ node: call,
2352
+ target: call.target,
2353
+ opts: call.opts,
2354
+ ctx
2355
+ });
2356
+ }
2357
+ /**
2358
+ * `let auth = http.post url { … }`. Only the unmistakable call is checked: with
2359
+ * no arguments the path could be a field of a variable this pass cannot see, and
2360
+ * a false "unknown action" is worse than a missed one.
2361
+ */
2362
+ function checkLet(stmt, ctx) {
2363
+ if (stmt.args.length === 0 && !stmt.opts) return [];
2364
+ const target = actionTarget(stmt.value);
2365
+ if (target === void 0) return [problem$2(stmt, ctx, CODES.VN2003_UNKNOWN_ACTION, "This is not an action to call.")];
2366
+ return checkTarget({
2367
+ node: stmt,
2368
+ target,
2369
+ opts: stmt.opts,
2370
+ ctx
2371
+ });
2372
+ }
2373
+ /** `capture` is folded into `let`; say so where it is written. */
2374
+ function checkCapture(stmt, ctx) {
2375
+ return problem$2(stmt, ctx, CODES.VN5001_REMOVED_KEYWORD, "`capture` was removed — use `let` for a value that changes, `const` for one that does not.");
2376
+ }
2377
+ /**
2378
+ * A namespace is only usable when this file brought it in with `use` (or it is
2379
+ * a resource). Loading the whole stdlib must not make `use` optional.
2380
+ */
2381
+ function checkTarget(args) {
2382
+ const { node, target, ctx } = args;
2383
+ if (PRELUDE.has(target)) return [];
2384
+ const written = splitTarget(target).namespace;
2385
+ if (ctx.bound.has(written)) return [];
2386
+ if (!ctx.imported.has(written)) return [missingImport(args, written)];
2387
+ const resolved = ctx.registry.action(resolveTarget(target, ctx.aliases));
2388
+ if (!resolved) return [problem$2(node, ctx, CODES.VN2003_UNKNOWN_ACTION, `Unknown action "${target}".`)];
2389
+ return checkOptions({
2390
+ opts: args.opts,
2391
+ params: resolved.action.params,
2392
+ ctx
2393
+ });
2394
+ }
2395
+ function missingImport(args, namespace) {
2396
+ if (!args.ctx.registry.hasNamespace(namespace)) {
2397
+ const title = `Unknown action "${args.target}" — no loaded plugin provides it.`;
2398
+ return problem$2(args.node, args.ctx, CODES.VN2003_UNKNOWN_ACTION, title);
2399
+ }
2400
+ const title = `"${namespace}" is not imported in this file — add a \`use\` for its package.`;
2401
+ return problem$2(args.node, args.ctx, CODES.VN2007_NAMESPACE_NOT_IMPORTED, title);
2402
+ }
2403
+ function problem$2(node, ctx, spec, title) {
2404
+ return buildProblem({
2405
+ spec,
2406
+ span: nodeSpan(node, ctx.uri),
2407
+ title
2408
+ });
2409
+ }
2410
+ //#endregion
2411
+ //#region src/check/check-deco-body.ts
2412
+ /**
2413
+ * Check a node written inside a `deco` body, which is not the program and so is
2414
+ * not resolved against the registry: `target.wrap(f)` is a verb on the handle
2415
+ * the body was handed, and expansion settles what the rest means. The one
2416
+ * refusal this pass can make is a verb from a plugin, which cannot work in a
2417
+ * `deco` because a decorator runs before the program exists.
2418
+ *
2419
+ * @returns The node's problems, or `undefined` when the node is not inside a
2420
+ * `deco`, which tells the caller to apply the ordinary document checks instead.
2421
+ */
2422
+ function checkInsideDeco(node, ctx) {
2423
+ const deco = enclosingDeco(node);
2424
+ if (!deco) return void 0;
2425
+ const target = calledTarget(node);
2426
+ const refused = target === void 0 ? void 0 : pluginVerb({
2427
+ deco,
2428
+ target,
2429
+ ctx
2430
+ });
2431
+ return refused ? [problem$1(node, ctx, refused)] : [];
2432
+ }
2433
+ /** The `deco` this node is written inside, if any. */
2434
+ function enclosingDeco(node) {
2435
+ for (let at = node; at; at = at.$container) if (isDecoDecl(at)) return at;
2436
+ }
2437
+ /** The dotted name this statement calls, if calling is what it does. */
2438
+ function calledTarget(node) {
2439
+ if (isActionCall(node)) return node.target;
2440
+ if (!isLetStmt(node) || node.args.length === 0 && !node.opts) return void 0;
2441
+ return actionTarget(node.value);
2442
+ }
2443
+ /**
2444
+ * Why this call cannot be made from here, if it cannot. Only a head the file can
2445
+ * already account for is refused: an imported namespace, a resource, a namespace
2446
+ * some loaded plugin owns. Anything else is a name only expansion resolves, and
2447
+ * guessing at it would put an error on every well-written decorator.
2448
+ */
2449
+ function pluginVerb(args) {
2450
+ const { namespace } = splitTarget(args.target);
2451
+ if (PRELUDE.has(args.target) || paramNames(args.deco).has(namespace)) return void 0;
2452
+ if (!reachesTheWorld(namespace, args.ctx)) return void 0;
2453
+ return decoCannotCall(args.target);
2454
+ }
2455
+ function reachesTheWorld(namespace, ctx) {
2456
+ return ctx.imported.has(namespace) || ctx.bound.has(namespace) || ctx.registry.hasNamespace(namespace);
2457
+ }
2458
+ function paramNames(deco) {
2459
+ return new Set((deco.params?.params ?? []).map((param) => param.name));
2460
+ }
2461
+ function problem$1(node, ctx, title) {
2462
+ return buildProblem({
2463
+ spec: CODES.VN2016_DECO_IMPURE,
2464
+ span: nodeSpan(node, ctx.uri),
2465
+ title
2466
+ });
2467
+ }
2468
+ //#endregion
2469
+ //#region src/check/check-env.ts
2470
+ /** `env.NAME` as it appears inside a `${…}` placeholder. */
2471
+ const ENV_READ = /\benv\.([A-Za-z_]\w*)/g;
2472
+ /** Always present: the runner sets it to the `--env` that was selected. */
2473
+ const BUILT_IN = /* @__PURE__ */ new Set(["name"]);
2474
+ /**
2475
+ * Match every `env.*` read against what `venn.toml` declares, so a typo is an
2476
+ * error rather than an empty string and a puzzling 404.
2477
+ *
2478
+ * Nothing is reported when the manifest could not be read: a wrong error about a
2479
+ * variable that does exist is worse than no error at all.
2480
+ */
2481
+ function checkEnv(node, ctx) {
2482
+ if (!isMember(node) || isMember(node.$container)) return [];
2483
+ const path = actionTarget(node);
2484
+ const name = path?.startsWith("env.") ? path.slice(4) : void 0;
2485
+ if (name === void 0 || name.includes(".")) return [];
2486
+ const span = nodeSpan(node, ctx.uri);
2487
+ if (!ctx.imported.has("env")) return [notImported(span)];
2488
+ if (!ctx.env || declared(name, ctx.env)) return [];
2489
+ return [envProblem(name, span, ctx)];
2490
+ }
2491
+ /**
2492
+ * Configuration is brought in like anything else. Reading `env.*` without saying
2493
+ * where it comes from is the same hole `use` closes for actions and matchers:
2494
+ * the reader should not have to know which names are magic.
2495
+ */
2496
+ function notImported(span) {
2497
+ return buildProblem({
2498
+ spec: CODES.VN2007_NAMESPACE_NOT_IMPORTED,
2499
+ span,
2500
+ title: "\"env\" is not imported in this file — add `use \"venn/env\"`."
2501
+ });
2502
+ }
2503
+ /** The same checks for text the parser never turned into nodes: `"${env.X}"`. */
2504
+ function envProblemsIn(source, span, ctx) {
2505
+ const names = [...source.matchAll(ENV_READ)].map((match) => match[1] ?? "").filter(Boolean);
2506
+ if (names.length === 0) return [];
2507
+ if (!ctx.imported.has("env")) return [notImported(span)];
2508
+ const env = ctx.env;
2509
+ if (!env) return [];
2510
+ return names.filter((name) => !declared(name, env)).map((name) => envProblem(name, span, ctx));
2511
+ }
2512
+ function declared(name, env) {
2513
+ return env.has(name) || BUILT_IN.has(name);
2514
+ }
2515
+ /** The undeclared-variable problem, with the nearest declared name as a hint. */
2516
+ function envProblem(name, span, ctx) {
2517
+ const hint = nearest(name, [...ctx.env ?? []]);
2518
+ const title = hint ? `"env.${name}" is not declared in venn.toml — did you mean "env.${hint}"?` : `"env.${name}" is not declared in venn.toml.`;
2519
+ return buildProblem({
2520
+ spec: CODES.VN2006_UNKNOWN_ENV,
2521
+ span,
2522
+ title
2523
+ });
2524
+ }
2525
+ function nearest(name, known) {
2526
+ const best = known.map((candidate) => ({
2527
+ candidate,
2528
+ distance: distance(name, candidate)
2529
+ })).sort((left, right) => left.distance - right.distance)[0];
2530
+ const tolerance = Math.max(2, Math.floor(name.length / 3));
2531
+ return best && best.distance <= tolerance ? best.candidate : void 0;
2532
+ }
2533
+ /** Levenshtein edit distance, kept to two rows because only the score is needed. */
2534
+ function distance(left, right) {
2535
+ let previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
2536
+ for (let i = 1; i <= left.length; i += 1) {
2537
+ const current = [i];
2538
+ for (let j = 1; j <= right.length; j += 1) {
2539
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
2540
+ current[j] = Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
2541
+ }
2542
+ previous = current;
2543
+ }
2544
+ return previous[right.length] ?? 0;
2545
+ }
2546
+ //#endregion
2547
+ //#region src/check/check-fragment-call.ts
2548
+ /**
2549
+ * Refuse a fragment called as though it were a function.
2550
+ *
2551
+ * The two look alike where they are written but are different kinds of thing: a
2552
+ * `fn` gives back a value, a `fragment` gives back steps, and steps are recorded
2553
+ * in the report, can fail and belong to a flow. So a fragment is invoked with
2554
+ * `run`. `run entrar(…)` never reaches here: its target is a name, not an
2555
+ * expression.
2556
+ *
2557
+ * @param call The call expression to inspect.
2558
+ * @param ctx The document's resolved check context.
2559
+ * @returns A `VN3013` problem, or `undefined` when the callee is not a fragment.
2560
+ */
2561
+ function checkFragmentCall(call, ctx) {
2562
+ const callee = call.callee;
2563
+ if (!isRef(callee) || !ctx.fragments.has(callee.name)) return void 0;
2564
+ return buildProblem({
2565
+ spec: CODES.VN3013_NOT_CALLABLE,
2566
+ span: nodeSpan(call, ctx.uri),
2567
+ title: `${callee.name} is a fragment, so it cannot be called for a value.`,
2568
+ note: `Invoke it with \`run ${callee.name}(…)\`, which records its steps in the report.`
2569
+ });
2570
+ }
2571
+ //#endregion
2572
+ //#region src/check/check-interpolation.ts
2573
+ /**
2574
+ * Check each `${…}` placeholder in a string literal and point at the placeholder
2575
+ * itself, not at the whole string. A slot that does not parse would otherwise
2576
+ * evaluate to an empty string, so a typo inside a URL fails as a puzzling 404.
2577
+ */
2578
+ function checkInterpolation(node, ctx) {
2579
+ const cst = node.$cstNode;
2580
+ if (!isStringLit(node) || !cst) return [];
2581
+ return scanInterpolations(cst.text).flatMap((slot) => inSlot(slot, spanOf(slot, {
2582
+ cst,
2583
+ uri: ctx.uri
2584
+ }), ctx));
2585
+ }
2586
+ /** A URL is where `env` reads live, so they are checked here as well as in the AST. */
2587
+ function inSlot(slot, span, ctx) {
2588
+ if (!parseExpression(slot.source)) return [unreadable(slot, span)];
2589
+ return envProblemsIn(slot.source, span, ctx);
2590
+ }
2591
+ function unreadable(slot, span) {
2592
+ return buildProblem({
2593
+ spec: CODES.VN1002_PARSE,
2594
+ span,
2595
+ title: `Cannot read \`\${${slot.source}}\` — that is not an expression.`
2596
+ });
2597
+ }
2598
+ /** The span of the placeholder itself: line and column follow the string's own start. */
2599
+ function spanOf(slot, target) {
2600
+ const start = target.cst.range?.start;
2601
+ return {
2602
+ uri: target.uri,
2603
+ offset: target.cst.offset + slot.start,
2604
+ length: slot.end - slot.start,
2605
+ line: (start?.line ?? 0) + 1,
2606
+ column: (start?.character ?? 0) + 1 + slot.start
2607
+ };
2608
+ }
2609
+ //#endregion
2610
+ //#region src/check/check-uncalled.ts
2611
+ /**
2612
+ * A plugin verb named but never called.
2613
+ *
2614
+ * `let id = data.faker.uuid` runs the verb, because the runtime recognises a
2615
+ * bare path that names an action. The same words inside an expression evaluate
2616
+ * to the verb itself, so a program meaning to read a value gets a function.
2617
+ * Rather than guess which was meant, this asks for the parentheses.
2618
+ */
2619
+ function checkUncalledAction(node, ctx) {
2620
+ if (!isMember(node) || !readsAsValue(node)) return void 0;
2621
+ const target = actionTarget(node);
2622
+ if (target === void 0) return void 0;
2623
+ if (!ctx.registry.action(resolveTarget(target, ctx.aliases))) return void 0;
2624
+ return buildProblem({
2625
+ spec: CODES.VN2008_UNCALLED_ACTION,
2626
+ span: nodeSpan(node, ctx.uri),
2627
+ title: `\`${target}\` is a verb, not a value — write \`${target}()\` to call it.`
2628
+ });
2629
+ }
2630
+ /**
2631
+ * Where naming the verb produces the verb. Skipped when it is the head of a
2632
+ * longer path or the thing being called, and when it stands as a statement's
2633
+ * value, which the runtime calls anyway.
2634
+ */
2635
+ function readsAsValue(node) {
2636
+ const parent = node.$container;
2637
+ return !isMember(parent) && !isCall(parent) && !isLetStmt(parent);
2638
+ }
2639
+ //#endregion
2640
+ //#region src/check/check-document.ts
2641
+ /**
2642
+ * Statically resolve every action, matcher and fragment reference in a parsed
2643
+ * document. The errors the runner would otherwise raise mid-run are surfaced all
2644
+ * at once, each with its source span.
2645
+ *
2646
+ * @param args Document, registry, known fragments and the declared `env` names.
2647
+ * @returns One `Problem` per unresolved reference; empty when the document is clean.
2648
+ */
2649
+ function checkDocument(args) {
2650
+ const ctx = {
2651
+ registry: args.registry,
2652
+ fragments: args.fragments,
2653
+ aliases: collectAliases(args.document, args.registry),
2654
+ imported: collectNamespaces(args.document, args.registry),
2655
+ bound: collectBoundNames(args.document),
2656
+ env: args.env ? new Set(args.env) : void 0,
2657
+ uri: args.uri ?? "memory://inline.vn"
2658
+ };
2659
+ const problems = [];
2660
+ for (const node of walkAst(args.document)) {
2661
+ const inDeco = checkInsideDeco(node, ctx);
2662
+ if (inDeco) problems.push(...inDeco);
2663
+ else problems.push(...everyCheck(node, ctx));
2664
+ }
2665
+ return problems;
2666
+ }
2667
+ function everyCheck(node, ctx) {
2668
+ return [
2669
+ ...checkNode(node, ctx),
2670
+ ...checkEnv(node, ctx),
2671
+ ...checkInterpolation(node, ctx),
2672
+ ...one(checkUncalledAction(node, ctx))
2673
+ ];
2674
+ }
2675
+ function checkNode(node, ctx) {
2676
+ if (isActionCall(node)) return checkAction(node, ctx);
2677
+ if (isLetStmt(node)) return checkLet(node, ctx);
2678
+ if (isCaptureStmt(node)) return [checkCapture(node, ctx)];
2679
+ if (isMatcherClause(node)) return one(checkMatcher(node, ctx));
2680
+ if (isRunStmt(node)) return one(checkFragment(node, ctx));
2681
+ if (isCall(node)) return one(checkFragmentCall(node, ctx));
2682
+ return [];
2683
+ }
2684
+ function one(problem) {
2685
+ return problem ? [problem] : [];
2686
+ }
2687
+ /**
2688
+ * A matcher comes from a plugin like any action, so the file has to bring that
2689
+ * plugin in. Resolving against the whole loaded stdlib would make `use` decorative.
2690
+ */
2691
+ function checkMatcher(clause, ctx) {
2692
+ const owner = ctx.registry.matcher(clause.name);
2693
+ if (!owner) return problem(clause, ctx, CODES.VN2004_UNKNOWN_MATCHER, `Unknown matcher "${clause.name}".`);
2694
+ if (ctx.imported.has(owner.plugin.namespace)) return void 0;
2695
+ const title = `"${clause.name}" comes from "${owner.plugin.namespace}", which is not imported in this file.`;
2696
+ return problem(clause, ctx, CODES.VN2007_NAMESPACE_NOT_IMPORTED, title);
2697
+ }
2698
+ function checkFragment(stmt, ctx) {
2699
+ if (ctx.fragments.has(stmt.target)) return void 0;
2700
+ return problem(stmt, ctx, CODES.VN2005_UNKNOWN_FRAGMENT, `Unknown fragment "${stmt.target}".`);
2701
+ }
2702
+ function problem(node, ctx, spec, title) {
2703
+ return buildProblem({
2704
+ spec,
2705
+ span: nodeSpan(node, ctx.uri),
2706
+ title
2707
+ });
2708
+ }
2709
+ //#endregion
2710
+ //#region src/check/check-imports.ts
2711
+ /**
2712
+ * Check every name a file imports against what the file it names publishes.
2713
+ * Otherwise a misspelt import stays quietly `undefined` until something calls
2714
+ * it, and the run blames the call site rather than the import.
2715
+ *
2716
+ * @param args The importing document, its URI, and the resolved import graph.
2717
+ * @returns One `VN2009` problem per name the target does not publish.
2718
+ */
2719
+ function checkImports(args) {
2720
+ const problems = [];
2721
+ for (const decl of args.document.imports) {
2722
+ if (!isValueImport(decl)) continue;
2723
+ const target = args.graph.resolve(args.uri, decl.path);
2724
+ const module = args.graph.modules.get(target);
2725
+ if (module) problems.push(...missing({
2726
+ decl,
2727
+ module,
2728
+ uri: args.uri,
2729
+ path: decl.path
2730
+ }));
2731
+ }
2732
+ return problems;
2733
+ }
2734
+ function missing(args) {
2735
+ const published = exported(args.module);
2736
+ return args.decl.names.filter((name) => !published.has(name)).map((name) => buildProblem({
2737
+ spec: CODES.VN2009_NOT_EXPORTED,
2738
+ span: nodeSpan(args.decl, args.uri),
2739
+ title: `"${args.path}" does not publish ${name}.`,
2740
+ note: hint(name, args.module)
2741
+ }));
2742
+ }
2743
+ /**
2744
+ * Why it is not there: written but kept private, or not written at all. The two
2745
+ * are different mistakes with different fixes, and a reader told only "not
2746
+ * published" goes looking for a typo they did not make.
2747
+ */
2748
+ function hint(name, module) {
2749
+ return module.decls.some((decl) => (isFnDecl(decl) || isFragmentDecl(decl) || isDecoDecl(decl)) && decl.name === name) ? `It is declared there, but not marked \`pub\`.` : `Nothing of that name is declared there.`;
2750
+ }
2751
+ /** Everything a file marked `pub`: functions, fragments and decorators alike. */
2752
+ function exported(document) {
2753
+ const names = /* @__PURE__ */ new Set();
2754
+ for (const decl of document.decls) {
2755
+ if (!isFnDecl(decl) && !isFragmentDecl(decl) && !isDecoDecl(decl)) continue;
2756
+ if (decl.export) names.add(decl.name);
2757
+ }
2758
+ return names;
2759
+ }
2760
+ //#endregion
2761
+ //#region src/decorators/builtin-decorators.ts
2762
+ /** Where each of these may sit. Named once, so a wrong target is caught, not ignored. */
2763
+ const RUNNABLE = [
2764
+ "FlowDecl",
2765
+ "StepDecl",
2766
+ "GroupDecl"
2767
+ ];
2768
+ /**
2769
+ * The decorators the language ships with, written the same way anyone else's
2770
+ * are. They go through the same expansion as a plugin decorator and leave
2771
+ * metadata on the node, because `@retry(2)` says something the grammar has no
2772
+ * other word for. Keeping them here rather than as special cases in the
2773
+ * scheduler is what makes the built-ins a stdlib instead of reserved words.
2774
+ */
2775
+ const builtinDecorators = [
2776
+ flag("skip", "Do not run this."),
2777
+ flag("only", "Run this and nothing beside it."),
2778
+ flag("serial", "Never run this alongside another flow."),
2779
+ {
2780
+ name: "tags",
2781
+ doc: "Label this for `--tags`.",
2782
+ targets: [...RUNNABLE],
2783
+ expand: (ctx) => ctx.meta("tags", ctx.args.map(String).filter((tag) => tag !== ""))
2784
+ },
2785
+ {
2786
+ name: "timeout",
2787
+ doc: "Give up after this long.",
2788
+ targets: [...RUNNABLE],
2789
+ expand: (ctx) => ctx.meta("timeout", durationMs(ctx.args[0]))
2790
+ },
2791
+ {
2792
+ name: "retry",
2793
+ doc: "Run again on failure, up to n times.",
2794
+ targets: [...RUNNABLE],
2795
+ expand: (ctx) => ctx.meta("retry", retryOf(ctx))
2796
+ },
2797
+ {
2798
+ name: "lock",
2799
+ doc: "Hold this name for the duration; nothing else holding it runs.",
2800
+ targets: [...RUNNABLE],
2801
+ expand: (ctx) => ctx.meta("lock", ctx.args[0] === void 0 ? void 0 : String(ctx.args[0]))
2802
+ },
2803
+ {
2804
+ name: "flaky",
2805
+ doc: "Tolerate failures up to this ratio.",
2806
+ targets: [...RUNNABLE],
2807
+ expand: (ctx) => ctx.meta("flaky", ratioOf(ctx.args[0]))
2808
+ }
2809
+ ];
2810
+ /** A decorator with nothing to say beyond having been written. */
2811
+ function flag(name, doc) {
2812
+ return {
2813
+ name,
2814
+ doc,
2815
+ targets: [...RUNNABLE],
2816
+ expand: (ctx) => ctx.meta(name, true)
2817
+ };
2818
+ }
2819
+ function retryOf(ctx) {
2820
+ const opts = asRecord(ctx.args[1]);
2821
+ return {
2822
+ attempts: Math.max(0, Math.floor(Number(ctx.args[0]) || 0)),
2823
+ backoffMs: durationMs(opts.backoff) ?? 0,
2824
+ factor: Number(opts.factor ?? 1) || 1
2825
+ };
2826
+ }
2827
+ /** A bare `@flaky` tolerates everything; a number is the ratio it tolerates. */
2828
+ function ratioOf(value) {
2829
+ if (value === void 0) return 1;
2830
+ return typeof value === "number" ? value : 0;
2831
+ }
2832
+ function durationMs(value) {
2833
+ if (typeof value === "number") return value;
2834
+ const duration = value;
2835
+ return duration?.kind === "duration" ? duration.ms : void 0;
2836
+ }
2837
+ function asRecord(value) {
2838
+ return typeof value === "object" && value !== null ? value : {};
2839
+ }
2840
+ //#endregion
2841
+ //#region src/decorators/create-decorator-source.ts
2842
+ /**
2843
+ * Every decorator this run understands: the ones the language ships with, plus
2844
+ * whatever the loaded plugins contribute.
2845
+ *
2846
+ * A plugin's decorator overrides a built-in of the same name on purpose. The
2847
+ * built-ins are a stdlib, not a reserved word list, and a project that wants its
2848
+ * own `@retry` is entitled to it.
2849
+ *
2850
+ * @param plugins The plugins loaded for this run, in load order.
2851
+ * @returns A source that looks a decorator up by name.
2852
+ */
2853
+ function createDecoratorSource(plugins) {
2854
+ const byName = /* @__PURE__ */ new Map();
2855
+ for (const decorator of builtinDecorators) byName.set(decorator.name, decorator);
2856
+ for (const plugin of plugins) for (const decorator of plugin.decorators ?? []) byName.set(decorator.name, decorator);
2857
+ return { get: (name) => byName.get(name) };
2858
+ }
2859
+ //#endregion
2860
+ //#region src/emit/create-emitter.ts
2861
+ /** Build the sole emitter: it owns `seq` and stamps `ts` from the runner's clock. */
2862
+ function createEmitter(args) {
2863
+ let seq = 0;
2864
+ return { emit: ({ kind, data, node }) => {
2865
+ seq += 1;
2866
+ const ts = new Date(args.clock.now()).toISOString();
2867
+ args.sink.emit({
2868
+ seq,
2869
+ ts,
2870
+ run: args.run,
2871
+ kind,
2872
+ node,
2873
+ data
2874
+ });
2875
+ } };
2876
+ }
2877
+ //#endregion
2878
+ //#region src/emit/run-id.ts
2879
+ /**
2880
+ * Mint a run id from the host clock and random source, so a seeded run is
2881
+ * reproducible. Real ULID minting comes later.
2882
+ */
2883
+ function newRunId(args) {
2884
+ const rand = Math.floor(args.random.next() * 1e9).toString(36);
2885
+ return `run-${args.clock.now().toString(36)}-${rand}`;
2886
+ }
2887
+ //#endregion
2888
+ //#region src/eventsink/event-sink.port.ts
2889
+ /**
2890
+ * The port every run event is written to. Bound at startup by whoever assembles
2891
+ * the host: the CLI binds NDJSON on stdout, tests bind the memory double.
2892
+ */
2893
+ const EventSinkPort = {
2894
+ id: "venn.port.event-sink",
2895
+ version: 1,
2896
+ requires: [],
2897
+ methods: ["emit"]
2898
+ };
2899
+ //#endregion
2900
+ //#region src/eventsink/memory-sink.ts
2901
+ /**
2902
+ * In-memory sink (the test double): keeps every envelope for assertions.
2903
+ *
2904
+ * @returns A sink whose `envelopes` array grows in emission order.
2905
+ */
2906
+ function createMemorySink() {
2907
+ const envelopes = [];
2908
+ return {
2909
+ envelopes,
2910
+ emit: (envelope) => {
2911
+ envelopes.push(envelope);
2912
+ }
2913
+ };
2914
+ }
2915
+ //#endregion
2916
+ //#region src/eventsink/ndjson-sink.ts
2917
+ /**
2918
+ * NDJSON sink: one JSON envelope per line. The `write` sink is injected (the CLI
2919
+ * passes stdout) so this file stays neutral and needs no `node:*`.
2920
+ *
2921
+ * @param args.write Receives each line, newline included.
2922
+ * @returns A sink that serialises every envelope it is given.
2923
+ */
2924
+ function createNdjsonSink(args) {
2925
+ return { emit: (envelope) => args.write(`${JSON.stringify(envelope)}\n`) };
2926
+ }
2927
+ //#endregion
2928
+ //#region src/ports/create-port-resolver.ts
2929
+ /**
2930
+ * Resolve ports from a set of bindings, running capability and shape negotiation
2931
+ * at the moment of resolution.
2932
+ *
2933
+ * @param args.bindings Every port the host chose to bind, with its implementation.
2934
+ * @param args.caps What this host offers, checked against each port's `requires`.
2935
+ * @returns A resolver whose `resolve` throws `VN7002` when the port is unbound,
2936
+ * and whatever `bindPort` raises when the capability or shape check fails.
2937
+ */
2938
+ function createPortResolver(args) {
2939
+ const byId = new Map(args.bindings.map((binding) => [binding.port.id, binding]));
2940
+ return { resolve: (port) => resolveOne({
2941
+ port,
2942
+ binding: byId.get(port.id),
2943
+ caps: args.caps
2944
+ }) };
2945
+ }
2946
+ function resolveOne(args) {
2947
+ if (!args.binding) throw unbound(args.port.id);
2948
+ return bindPort({
2949
+ port: args.port,
2950
+ impl: args.binding.impl,
2951
+ caps: args.caps
2952
+ });
2953
+ }
2954
+ function unbound(id) {
2955
+ return new VennError({
2956
+ code: "VN7002",
2957
+ message: `No implementation bound for port "${id}".`,
2958
+ detail: { port: id }
2959
+ });
2960
+ }
2961
+ //#endregion
2962
+ //#region src/registry/build-registry.ts
2963
+ /**
2964
+ * Ingest plugins after negotiating each plugin's capabilities against the host.
2965
+ *
2966
+ * @param args.plugins The plugins to index, in load order; later ones win a clash.
2967
+ * @param args.caps The capabilities this host offers.
2968
+ * @returns A registry resolving actions, matchers and namespaces.
2969
+ * @throws VennError `VN2010` when a plugin requires a capability the host lacks.
2970
+ */
2971
+ function buildRegistry(args) {
2972
+ for (const plugin of args.plugins) assertPluginCaps({
2973
+ plugin,
2974
+ caps: args.caps
2975
+ });
2976
+ return indexPlugins(args.plugins);
2977
+ }
2978
+ function assertPluginCaps(args) {
2979
+ const missing = missingCapabilities({
2980
+ requires: args.plugin.requires ?? [],
2981
+ caps: args.caps
2982
+ });
2983
+ if (missing.length === 0) return;
2984
+ const list = missing.map((cap) => `"${cap}"`).join(", ");
2985
+ throw new VennError({
2986
+ code: "VN2010",
2987
+ message: `Plugin "${args.plugin.name}" requires capability ${list}, absent from this host.`,
2988
+ detail: {
2989
+ plugin: args.plugin.name,
2990
+ missing
2991
+ }
2992
+ });
2993
+ }
2994
+ function indexPlugins(plugins) {
2995
+ const actions = /* @__PURE__ */ new Map();
2996
+ const matchers = /* @__PURE__ */ new Map();
2997
+ const namespaces = /* @__PURE__ */ new Set();
2998
+ const packages = /* @__PURE__ */ new Map();
2999
+ for (const plugin of plugins) addPlugin({
3000
+ plugin,
3001
+ actions,
3002
+ matchers,
3003
+ namespaces,
3004
+ packages
3005
+ });
3006
+ return {
3007
+ action: ({ namespace, name }) => actions.get(`${namespace}.${name}`),
3008
+ matcher: (name) => matchers.get(name),
3009
+ hasNamespace: (namespace) => namespaces.has(namespace),
3010
+ namespaceOf: (pkg) => packages.get(pkg),
3011
+ actions: () => listActions(actions)
3012
+ };
3013
+ }
3014
+ function listActions(actions) {
3015
+ return [...actions].map(([key, resolved]) => ({
3016
+ namespace: key.slice(0, key.indexOf(".")),
3017
+ name: key.slice(key.indexOf(".") + 1),
3018
+ action: resolved.action
3019
+ }));
3020
+ }
3021
+ function addPlugin(args) {
3022
+ const { plugin, actions, matchers, namespaces, packages } = args;
3023
+ namespaces.add(plugin.namespace);
3024
+ packages.set(plugin.name, plugin.namespace);
3025
+ for (const action of plugin.actions ?? []) actions.set(`${plugin.namespace}.${action.name}`, {
3026
+ plugin,
3027
+ action
3028
+ });
3029
+ for (const matcher of plugin.matchers ?? []) matchers.set(matcher.name, {
3030
+ plugin,
3031
+ matcher
3032
+ });
3033
+ }
3034
+ //#endregion
3035
+ //#region src/context/action-context.ts
3036
+ /** Build the context an action's `run` receives from the host and port resolver. */
3037
+ function createActionContext(args) {
3038
+ return {
3039
+ port: (port) => args.ports.resolve(port),
3040
+ secrets: args.host.secrets,
3041
+ config: args.config ?? {},
3042
+ log: (message) => args.host.log.log({
3043
+ level: "info",
3044
+ message
3045
+ }),
3046
+ redact: () => {},
3047
+ invoke: (fn, values) => invoke(fn, values)
3048
+ };
3049
+ }
3050
+ //#endregion
3051
+ //#region src/run/create-runner.ts
3052
+ /**
3053
+ * Build a runner: negotiate plugins and wire ports once, then reuse them for
3054
+ * every run.
3055
+ *
3056
+ * @param args The host, the plugins, the event sink and the per-run options.
3057
+ * @returns A runner exposing test mode (`run`) and script mode (`script`).
3058
+ * @throws VennError `VN2010` when a plugin requires a capability the host lacks.
3059
+ */
3060
+ function createRunner(args) {
3061
+ const registry = buildRegistry({
3062
+ plugins: args.plugins,
3063
+ caps: args.host.caps
3064
+ });
3065
+ const resolver = createPortResolver({
3066
+ bindings: args.ports ?? [],
3067
+ caps: args.host.caps
3068
+ });
3069
+ const decorators = createDecoratorSource(args.plugins);
3070
+ const drive = (walk) => (document) => {
3071
+ const expansion = expand({
3072
+ document,
3073
+ decorators,
3074
+ uri: args.uri,
3075
+ imported: args.moduleDecos
3076
+ });
3077
+ return runOnce({
3078
+ args,
3079
+ registry,
3080
+ resolver,
3081
+ document
3082
+ }, walk, expansion.problems);
3083
+ };
3084
+ return {
3085
+ run: drive(runDocument),
3086
+ script: drive(runScript)
3087
+ };
3088
+ }
3089
+ async function runOnce(input, walk, problems) {
3090
+ const run = newRunId({
3091
+ clock: input.args.host.clock,
3092
+ random: input.args.host.random
3093
+ });
3094
+ const engine = buildEngine(input, run);
3095
+ await absorbExit(engine, () => walk(engine, input.document));
3096
+ return {
3097
+ run,
3098
+ problems,
3099
+ passed: engine.result.passed,
3100
+ failed: engine.result.failed,
3101
+ exitCode: engine.exit
3102
+ };
3103
+ }
3104
+ function buildEngine(input, run) {
3105
+ const { args, registry } = input;
3106
+ return {
3107
+ registry,
3108
+ emitter: createEmitter({
3109
+ sink: args.sink,
3110
+ run,
3111
+ clock: args.host.clock
3112
+ }),
3113
+ clock: args.host.clock,
3114
+ lock: args.host.lock,
3115
+ uri: args.uri ?? "memory://inline.vn",
3116
+ result: {
3117
+ passed: 0,
3118
+ failed: 0
3119
+ },
3120
+ flaky: /* @__PURE__ */ new Map(),
3121
+ filter: args.filter ?? {},
3122
+ bail: args.bail,
3123
+ cleanup: args.cleanup ?? createCleanupList(),
3124
+ ...documentParts(input)
3125
+ };
3126
+ }
3127
+ /** Everything derived from the document being run: config, fragments, aliases. */
3128
+ function documentParts(input) {
3129
+ const { args, registry, resolver, document } = input;
3130
+ const env = args.env ?? {};
3131
+ const config = collectConfig(document, env);
3132
+ return {
3133
+ ctx: createActionContext({
3134
+ host: args.host,
3135
+ ports: resolver,
3136
+ config
3137
+ }),
3138
+ fragments: mergeFragments(collectFragments(document), args.moduleFragments),
3139
+ imports: args.modules,
3140
+ aliases: collectAliases(document, registry),
3141
+ env
3142
+ };
3143
+ }
3144
+ /** Local fragments take precedence over imported ones of the same name. */
3145
+ function mergeFragments(local, imported) {
3146
+ if (!imported) return local;
3147
+ const merged = new Map(imported);
3148
+ for (const [name, fragment] of local) merged.set(name, fragment);
3149
+ return merged;
3150
+ }
3151
+ //#endregion
3152
+ //#region src/run/resolve-imports.ts
3153
+ /**
3154
+ * Walk the import graph from one document, parsing every `.vn` file it reaches
3155
+ * and loading every package it names. A file already seen is skipped, so a cycle
3156
+ * ends rather than loops. A file that cannot be read is skipped in silence:
3157
+ * whoever tried to read it reports the failure.
3158
+ *
3159
+ * @param args The entry document, its URI, the source reader and the optional
3160
+ * package loader.
3161
+ * @returns The exported fragments and decos, the packages, the loaded npm
3162
+ * modules and every parsed document, keyed by resolved URI.
3163
+ */
3164
+ async function resolveImports(args) {
3165
+ const found = {
3166
+ fragments: /* @__PURE__ */ new Map(),
3167
+ decos: /* @__PURE__ */ new Map()
3168
+ };
3169
+ const packages = /* @__PURE__ */ new Set();
3170
+ const modules = /* @__PURE__ */ new Map();
3171
+ const npm = /* @__PURE__ */ new Map();
3172
+ collectUses(args.document, packages);
3173
+ await loadInto({
3174
+ document: args.document,
3175
+ uri: args.uri,
3176
+ io: args.io,
3177
+ npmLoader: args.npm,
3178
+ found,
3179
+ packages,
3180
+ modules,
3181
+ npm,
3182
+ seen: /* @__PURE__ */ new Set([args.uri])
3183
+ });
3184
+ return {
3185
+ ...found,
3186
+ packages,
3187
+ modules,
3188
+ npm
3189
+ };
3190
+ }
3191
+ /**
3192
+ * Add the packages a file declares with `use` to `into`. Collected across the
3193
+ * whole graph: a fragment imported from elsewhere calls the verbs *its* file
3194
+ * asked for.
3195
+ *
3196
+ * @param document The file to read `use` declarations from.
3197
+ * @param into The accumulating set of package specifiers, mutated in place.
3198
+ */
3199
+ function collectUses(document, into) {
3200
+ for (const decl of document.imports) if (isUseDecl(decl)) into.add(decl.pkg);
3201
+ }
3202
+ async function loadInto(state) {
3203
+ for (const decl of state.document.imports) {
3204
+ if (!isValueImport(decl)) continue;
3205
+ if (isPackageSpecifier(decl.path)) await loadPackage(state, decl.path);
3206
+ else await loadModule(state, decl.path);
3207
+ }
3208
+ }
3209
+ /**
3210
+ * A package that was installed, loaded once however many files name it.
3211
+ *
3212
+ * A host with no loader has no packages, which is the state of every run in a
3213
+ * Web Worker. The import is left unbound and the name reports itself as unknown,
3214
+ * rather than the load failing somewhere nobody can see.
3215
+ */
3216
+ async function loadPackage(state, spec) {
3217
+ if (state.npm.has(spec) || !state.npmLoader) return;
3218
+ const found = await state.npmLoader.load(spec).catch(() => void 0);
3219
+ if (found) state.npm.set(spec, found);
3220
+ }
3221
+ async function loadModule(state, spec) {
3222
+ const target = state.io.resolve(state.uri, spec);
3223
+ if (state.seen.has(target)) return;
3224
+ state.seen.add(target);
3225
+ const source = await state.io.read(target).catch(() => void 0);
3226
+ if (source === void 0) return;
3227
+ const { ast } = parse(source, { uri: target });
3228
+ state.modules.set(target, ast);
3229
+ collectExports({
3230
+ document: ast,
3231
+ uri: target,
3232
+ found: state.found
3233
+ });
3234
+ collectUses(ast, state.packages);
3235
+ await loadInto({
3236
+ ...state,
3237
+ document: ast,
3238
+ uri: target
3239
+ });
3240
+ }
3241
+ /** Everything a file marked `pub`. A `deco` keeps its file: its faults are its own. */
3242
+ function collectExports(args) {
3243
+ for (const decl of args.document.decls) {
3244
+ if (isFragmentDecl(decl) && decl.export) args.found.fragments.set(decl.name, decl);
3245
+ if (isDecoDecl(decl) && decl.export) args.found.decos.set(decl.name, {
3246
+ decl,
3247
+ uri: args.uri
3248
+ });
3249
+ }
3250
+ }
3251
+ //#endregion
3252
+ //#region src/types/create-type-catalog.ts
3253
+ /**
3254
+ * Turn what the loaded plugins publish into what the checker can ask.
3255
+ *
3256
+ * This is the seam the core deliberately does not cross: `@venn-lang/core` knows
3257
+ * nothing about plugins and gets told. Names are qualified here, once: a plugin
3258
+ * says `Request` and a flow writes `http.Request`.
3259
+ *
3260
+ * @param plugins The plugins loaded for this run.
3261
+ * @returns A catalog answering a type name or an action's signature.
3262
+ */
3263
+ function createTypeCatalog(plugins) {
3264
+ const published = {
3265
+ types: /* @__PURE__ */ new Map(),
3266
+ signatures: /* @__PURE__ */ new Map()
3267
+ };
3268
+ for (const plugin of plugins) collect(plugin, published);
3269
+ const reader = createReader(published);
3270
+ return {
3271
+ typeOf: (name) => reader.read(published.types, name),
3272
+ signatureOf: (target) => asFn(reader.read(published.signatures, target))
3273
+ };
3274
+ }
3275
+ function collect(plugin, into) {
3276
+ for (const [name, spec] of Object.entries(plugin.typeDefs ?? {})) into.types.set(`${plugin.namespace}.${name}`, spec);
3277
+ for (const action of plugin.actions ?? []) {
3278
+ const key = `${plugin.namespace}.${action.name}`;
3279
+ if (action.signature) into.signatures.set(key, action.signature);
3280
+ }
3281
+ }
3282
+ /**
3283
+ * Read a published spec once and keep the result.
3284
+ *
3285
+ * The same `http.Request` has to be the same object every time it is asked for:
3286
+ * inference writes into what it is handed, and two copies would drift apart in
3287
+ * the middle of a file. `open` guards a type that refers to itself, so the
3288
+ * second visit answers "unknown" rather than recurring forever.
3289
+ */
3290
+ function createReader(published) {
3291
+ const cache = /* @__PURE__ */ new Map();
3292
+ const open = /* @__PURE__ */ new Set();
3293
+ const read = (from, name) => {
3294
+ const cached = cache.get(name);
3295
+ if (cached) return cached;
3296
+ const spec = from.get(name);
3297
+ if (!spec || open.has(name)) return void 0;
3298
+ open.add(name);
3299
+ const type = specToType(spec, (ref) => read(published.types, ref));
3300
+ open.delete(name);
3301
+ cache.set(name, type);
3302
+ return type;
3303
+ };
3304
+ return { read };
3305
+ }
3306
+ function asFn(type) {
3307
+ if (!type) return void 0;
3308
+ const pruned = prune(type);
3309
+ return pruned.kind === "fn" ? pruned : void 0;
3310
+ }
3311
+ //#endregion
3312
+ export { EventSinkPort, buildRegistry, builtinDecorators, checkDocument, checkFragmentCall, checkImports, collectFragments, collectUses, createCleanupList, createDecoratorSource, createEmitter, createMemorySink, createNdjsonSink, createPortResolver, createRunner, createScope, createTypeCatalog, matchesTitle, newRunId, resolveImports };
3313
+
3314
+ //# sourceMappingURL=index.js.map