@stackables/bridge-compiler 1.0.1 → 1.0.3

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 (73) hide show
  1. package/build/bridge-format.d.ts.map +1 -0
  2. package/build/{bridge-compiler/src/bridge-lint.d.ts.map → bridge-lint.d.ts.map} +1 -1
  3. package/build/index.d.ts.map +1 -0
  4. package/build/language-service.d.ts.map +1 -0
  5. package/build/parser/index.d.ts.map +1 -0
  6. package/build/parser/lexer.d.ts.map +1 -0
  7. package/build/parser/parser.d.ts.map +1 -0
  8. package/package.json +3 -3
  9. package/build/bridge-compiler/src/bridge-format.d.ts.map +0 -1
  10. package/build/bridge-compiler/src/index.d.ts.map +0 -1
  11. package/build/bridge-compiler/src/language-service.d.ts.map +0 -1
  12. package/build/bridge-compiler/src/parser/index.d.ts.map +0 -1
  13. package/build/bridge-compiler/src/parser/lexer.d.ts.map +0 -1
  14. package/build/bridge-compiler/src/parser/parser.d.ts.map +0 -1
  15. package/build/bridge-core/src/ExecutionTree.d.ts +0 -213
  16. package/build/bridge-core/src/ExecutionTree.d.ts.map +0 -1
  17. package/build/bridge-core/src/ExecutionTree.js +0 -1323
  18. package/build/bridge-core/src/execute-bridge.d.ts +0 -69
  19. package/build/bridge-core/src/execute-bridge.d.ts.map +0 -1
  20. package/build/bridge-core/src/execute-bridge.js +0 -52
  21. package/build/bridge-core/src/index.d.ts +0 -18
  22. package/build/bridge-core/src/index.d.ts.map +0 -1
  23. package/build/bridge-core/src/index.js +0 -20
  24. package/build/bridge-core/src/merge-documents.d.ts +0 -25
  25. package/build/bridge-core/src/merge-documents.d.ts.map +0 -1
  26. package/build/bridge-core/src/merge-documents.js +0 -91
  27. package/build/bridge-core/src/tools/index.d.ts +0 -2
  28. package/build/bridge-core/src/tools/index.d.ts.map +0 -1
  29. package/build/bridge-core/src/tools/index.js +0 -1
  30. package/build/bridge-core/src/tools/internal.d.ts +0 -71
  31. package/build/bridge-core/src/tools/internal.d.ts.map +0 -1
  32. package/build/bridge-core/src/tools/internal.js +0 -59
  33. package/build/bridge-core/src/types.d.ts +0 -349
  34. package/build/bridge-core/src/types.d.ts.map +0 -1
  35. package/build/bridge-core/src/types.js +0 -3
  36. package/build/bridge-core/src/utils.d.ts +0 -9
  37. package/build/bridge-core/src/utils.d.ts.map +0 -1
  38. package/build/bridge-core/src/utils.js +0 -23
  39. package/build/bridge-core/src/version-check.d.ts +0 -64
  40. package/build/bridge-core/src/version-check.d.ts.map +0 -1
  41. package/build/bridge-core/src/version-check.js +0 -205
  42. package/build/bridge-stdlib/src/index.d.ts +0 -34
  43. package/build/bridge-stdlib/src/index.d.ts.map +0 -1
  44. package/build/bridge-stdlib/src/index.js +0 -40
  45. package/build/bridge-stdlib/src/tools/arrays.d.ts +0 -28
  46. package/build/bridge-stdlib/src/tools/arrays.d.ts.map +0 -1
  47. package/build/bridge-stdlib/src/tools/arrays.js +0 -50
  48. package/build/bridge-stdlib/src/tools/audit.d.ts +0 -36
  49. package/build/bridge-stdlib/src/tools/audit.d.ts.map +0 -1
  50. package/build/bridge-stdlib/src/tools/audit.js +0 -39
  51. package/build/bridge-stdlib/src/tools/http-call.d.ts +0 -35
  52. package/build/bridge-stdlib/src/tools/http-call.d.ts.map +0 -1
  53. package/build/bridge-stdlib/src/tools/http-call.js +0 -118
  54. package/build/bridge-stdlib/src/tools/strings.d.ts +0 -13
  55. package/build/bridge-stdlib/src/tools/strings.d.ts.map +0 -1
  56. package/build/bridge-stdlib/src/tools/strings.js +0 -12
  57. package/build/bridge-types/src/index.d.ts +0 -63
  58. package/build/bridge-types/src/index.d.ts.map +0 -1
  59. package/build/bridge-types/src/index.js +0 -8
  60. /package/build/{bridge-compiler/src/bridge-format.d.ts → bridge-format.d.ts} +0 -0
  61. /package/build/{bridge-compiler/src/bridge-format.js → bridge-format.js} +0 -0
  62. /package/build/{bridge-compiler/src/bridge-lint.d.ts → bridge-lint.d.ts} +0 -0
  63. /package/build/{bridge-compiler/src/bridge-lint.js → bridge-lint.js} +0 -0
  64. /package/build/{bridge-compiler/src/index.d.ts → index.d.ts} +0 -0
  65. /package/build/{bridge-compiler/src/index.js → index.js} +0 -0
  66. /package/build/{bridge-compiler/src/language-service.d.ts → language-service.d.ts} +0 -0
  67. /package/build/{bridge-compiler/src/language-service.js → language-service.js} +0 -0
  68. /package/build/{bridge-compiler/src/parser → parser}/index.d.ts +0 -0
  69. /package/build/{bridge-compiler/src/parser → parser}/index.js +0 -0
  70. /package/build/{bridge-compiler/src/parser → parser}/lexer.d.ts +0 -0
  71. /package/build/{bridge-compiler/src/parser → parser}/lexer.js +0 -0
  72. /package/build/{bridge-compiler/src/parser → parser}/parser.d.ts +0 -0
  73. /package/build/{bridge-compiler/src/parser → parser}/parser.js +0 -0
