@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
@@ -0,0 +1,87 @@
1
+ import type { DecoratorDefinition, ExpandContext } from "@venn-lang/core";
2
+ import type { RetrySpec } from "../scheduler/annotations.js";
3
+
4
+ /** Where each of these may sit. Named once, so a wrong target is caught, not ignored. */
5
+ const RUNNABLE = ["FlowDecl", "StepDecl", "GroupDecl"] as const;
6
+
7
+ /**
8
+ * The decorators the language ships with, written the same way anyone else's
9
+ * are. They go through the same expansion as a plugin decorator and leave
10
+ * metadata on the node, because `@retry(2)` says something the grammar has no
11
+ * other word for. Keeping them here rather than as special cases in the
12
+ * scheduler is what makes the built-ins a stdlib instead of reserved words.
13
+ */
14
+ export const builtinDecorators: readonly DecoratorDefinition[] = [
15
+ flag("skip", "Do not run this."),
16
+ flag("only", "Run this and nothing beside it."),
17
+ flag("serial", "Never run this alongside another flow."),
18
+ {
19
+ name: "tags",
20
+ doc: "Label this for `--tags`.",
21
+ targets: [...RUNNABLE],
22
+ expand: (ctx) =>
23
+ ctx.meta(
24
+ "tags",
25
+ ctx.args.map(String).filter((tag) => tag !== ""),
26
+ ),
27
+ },
28
+ {
29
+ name: "timeout",
30
+ doc: "Give up after this long.",
31
+ targets: [...RUNNABLE],
32
+ expand: (ctx) => ctx.meta("timeout", durationMs(ctx.args[0])),
33
+ },
34
+ {
35
+ name: "retry",
36
+ doc: "Run again on failure, up to n times.",
37
+ targets: [...RUNNABLE],
38
+ expand: (ctx) => ctx.meta("retry", retryOf(ctx)),
39
+ },
40
+ {
41
+ name: "lock",
42
+ doc: "Hold this name for the duration; nothing else holding it runs.",
43
+ targets: [...RUNNABLE],
44
+ expand: (ctx) => ctx.meta("lock", ctx.args[0] === undefined ? undefined : String(ctx.args[0])),
45
+ },
46
+ {
47
+ name: "flaky",
48
+ doc: "Tolerate failures up to this ratio.",
49
+ targets: [...RUNNABLE],
50
+ expand: (ctx) => ctx.meta("flaky", ratioOf(ctx.args[0])),
51
+ },
52
+ ];
53
+
54
+ /** A decorator with nothing to say beyond having been written. */
55
+ function flag(name: string, doc: string): DecoratorDefinition {
56
+ return {
57
+ name,
58
+ doc,
59
+ targets: [...RUNNABLE],
60
+ expand: (ctx) => ctx.meta(name, true),
61
+ };
62
+ }
63
+
64
+ function retryOf(ctx: ExpandContext): RetrySpec {
65
+ const opts = asRecord(ctx.args[1]);
66
+ return {
67
+ attempts: Math.max(0, Math.floor(Number(ctx.args[0]) || 0)),
68
+ backoffMs: durationMs(opts.backoff) ?? 0,
69
+ factor: Number(opts.factor ?? 1) || 1,
70
+ };
71
+ }
72
+
73
+ /** A bare `@flaky` tolerates everything; a number is the ratio it tolerates. */
74
+ function ratioOf(value: unknown): number {
75
+ if (value === undefined) return 1;
76
+ return typeof value === "number" ? value : 0;
77
+ }
78
+
79
+ function durationMs(value: unknown): number | undefined {
80
+ if (typeof value === "number") return value;
81
+ const duration = value as { kind?: string; ms?: number };
82
+ return duration?.kind === "duration" ? duration.ms : undefined;
83
+ }
84
+
85
+ function asRecord(value: unknown): Record<string, unknown> {
86
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
87
+ }
@@ -0,0 +1,25 @@
1
+ import type { DecoratorDefinition, DecoratorSource } from "@venn-lang/core";
2
+ import type { PluginDefinition } from "@venn-lang/sdk";
3
+ import { builtinDecorators } from "./builtin-decorators.js";
4
+
5
+ /**
6
+ * Every decorator this run understands: the ones the language ships with, plus
7
+ * whatever the loaded plugins contribute.
8
+ *
9
+ * A plugin's decorator overrides a built-in of the same name on purpose. The
10
+ * built-ins are a stdlib, not a reserved word list, and a project that wants its
11
+ * own `@retry` is entitled to it.
12
+ *
13
+ * @param plugins The plugins loaded for this run, in load order.
14
+ * @returns A source that looks a decorator up by name.
15
+ */
16
+ export function createDecoratorSource(plugins: readonly PluginDefinition[]): DecoratorSource {
17
+ const byName = new Map<string, DecoratorDefinition>();
18
+ for (const decorator of builtinDecorators) byName.set(decorator.name, decorator);
19
+ for (const plugin of plugins) {
20
+ for (const decorator of plugin.decorators ?? []) {
21
+ byName.set(decorator.name, decorator as DecoratorDefinition);
22
+ }
23
+ }
24
+ return { get: (name) => byName.get(name) };
25
+ }
@@ -0,0 +1,2 @@
1
+ export { builtinDecorators } from "./builtin-decorators.js";
2
+ export { createDecoratorSource } from "./create-decorator-source.js";
@@ -0,0 +1,16 @@
1
+ import type { Clock } from "@venn-lang/contracts";
2
+ import type { Envelope, RunId } from "@venn-lang/core";
3
+ import type { EventSink } from "../eventsink/index.js";
4
+ import type { Emitter } from "./emitter.types.js";
5
+
6
+ /** Build the sole emitter: it owns `seq` and stamps `ts` from the runner's clock. */
7
+ export function createEmitter(args: { sink: EventSink; run: RunId; clock: Clock }): Emitter {
8
+ let seq = 0;
9
+ return {
10
+ emit: ({ kind, data, node }) => {
11
+ seq += 1;
12
+ const ts = new Date(args.clock.now()).toISOString();
13
+ args.sink.emit({ seq, ts, run: args.run, kind, node, data } as Envelope);
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,6 @@
1
+ import type { EventData, EventKind, NodePath } from "@venn-lang/core";
2
+
3
+ /** The single place `seq` increments and `ts` is stamped. Handlers emit only here. */
4
+ export interface Emitter {
5
+ emit<K extends EventKind>(args: { kind: K; data: EventData[K]; node?: NodePath }): void;
6
+ }
@@ -0,0 +1,3 @@
1
+ export { createEmitter } from "./create-emitter.js";
2
+ export type { Emitter } from "./emitter.types.js";
3
+ export { newRunId } from "./run-id.js";
@@ -0,0 +1,11 @@
1
+ import type { Clock, Random } from "@venn-lang/contracts";
2
+ import type { RunId } from "@venn-lang/core";
3
+
4
+ /**
5
+ * Mint a run id from the host clock and random source, so a seeded run is
6
+ * reproducible. Real ULID minting comes later.
7
+ */
8
+ export function newRunId(args: { clock: Clock; random: Random }): RunId {
9
+ const rand = Math.floor(args.random.next() * 1e9).toString(36);
10
+ return `run-${args.clock.now().toString(36)}-${rand}` as RunId;
11
+ }
@@ -0,0 +1,13 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { EventSink } from "./event-sink.types.js";
3
+
4
+ /**
5
+ * The port every run event is written to. Bound at startup by whoever assembles
6
+ * the host: the CLI binds NDJSON on stdout, tests bind the memory double.
7
+ */
8
+ export const EventSinkPort: Port<EventSink> = {
9
+ id: "venn.port.event-sink",
10
+ version: 1,
11
+ requires: [],
12
+ methods: ["emit"],
13
+ };
@@ -0,0 +1,11 @@
1
+ import type { Envelope } from "@venn-lang/core";
2
+
3
+ /** The destination of the event stream. Everything the UI shows derives from it. */
4
+ export interface EventSink {
5
+ emit(envelope: Envelope): void;
6
+ }
7
+
8
+ /** An EventSink that also retains every envelope (the test double). */
9
+ export interface MemorySink extends EventSink {
10
+ readonly envelopes: readonly Envelope[];
11
+ }
@@ -0,0 +1,4 @@
1
+ export { EventSinkPort } from "./event-sink.port.js";
2
+ export type { EventSink, MemorySink } from "./event-sink.types.js";
3
+ export { createMemorySink } from "./memory-sink.js";
4
+ export { createNdjsonSink } from "./ndjson-sink.js";
@@ -0,0 +1,17 @@
1
+ import type { Envelope } from "@venn-lang/core";
2
+ import type { MemorySink } from "./event-sink.types.js";
3
+
4
+ /**
5
+ * In-memory sink (the test double): keeps every envelope for assertions.
6
+ *
7
+ * @returns A sink whose `envelopes` array grows in emission order.
8
+ */
9
+ export function createMemorySink(): MemorySink {
10
+ const envelopes: Envelope[] = [];
11
+ return {
12
+ envelopes,
13
+ emit: (envelope) => {
14
+ envelopes.push(envelope);
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,14 @@
1
+ import type { EventSink } from "./event-sink.types.js";
2
+
3
+ /**
4
+ * NDJSON sink: one JSON envelope per line. The `write` sink is injected (the CLI
5
+ * passes stdout) so this file stays neutral and needs no `node:*`.
6
+ *
7
+ * @param args.write Receives each line, newline included.
8
+ * @returns A sink that serialises every envelope it is given.
9
+ */
10
+ export function createNdjsonSink(args: { write: (line: string) => void }): EventSink {
11
+ return {
12
+ emit: (envelope) => args.write(`${JSON.stringify(envelope)}\n`),
13
+ };
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ // @venn-lang/runtime: the registry and the sequential runner. Depends only on core,
2
+ // contracts and sdk (types); no concrete node impls, no `node:*`.
3
+
4
+ export * from "./check/index.js";
5
+ export * from "./decorators/index.js";
6
+ export * from "./emit/index.js";
7
+ export * from "./eventsink/index.js";
8
+ export * from "./ports/index.js";
9
+ export * from "./registry/index.js";
10
+ export * from "./run/index.js";
11
+ export type { Cleanup, CleanupList, CleanupSink, RunFilter } from "./scheduler/index.js";
12
+ export { collectFragments, createCleanupList, matchesTitle } from "./scheduler/index.js";
13
+ export * from "./scope/index.js";
14
+ export * from "./types/index.js";
@@ -0,0 +1,39 @@
1
+ import { bindPort, type HostCapability, type Port, VennError } from "@venn-lang/contracts";
2
+ import type { PortBinding, PortResolver } from "./port-resolver.types.js";
3
+
4
+ /**
5
+ * Resolve ports from a set of bindings, running capability and shape negotiation
6
+ * at the moment of resolution.
7
+ *
8
+ * @param args.bindings Every port the host chose to bind, with its implementation.
9
+ * @param args.caps What this host offers, checked against each port's `requires`.
10
+ * @returns A resolver whose `resolve` throws `VN7002` when the port is unbound,
11
+ * and whatever `bindPort` raises when the capability or shape check fails.
12
+ */
13
+ export function createPortResolver(args: {
14
+ bindings: readonly PortBinding[];
15
+ caps: readonly HostCapability[];
16
+ }): PortResolver {
17
+ const byId = new Map(args.bindings.map((binding) => [binding.port.id, binding] as const));
18
+ return {
19
+ resolve: <T>(port: Port<T>): T =>
20
+ resolveOne({ port, binding: byId.get(port.id), caps: args.caps }),
21
+ };
22
+ }
23
+
24
+ function resolveOne<T>(args: {
25
+ port: Port<T>;
26
+ binding: PortBinding | undefined;
27
+ caps: readonly HostCapability[];
28
+ }): T {
29
+ if (!args.binding) throw unbound(args.port.id);
30
+ return bindPort({ port: args.port, impl: args.binding.impl, caps: args.caps });
31
+ }
32
+
33
+ function unbound(id: string): VennError {
34
+ return new VennError({
35
+ code: "VN7002",
36
+ message: `No implementation bound for port "${id}".`,
37
+ detail: { port: id },
38
+ });
39
+ }
@@ -0,0 +1,2 @@
1
+ export { createPortResolver } from "./create-port-resolver.js";
2
+ export type { PortBinding, PortResolver } from "./port-resolver.types.js";
@@ -0,0 +1,12 @@
1
+ import type { AnyPort, Port } from "@venn-lang/contracts";
2
+
3
+ /** A port paired with the implementation a host/CLI chose to bind. */
4
+ export interface PortBinding {
5
+ port: AnyPort;
6
+ impl: unknown;
7
+ }
8
+
9
+ /** Resolves a port descriptor to its bound, negotiated implementation. */
10
+ export interface PortResolver {
11
+ resolve<T>(port: Port<T>): T;
12
+ }
@@ -0,0 +1,76 @@
1
+ import { type HostCapability, missingCapabilities, VennError } from "@venn-lang/contracts";
2
+ import type { ActionDefinition, PluginDefinition } from "@venn-lang/sdk";
3
+ import type { Registry, ResolvedAction, ResolvedMatcher } from "./registry.types.js";
4
+
5
+ /**
6
+ * Ingest plugins after negotiating each plugin's capabilities against the host.
7
+ *
8
+ * @param args.plugins The plugins to index, in load order; later ones win a clash.
9
+ * @param args.caps The capabilities this host offers.
10
+ * @returns A registry resolving actions, matchers and namespaces.
11
+ * @throws VennError `VN2010` when a plugin requires a capability the host lacks.
12
+ */
13
+ export function buildRegistry(args: {
14
+ plugins: readonly PluginDefinition[];
15
+ caps: readonly HostCapability[];
16
+ }): Registry {
17
+ for (const plugin of args.plugins) assertPluginCaps({ plugin, caps: args.caps });
18
+ return indexPlugins(args.plugins);
19
+ }
20
+
21
+ function assertPluginCaps(args: {
22
+ plugin: PluginDefinition;
23
+ caps: readonly HostCapability[];
24
+ }): void {
25
+ const missing = missingCapabilities({ requires: args.plugin.requires ?? [], caps: args.caps });
26
+ if (missing.length === 0) return;
27
+ const list = missing.map((cap) => `"${cap}"`).join(", ");
28
+ throw new VennError({
29
+ code: "VN2010",
30
+ message: `Plugin "${args.plugin.name}" requires capability ${list}, absent from this host.`,
31
+ detail: { plugin: args.plugin.name, missing },
32
+ });
33
+ }
34
+
35
+ function indexPlugins(plugins: readonly PluginDefinition[]): Registry {
36
+ const actions = new Map<string, ResolvedAction>();
37
+ const matchers = new Map<string, ResolvedMatcher>();
38
+ const namespaces = new Set<string>();
39
+ const packages = new Map<string, string>();
40
+ for (const plugin of plugins) addPlugin({ plugin, actions, matchers, namespaces, packages });
41
+ return {
42
+ action: ({ namespace, name }) => actions.get(`${namespace}.${name}`),
43
+ matcher: (name) => matchers.get(name),
44
+ hasNamespace: (namespace) => namespaces.has(namespace),
45
+ namespaceOf: (pkg) => packages.get(pkg),
46
+ actions: () => listActions(actions),
47
+ };
48
+ }
49
+
50
+ function listActions(
51
+ actions: Map<string, ResolvedAction>,
52
+ ): { namespace: string; name: string; action: ActionDefinition }[] {
53
+ return [...actions].map(([key, resolved]) => ({
54
+ namespace: key.slice(0, key.indexOf(".")),
55
+ name: key.slice(key.indexOf(".") + 1),
56
+ action: resolved.action,
57
+ }));
58
+ }
59
+
60
+ function addPlugin(args: {
61
+ plugin: PluginDefinition;
62
+ actions: Map<string, ResolvedAction>;
63
+ matchers: Map<string, ResolvedMatcher>;
64
+ namespaces: Set<string>;
65
+ packages: Map<string, string>;
66
+ }): void {
67
+ const { plugin, actions, matchers, namespaces, packages } = args;
68
+ namespaces.add(plugin.namespace);
69
+ packages.set(plugin.name, plugin.namespace);
70
+ for (const action of plugin.actions ?? []) {
71
+ actions.set(`${plugin.namespace}.${action.name}`, { plugin, action });
72
+ }
73
+ for (const matcher of plugin.matchers ?? []) {
74
+ matchers.set(matcher.name, { plugin, matcher });
75
+ }
76
+ }
@@ -0,0 +1,2 @@
1
+ export { buildRegistry } from "./build-registry.js";
2
+ export type { Registry, ResolvedAction, ResolvedMatcher } from "./registry.types.js";
@@ -0,0 +1,24 @@
1
+ import type { ActionDefinition, MatcherDefinition, PluginDefinition } from "@venn-lang/sdk";
2
+
3
+ /** An action together with the plugin that owns it. */
4
+ export interface ResolvedAction {
5
+ plugin: PluginDefinition;
6
+ action: ActionDefinition;
7
+ }
8
+
9
+ /** A matcher together with the plugin that owns it. */
10
+ export interface ResolvedMatcher {
11
+ plugin: PluginDefinition;
12
+ matcher: MatcherDefinition;
13
+ }
14
+
15
+ /** Resolves `namespace.action`, matchers, and namespaces from ingested plugins. */
16
+ export interface Registry {
17
+ action(args: { namespace: string; name: string }): ResolvedAction | undefined;
18
+ matcher(name: string): ResolvedMatcher | undefined;
19
+ hasNamespace(namespace: string): boolean;
20
+ /** The namespace a package contributes, for `use "venn/http" as h`. */
21
+ namespaceOf(pkg: string): string | undefined;
22
+ /** Every action, for binding namespaces as values in the evaluator scope. */
23
+ actions(): readonly { namespace: string; name: string; action: ActionDefinition }[];
24
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ type Document,
3
+ expand,
4
+ type FragmentDecl,
5
+ type Problem,
6
+ type RunId,
7
+ } from "@venn-lang/core";
8
+ import { createActionContext } from "../context/index.js";
9
+ import { createDecoratorSource } from "../decorators/index.js";
10
+ import { createEmitter, newRunId } from "../emit/index.js";
11
+ import { createPortResolver } from "../ports/index.js";
12
+ import { buildRegistry } from "../registry/index.js";
13
+ import {
14
+ absorbExit,
15
+ collectAliases,
16
+ collectConfig,
17
+ collectFragments,
18
+ createCleanupList,
19
+ type Engine,
20
+ runDocument,
21
+ runScript,
22
+ } from "../scheduler/index.js";
23
+ import type { Runner, RunnerArgs, RunOnceInput, RunResult } from "./runner.types.js";
24
+
25
+ /**
26
+ * Build a runner: negotiate plugins and wire ports once, then reuse them for
27
+ * every run.
28
+ *
29
+ * @param args The host, the plugins, the event sink and the per-run options.
30
+ * @returns A runner exposing test mode (`run`) and script mode (`script`).
31
+ * @throws VennError `VN2010` when a plugin requires a capability the host lacks.
32
+ */
33
+ export function createRunner(args: RunnerArgs): Runner {
34
+ const registry = buildRegistry({ plugins: args.plugins, caps: args.host.caps });
35
+ const resolver = createPortResolver({ bindings: args.ports ?? [], caps: args.host.caps });
36
+ const decorators = createDecoratorSource(args.plugins);
37
+ const drive = (walk: Walk) => (document: Document) => {
38
+ // Decorators run first, on every path: what the scheduler walks is the tree
39
+ // they left, not the one the parser produced.
40
+ const expansion = expand({ document, decorators, uri: args.uri, imported: args.moduleDecos });
41
+ return runOnce({ args, registry, resolver, document }, walk, expansion.problems);
42
+ };
43
+ return { run: drive(runDocument), script: drive(runScript) };
44
+ }
45
+
46
+ type Walk = (engine: Engine, document: Document) => Promise<void>;
47
+
48
+ async function runOnce(input: RunOnceInput, walk: Walk, problems: Problem[]): Promise<RunResult> {
49
+ const run = newRunId({ clock: input.args.host.clock, random: input.args.host.random });
50
+ const engine = buildEngine(input, run);
51
+ // The walk absorbs an `exit` of its own so the run still ends tidily; this is
52
+ // the backstop for one thrown where no walk was left to absorb it.
53
+ await absorbExit(engine, () => walk(engine, input.document));
54
+ return {
55
+ run,
56
+ problems,
57
+ passed: engine.result.passed,
58
+ failed: engine.result.failed,
59
+ exitCode: engine.exit,
60
+ };
61
+ }
62
+
63
+ function buildEngine(input: RunOnceInput, run: RunId): Engine {
64
+ const { args, registry } = input;
65
+ return {
66
+ registry,
67
+ emitter: createEmitter({ sink: args.sink, run, clock: args.host.clock }),
68
+ clock: args.host.clock,
69
+ lock: args.host.lock,
70
+ uri: args.uri ?? "memory://inline.vn",
71
+ result: { passed: 0, failed: 0 },
72
+ flaky: new Map(),
73
+ filter: args.filter ?? {},
74
+ bail: args.bail,
75
+ cleanup: args.cleanup ?? createCleanupList(),
76
+ ...documentParts(input),
77
+ };
78
+ }
79
+
80
+ /** Everything derived from the document being run: config, fragments, aliases. */
81
+ function documentParts(input: RunOnceInput) {
82
+ const { args, registry, resolver, document } = input;
83
+ const env = args.env ?? {};
84
+ const config = collectConfig(document, env);
85
+ return {
86
+ ctx: createActionContext({ host: args.host, ports: resolver, config }),
87
+ fragments: mergeFragments(collectFragments(document), args.moduleFragments),
88
+ imports: args.modules,
89
+ aliases: collectAliases(document, registry),
90
+ env,
91
+ };
92
+ }
93
+
94
+ /** Local fragments take precedence over imported ones of the same name. */
95
+ function mergeFragments(
96
+ local: Map<string, FragmentDecl>,
97
+ imported: Map<string, FragmentDecl> | undefined,
98
+ ): Map<string, FragmentDecl> {
99
+ if (!imported) return local;
100
+ const merged = new Map(imported);
101
+ for (const [name, fragment] of local) merged.set(name, fragment);
102
+ return merged;
103
+ }
@@ -0,0 +1,4 @@
1
+ export { createRunner } from "./create-runner.js";
2
+ export type { ModuleIo, NpmModules, ResolvedImports } from "./resolve-imports.js";
3
+ export { collectUses, resolveImports } from "./resolve-imports.js";
4
+ export type { Runner, RunnerArgs, RunResult } from "./runner.types.js";