@stackables/bridge-core 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +55 -0
  2. package/build/{ExecutionTree.d.ts → bridge-core/src/ExecutionTree.d.ts} +14 -13
  3. package/build/bridge-core/src/ExecutionTree.d.ts.map +1 -0
  4. package/build/{ExecutionTree.js → bridge-core/src/ExecutionTree.js} +155 -93
  5. package/build/{execute-bridge.d.ts → bridge-core/src/execute-bridge.d.ts} +19 -7
  6. package/build/bridge-core/src/execute-bridge.d.ts.map +1 -0
  7. package/build/{execute-bridge.js → bridge-core/src/execute-bridge.js} +13 -6
  8. package/build/{index.d.ts → bridge-core/src/index.d.ts} +4 -2
  9. package/build/bridge-core/src/index.d.ts.map +1 -0
  10. package/build/{index.js → bridge-core/src/index.js} +5 -1
  11. package/build/bridge-core/src/merge-documents.d.ts +25 -0
  12. package/build/bridge-core/src/merge-documents.d.ts.map +1 -0
  13. package/build/bridge-core/src/merge-documents.js +91 -0
  14. package/build/bridge-core/src/tools/index.d.ts.map +1 -0
  15. package/build/bridge-core/src/tools/internal.d.ts.map +1 -0
  16. package/build/{types.d.ts → bridge-core/src/types.d.ts} +28 -1
  17. package/build/bridge-core/src/types.d.ts.map +1 -0
  18. package/build/{types.js → bridge-core/src/types.js} +1 -0
  19. package/build/bridge-core/src/utils.d.ts.map +1 -0
  20. package/build/bridge-core/src/version-check.d.ts +64 -0
  21. package/build/bridge-core/src/version-check.d.ts.map +1 -0
  22. package/build/bridge-core/src/version-check.js +205 -0
  23. package/build/bridge-stdlib/src/index.d.ts +34 -0
  24. package/build/bridge-stdlib/src/index.d.ts.map +1 -0
  25. package/build/bridge-stdlib/src/index.js +40 -0
  26. package/build/bridge-stdlib/src/tools/arrays.d.ts +28 -0
  27. package/build/bridge-stdlib/src/tools/arrays.d.ts.map +1 -0
  28. package/build/bridge-stdlib/src/tools/arrays.js +50 -0
  29. package/build/bridge-stdlib/src/tools/audit.d.ts +36 -0
  30. package/build/bridge-stdlib/src/tools/audit.d.ts.map +1 -0
  31. package/build/bridge-stdlib/src/tools/audit.js +39 -0
  32. package/build/bridge-stdlib/src/tools/http-call.d.ts +35 -0
  33. package/build/bridge-stdlib/src/tools/http-call.d.ts.map +1 -0
  34. package/build/bridge-stdlib/src/tools/http-call.js +118 -0
  35. package/build/bridge-stdlib/src/tools/strings.d.ts +13 -0
  36. package/build/bridge-stdlib/src/tools/strings.d.ts.map +1 -0
  37. package/build/bridge-stdlib/src/tools/strings.js +12 -0
  38. package/build/bridge-types/src/index.d.ts +63 -0
  39. package/build/bridge-types/src/index.d.ts.map +1 -0
  40. package/build/bridge-types/src/index.js +8 -0
  41. package/package.json +6 -4
  42. package/build/ExecutionTree.d.ts.map +0 -1
  43. package/build/execute-bridge.d.ts.map +0 -1
  44. package/build/index.d.ts.map +0 -1
  45. package/build/tools/index.d.ts.map +0 -1
  46. package/build/tools/internal.d.ts.map +0 -1
  47. package/build/types.d.ts.map +0 -1
  48. package/build/utils.d.ts.map +0 -1
  49. /package/build/{tools → bridge-core/src/tools}/index.d.ts +0 -0
  50. /package/build/{tools → bridge-core/src/tools}/index.js +0 -0
  51. /package/build/{tools → bridge-core/src/tools}/internal.d.ts +0 -0
  52. /package/build/{tools → bridge-core/src/tools}/internal.js +0 -0
  53. /package/build/{utils.d.ts → bridge-core/src/utils.d.ts} +0 -0
  54. /package/build/{utils.js → bridge-core/src/utils.js} +0 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ [![github](https://img.shields.io/badge/github-stackables/bridge-blue?logo=github)](https://github.com/stackables/bridge)
2
+
3
+ # The Bridge Runtime
4
+
5
+ The lightweight runtime engine for [The Bridge](https://github.com/stackables/bridge).
6
+
7
+ This is **The Engine** — it takes pre-compiled bridge instructions (a JSON AST) and executes them. No parser, no GraphQL, no heavy dependencies. If you're deploying to a Cloudflare Worker, a Vercel Edge function, or any environment where bundle size matters, this is the package you want in production.
8
+
9
+ ## Installing
10
+
11
+ ```bash
12
+ npm install @stackables/bridge-core
13
+ ```
14
+
15
+ ## When to Use This
16
+
17
+ The most common pattern is the **Ahead-of-Time (AOT) workflow**: compile your `.bridge` files to JSON during CI/CD, then ship only the instructions + this engine to production. The parser and its dependencies never touch your production bundle.
18
+
19
+ ```ts
20
+ import { executeBridge } from "@stackables/bridge-core";
21
+ import instructions from "./compiled-bridge.json" assert { type: "json" };
22
+
23
+ const { data } = await executeBridge({
24
+ instructions,
25
+ operation: "Query.searchTrains",
26
+ input: { from: "Bern", to: "Zürich" },
27
+ });
28
+
29
+ console.log(data);
30
+ ```
31
+
32
+ ## Options
33
+
34
+ | Option | What it does |
35
+ | -------------- | -------------------------------------------------------- |
36
+ | `instructions` | Pre-compiled bridge instructions (from the compiler) |
37
+ | `operation` | Which bridge to run, e.g. `"Query.myField"` |
38
+ | `input` | Input arguments — like GraphQL field args |
39
+ | `tools` | Your custom tool functions (merged with built-in `std`) |
40
+ | `context` | Shared data available via `with context` in bridge files |
41
+ | `trace` | Tool-call tracing: `"off"`, `"basic"`, or `"full"` |
42
+ | `logger` | Plug in pino, winston, console — whatever you use |
43
+ | `signal` | Pass an `AbortSignal` to cancel execution mid-flight |
44
+
45
+ Returns `{ data, traces }`.
46
+
47
+ ## Part of the Bridge Ecosystem
48
+
49
+ | Package | What it does |
50
+ | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
51
+ | [`@stackables/bridge`](https://www.npmjs.com/package/@stackables/bridge) | **The All-in-One** — everything in a single install |
52
+ | [`@stackables/bridge-compiler`](https://www.npmjs.com/package/@stackables/bridge-compiler) | **The Parser** — turns `.bridge` text into the instructions this engine runs |
53
+ | [`@stackables/bridge-graphql`](https://www.npmjs.com/package/@stackables/bridge-graphql) | **The Adapter** — wires bridges into a GraphQL schema |
54
+ | [`@stackables/bridge-stdlib`](https://www.npmjs.com/package/@stackables/bridge-stdlib) | **The Standard Library** — httpCall, strings, arrays, and more |
55
+ | [`@stackables/bridge-types`](https://www.npmjs.com/package/@stackables/bridge-types) | **Shared Types** — `ToolCallFn`, `ToolMap`, `CacheStore` |
@@ -1,4 +1,4 @@
1
- import type { Bridge, Instruction, NodeRef, ToolMap, Wire } from "./types.ts";
1
+ import type { Bridge, BridgeDocument, ToolMap } from "./types.ts";
2
2
  /** Fatal panic error — bypasses all error boundaries (`?.` and `catch`). */
3
3
  export declare class BridgePanicError extends Error {
4
4
  constructor(message: string);
@@ -7,6 +7,8 @@ export declare class BridgePanicError extends Error {
7
7
  export declare class BridgeAbortError extends Error {
8
8
  constructor(message?: string);
9
9
  }
10
+ /** Maximum shadow-tree nesting depth before a BridgePanicError is thrown. */
11
+ export declare const MAX_EXECUTION_DEPTH = 30;
10
12
  /**
11
13
  * Structured logger interface for Bridge engine events.
12
14
  * Accepts any compatible logger: pino, winston, bunyan, `console`, etc.
@@ -74,7 +76,7 @@ export declare class TraceCollector {
74
76
  }
75
77
  export declare class ExecutionTree {
76
78
  trunk: Trunk;
77
- private instructions;
79
+ private document;
78
80
  private context?;
79
81
  private parent?;
80
82
  state: Record<string, any>;
@@ -82,6 +84,12 @@ export declare class ExecutionTree {
82
84
  private toolDepCache;
83
85
  private toolDefCache;
84
86
  private pipeHandleMap;
87
+ /**
88
+ * Maps trunk keys to `@version` strings from handle bindings.
89
+ * Populated in the constructor so `schedule()` can prefer versioned
90
+ * tool lookups (e.g. `std.str.toLowerCase@999.1`) over the default.
91
+ */
92
+ private handleVersionMap;
85
93
  /** Promise that resolves when all critical `force` handles have settled. */
86
94
  private forcedExecution?;
87
95
  /** Shared trace collector — present only when tracing is enabled. */
@@ -91,7 +99,9 @@ export declare class ExecutionTree {
91
99
  /** External abort signal — cancels execution when triggered. */
92
100
  signal?: AbortSignal;
93
101
  private toolFns?;
94
- constructor(trunk: Trunk, instructions: Instruction[], toolFns?: ToolMap, context?: Record<string, any> | undefined, parent?: ExecutionTree | undefined);
102
+ /** Shadow-tree nesting depth (0 for root). */
103
+ private depth;
104
+ constructor(trunk: Trunk, document: BridgeDocument, toolFns?: ToolMap, context?: Record<string, any> | undefined, parent?: ExecutionTree | undefined);
95
105
  /** Derive tool name from a trunk */
96
106
  private getToolName;
97
107
  /** Deep-lookup a tool function by dotted name (e.g. "std.str.toUpperCase").
@@ -105,7 +115,7 @@ export declare class ExecutionTree {
105
115
  private resolveToolSource;
106
116
  /** Call a tool dependency (cached per request) */
107
117
  private resolveToolDep;
108
- schedule(target: Trunk): any;
118
+ schedule(target: Trunk, pullChain?: Set<string>): any;
109
119
  /**
110
120
  * Invoke a tool function, recording both an OpenTelemetry span and (when
111
121
  * tracing is enabled) a ToolTrace entry. All three tool-call sites in the
@@ -116,15 +126,6 @@ export declare class ExecutionTree {
116
126
  /** Returns collected traces (empty array when tracing is disabled). */
117
127
  getTraces(): ToolTrace[];
118
128
  private pullSingle;
119
- pull(refs: NodeRef[]): Promise<any>;
120
- /**
121
- * Safe execution pull: wraps individual safe-flagged pulls in try/catch.
122
- * Wires with `safe: true` swallow errors and return undefined.
123
- * Non-safe wires propagate errors normally.
124
- */
125
- pullSafe(pulls: Extract<Wire, {
126
- from: NodeRef;
127
- }>[]): Promise<any>;
128
129
  push(args: Record<string, any>): void;
129
130
  /** Store the aggregated promise for critical forced handles so
130
131
  * `response()` can await it exactly once per bridge execution. */
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../../../src/ExecutionTree.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,8 @@ export class BridgeAbortError extends Error {
20
20
  const CONTINUE_SYM = Symbol.for("BRIDGE_CONTINUE");
21
21
  /** Sentinel for `break` — halt array iteration */
22
22
  const BREAK_SYM = Symbol.for("BRIDGE_BREAK");
23
+ /** Maximum shadow-tree nesting depth before a BridgePanicError is thrown. */
24
+ export const MAX_EXECUTION_DEPTH = 30;
23
25
  const otelTracer = trace.getTracer("@stackables/bridge");
24
26
  const otelMeter = metrics.getMeter("@stackables/bridge");
25
27
  const toolCallCounter = otelMeter.createCounter("bridge.tool.calls", {
@@ -154,7 +156,7 @@ function setNested(obj, path, value) {
154
156
  }
155
157
  export class ExecutionTree {
156
158
  trunk;
157
- instructions;
159
+ document;
158
160
  context;
159
161
  parent;
160
162
  state = {};
@@ -162,6 +164,12 @@ export class ExecutionTree {
162
164
  toolDepCache = new Map();
163
165
  toolDefCache = new Map();
164
166
  pipeHandleMap;
167
+ /**
168
+ * Maps trunk keys to `@version` strings from handle bindings.
169
+ * Populated in the constructor so `schedule()` can prefer versioned
170
+ * tool lookups (e.g. `std.str.toLowerCase@999.1`) over the default.
171
+ */
172
+ handleVersionMap = new Map();
165
173
  /** Promise that resolves when all critical `force` handles have settled. */
166
174
  forcedExecution;
167
175
  /** Shared trace collector — present only when tracing is enabled. */
@@ -171,16 +179,52 @@ export class ExecutionTree {
171
179
  /** External abort signal — cancels execution when triggered. */
172
180
  signal;
173
181
  toolFns;
174
- constructor(trunk, instructions, toolFns, context, parent) {
182
+ /** Shadow-tree nesting depth (0 for root). */
183
+ depth;
184
+ constructor(trunk, document, toolFns, context, parent) {
175
185
  this.trunk = trunk;
176
- this.instructions = instructions;
186
+ this.document = document;
177
187
  this.context = context;
178
188
  this.parent = parent;
189
+ this.depth = parent ? parent.depth + 1 : 0;
190
+ if (this.depth > MAX_EXECUTION_DEPTH) {
191
+ throw new BridgePanicError(`Maximum execution depth exceeded (${this.depth}) at ${trunkKey(trunk)}. Check for infinite recursion or circular array mappings.`);
192
+ }
179
193
  this.toolFns = { internal, ...(toolFns ?? {}) };
194
+ const instructions = document.instructions;
180
195
  this.bridge = instructions.find((i) => i.kind === "bridge" && i.type === trunk.type && i.field === trunk.field);
181
196
  if (this.bridge?.pipeHandles) {
182
197
  this.pipeHandleMap = new Map(this.bridge.pipeHandles.map((ph) => [ph.key, ph]));
183
198
  }
199
+ // Build handle→version map from bridge handle bindings
200
+ if (this.bridge) {
201
+ const instanceCounters = new Map();
202
+ for (const h of this.bridge.handles) {
203
+ if (h.kind !== "tool")
204
+ continue;
205
+ const name = h.name;
206
+ const lastDot = name.lastIndexOf(".");
207
+ let module, field, counterKey, type;
208
+ if (lastDot !== -1) {
209
+ module = name.substring(0, lastDot);
210
+ field = name.substring(lastDot + 1);
211
+ counterKey = `${module}:${field}`;
212
+ type = this.trunk.type;
213
+ }
214
+ else {
215
+ module = SELF_MODULE;
216
+ field = name;
217
+ counterKey = `Tools:${name}`;
218
+ type = "Tools";
219
+ }
220
+ const instance = (instanceCounters.get(counterKey) ?? 0) + 1;
221
+ instanceCounters.set(counterKey, instance);
222
+ if (h.version) {
223
+ const key = trunkKey({ module, type, field, instance });
224
+ this.handleVersionMap.set(key, h.version);
225
+ }
226
+ }
227
+ }
184
228
  if (context) {
185
229
  this.state[trunkKey({ module: SELF_MODULE, type: "Context", field: "context" })] = context;
186
230
  }
@@ -221,7 +265,35 @@ export class ExecutionTree {
221
265
  return current;
222
266
  // Fall back to flat key (e.g. "hereapi.geocode" as a literal property name)
223
267
  const flat = this.toolFns?.[name];
224
- return typeof flat === "function" ? flat : undefined;
268
+ if (typeof flat === "function")
269
+ return flat;
270
+ // Try versioned namespace keys (e.g. "std.str@999.1" → { toLowerCase })
271
+ // For "std.str.toLowerCase@999.1", check:
272
+ // toolFns["std.str@999.1"]?.toLowerCase
273
+ // toolFns["std@999.1"]?.str?.toLowerCase
274
+ const atIdx = name.lastIndexOf("@");
275
+ if (atIdx > 0) {
276
+ const baseName = name.substring(0, atIdx);
277
+ const version = name.substring(atIdx + 1);
278
+ const nameParts = baseName.split(".");
279
+ for (let i = nameParts.length - 1; i >= 1; i--) {
280
+ const nsKey = nameParts.slice(0, i).join(".") + "@" + version;
281
+ const remainder = nameParts.slice(i);
282
+ let ns = this.toolFns?.[nsKey];
283
+ if (ns != null && typeof ns === "object") {
284
+ for (const part of remainder) {
285
+ if (ns == null || typeof ns !== "object") {
286
+ ns = undefined;
287
+ break;
288
+ }
289
+ ns = ns[part];
290
+ }
291
+ if (typeof ns === "function")
292
+ return ns;
293
+ }
294
+ }
295
+ }
296
+ return undefined;
225
297
  }
226
298
  // Try root level first
227
299
  const fn = this.toolFns?.[name];
@@ -239,7 +311,7 @@ export class ExecutionTree {
239
311
  resolveToolDefByName(name) {
240
312
  if (this.toolDefCache.has(name))
241
313
  return this.toolDefCache.get(name) ?? undefined;
242
- const toolDefs = this.instructions.filter((i) => i.kind === "tool");
314
+ const toolDefs = this.document.instructions.filter((i) => i.kind === "tool");
243
315
  const base = toolDefs.find((t) => t.name === name);
244
316
  if (!base) {
245
317
  this.toolDefCache.set(name, null);
@@ -322,7 +394,6 @@ export class ExecutionTree {
322
394
  let value;
323
395
  if (dep.kind === "context") {
324
396
  // Walk the full parent chain for context
325
- // eslint-disable-next-line @typescript-eslint/no-this-alias
326
397
  let cursor = this;
327
398
  while (cursor && value === undefined) {
328
399
  value = cursor.context;
@@ -336,7 +407,6 @@ export class ExecutionTree {
336
407
  type: "Const",
337
408
  field: "const",
338
409
  });
339
- // eslint-disable-next-line @typescript-eslint/no-this-alias
340
410
  let cursor = this;
341
411
  while (cursor && value === undefined) {
342
412
  value = cursor.state[constKey];
@@ -383,7 +453,7 @@ export class ExecutionTree {
383
453
  this.toolDepCache.set(toolName, promise);
384
454
  return promise;
385
455
  }
386
- schedule(target) {
456
+ schedule(target, pullChain) {
387
457
  // Delegate to parent (shadow trees don't schedule directly) unless
388
458
  // the target fork has bridge wires sourced from element data,
389
459
  // or a __local binding whose source chain touches element data.
@@ -409,7 +479,7 @@ export class ExecutionTree {
409
479
  return (this.bridge?.wires.some((iw) => sameTrunk(iw.to, srcTrunk) && "from" in iw && !!iw.from.element) ?? false);
410
480
  });
411
481
  if (!hasElementSource && !hasTransitiveElementSource) {
412
- return this.parent.schedule(target);
482
+ return this.parent.schedule(target, pullChain);
413
483
  }
414
484
  }
415
485
  return (async () => {
@@ -449,7 +519,7 @@ export class ExecutionTree {
449
519
  }
450
520
  const groupEntries = Array.from(wireGroups.entries());
451
521
  const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
452
- const value = await this.resolveWires(group);
522
+ const value = await this.resolveWires(group, pullChain);
453
523
  return [group[0].to.path, value];
454
524
  }));
455
525
  for (const [path, value] of resolved) {
@@ -478,8 +548,21 @@ export class ExecutionTree {
478
548
  return this.resolveToolSource(onErrorWire.source, toolDef);
479
549
  }
480
550
  }
481
- // Direct tool function lookup by name (simple or dotted)
482
- const directFn = this.lookupToolFn(toolName);
551
+ // Direct tool function lookup by name (simple or dotted).
552
+ // When the handle carries a @version tag, try the versioned key first
553
+ // (e.g. "std.str.toLowerCase@999.1") so user-injected overrides win.
554
+ // For pipe forks, fall back to the baseTrunk's version since forks
555
+ // use synthetic instance numbers (100000+).
556
+ const handleVersion = this.handleVersionMap.get(trunkKey(target)) ??
557
+ (baseTrunk
558
+ ? this.handleVersionMap.get(trunkKey(baseTrunk))
559
+ : undefined);
560
+ let directFn = handleVersion
561
+ ? this.lookupToolFn(`${toolName}@${handleVersion}`)
562
+ : undefined;
563
+ if (!directFn) {
564
+ directFn = this.lookupToolFn(toolName);
565
+ }
483
566
  if (directFn) {
484
567
  return this.callTool(toolName, toolName, directFn, input);
485
568
  }
@@ -574,7 +657,7 @@ export class ExecutionTree {
574
657
  });
575
658
  }
576
659
  shadow() {
577
- const child = new ExecutionTree(this.trunk, this.instructions, this.toolFns, undefined, this);
660
+ const child = new ExecutionTree(this.trunk, this.document, this.toolFns, undefined, this);
578
661
  child.tracer = this.tracer;
579
662
  child.logger = this.logger;
580
663
  child.signal = this.signal;
@@ -584,17 +667,23 @@ export class ExecutionTree {
584
667
  getTraces() {
585
668
  return this.tracer?.traces ?? [];
586
669
  }
587
- async pullSingle(ref) {
670
+ async pullSingle(ref, pullChain = new Set()) {
588
671
  const key = trunkKey(ref);
672
+ // ── Cycle detection ─────────────────────────────────────────────
673
+ // If this exact key is already in our active pull chain, it is a
674
+ // circular dependency that would deadlock (await-on-self).
675
+ if (pullChain.has(key)) {
676
+ throw new BridgePanicError(`Circular dependency detected: "${key}" depends on itself`);
677
+ }
589
678
  // Walk the full parent chain — shadow trees may be nested multiple levels
590
679
  let value = undefined;
591
- // eslint-disable-next-line @typescript-eslint/no-this-alias
592
680
  let cursor = this;
593
681
  while (cursor && value === undefined) {
594
682
  value = cursor.state[key];
595
683
  cursor = cursor.parent;
596
684
  }
597
685
  if (value === undefined) {
686
+ const nextChain = new Set(pullChain).add(key);
598
687
  // ── Lazy define field resolution ────────────────────────────────
599
688
  // For define trunks (__define_in_* / __define_out_*) with a specific
600
689
  // field path, resolve ONLY the wire(s) targeting that field instead
@@ -604,10 +693,10 @@ export class ExecutionTree {
604
693
  if (ref.path.length > 0 && ref.module.startsWith("__define_")) {
605
694
  const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, ref) && pathEquals(w.to.path, ref.path)) ?? [];
606
695
  if (fieldWires.length > 0) {
607
- return this.resolveWires(fieldWires);
696
+ return this.resolveWires(fieldWires, nextChain);
608
697
  }
609
698
  }
610
- this.state[key] = this.schedule(ref);
699
+ this.state[key] = this.schedule(ref, nextChain);
611
700
  value = this.state[key];
612
701
  }
613
702
  // Always await in case the stored value is a Promise (e.g. from schedule()).
@@ -640,64 +729,6 @@ export class ExecutionTree {
640
729
  }
641
730
  return result;
642
731
  }
643
- async pull(refs) {
644
- if (refs.length === 1)
645
- return this.pullSingle(refs[0]);
646
- // Strict left-to-right sequential evaluation with short-circuit.
647
- // We respect the exact fallback priority authored by the developer.
648
- const errors = [];
649
- for (const ref of refs) {
650
- try {
651
- const value = await this.pullSingle(ref);
652
- if (value != null)
653
- return value; // Short-circuit: found data
654
- }
655
- catch (err) {
656
- errors.push(err);
657
- }
658
- }
659
- // All resolved to null/undefined, or all threw
660
- if (errors.length === refs.length) {
661
- throw new AggregateError(errors, "All sources failed");
662
- }
663
- return undefined;
664
- }
665
- /**
666
- * Safe execution pull: wraps individual safe-flagged pulls in try/catch.
667
- * Wires with `safe: true` swallow errors and return undefined.
668
- * Non-safe wires propagate errors normally.
669
- */
670
- async pullSafe(pulls) {
671
- if (pulls.length === 1) {
672
- const w = pulls[0];
673
- if (w.safe) {
674
- try {
675
- return await this.pullSingle(w.from);
676
- }
677
- catch {
678
- return undefined;
679
- }
680
- }
681
- return this.pullSingle(w.from);
682
- }
683
- const errors = [];
684
- for (const w of pulls) {
685
- try {
686
- const value = w.safe
687
- ? await this.pullSingle(w.from).catch(() => undefined)
688
- : await this.pullSingle(w.from);
689
- if (value != null)
690
- return value;
691
- }
692
- catch (err) {
693
- errors.push(err);
694
- }
695
- }
696
- if (errors.length === pulls.length) {
697
- throw new AggregateError(errors, "All sources failed");
698
- }
699
- return undefined;
700
- }
701
732
  push(args) {
702
733
  this.state[trunkKey(this.trunk)] = args;
703
734
  }
@@ -769,7 +800,7 @@ export class ExecutionTree {
769
800
  * After layers 1–2b, the overdefinition boundary (`!= null`) decides whether
770
801
  * to return or continue to the next wire.
771
802
  */
772
- async resolveWires(wires) {
803
+ async resolveWires(wires, pullChain) {
773
804
  let lastError;
774
805
  for (const w of wires) {
775
806
  // Constant wire — always wins, no modifiers
@@ -779,16 +810,16 @@ export class ExecutionTree {
779
810
  // --- Layer 1: Execution ---
780
811
  let resolvedValue;
781
812
  if ("cond" in w) {
782
- const condValue = await this.pullSingle(w.cond);
813
+ const condValue = await this.pullSingle(w.cond, pullChain);
783
814
  if (condValue) {
784
815
  if (w.thenRef !== undefined)
785
- resolvedValue = await this.pullSingle(w.thenRef);
816
+ resolvedValue = await this.pullSingle(w.thenRef, pullChain);
786
817
  else if (w.thenValue !== undefined)
787
818
  resolvedValue = coerceConstant(w.thenValue);
788
819
  }
789
820
  else {
790
821
  if (w.elseRef !== undefined)
791
- resolvedValue = await this.pullSingle(w.elseRef);
822
+ resolvedValue = await this.pullSingle(w.elseRef, pullChain);
792
823
  else if (w.elseValue !== undefined)
793
824
  resolvedValue = coerceConstant(w.elseValue);
794
825
  }
@@ -796,23 +827,23 @@ export class ExecutionTree {
796
827
  else if ("condAnd" in w) {
797
828
  const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condAnd;
798
829
  const leftVal = isSafe
799
- ? await this.pullSingle(leftRef).catch((e) => {
830
+ ? await this.pullSingle(leftRef, pullChain).catch((e) => {
800
831
  if (isFatalError(e))
801
832
  throw e;
802
833
  return undefined;
803
834
  })
804
- : await this.pullSingle(leftRef);
835
+ : await this.pullSingle(leftRef, pullChain);
805
836
  if (!leftVal) {
806
837
  resolvedValue = false;
807
838
  }
808
839
  else if (rightRef !== undefined) {
809
840
  const rightVal = rightSafe
810
- ? await this.pullSingle(rightRef).catch((e) => {
841
+ ? await this.pullSingle(rightRef, pullChain).catch((e) => {
811
842
  if (isFatalError(e))
812
843
  throw e;
813
844
  return undefined;
814
845
  })
815
- : await this.pullSingle(rightRef);
846
+ : await this.pullSingle(rightRef, pullChain);
816
847
  resolvedValue = Boolean(rightVal);
817
848
  }
818
849
  else if (rightValue !== undefined) {
@@ -825,23 +856,23 @@ export class ExecutionTree {
825
856
  else if ("condOr" in w) {
826
857
  const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condOr;
827
858
  const leftVal = isSafe
828
- ? await this.pullSingle(leftRef).catch((e) => {
859
+ ? await this.pullSingle(leftRef, pullChain).catch((e) => {
829
860
  if (isFatalError(e))
830
861
  throw e;
831
862
  return undefined;
832
863
  })
833
- : await this.pullSingle(leftRef);
864
+ : await this.pullSingle(leftRef, pullChain);
834
865
  if (leftVal) {
835
866
  resolvedValue = true;
836
867
  }
837
868
  else if (rightRef !== undefined) {
838
869
  const rightVal = rightSafe
839
- ? await this.pullSingle(rightRef).catch((e) => {
870
+ ? await this.pullSingle(rightRef, pullChain).catch((e) => {
840
871
  if (isFatalError(e))
841
872
  throw e;
842
873
  return undefined;
843
874
  })
844
- : await this.pullSingle(rightRef);
875
+ : await this.pullSingle(rightRef, pullChain);
845
876
  resolvedValue = Boolean(rightVal);
846
877
  }
847
878
  else if (rightValue !== undefined) {
@@ -854,7 +885,7 @@ export class ExecutionTree {
854
885
  else if ("from" in w) {
855
886
  if (w.safe) {
856
887
  try {
857
- resolvedValue = await this.pullSingle(w.from);
888
+ resolvedValue = await this.pullSingle(w.from, pullChain);
858
889
  }
859
890
  catch (err) {
860
891
  if (isFatalError(err))
@@ -863,7 +894,7 @@ export class ExecutionTree {
863
894
  }
864
895
  }
865
896
  else {
866
- resolvedValue = await this.pullSingle(w.from);
897
+ resolvedValue = await this.pullSingle(w.from, pullChain);
867
898
  }
868
899
  }
869
900
  else {
@@ -874,7 +905,7 @@ export class ExecutionTree {
874
905
  for (const ref of w.falsyFallbackRefs) {
875
906
  // Assign the fallback value regardless of whether it is truthy or falsy.
876
907
  // e.g. `false || 0` will correctly update resolvedValue to `0`.
877
- resolvedValue = await this.pullSingle(ref);
908
+ resolvedValue = await this.pullSingle(ref, pullChain);
878
909
  // If it is truthy, we are done! Short-circuit the || chain.
879
910
  if (resolvedValue)
880
911
  break;
@@ -894,7 +925,7 @@ export class ExecutionTree {
894
925
  resolvedValue = applyControlFlow(w.nullishControl);
895
926
  }
896
927
  else if (w.nullishFallbackRef) {
897
- resolvedValue = await this.pullSingle(w.nullishFallbackRef);
928
+ resolvedValue = await this.pullSingle(w.nullishFallbackRef, pullChain);
898
929
  }
899
930
  else if (w.nullishFallback != null) {
900
931
  resolvedValue = coerceConstant(w.nullishFallback);
@@ -911,7 +942,7 @@ export class ExecutionTree {
911
942
  if (w.catchControl)
912
943
  return applyControlFlow(w.catchControl);
913
944
  if (w.catchFallbackRef)
914
- return this.pullSingle(w.catchFallbackRef);
945
+ return this.pullSingle(w.catchFallbackRef, pullChain);
915
946
  if (w.catchFallback != null)
916
947
  return coerceConstant(w.catchFallback);
917
948
  lastError = err;
@@ -1024,9 +1055,40 @@ export class ExecutionTree {
1024
1055
  `Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).`);
1025
1056
  }
1026
1057
  const result = {};
1058
+ // Resolves a single output field at `prefix` — either via an exact-match
1059
+ // wire (leaf), or by collecting sub-fields from deeper wires (nested object).
1060
+ const resolveField = async (prefix) => {
1061
+ const exactWires = bridge.wires.filter((w) => w.to.module === SELF_MODULE &&
1062
+ w.to.type === type &&
1063
+ w.to.field === field &&
1064
+ pathEquals(w.to.path, prefix));
1065
+ if (exactWires.length > 0) {
1066
+ return this.resolveWires(exactWires);
1067
+ }
1068
+ // No exact wire — gather sub-field names from deeper-path wires
1069
+ // (e.g. `o.why { .temperature <- ... }` produces path ["why","temperature"])
1070
+ const subFields = new Set();
1071
+ for (const wire of bridge.wires) {
1072
+ const p = wire.to.path;
1073
+ if (wire.to.module === SELF_MODULE &&
1074
+ wire.to.type === type &&
1075
+ wire.to.field === field &&
1076
+ p.length > prefix.length &&
1077
+ prefix.every((seg, i) => p[i] === seg)) {
1078
+ subFields.add(p[prefix.length]);
1079
+ }
1080
+ }
1081
+ if (subFields.size === 0)
1082
+ return undefined;
1083
+ const obj = {};
1084
+ await Promise.all([...subFields].map(async (sub) => {
1085
+ obj[sub] = await resolveField([...prefix, sub]);
1086
+ }));
1087
+ return obj;
1088
+ };
1027
1089
  await Promise.all([
1028
1090
  ...[...outputFields].map(async (name) => {
1029
- result[name] = await this.pullOutputField([name]);
1091
+ result[name] = await resolveField([name]);
1030
1092
  }),
1031
1093
  ...forcePromises,
1032
1094
  ]);
@@ -1,8 +1,8 @@
1
1
  import type { Logger, ToolTrace, TraceLevel } from "./ExecutionTree.ts";
2
- import type { Instruction, ToolMap } from "./types.ts";
2
+ import type { BridgeDocument, ToolMap } from "./types.ts";
3
3
  export type ExecuteBridgeOptions = {
4
- /** Parsed bridge instructions (from `parseBridgeDiagnostics`). */
5
- instructions: Instruction[];
4
+ /** Parsed bridge document (from `parseBridge` or `parseBridgeDiagnostics`). */
5
+ document: BridgeDocument;
6
6
  /**
7
7
  * Which bridge to execute, as `"Type.field"`.
8
8
  * Mirrors the `bridge Type.field { ... }` declaration.
@@ -11,7 +11,19 @@ export type ExecuteBridgeOptions = {
11
11
  operation: string;
12
12
  /** Input arguments — equivalent to GraphQL field arguments. */
13
13
  input?: Record<string, unknown>;
14
- /** Additional tools to merge with the built-in `std` namespace. */
14
+ /**
15
+ * Tool functions available to the engine.
16
+ *
17
+ * Supports namespaced nesting: `{ myNamespace: { myTool } }`.
18
+ * The built-in `std` namespace is always included; user tools are
19
+ * merged on top (shallow).
20
+ *
21
+ * To provide a specific version of std (e.g. when the bridge file
22
+ * targets an older major), use a versioned namespace key:
23
+ * ```ts
24
+ * tools: { "std@1.5": oldStdNamespace }
25
+ * ```
26
+ */
15
27
  tools?: ToolMap;
16
28
  /** Context available via `with context as ctx` inside the bridge. */
17
29
  context?: Record<string, unknown>;
@@ -41,12 +53,12 @@ export type ExecuteBridgeResult<T = unknown> = {
41
53
  *
42
54
  * @example
43
55
  * ```ts
44
- * import { parseBridgeDiagnostics, executeBridge } from "@stackables/bridge";
56
+ * import { parseBridge, executeBridge } from "@stackables/bridge";
45
57
  * import { readFileSync } from "node:fs";
46
58
  *
47
- * const { instructions } = parseBridgeDiagnostics(readFileSync("my.bridge", "utf8"));
59
+ * const document = parseBridge(readFileSync("my.bridge", "utf8"));
48
60
  * const { data } = await executeBridge({
49
- * instructions,
61
+ * document,
50
62
  * operation: "Query.myField",
51
63
  * input: { city: "Berlin" },
52
64
  * });
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute-bridge.d.ts","sourceRoot":"","sources":["../../../src/execute-bridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAQ1D,MAAM,MAAM,oBAAoB,GAAG;IACjC,+EAA+E;IAC/E,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,CAAC,CAAC,GAAG,OAAO,IAAI;IAC7C,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,OAAO,EAC7C,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAyCjC"}