@@ -1,1323 +0,0 @@
1
- import { SpanStatusCode, metrics, trace } from "@opentelemetry/api";
2
- import { parsePath } from "./utils.js";
3
- import { internal } from "./tools/index.js";
4
- import { SELF_MODULE } from "./types.js";
5
- /** Fatal panic error — bypasses all error boundaries (`?.` and `catch`). */
6
- export class BridgePanicError extends Error {
7
- constructor(message) {
8
- super(message);
9
- this.name = "BridgePanicError";
10
- }
11
- }
12
- /** Abort error — raised when an external AbortSignal cancels execution. */
13
- export class BridgeAbortError extends Error {
14
- constructor(message = "Execution aborted by external signal") {
15
- super(message);
16
- this.name = "BridgeAbortError";
17
- }
18
- }
19
- /** Sentinel for `continue` — skip the current array element */
20
- const CONTINUE_SYM = Symbol.for("BRIDGE_CONTINUE");
21
- /** Sentinel for `break` — halt array iteration */
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;
25
- const otelTracer = trace.getTracer("@stackables/bridge");
26
- const otelMeter = metrics.getMeter("@stackables/bridge");
27
- const toolCallCounter = otelMeter.createCounter("bridge.tool.calls", {
28
- description: "Total number of tool invocations",
29
- });
30
- const toolDurationHistogram = otelMeter.createHistogram("bridge.tool.duration", {
31
- description: "Tool call duration in milliseconds",
32
- unit: "ms",
33
- });
34
- const toolErrorCounter = otelMeter.createCounter("bridge.tool.errors", {
35
- description: "Total number of tool invocation errors",
36
- });
37
- /** Round milliseconds to 2 decimal places */
38
- function roundMs(ms) {
39
- return Math.round(ms * 100) / 100;
40
- }
41
- /** Stable string key for the state map */
42
- function trunkKey(ref) {
43
- if (ref.element)
44
- return `${ref.module}:${ref.type}:${ref.field}:*`;
45
- return `${ref.module}:${ref.type}:${ref.field}${ref.instance != null ? `:${ref.instance}` : ""}`;
46
- }
47
- /** Match two trunks (ignoring path and element) */
48
- function sameTrunk(a, b) {
49
- return (a.module === b.module &&
50
- a.type === b.type &&
51
- a.field === b.field &&
52
- (a.instance ?? undefined) === (b.instance ?? undefined));
53
- }
54
- /** Strict path equality */
55
- function pathEquals(a, b) {
56
- return a.length === b.length && a.every((v, i) => v === b[i]);
57
- }
58
- /** Check whether an error is a fatal halt (abort or panic) that must bypass all error boundaries. */
59
- function isFatalError(err) {
60
- return (err instanceof BridgePanicError ||
61
- err instanceof BridgeAbortError ||
62
- err?.name === "BridgeAbortError" ||
63
- err?.name === "BridgePanicError");
64
- }
65
- /** Execute a control flow instruction, returning a sentinel or throwing. */
66
- function applyControlFlow(ctrl) {
67
- if (ctrl.kind === "throw")
68
- throw new Error(ctrl.message);
69
- if (ctrl.kind === "panic")
70
- throw new BridgePanicError(ctrl.message);
71
- if (ctrl.kind === "continue")
72
- return CONTINUE_SYM;
73
- /* ctrl.kind === "break" */
74
- return BREAK_SYM;
75
- }
76
- /** Shared trace collector — one per request, passed through the tree. */
77
- export class TraceCollector {
78
- traces = [];
79
- level;
80
- epoch = performance.now();
81
- constructor(level = "full") {
82
- this.level = level;
83
- }
84
- /** Returns ms since the collector was created */
85
- now() {
86
- return roundMs(performance.now() - this.epoch);
87
- }
88
- record(trace) {
89
- this.traces.push(trace);
90
- }
91
- /** Build a trace entry, omitting input/output for basic level. */
92
- entry(base) {
93
- if (this.level === "basic") {
94
- const t = {
95
- tool: base.tool,
96
- fn: base.fn,
97
- durationMs: base.durationMs,
98
- startedAt: base.startedAt,
99
- };
100
- if (base.error)
101
- t.error = base.error;
102
- return t;
103
- }
104
- // full
105
- const t = {
106
- tool: base.tool,
107
- fn: base.fn,
108
- durationMs: base.durationMs,
109
- startedAt: base.startedAt,
110
- };
111
- if (base.input)
112
- t.input = structuredClone(base.input);
113
- if (base.error)
114
- t.error = base.error;
115
- else if (base.output !== undefined)
116
- t.output = base.output;
117
- return t;
118
- }
119
- }
120
- /** Set a value at a nested path, creating intermediate objects/arrays as needed */
121
- /**
122
- * Coerce a constant wire value string to its proper JS type.
123
- *
124
- * The parser stores all bare constants as strings (because the Wire type
125
- * uses `value: string`). JSON.parse recovers the original type:
126
- * "true" → true, "false" → false, "null" → null, "42" → 42
127
- * Plain strings that aren't valid JSON (like "hello", "/search") fall
128
- * through and are returned as-is.
129
- */
130
- function coerceConstant(raw) {
131
- try {
132
- return JSON.parse(raw);
133
- }
134
- catch {
135
- return raw;
136
- }
137
- }
138
- const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
139
- function setNested(obj, path, value) {
140
- for (let i = 0; i < path.length - 1; i++) {
141
- const key = path[i];
142
- if (UNSAFE_KEYS.has(key))
143
- throw new Error(`Unsafe assignment key: ${key}`);
144
- const nextKey = path[i + 1];
145
- if (obj[key] == null) {
146
- obj[key] = /^\d+$/.test(nextKey) ? [] : {};
147
- }
148
- obj = obj[key];
149
- }
150
- if (path.length > 0) {
151
- const finalKey = path[path.length - 1];
152
- if (UNSAFE_KEYS.has(finalKey))
153
- throw new Error(`Unsafe assignment key: ${finalKey}`);
154
- obj[finalKey] = value;
155
- }
156
- }
157
- export class ExecutionTree {
158
- trunk;
159
- document;
160
- context;
161
- parent;
162
- state = {};
163
- bridge;
164
- toolDepCache = new Map();
165
- toolDefCache = new Map();
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();
173
- /** Promise that resolves when all critical `force` handles have settled. */
174
- forcedExecution;
175
- /** Shared trace collector — present only when tracing is enabled. */
176
- tracer;
177
- /** Structured logger passed from BridgeOptions. Defaults to no-ops. */
178
- logger;
179
- /** External abort signal — cancels execution when triggered. */
180
- signal;
181
- toolFns;
182
- /** Shadow-tree nesting depth (0 for root). */
183
- depth;
184
- constructor(trunk, document, toolFns, context, parent) {
185
- this.trunk = trunk;
186
- this.document = document;
187
- this.context = context;
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
- }
193
- this.toolFns = { internal, ...(toolFns ?? {}) };
194
- const instructions = document.instructions;
195
- this.bridge = instructions.find((i) => i.kind === "bridge" && i.type === trunk.type && i.field === trunk.field);
196
- if (this.bridge?.pipeHandles) {
197
- this.pipeHandleMap = new Map(this.bridge.pipeHandles.map((ph) => [ph.key, ph]));
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
- }
228
- if (context) {
229
- this.state[trunkKey({ module: SELF_MODULE, type: "Context", field: "context" })] = context;
230
- }
231
- // Collect const definitions into a single namespace object
232
- const constObj = {};
233
- for (const inst of instructions) {
234
- if (inst.kind === "const") {
235
- constObj[inst.name] = JSON.parse(inst.value);
236
- }
237
- }
238
- if (Object.keys(constObj).length > 0) {
239
- this.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })] = constObj;
240
- }
241
- }
242
- /** Derive tool name from a trunk */
243
- getToolName(target) {
244
- if (target.module === SELF_MODULE)
245
- return target.field;
246
- return `${target.module}.${target.field}`;
247
- }
248
- /** Deep-lookup a tool function by dotted name (e.g. "std.str.toUpperCase").
249
- * Falls back to a flat key lookup for backward compat (e.g. "hereapi.geocode" as literal key). */
250
- lookupToolFn(name) {
251
- if (name.includes(".")) {
252
- // Try namespace traversal first
253
- const parts = name.split(".");
254
- let current = this.toolFns;
255
- for (const part of parts) {
256
- if (UNSAFE_KEYS.has(part))
257
- return undefined;
258
- if (current == null || typeof current !== "object") {
259
- current = undefined;
260
- break;
261
- }
262
- current = current[part];
263
- }
264
- if (typeof current === "function")
265
- return current;
266
- // Fall back to flat key (e.g. "hereapi.geocode" as a literal property name)
267
- const flat = this.toolFns?.[name];
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;
297
- }
298
- // Try root level first
299
- const fn = this.toolFns?.[name];
300
- if (typeof fn === "function")
301
- return fn;
302
- // Fall back to std namespace (builtins are callable without std. prefix)
303
- const stdFn = this.toolFns?.std?.[name];
304
- if (typeof stdFn === "function")
305
- return stdFn;
306
- // Fall back to internal namespace (engine-internal tools: math ops, concat, etc.)
307
- const internalFn = this.toolFns?.internal?.[name];
308
- return typeof internalFn === "function" ? internalFn : undefined;
309
- }
310
- /** Resolve a ToolDef by name, merging the extends chain (cached) */
311
- resolveToolDefByName(name) {
312
- if (this.toolDefCache.has(name))
313
- return this.toolDefCache.get(name) ?? undefined;
314
- const toolDefs = this.document.instructions.filter((i) => i.kind === "tool");
315
- const base = toolDefs.find((t) => t.name === name);
316
- if (!base) {
317
- this.toolDefCache.set(name, null);
318
- return undefined;
319
- }
320
- // Build extends chain: root → ... → leaf
321
- const chain = [base];
322
- let current = base;
323
- while (current.extends) {
324
- const parent = toolDefs.find((t) => t.name === current.extends);
325
- if (!parent)
326
- throw new Error(`Tool "${current.name}" extends unknown tool "${current.extends}"`);
327
- chain.unshift(parent);
328
- current = parent;
329
- }
330
- // Merge: root provides base, each child overrides
331
- const merged = {
332
- kind: "tool",
333
- name,
334
- fn: chain[0].fn, // fn from root ancestor
335
- deps: [],
336
- wires: [],
337
- };
338
- for (const def of chain) {
339
- // Merge deps (dedupe by handle)
340
- for (const dep of def.deps) {
341
- if (!merged.deps.some((d) => d.handle === dep.handle)) {
342
- merged.deps.push(dep);
343
- }
344
- }
345
- // Merge wires (child overrides parent by target; onError replaces onError)
346
- for (const wire of def.wires) {
347
- if (wire.kind === "onError") {
348
- const idx = merged.wires.findIndex((w) => w.kind === "onError");
349
- if (idx >= 0)
350
- merged.wires[idx] = wire;
351
- else
352
- merged.wires.push(wire);
353
- }
354
- else {
355
- const idx = merged.wires.findIndex((w) => "target" in w && w.target === wire.target);
356
- if (idx >= 0)
357
- merged.wires[idx] = wire;
358
- else
359
- merged.wires.push(wire);
360
- }
361
- }
362
- }
363
- this.toolDefCache.set(name, merged);
364
- return merged;
365
- }
366
- /** Resolve a tool definition's wires into a nested input object */
367
- async resolveToolWires(toolDef, input) {
368
- // Constants applied synchronously
369
- for (const wire of toolDef.wires) {
370
- if (wire.kind === "constant") {
371
- setNested(input, parsePath(wire.target), coerceConstant(wire.value));
372
- }
373
- }
374
- // Pull wires resolved in parallel (independent deps shouldn't wait on each other)
375
- const pullWires = toolDef.wires.filter((w) => w.kind === "pull");
376
- if (pullWires.length > 0) {
377
- const resolved = await Promise.all(pullWires.map(async (wire) => ({
378
- target: wire.target,
379
- value: await this.resolveToolSource(wire.source, toolDef),
380
- })));
381
- for (const { target, value } of resolved) {
382
- setNested(input, parsePath(target), value);
383
- }
384
- }
385
- }
386
- /** Resolve a source reference from a tool wire against its dependencies */
387
- async resolveToolSource(source, toolDef) {
388
- const dotIdx = source.indexOf(".");
389
- const handle = dotIdx === -1 ? source : source.substring(0, dotIdx);
390
- const restPath = dotIdx === -1 ? [] : source.substring(dotIdx + 1).split(".");
391
- const dep = toolDef.deps.find((d) => d.handle === handle);
392
- if (!dep)
393
- throw new Error(`Unknown source "${handle}" in tool "${toolDef.name}"`);
394
- let value;
395
- if (dep.kind === "context") {
396
- // Walk the full parent chain for context
397
- let cursor = this;
398
- while (cursor && value === undefined) {
399
- value = cursor.context;
400
- cursor = cursor.parent;
401
- }
402
- }
403
- else if (dep.kind === "const") {
404
- // Walk the full parent chain for const state
405
- const constKey = trunkKey({
406
- module: SELF_MODULE,
407
- type: "Const",
408
- field: "const",
409
- });
410
- let cursor = this;
411
- while (cursor && value === undefined) {
412
- value = cursor.state[constKey];
413
- cursor = cursor.parent;
414
- }
415
- }
416
- else if (dep.kind === "tool") {
417
- value = await this.resolveToolDep(dep.tool);
418
- }
419
- for (const segment of restPath) {
420
- value = value?.[segment];
421
- }
422
- return value;
423
- }
424
- /** Call a tool dependency (cached per request) */
425
- resolveToolDep(toolName) {
426
- // Check parent first (shadow trees delegate)
427
- if (this.parent)
428
- return this.parent.resolveToolDep(toolName);
429
- if (this.toolDepCache.has(toolName))
430
- return this.toolDepCache.get(toolName);
431
- const promise = (async () => {
432
- const toolDef = this.resolveToolDefByName(toolName);
433
- if (!toolDef)
434
- throw new Error(`Tool dependency "${toolName}" not found`);
435
- const input = {};
436
- await this.resolveToolWires(toolDef, input);
437
- const fn = this.lookupToolFn(toolDef.fn);
438
- if (!fn)
439
- throw new Error(`Tool function "${toolDef.fn}" not registered`);
440
- // on error: wrap the tool call with fallback from onError wire
441
- const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
442
- try {
443
- return await this.callTool(toolName, toolDef.fn, fn, input);
444
- }
445
- catch (err) {
446
- if (!onErrorWire)
447
- throw err;
448
- if ("value" in onErrorWire)
449
- return JSON.parse(onErrorWire.value);
450
- return this.resolveToolSource(onErrorWire.source, toolDef);
451
- }
452
- })();
453
- this.toolDepCache.set(toolName, promise);
454
- return promise;
455
- }
456
- schedule(target, pullChain) {
457
- // Delegate to parent (shadow trees don't schedule directly) unless
458
- // the target fork has bridge wires sourced from element data,
459
- // or a __local binding whose source chain touches element data.
460
- if (this.parent) {
461
- const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
462
- const hasElementSource = forkWires.some((w) => ("from" in w && !!w.from.element) ||
463
- ("condAnd" in w &&
464
- (!!w.condAnd.leftRef.element || !!w.condAnd.rightRef?.element)) ||
465
- ("condOr" in w &&
466
- (!!w.condOr.leftRef.element || !!w.condOr.rightRef?.element)));
467
- // For __local trunks, also check transitively: if the source is a
468
- // pipe fork whose own wires reference element data, keep it local.
469
- const hasTransitiveElementSource = target.module === "__local" &&
470
- forkWires.some((w) => {
471
- if (!("from" in w))
472
- return false;
473
- const srcTrunk = {
474
- module: w.from.module,
475
- type: w.from.type,
476
- field: w.from.field,
477
- instance: w.from.instance,
478
- };
479
- return (this.bridge?.wires.some((iw) => sameTrunk(iw.to, srcTrunk) && "from" in iw && !!iw.from.element) ?? false);
480
- });
481
- if (!hasElementSource && !hasTransitiveElementSource) {
482
- return this.parent.schedule(target, pullChain);
483
- }
484
- }
485
- return (async () => {
486
- // If this target is a pipe fork, also apply bridge wires from its base
487
- // handle (non-pipe wires, e.g. `c.currency <- i.currency`) as defaults
488
- // before the fork-specific pipe wires.
489
- const targetKey = trunkKey(target);
490
- const pipeFork = this.pipeHandleMap?.get(targetKey);
491
- const baseTrunk = pipeFork?.baseTrunk;
492
- const baseWires = baseTrunk
493
- ? (this.bridge?.wires.filter((w) => !("pipe" in w) && sameTrunk(w.to, baseTrunk)) ?? [])
494
- : [];
495
- // Fork-specific wires (pipe wires targeting the fork's own instance)
496
- const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
497
- // Merge: base provides defaults, fork overrides
498
- const bridgeWires = [...baseWires, ...forkWires];
499
- // Look up ToolDef for this target
500
- const toolName = this.getToolName(target);
501
- const toolDef = this.resolveToolDefByName(toolName);
502
- // Build input object: tool wires first (base), then bridge wires (override)
503
- const input = {};
504
- if (toolDef) {
505
- await this.resolveToolWires(toolDef, input);
506
- }
507
- // Resolve bridge wires and apply on top.
508
- // Group wires by target path so that || (null-fallback) and ??
509
- // (error-fallback) semantics are honoured via resolveWires().
510
- const wireGroups = new Map();
511
- for (const w of bridgeWires) {
512
- const key = w.to.path.join(".");
513
- let group = wireGroups.get(key);
514
- if (!group) {
515
- group = [];
516
- wireGroups.set(key, group);
517
- }
518
- group.push(w);
519
- }
520
- const groupEntries = Array.from(wireGroups.entries());
521
- const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
522
- const value = await this.resolveWires(group, pullChain);
523
- return [group[0].to.path, value];
524
- }));
525
- for (const [path, value] of resolved) {
526
- if (path.length === 0 && value != null && typeof value === "object") {
527
- Object.assign(input, value);
528
- }
529
- else {
530
- setNested(input, path, value);
531
- }
532
- }
533
- // Call ToolDef-backed tool function
534
- if (toolDef) {
535
- const fn = this.lookupToolFn(toolDef.fn);
536
- if (!fn)
537
- throw new Error(`Tool function "${toolDef.fn}" not registered`);
538
- // on error: wrap the tool call with fallback from onError wire
539
- const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
540
- try {
541
- return await this.callTool(toolName, toolDef.fn, fn, input);
542
- }
543
- catch (err) {
544
- if (!onErrorWire)
545
- throw err;
546
- if ("value" in onErrorWire)
547
- return JSON.parse(onErrorWire.value);
548
- return this.resolveToolSource(onErrorWire.source, toolDef);
549
- }
550
- }
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
- }
566
- if (directFn) {
567
- return this.callTool(toolName, toolName, directFn, input);
568
- }
569
- // Define pass-through: synthetic trunks created by define inlining
570
- // act as data containers — bridge wires set their values, no tool needed.
571
- if (target.module.startsWith("__define_")) {
572
- return input;
573
- }
574
- // Local binding or logic node: the wire resolves the source and stores
575
- // the result — no tool call needed. For path=[] wires the resolved
576
- // value may be a primitive (boolean from condAnd/condOr, string from
577
- // a pipe tool like upperCase), so return the resolved value directly.
578
- if (target.module === "__local" ||
579
- target.field === "__and" ||
580
- target.field === "__or") {
581
- for (const [path, value] of resolved) {
582
- if (path.length === 0)
583
- return value;
584
- }
585
- return input;
586
- }
587
- throw new Error(`No tool found for "${toolName}"`);
588
- })();
589
- }
590
- /**
591
- * Invoke a tool function, recording both an OpenTelemetry span and (when
592
- * tracing is enabled) a ToolTrace entry. All three tool-call sites in the
593
- * engine delegate here so instrumentation lives in exactly one place.
594
- */
595
- async callTool(toolName, fnName, fnImpl, input) {
596
- // Short-circuit before starting if externally aborted
597
- if (this.signal?.aborted) {
598
- throw new BridgeAbortError();
599
- }
600
- const tracer = this.tracer;
601
- const logger = this.logger;
602
- const toolContext = {
603
- logger: logger ?? {},
604
- signal: this.signal,
605
- };
606
- const traceStart = tracer?.now();
607
- const metricAttrs = {
608
- "bridge.tool.name": toolName,
609
- "bridge.tool.fn": fnName,
610
- };
611
- return otelTracer.startActiveSpan(`bridge.tool.${toolName}.${fnName}`, { attributes: metricAttrs }, async (span) => {
612
- const wallStart = performance.now();
613
- try {
614
- const result = await fnImpl(input, toolContext);
615
- const durationMs = roundMs(performance.now() - wallStart);
616
- toolCallCounter.add(1, metricAttrs);
617
- toolDurationHistogram.record(durationMs, metricAttrs);
618
- if (tracer && traceStart != null) {
619
- tracer.record(tracer.entry({
620
- tool: toolName,
621
- fn: fnName,
622
- input,
623
- output: result,
624
- durationMs: roundMs(tracer.now() - traceStart),
625
- startedAt: traceStart,
626
- }));
627
- }
628
- logger?.debug?.("[bridge] tool %s (%s) completed in %dms", toolName, fnName, durationMs);
629
- return result;
630
- }
631
- catch (err) {
632
- const durationMs = roundMs(performance.now() - wallStart);
633
- toolCallCounter.add(1, metricAttrs);
634
- toolDurationHistogram.record(durationMs, metricAttrs);
635
- toolErrorCounter.add(1, metricAttrs);
636
- if (tracer && traceStart != null) {
637
- tracer.record(tracer.entry({
638
- tool: toolName,
639
- fn: fnName,
640
- input,
641
- error: err.message,
642
- durationMs: roundMs(tracer.now() - traceStart),
643
- startedAt: traceStart,
644
- }));
645
- }
646
- span.recordException(err);
647
- span.setStatus({
648
- code: SpanStatusCode.ERROR,
649
- message: err.message,
650
- });
651
- logger?.error?.("[bridge] tool %s (%s) failed: %s", toolName, fnName, err.message);
652
- throw err;
653
- }
654
- finally {
655
- span.end();
656
- }
657
- });
658
- }
659
- shadow() {
660
- const child = new ExecutionTree(this.trunk, this.document, this.toolFns, undefined, this);
661
- child.tracer = this.tracer;
662
- child.logger = this.logger;
663
- child.signal = this.signal;
664
- return child;
665
- }
666
- /** Returns collected traces (empty array when tracing is disabled). */
667
- getTraces() {
668
- return this.tracer?.traces ?? [];
669
- }
670
- async pullSingle(ref, pullChain = new Set()) {
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
- }
678
- // Walk the full parent chain — shadow trees may be nested multiple levels
679
- let value = undefined;
680
- let cursor = this;
681
- while (cursor && value === undefined) {
682
- value = cursor.state[key];
683
- cursor = cursor.parent;
684
- }
685
- if (value === undefined) {
686
- const nextChain = new Set(pullChain).add(key);
687
- // ── Lazy define field resolution ────────────────────────────────
688
- // For define trunks (__define_in_* / __define_out_*) with a specific
689
- // field path, resolve ONLY the wire(s) targeting that field instead
690
- // of scheduling the entire trunk. This avoids triggering unrelated
691
- // dependency chains (e.g. requesting "city" should not fire the
692
- // lat/lon coalesce chains that call the geo tool).
693
- if (ref.path.length > 0 && ref.module.startsWith("__define_")) {
694
- const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, ref) && pathEquals(w.to.path, ref.path)) ?? [];
695
- if (fieldWires.length > 0) {
696
- return this.resolveWires(fieldWires, nextChain);
697
- }
698
- }
699
- this.state[key] = this.schedule(ref, nextChain);
700
- value = this.state[key];
701
- }
702
- // Always await in case the stored value is a Promise (e.g. from schedule()).
703
- const resolved = await Promise.resolve(value);
704
- if (!ref.path.length) {
705
- return resolved;
706
- }
707
- let result = resolved;
708
- // Root-level null check: if root data is null/undefined
709
- if (result == null && ref.path.length > 0) {
710
- if (ref.rootSafe)
711
- return undefined;
712
- throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[0]}')`);
713
- }
714
- for (let i = 0; i < ref.path.length; i++) {
715
- const segment = ref.path[i];
716
- if (UNSAFE_KEYS.has(segment))
717
- throw new Error(`Unsafe property traversal: ${segment}`);
718
- if (Array.isArray(result) && !/^\d+$/.test(segment)) {
719
- this.logger?.warn?.(`[bridge] Accessing ".${segment}" on an array (${result.length} items) — did you mean to use pickFirst or array mapping? Source: ${trunkKey(ref)}.${ref.path.join(".")}`);
720
- }
721
- result = result[segment];
722
- // Check for null/undefined AFTER access, before next segment
723
- if (result == null && i < ref.path.length - 1) {
724
- const nextSafe = ref.pathSafe?.[i + 1] ?? false;
725
- if (nextSafe)
726
- return undefined;
727
- throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[i + 1]}')`);
728
- }
729
- }
730
- return result;
731
- }
732
- push(args) {
733
- this.state[trunkKey(this.trunk)] = args;
734
- }
735
- /** Store the aggregated promise for critical forced handles so
736
- * `response()` can await it exactly once per bridge execution. */
737
- setForcedExecution(p) {
738
- this.forcedExecution = p;
739
- }
740
- /** Return the critical forced-execution promise (if any). */
741
- getForcedExecution() {
742
- return this.forcedExecution;
743
- }
744
- /**
745
- * Eagerly schedule tools targeted by `force <handle>` statements.
746
- *
747
- * Returns an array of promises for **critical** forced handles (those
748
- * without `?? null`). Fire-and-forget handles (`catchError: true`) are
749
- * scheduled but their errors are silently suppressed.
750
- *
751
- * Callers must `await Promise.all(...)` the returned promises so that a
752
- * critical force failure propagates as a standard error.
753
- */
754
- executeForced() {
755
- const forces = this.bridge?.forces;
756
- if (!forces || forces.length === 0)
757
- return [];
758
- const critical = [];
759
- const scheduled = new Set();
760
- for (const f of forces) {
761
- const trunk = {
762
- module: f.module,
763
- type: f.type,
764
- field: f.field,
765
- instance: f.instance,
766
- };
767
- const key = trunkKey(trunk);
768
- if (scheduled.has(key) || this.state[key] !== undefined)
769
- continue;
770
- scheduled.add(key);
771
- this.state[key] = this.schedule(trunk);
772
- if (f.catchError) {
773
- // Fire-and-forget: suppress unhandled rejection.
774
- Promise.resolve(this.state[key]).catch(() => { });
775
- }
776
- else {
777
- // Critical: caller must await and let failure propagate.
778
- critical.push(Promise.resolve(this.state[key]));
779
- }
780
- }
781
- return critical;
782
- }
783
- /**
784
- * Resolve a set of matched wires.
785
- *
786
- * Architecture: two distinct resolution axes —
787
- *
788
- * **Falsy Gate** (`||`, within a wire): `falsyFallbackRefs` + `falsyFallback`
789
- * → truthy check — falsy values (0, "", false) trigger fallback chain.
790
- *
791
- * **Overdefinition** (across wires): multiple wires target the same path
792
- * → nullish check — only null/undefined falls through to the next wire.
793
- *
794
- * Per-wire layers:
795
- * Layer 1 — Execution (pullSingle + safe modifier)
796
- * Layer 2a — Falsy Gate (falsyFallbackRefs → falsyFallback / falsyControl)
797
- * Layer 2b — Nullish Gate (nullishFallbackRef / nullishFallback / nullishControl)
798
- * Layer 3 — Catch (catchFallbackRef / catchFallback / catchControl)
799
- *
800
- * After layers 1–2b, the overdefinition boundary (`!= null`) decides whether
801
- * to return or continue to the next wire.
802
- */
803
- async resolveWires(wires, pullChain) {
804
- let lastError;
805
- for (const w of wires) {
806
- // Constant wire — always wins, no modifiers
807
- if ("value" in w)
808
- return coerceConstant(w.value);
809
- try {
810
- // --- Layer 1: Execution ---
811
- let resolvedValue;
812
- if ("cond" in w) {
813
- const condValue = await this.pullSingle(w.cond, pullChain);
814
- if (condValue) {
815
- if (w.thenRef !== undefined)
816
- resolvedValue = await this.pullSingle(w.thenRef, pullChain);
817
- else if (w.thenValue !== undefined)
818
- resolvedValue = coerceConstant(w.thenValue);
819
- }
820
- else {
821
- if (w.elseRef !== undefined)
822
- resolvedValue = await this.pullSingle(w.elseRef, pullChain);
823
- else if (w.elseValue !== undefined)
824
- resolvedValue = coerceConstant(w.elseValue);
825
- }
826
- }
827
- else if ("condAnd" in w) {
828
- const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condAnd;
829
- const leftVal = isSafe
830
- ? await this.pullSingle(leftRef, pullChain).catch((e) => {
831
- if (isFatalError(e))
832
- throw e;
833
- return undefined;
834
- })
835
- : await this.pullSingle(leftRef, pullChain);
836
- if (!leftVal) {
837
- resolvedValue = false;
838
- }
839
- else if (rightRef !== undefined) {
840
- const rightVal = rightSafe
841
- ? await this.pullSingle(rightRef, pullChain).catch((e) => {
842
- if (isFatalError(e))
843
- throw e;
844
- return undefined;
845
- })
846
- : await this.pullSingle(rightRef, pullChain);
847
- resolvedValue = Boolean(rightVal);
848
- }
849
- else if (rightValue !== undefined) {
850
- resolvedValue = Boolean(coerceConstant(rightValue));
851
- }
852
- else {
853
- resolvedValue = Boolean(leftVal);
854
- }
855
- }
856
- else if ("condOr" in w) {
857
- const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condOr;
858
- const leftVal = isSafe
859
- ? await this.pullSingle(leftRef, pullChain).catch((e) => {
860
- if (isFatalError(e))
861
- throw e;
862
- return undefined;
863
- })
864
- : await this.pullSingle(leftRef, pullChain);
865
- if (leftVal) {
866
- resolvedValue = true;
867
- }
868
- else if (rightRef !== undefined) {
869
- const rightVal = rightSafe
870
- ? await this.pullSingle(rightRef, pullChain).catch((e) => {
871
- if (isFatalError(e))
872
- throw e;
873
- return undefined;
874
- })
875
- : await this.pullSingle(rightRef, pullChain);
876
- resolvedValue = Boolean(rightVal);
877
- }
878
- else if (rightValue !== undefined) {
879
- resolvedValue = Boolean(coerceConstant(rightValue));
880
- }
881
- else {
882
- resolvedValue = Boolean(leftVal);
883
- }
884
- }
885
- else if ("from" in w) {
886
- if (w.safe) {
887
- try {
888
- resolvedValue = await this.pullSingle(w.from, pullChain);
889
- }
890
- catch (err) {
891
- if (isFatalError(err))
892
- throw err;
893
- resolvedValue = undefined;
894
- }
895
- }
896
- else {
897
- resolvedValue = await this.pullSingle(w.from, pullChain);
898
- }
899
- }
900
- else {
901
- continue;
902
- }
903
- // --- Layer 2a: Falsy Gate (||) ---
904
- if (!resolvedValue && w.falsyFallbackRefs?.length) {
905
- for (const ref of w.falsyFallbackRefs) {
906
- // Assign the fallback value regardless of whether it is truthy or falsy.
907
- // e.g. `false || 0` will correctly update resolvedValue to `0`.
908
- resolvedValue = await this.pullSingle(ref, pullChain);
909
- // If it is truthy, we are done! Short-circuit the || chain.
910
- if (resolvedValue)
911
- break;
912
- }
913
- }
914
- if (!resolvedValue) {
915
- if (w.falsyControl) {
916
- resolvedValue = applyControlFlow(w.falsyControl);
917
- }
918
- else if (w.falsyFallback != null) {
919
- resolvedValue = coerceConstant(w.falsyFallback);
920
- }
921
- }
922
- // --- Layer 2b: Nullish Gate (??) ---
923
- if (resolvedValue == null) {
924
- if (w.nullishControl) {
925
- resolvedValue = applyControlFlow(w.nullishControl);
926
- }
927
- else if (w.nullishFallbackRef) {
928
- resolvedValue = await this.pullSingle(w.nullishFallbackRef, pullChain);
929
- }
930
- else if (w.nullishFallback != null) {
931
- resolvedValue = coerceConstant(w.nullishFallback);
932
- }
933
- }
934
- // --- Overdefinition Boundary ---
935
- if (resolvedValue != null)
936
- return resolvedValue;
937
- }
938
- catch (err) {
939
- // --- Layer 3: Catch ---
940
- if (isFatalError(err))
941
- throw err;
942
- if (w.catchControl)
943
- return applyControlFlow(w.catchControl);
944
- if (w.catchFallbackRef)
945
- return this.pullSingle(w.catchFallbackRef, pullChain);
946
- if (w.catchFallback != null)
947
- return coerceConstant(w.catchFallback);
948
- lastError = err;
949
- }
950
- }
951
- if (lastError)
952
- throw lastError;
953
- return undefined;
954
- }
955
- /**
956
- * Resolve an output field by path for use outside of a GraphQL resolver.
957
- *
958
- * This is the non-GraphQL equivalent of what `response()` does per field:
959
- * it finds all wires targeting `this.trunk` at `path` and resolves them.
960
- *
961
- * Used by `executeBridge()` so standalone bridge execution does not need to
962
- * fabricate GraphQL Path objects to pull output data.
963
- *
964
- * @param path - Output field path, e.g. `["lat"]`. Pass `[]` for whole-output
965
- * array bridges (`o <- items[] as x { ... }`).
966
- * @param array - When `true` and the result is an array, wraps each element
967
- * in a shadow tree (mirrors `response()` array handling).
968
- */
969
- async pullOutputField(path, array = false) {
970
- const matches = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) && pathEquals(w.to.path, path)) ?? [];
971
- if (matches.length === 0)
972
- return undefined;
973
- const result = this.resolveWires(matches);
974
- if (!array)
975
- return result;
976
- const resolved = await result;
977
- if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
978
- return [];
979
- const items = resolved;
980
- const finalShadowTrees = [];
981
- for (const item of items) {
982
- if (item === BREAK_SYM)
983
- break;
984
- if (item === CONTINUE_SYM)
985
- continue;
986
- const s = this.shadow();
987
- s.state[trunkKey({ ...this.trunk, element: true })] = item;
988
- finalShadowTrees.push(s);
989
- }
990
- return finalShadowTrees;
991
- }
992
- /**
993
- * Execute the bridge end-to-end without GraphQL.
994
- *
995
- * Injects `input` as the trunk arguments, runs forced wires, then pulls
996
- * and materialises every output field into a plain JS object (or array of
997
- * objects for array-mapped bridges).
998
- *
999
- * This is the single entry-point used by `executeBridge()`.
1000
- */
1001
- async run(input) {
1002
- const bridge = this.bridge;
1003
- if (!bridge) {
1004
- throw new Error(`No bridge definition found for ${this.trunk.type}.${this.trunk.field}`);
1005
- }
1006
- this.push(input);
1007
- const forcePromises = this.executeForced();
1008
- const { type, field } = this.trunk;
1009
- // Is there a root-level wire targeting the output with path []?
1010
- const hasRootWire = bridge.wires.some((w) => "from" in w &&
1011
- w.to.module === SELF_MODULE &&
1012
- w.to.type === type &&
1013
- w.to.field === field &&
1014
- w.to.path.length === 0);
1015
- // Array-mapped output (`o <- items[] as x { ... }`) has BOTH a root wire
1016
- // AND element-level wires (from.element === true). A plain passthrough
1017
- // (`o <- api.user`) only has the root wire.
1018
- // Local bindings (from.__local) are also element-scoped.
1019
- // Pipe fork output wires in element context (e.g. concat template strings)
1020
- // may have to.element === true instead.
1021
- const hasElementWires = bridge.wires.some((w) => "from" in w &&
1022
- (w.from.element === true ||
1023
- w.from.module === "__local" ||
1024
- w.to.element === true) &&
1025
- w.to.module === SELF_MODULE &&
1026
- w.to.type === type &&
1027
- w.to.field === field);
1028
- if (hasRootWire && hasElementWires) {
1029
- const [shadows] = await Promise.all([
1030
- this.pullOutputField([], true),
1031
- ...forcePromises,
1032
- ]);
1033
- return this.materializeShadows(shadows, []);
1034
- }
1035
- // Whole-object passthrough: `o <- api.user`
1036
- if (hasRootWire) {
1037
- const [result] = await Promise.all([
1038
- this.pullOutputField([]),
1039
- ...forcePromises,
1040
- ]);
1041
- return result;
1042
- }
1043
- // Object output — collect unique top-level field names
1044
- const outputFields = new Set();
1045
- for (const wire of bridge.wires) {
1046
- if (wire.to.module === SELF_MODULE &&
1047
- wire.to.type === type &&
1048
- wire.to.field === field &&
1049
- wire.to.path.length > 0) {
1050
- outputFields.add(wire.to.path[0]);
1051
- }
1052
- }
1053
- if (outputFields.size === 0) {
1054
- throw new Error(`Bridge "${type}.${field}" has no output wires. ` +
1055
- `Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).`);
1056
- }
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
- };
1089
- await Promise.all([
1090
- ...[...outputFields].map(async (name) => {
1091
- result[name] = await resolveField([name]);
1092
- }),
1093
- ...forcePromises,
1094
- ]);
1095
- return result;
1096
- }
1097
- /**
1098
- * Recursively convert shadow trees into plain JS objects.
1099
- *
1100
- * Wire categories at each level (prefix = P):
1101
- * Leaf — `to.path = [...P, name]`, no deeper paths → scalar
1102
- * Array — direct wire AND deeper paths → pull as array, recurse
1103
- * Nested object — only deeper paths, no direct wire → pull each
1104
- * full path and assemble via setNested
1105
- */
1106
- async materializeShadows(items, pathPrefix) {
1107
- const wires = this.bridge.wires;
1108
- const { type, field } = this.trunk;
1109
- const directFields = new Set();
1110
- const deepPaths = new Map();
1111
- for (const wire of wires) {
1112
- const p = wire.to.path;
1113
- if (wire.to.module !== SELF_MODULE ||
1114
- wire.to.type !== type ||
1115
- wire.to.field !== field)
1116
- continue;
1117
- if (p.length <= pathPrefix.length)
1118
- continue;
1119
- if (!pathPrefix.every((seg, i) => p[i] === seg))
1120
- continue;
1121
- const name = p[pathPrefix.length];
1122
- if (p.length === pathPrefix.length + 1) {
1123
- directFields.add(name);
1124
- }
1125
- else {
1126
- let arr = deepPaths.get(name);
1127
- if (!arr) {
1128
- arr = [];
1129
- deepPaths.set(name, arr);
1130
- }
1131
- arr.push(p);
1132
- }
1133
- }
1134
- const rawResults = await Promise.all(items.map(async (shadow) => {
1135
- const obj = {};
1136
- const tasks = [];
1137
- for (const name of directFields) {
1138
- const fullPath = [...pathPrefix, name];
1139
- const hasDeeper = deepPaths.has(name);
1140
- tasks.push((async () => {
1141
- if (hasDeeper) {
1142
- const children = await shadow.pullOutputField(fullPath, true);
1143
- obj[name] = Array.isArray(children)
1144
- ? await this.materializeShadows(children, fullPath)
1145
- : children;
1146
- }
1147
- else {
1148
- obj[name] = await shadow.pullOutputField(fullPath);
1149
- }
1150
- })());
1151
- }
1152
- for (const [name, paths] of deepPaths) {
1153
- if (directFields.has(name))
1154
- continue;
1155
- tasks.push((async () => {
1156
- const nested = {};
1157
- await Promise.all(paths.map(async (fullPath) => {
1158
- const value = await shadow.pullOutputField(fullPath);
1159
- setNested(nested, fullPath.slice(pathPrefix.length + 1), value);
1160
- }));
1161
- obj[name] = nested;
1162
- })());
1163
- }
1164
- await Promise.all(tasks);
1165
- // Check if any field resolved to a sentinel — propagate it
1166
- for (const v of Object.values(obj)) {
1167
- if (v === CONTINUE_SYM)
1168
- return CONTINUE_SYM;
1169
- if (v === BREAK_SYM)
1170
- return BREAK_SYM;
1171
- }
1172
- return obj;
1173
- }));
1174
- // Filter sentinels from the final result
1175
- const finalResults = [];
1176
- for (const item of rawResults) {
1177
- if (item === BREAK_SYM)
1178
- break;
1179
- if (item === CONTINUE_SYM)
1180
- continue;
1181
- finalResults.push(item);
1182
- }
1183
- return finalResults;
1184
- }
1185
- async response(ipath, array) {
1186
- // Build path segments from GraphQL resolver info
1187
- const pathSegments = [];
1188
- let index = ipath;
1189
- while (index.prev) {
1190
- pathSegments.unshift(`${index.key}`);
1191
- index = index.prev;
1192
- }
1193
- if (pathSegments.length === 0) {
1194
- // Direct output for scalar/list return types (e.g. [String!])
1195
- const directOutput = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) &&
1196
- w.to.path.length === 1 &&
1197
- w.to.path[0] === this.trunk.field) ?? [];
1198
- if (directOutput.length > 0) {
1199
- return this.resolveWires(directOutput);
1200
- }
1201
- }
1202
- // Strip numeric indices (array positions) from path for wire matching
1203
- const cleanPath = pathSegments.filter((p) => !/^\d+$/.test(p));
1204
- // Find wires whose target matches this trunk + path
1205
- const matches = this.bridge?.wires.filter((w) => (w.to.element ? !!this.parent : true) &&
1206
- sameTrunk(w.to, this.trunk) &&
1207
- pathEquals(w.to.path, cleanPath)) ?? [];
1208
- if (matches.length > 0) {
1209
- // ── Lazy define resolution ──────────────────────────────────────
1210
- // When ALL matches at the root object level (path=[]) are
1211
- // whole-object wires sourced from define output modules, defer
1212
- // resolution to field-by-field GraphQL traversal. This avoids
1213
- // eagerly scheduling every tool inside the define block — only
1214
- // fields actually requested by the query will trigger their
1215
- // dependency chains.
1216
- if (cleanPath.length === 0 &&
1217
- !array &&
1218
- matches.every((w) => "from" in w &&
1219
- w.from.module.startsWith("__define_out_") &&
1220
- w.from.path.length === 0)) {
1221
- return this;
1222
- }
1223
- const response = this.resolveWires(matches);
1224
- if (!array) {
1225
- return response;
1226
- }
1227
- // Array: create shadow trees for per-element resolution
1228
- const resolved = await response;
1229
- if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
1230
- return [];
1231
- const items = resolved;
1232
- const shadowTrees = [];
1233
- for (const item of items) {
1234
- if (item === BREAK_SYM)
1235
- break;
1236
- if (item === CONTINUE_SYM)
1237
- continue;
1238
- const s = this.shadow();
1239
- s.state[trunkKey({ ...this.trunk, element: true })] = item;
1240
- shadowTrees.push(s);
1241
- }
1242
- return shadowTrees;
1243
- }
1244
- // ── Resolve field from deferred define ────────────────────────────
1245
- // No direct wires for this field path — check whether a define
1246
- // forward wire exists at the root level (`o <- defineHandle`) and
1247
- // resolve only the matching field wire from the define's output.
1248
- if (cleanPath.length > 0) {
1249
- const defineFieldWires = this.findDefineFieldWires(cleanPath);
1250
- if (defineFieldWires.length > 0) {
1251
- const response = this.resolveWires(defineFieldWires);
1252
- if (!array)
1253
- return response;
1254
- const resolved = await response;
1255
- if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
1256
- return [];
1257
- const items = resolved;
1258
- const shadowTrees = [];
1259
- for (const item of items) {
1260
- if (item === BREAK_SYM)
1261
- break;
1262
- if (item === CONTINUE_SYM)
1263
- continue;
1264
- const s = this.shadow();
1265
- s.state[trunkKey({ ...this.trunk, element: true })] = item;
1266
- shadowTrees.push(s);
1267
- }
1268
- return shadowTrees;
1269
- }
1270
- }
1271
- // Fallback: if this shadow tree has stored element data, resolve the
1272
- // requested field directly from it. This handles passthrough arrays
1273
- // where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
1274
- // doesn't explicitly wire each scalar field on the element type.
1275
- if (this.parent) {
1276
- const elementKey = trunkKey({ ...this.trunk, element: true });
1277
- const elementData = this.state[elementKey];
1278
- if (elementData != null &&
1279
- typeof elementData === "object" &&
1280
- !Array.isArray(elementData)) {
1281
- const fieldName = cleanPath[cleanPath.length - 1];
1282
- if (fieldName !== undefined && fieldName in elementData) {
1283
- const value = elementData[fieldName];
1284
- if (array && Array.isArray(value)) {
1285
- // Nested array: wrap items in shadow trees so they can
1286
- // resolve their own fields via this same fallback path.
1287
- return value.map((item) => {
1288
- const s = this.shadow();
1289
- s.state[elementKey] = item;
1290
- return s;
1291
- });
1292
- }
1293
- return value;
1294
- }
1295
- }
1296
- }
1297
- // Return self to trigger downstream resolvers
1298
- return this;
1299
- }
1300
- /**
1301
- * Find define output wires for a specific field path.
1302
- *
1303
- * Looks for whole-object define forward wires (`o <- defineHandle`)
1304
- * at path=[] for this trunk, then searches the define's output wires
1305
- * for ones matching the requested field path.
1306
- */
1307
- findDefineFieldWires(cleanPath) {
1308
- const forwards = this.bridge?.wires.filter((w) => "from" in w &&
1309
- sameTrunk(w.to, this.trunk) &&
1310
- w.to.path.length === 0 &&
1311
- w.from.module.startsWith("__define_out_") &&
1312
- w.from.path.length === 0) ?? [];
1313
- if (forwards.length === 0)
1314
- return [];
1315
- const result = [];
1316
- for (const fw of forwards) {
1317
- const defOutTrunk = fw.from;
1318
- const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, defOutTrunk) && pathEquals(w.to.path, cleanPath)) ?? [];
1319
- result.push(...fieldWires);
1320
- }
1321
- return result;
1322
- }
1323
- }