@stackables/bridge-graphql 1.0.0 → 1.0.2

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/build/bridge-core/src/ExecutionTree.d.ts +270 -0
  2. package/build/bridge-core/src/ExecutionTree.d.ts.map +1 -0
  3. package/build/bridge-core/src/ExecutionTree.js +1670 -0
  4. package/build/bridge-core/src/execute-bridge.d.ts +69 -0
  5. package/build/bridge-core/src/execute-bridge.d.ts.map +1 -0
  6. package/build/bridge-core/src/execute-bridge.js +52 -0
  7. package/build/bridge-core/src/index.d.ts +18 -0
  8. package/build/bridge-core/src/index.d.ts.map +1 -0
  9. package/build/bridge-core/src/index.js +20 -0
  10. package/build/bridge-core/src/merge-documents.d.ts +25 -0
  11. package/build/bridge-core/src/merge-documents.d.ts.map +1 -0
  12. package/build/bridge-core/src/merge-documents.js +91 -0
  13. package/build/bridge-core/src/tools/index.d.ts +2 -0
  14. package/build/bridge-core/src/tools/index.d.ts.map +1 -0
  15. package/build/bridge-core/src/tools/index.js +1 -0
  16. package/build/bridge-core/src/tools/internal.d.ts +71 -0
  17. package/build/bridge-core/src/tools/internal.d.ts.map +1 -0
  18. package/build/bridge-core/src/tools/internal.js +59 -0
  19. package/build/bridge-core/src/types.d.ts +349 -0
  20. package/build/bridge-core/src/types.d.ts.map +1 -0
  21. package/build/bridge-core/src/types.js +3 -0
  22. package/build/bridge-core/src/utils.d.ts +9 -0
  23. package/build/bridge-core/src/utils.d.ts.map +1 -0
  24. package/build/bridge-core/src/utils.js +23 -0
  25. package/build/bridge-core/src/version-check.d.ts +64 -0
  26. package/build/bridge-core/src/version-check.d.ts.map +1 -0
  27. package/build/bridge-core/src/version-check.js +205 -0
  28. package/build/bridge-graphql/src/bridge-transform.d.ts.map +1 -0
  29. package/build/{bridge-transform.js → bridge-graphql/src/bridge-transform.js} +17 -2
  30. package/build/bridge-graphql/src/index.d.ts.map +1 -0
  31. package/build/bridge-stdlib/src/index.d.ts +34 -0
  32. package/build/bridge-stdlib/src/index.d.ts.map +1 -0
  33. package/build/bridge-stdlib/src/index.js +40 -0
  34. package/build/bridge-stdlib/src/tools/arrays.d.ts +28 -0
  35. package/build/bridge-stdlib/src/tools/arrays.d.ts.map +1 -0
  36. package/build/bridge-stdlib/src/tools/arrays.js +50 -0
  37. package/build/bridge-stdlib/src/tools/audit.d.ts +36 -0
  38. package/build/bridge-stdlib/src/tools/audit.d.ts.map +1 -0
  39. package/build/bridge-stdlib/src/tools/audit.js +39 -0
  40. package/build/bridge-stdlib/src/tools/http-call.d.ts +35 -0
  41. package/build/bridge-stdlib/src/tools/http-call.d.ts.map +1 -0
  42. package/build/bridge-stdlib/src/tools/http-call.js +118 -0
  43. package/build/bridge-stdlib/src/tools/strings.d.ts +13 -0
  44. package/build/bridge-stdlib/src/tools/strings.d.ts.map +1 -0
  45. package/build/bridge-stdlib/src/tools/strings.js +12 -0
  46. package/build/bridge-types/src/index.d.ts +63 -0
  47. package/build/bridge-types/src/index.d.ts.map +1 -0
  48. package/build/bridge-types/src/index.js +8 -0
  49. package/package.json +3 -3
  50. package/build/bridge-transform.d.ts.map +0 -1
  51. package/build/index.d.ts.map +0 -1
  52. /package/build/{bridge-transform.d.ts → bridge-graphql/src/bridge-transform.d.ts} +0 -0
  53. /package/build/{index.d.ts → bridge-graphql/src/index.d.ts} +0 -0
  54. /package/build/{index.js → bridge-graphql/src/index.js} +0 -0
@@ -0,0 +1,1670 @@
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
+ /**
27
+ * Lazily detect whether the OpenTelemetry tracer is a real (recording)
28
+ * tracer or the default no-op. Probed once on first tool call; result
29
+ * is cached for the lifetime of the process.
30
+ *
31
+ * If the SDK has not been registered by the time the first tool runs,
32
+ * all subsequent calls will skip OTel instrumentation.
33
+ */
34
+ let _otelActive;
35
+ function isOtelActive() {
36
+ if (_otelActive === undefined) {
37
+ const probe = otelTracer.startSpan("_bridge_probe_");
38
+ _otelActive = probe.isRecording();
39
+ probe.end();
40
+ }
41
+ return _otelActive;
42
+ }
43
+ const otelMeter = metrics.getMeter("@stackables/bridge");
44
+ const toolCallCounter = otelMeter.createCounter("bridge.tool.calls", {
45
+ description: "Total number of tool invocations",
46
+ });
47
+ const toolDurationHistogram = otelMeter.createHistogram("bridge.tool.duration", {
48
+ description: "Tool call duration in milliseconds",
49
+ unit: "ms",
50
+ });
51
+ const toolErrorCounter = otelMeter.createCounter("bridge.tool.errors", {
52
+ description: "Total number of tool invocation errors",
53
+ });
54
+ /** Round milliseconds to 2 decimal places */
55
+ function roundMs(ms) {
56
+ return Math.round(ms * 100) / 100;
57
+ }
58
+ /** Stable string key for the state map */
59
+ function trunkKey(ref) {
60
+ if (ref.element)
61
+ return `${ref.module}:${ref.type}:${ref.field}:*`;
62
+ return `${ref.module}:${ref.type}:${ref.field}${ref.instance != null ? `:${ref.instance}` : ""}`;
63
+ }
64
+ /** Match two trunks (ignoring path and element) */
65
+ function sameTrunk(a, b) {
66
+ return (a.module === b.module &&
67
+ a.type === b.type &&
68
+ a.field === b.field &&
69
+ (a.instance ?? undefined) === (b.instance ?? undefined));
70
+ }
71
+ /** Strict path equality — manual loop avoids `.every()` closure allocation. See docs/performance.md (#7). */
72
+ function pathEquals(a, b) {
73
+ if (!a || !b)
74
+ return a === b;
75
+ if (a.length !== b.length)
76
+ return false;
77
+ for (let i = 0; i < a.length; i++) {
78
+ if (a[i] !== b[i])
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ /** Check whether an error is a fatal halt (abort or panic) that must bypass all error boundaries. */
84
+ function isFatalError(err) {
85
+ return (err instanceof BridgePanicError ||
86
+ err instanceof BridgeAbortError ||
87
+ err?.name === "BridgeAbortError" ||
88
+ err?.name === "BridgePanicError");
89
+ }
90
+ /** Returns `true` when `value` is a thenable (Promise or Promise-like). */
91
+ function isPromise(value) {
92
+ return typeof value?.then === "function";
93
+ }
94
+ /**
95
+ * Returns the `from` NodeRef when a wire qualifies for the simple-pull fast
96
+ * path (single `from` wire, no safe/falsy/nullish/catch modifiers). Returns
97
+ * `null` otherwise. The result is cached on the wire object so subsequent
98
+ * calls are a single property read. See docs/performance.md (#11).
99
+ */
100
+ function getSimplePullRef(w) {
101
+ let ref = w.__simplePullRef;
102
+ if (ref !== undefined)
103
+ return ref;
104
+ ref =
105
+ "from" in w &&
106
+ !w.safe &&
107
+ !w.falsyFallbackRefs?.length &&
108
+ w.falsyControl == null &&
109
+ w.falsyFallback == null &&
110
+ w.nullishControl == null &&
111
+ !w.nullishFallbackRef &&
112
+ w.nullishFallback == null &&
113
+ !w.catchControl &&
114
+ !w.catchFallbackRef &&
115
+ w.catchFallback == null
116
+ ? w.from
117
+ : null;
118
+ w.__simplePullRef = ref;
119
+ return ref;
120
+ }
121
+ /** Execute a control flow instruction, returning a sentinel or throwing. */
122
+ function applyControlFlow(ctrl) {
123
+ if (ctrl.kind === "throw")
124
+ throw new Error(ctrl.message);
125
+ if (ctrl.kind === "panic")
126
+ throw new BridgePanicError(ctrl.message);
127
+ if (ctrl.kind === "continue")
128
+ return CONTINUE_SYM;
129
+ /* ctrl.kind === "break" */
130
+ return BREAK_SYM;
131
+ }
132
+ /** Shared trace collector — one per request, passed through the tree. */
133
+ export class TraceCollector {
134
+ traces = [];
135
+ level;
136
+ epoch = performance.now();
137
+ constructor(level = "full") {
138
+ this.level = level;
139
+ }
140
+ /** Returns ms since the collector was created */
141
+ now() {
142
+ return roundMs(performance.now() - this.epoch);
143
+ }
144
+ record(trace) {
145
+ this.traces.push(trace);
146
+ }
147
+ /** Build a trace entry, omitting input/output for basic level. */
148
+ entry(base) {
149
+ if (this.level === "basic") {
150
+ const t = {
151
+ tool: base.tool,
152
+ fn: base.fn,
153
+ durationMs: base.durationMs,
154
+ startedAt: base.startedAt,
155
+ };
156
+ if (base.error)
157
+ t.error = base.error;
158
+ return t;
159
+ }
160
+ // full
161
+ const t = {
162
+ tool: base.tool,
163
+ fn: base.fn,
164
+ durationMs: base.durationMs,
165
+ startedAt: base.startedAt,
166
+ };
167
+ if (base.input)
168
+ t.input = structuredClone(base.input);
169
+ if (base.error)
170
+ t.error = base.error;
171
+ else if (base.output !== undefined)
172
+ t.output = base.output;
173
+ return t;
174
+ }
175
+ }
176
+ /** Set a value at a nested path, creating intermediate objects/arrays as needed */
177
+ /**
178
+ * Coerce a constant wire value string to its proper JS type.
179
+ *
180
+ * The parser stores all bare constants as strings (because the Wire type
181
+ * uses `value: string`). JSON.parse recovers the original type:
182
+ * "true" → true, "false" → false, "null" → null, "42" → 42
183
+ * Plain strings that aren't valid JSON (like "hello", "/search") fall
184
+ * through and are returned as-is.
185
+ *
186
+ * Results are cached in a module-level Map because the same constant
187
+ * strings appear repeatedly across shadow trees. Only safe for
188
+ * immutable values (primitives); callers must not mutate the returned
189
+ * value. See docs/performance.md (#6).
190
+ */
191
+ const constantCache = new Map();
192
+ function coerceConstant(raw) {
193
+ const cached = constantCache.get(raw);
194
+ if (cached !== undefined)
195
+ return cached;
196
+ let result;
197
+ try {
198
+ result = JSON.parse(raw);
199
+ }
200
+ catch {
201
+ result = raw;
202
+ }
203
+ // Hard cap to prevent unbounded growth over long-lived processes.
204
+ if (constantCache.size > 10_000)
205
+ constantCache.clear();
206
+ constantCache.set(raw, result);
207
+ return result;
208
+ }
209
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
210
+ function setNested(obj, path, value) {
211
+ for (let i = 0; i < path.length - 1; i++) {
212
+ const key = path[i];
213
+ if (UNSAFE_KEYS.has(key))
214
+ throw new Error(`Unsafe assignment key: ${key}`);
215
+ const nextKey = path[i + 1];
216
+ if (obj[key] == null) {
217
+ obj[key] = /^\d+$/.test(nextKey) ? [] : {};
218
+ }
219
+ obj = obj[key];
220
+ }
221
+ if (path.length > 0) {
222
+ const finalKey = path[path.length - 1];
223
+ if (UNSAFE_KEYS.has(finalKey))
224
+ throw new Error(`Unsafe assignment key: ${finalKey}`);
225
+ obj[finalKey] = value;
226
+ }
227
+ }
228
+ export class ExecutionTree {
229
+ trunk;
230
+ document;
231
+ context;
232
+ parent;
233
+ state = {};
234
+ bridge;
235
+ toolDepCache = new Map();
236
+ toolDefCache = new Map();
237
+ pipeHandleMap;
238
+ /**
239
+ * Maps trunk keys to `@version` strings from handle bindings.
240
+ * Populated in the constructor so `schedule()` can prefer versioned
241
+ * tool lookups (e.g. `std.str.toLowerCase@999.1`) over the default.
242
+ */
243
+ handleVersionMap = new Map();
244
+ /** Promise that resolves when all critical `force` handles have settled. */
245
+ forcedExecution;
246
+ /** Shared trace collector — present only when tracing is enabled. */
247
+ tracer;
248
+ /** Structured logger passed from BridgeOptions. Defaults to no-ops. */
249
+ logger;
250
+ /** External abort signal — cancels execution when triggered. */
251
+ signal;
252
+ toolFns;
253
+ /** Shadow-tree nesting depth (0 for root). */
254
+ depth;
255
+ /** Pre-computed `trunkKey({ ...this.trunk, element: true })`. See docs/performance.md (#4). */
256
+ elementTrunkKey;
257
+ constructor(trunk, document, toolFns, context, parent) {
258
+ this.trunk = trunk;
259
+ this.document = document;
260
+ this.context = context;
261
+ this.parent = parent;
262
+ this.depth = parent ? parent.depth + 1 : 0;
263
+ if (this.depth > MAX_EXECUTION_DEPTH) {
264
+ throw new BridgePanicError(`Maximum execution depth exceeded (${this.depth}) at ${trunkKey(trunk)}. Check for infinite recursion or circular array mappings.`);
265
+ }
266
+ this.elementTrunkKey = `${trunk.module}:${trunk.type}:${trunk.field}:*`;
267
+ this.toolFns = { internal, ...(toolFns ?? {}) };
268
+ const instructions = document.instructions;
269
+ this.bridge = instructions.find((i) => i.kind === "bridge" && i.type === trunk.type && i.field === trunk.field);
270
+ if (this.bridge?.pipeHandles) {
271
+ this.pipeHandleMap = new Map(this.bridge.pipeHandles.map((ph) => [ph.key, ph]));
272
+ }
273
+ // Build handle→version map from bridge handle bindings
274
+ if (this.bridge) {
275
+ const instanceCounters = new Map();
276
+ for (const h of this.bridge.handles) {
277
+ if (h.kind !== "tool")
278
+ continue;
279
+ const name = h.name;
280
+ const lastDot = name.lastIndexOf(".");
281
+ let module, field, counterKey, type;
282
+ if (lastDot !== -1) {
283
+ module = name.substring(0, lastDot);
284
+ field = name.substring(lastDot + 1);
285
+ counterKey = `${module}:${field}`;
286
+ type = this.trunk.type;
287
+ }
288
+ else {
289
+ module = SELF_MODULE;
290
+ field = name;
291
+ counterKey = `Tools:${name}`;
292
+ type = "Tools";
293
+ }
294
+ const instance = (instanceCounters.get(counterKey) ?? 0) + 1;
295
+ instanceCounters.set(counterKey, instance);
296
+ if (h.version) {
297
+ const key = trunkKey({ module, type, field, instance });
298
+ this.handleVersionMap.set(key, h.version);
299
+ }
300
+ }
301
+ }
302
+ if (context) {
303
+ this.state[trunkKey({ module: SELF_MODULE, type: "Context", field: "context" })] = context;
304
+ }
305
+ // Collect const definitions into a single namespace object
306
+ const constObj = {};
307
+ for (const inst of instructions) {
308
+ if (inst.kind === "const") {
309
+ constObj[inst.name] = JSON.parse(inst.value);
310
+ }
311
+ }
312
+ if (Object.keys(constObj).length > 0) {
313
+ this.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })] = constObj;
314
+ }
315
+ }
316
+ /** Derive tool name from a trunk */
317
+ getToolName(target) {
318
+ if (target.module === SELF_MODULE)
319
+ return target.field;
320
+ return `${target.module}.${target.field}`;
321
+ }
322
+ /** Deep-lookup a tool function by dotted name (e.g. "std.str.toUpperCase").
323
+ * Falls back to a flat key lookup for backward compat (e.g. "hereapi.geocode" as literal key). */
324
+ lookupToolFn(name) {
325
+ if (name.includes(".")) {
326
+ // Try namespace traversal first
327
+ const parts = name.split(".");
328
+ let current = this.toolFns;
329
+ for (const part of parts) {
330
+ if (UNSAFE_KEYS.has(part))
331
+ return undefined;
332
+ if (current == null || typeof current !== "object") {
333
+ current = undefined;
334
+ break;
335
+ }
336
+ current = current[part];
337
+ }
338
+ if (typeof current === "function")
339
+ return current;
340
+ // Fall back to flat key (e.g. "hereapi.geocode" as a literal property name)
341
+ const flat = this.toolFns?.[name];
342
+ if (typeof flat === "function")
343
+ return flat;
344
+ // Try versioned namespace keys (e.g. "std.str@999.1" → { toLowerCase })
345
+ // For "std.str.toLowerCase@999.1", check:
346
+ // toolFns["std.str@999.1"]?.toLowerCase
347
+ // toolFns["std@999.1"]?.str?.toLowerCase
348
+ const atIdx = name.lastIndexOf("@");
349
+ if (atIdx > 0) {
350
+ const baseName = name.substring(0, atIdx);
351
+ const version = name.substring(atIdx + 1);
352
+ const nameParts = baseName.split(".");
353
+ for (let i = nameParts.length - 1; i >= 1; i--) {
354
+ const nsKey = nameParts.slice(0, i).join(".") + "@" + version;
355
+ const remainder = nameParts.slice(i);
356
+ let ns = this.toolFns?.[nsKey];
357
+ if (ns != null && typeof ns === "object") {
358
+ for (const part of remainder) {
359
+ if (ns == null || typeof ns !== "object") {
360
+ ns = undefined;
361
+ break;
362
+ }
363
+ ns = ns[part];
364
+ }
365
+ if (typeof ns === "function")
366
+ return ns;
367
+ }
368
+ }
369
+ }
370
+ return undefined;
371
+ }
372
+ // Try root level first
373
+ const fn = this.toolFns?.[name];
374
+ if (typeof fn === "function")
375
+ return fn;
376
+ // Fall back to std namespace (builtins are callable without std. prefix)
377
+ const stdFn = this.toolFns?.std?.[name];
378
+ if (typeof stdFn === "function")
379
+ return stdFn;
380
+ // Fall back to internal namespace (engine-internal tools: math ops, concat, etc.)
381
+ const internalFn = this.toolFns?.internal?.[name];
382
+ return typeof internalFn === "function" ? internalFn : undefined;
383
+ }
384
+ /** Resolve a ToolDef by name, merging the extends chain (cached) */
385
+ resolveToolDefByName(name) {
386
+ if (this.toolDefCache.has(name))
387
+ return this.toolDefCache.get(name) ?? undefined;
388
+ const toolDefs = this.document.instructions.filter((i) => i.kind === "tool");
389
+ const base = toolDefs.find((t) => t.name === name);
390
+ if (!base) {
391
+ this.toolDefCache.set(name, null);
392
+ return undefined;
393
+ }
394
+ // Build extends chain: root → ... → leaf
395
+ const chain = [base];
396
+ let current = base;
397
+ while (current.extends) {
398
+ const parent = toolDefs.find((t) => t.name === current.extends);
399
+ if (!parent)
400
+ throw new Error(`Tool "${current.name}" extends unknown tool "${current.extends}"`);
401
+ chain.unshift(parent);
402
+ current = parent;
403
+ }
404
+ // Merge: root provides base, each child overrides
405
+ const merged = {
406
+ kind: "tool",
407
+ name,
408
+ fn: chain[0].fn, // fn from root ancestor
409
+ deps: [],
410
+ wires: [],
411
+ };
412
+ for (const def of chain) {
413
+ // Merge deps (dedupe by handle)
414
+ for (const dep of def.deps) {
415
+ if (!merged.deps.some((d) => d.handle === dep.handle)) {
416
+ merged.deps.push(dep);
417
+ }
418
+ }
419
+ // Merge wires (child overrides parent by target; onError replaces onError)
420
+ for (const wire of def.wires) {
421
+ if (wire.kind === "onError") {
422
+ const idx = merged.wires.findIndex((w) => w.kind === "onError");
423
+ if (idx >= 0)
424
+ merged.wires[idx] = wire;
425
+ else
426
+ merged.wires.push(wire);
427
+ }
428
+ else {
429
+ const idx = merged.wires.findIndex((w) => "target" in w && w.target === wire.target);
430
+ if (idx >= 0)
431
+ merged.wires[idx] = wire;
432
+ else
433
+ merged.wires.push(wire);
434
+ }
435
+ }
436
+ }
437
+ this.toolDefCache.set(name, merged);
438
+ return merged;
439
+ }
440
+ /** Resolve a tool definition's wires into a nested input object */
441
+ async resolveToolWires(toolDef, input) {
442
+ // Constants applied synchronously
443
+ for (const wire of toolDef.wires) {
444
+ if (wire.kind === "constant") {
445
+ setNested(input, parsePath(wire.target), coerceConstant(wire.value));
446
+ }
447
+ }
448
+ // Pull wires resolved in parallel (independent deps shouldn't wait on each other)
449
+ const pullWires = toolDef.wires.filter((w) => w.kind === "pull");
450
+ if (pullWires.length > 0) {
451
+ const resolved = await Promise.all(pullWires.map(async (wire) => ({
452
+ target: wire.target,
453
+ value: await this.resolveToolSource(wire.source, toolDef),
454
+ })));
455
+ for (const { target, value } of resolved) {
456
+ setNested(input, parsePath(target), value);
457
+ }
458
+ }
459
+ }
460
+ /** Resolve a source reference from a tool wire against its dependencies */
461
+ async resolveToolSource(source, toolDef) {
462
+ const dotIdx = source.indexOf(".");
463
+ const handle = dotIdx === -1 ? source : source.substring(0, dotIdx);
464
+ const restPath = dotIdx === -1 ? [] : source.substring(dotIdx + 1).split(".");
465
+ const dep = toolDef.deps.find((d) => d.handle === handle);
466
+ if (!dep)
467
+ throw new Error(`Unknown source "${handle}" in tool "${toolDef.name}"`);
468
+ let value;
469
+ if (dep.kind === "context") {
470
+ // Walk the full parent chain for context
471
+ let cursor = this;
472
+ while (cursor && value === undefined) {
473
+ value = cursor.context;
474
+ cursor = cursor.parent;
475
+ }
476
+ }
477
+ else if (dep.kind === "const") {
478
+ // Walk the full parent chain for const state
479
+ const constKey = trunkKey({
480
+ module: SELF_MODULE,
481
+ type: "Const",
482
+ field: "const",
483
+ });
484
+ let cursor = this;
485
+ while (cursor && value === undefined) {
486
+ value = cursor.state[constKey];
487
+ cursor = cursor.parent;
488
+ }
489
+ }
490
+ else if (dep.kind === "tool") {
491
+ value = await this.resolveToolDep(dep.tool);
492
+ }
493
+ for (const segment of restPath) {
494
+ value = value?.[segment];
495
+ }
496
+ return value;
497
+ }
498
+ /** Call a tool dependency (cached per request) */
499
+ resolveToolDep(toolName) {
500
+ // Check parent first (shadow trees delegate)
501
+ if (this.parent)
502
+ return this.parent.resolveToolDep(toolName);
503
+ if (this.toolDepCache.has(toolName))
504
+ return this.toolDepCache.get(toolName);
505
+ const promise = (async () => {
506
+ const toolDef = this.resolveToolDefByName(toolName);
507
+ if (!toolDef)
508
+ throw new Error(`Tool dependency "${toolName}" not found`);
509
+ const input = {};
510
+ await this.resolveToolWires(toolDef, input);
511
+ const fn = this.lookupToolFn(toolDef.fn);
512
+ if (!fn)
513
+ throw new Error(`Tool function "${toolDef.fn}" not registered`);
514
+ // on error: wrap the tool call with fallback from onError wire
515
+ const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
516
+ try {
517
+ return await this.callTool(toolName, toolDef.fn, fn, input);
518
+ }
519
+ catch (err) {
520
+ if (!onErrorWire)
521
+ throw err;
522
+ if ("value" in onErrorWire)
523
+ return JSON.parse(onErrorWire.value);
524
+ return this.resolveToolSource(onErrorWire.source, toolDef);
525
+ }
526
+ })();
527
+ this.toolDepCache.set(toolName, promise);
528
+ return promise;
529
+ }
530
+ schedule(target, pullChain) {
531
+ // Delegate to parent (shadow trees don't schedule directly) unless
532
+ // the target fork has bridge wires sourced from element data,
533
+ // or a __local binding whose source chain touches element data.
534
+ if (this.parent) {
535
+ const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
536
+ const hasElementSource = forkWires.some((w) => ("from" in w && !!w.from.element) ||
537
+ ("condAnd" in w &&
538
+ (!!w.condAnd.leftRef.element || !!w.condAnd.rightRef?.element)) ||
539
+ ("condOr" in w &&
540
+ (!!w.condOr.leftRef.element || !!w.condOr.rightRef?.element)));
541
+ // For __local trunks, also check transitively: if the source is a
542
+ // pipe fork whose own wires reference element data, keep it local.
543
+ const hasTransitiveElementSource = target.module === "__local" &&
544
+ forkWires.some((w) => {
545
+ if (!("from" in w))
546
+ return false;
547
+ const srcTrunk = {
548
+ module: w.from.module,
549
+ type: w.from.type,
550
+ field: w.from.field,
551
+ instance: w.from.instance,
552
+ };
553
+ return (this.bridge?.wires.some((iw) => sameTrunk(iw.to, srcTrunk) && "from" in iw && !!iw.from.element) ?? false);
554
+ });
555
+ if (!hasElementSource && !hasTransitiveElementSource) {
556
+ return this.parent.schedule(target, pullChain);
557
+ }
558
+ }
559
+ // ── Sync work: collect and group bridge wires ─────────────────
560
+ // If this target is a pipe fork, also apply bridge wires from its base
561
+ // handle (non-pipe wires, e.g. `c.currency <- i.currency`) as defaults
562
+ // before the fork-specific pipe wires.
563
+ const targetKey = trunkKey(target);
564
+ const pipeFork = this.pipeHandleMap?.get(targetKey);
565
+ const baseTrunk = pipeFork?.baseTrunk;
566
+ const baseWires = baseTrunk
567
+ ? (this.bridge?.wires.filter((w) => !("pipe" in w) && sameTrunk(w.to, baseTrunk)) ?? [])
568
+ : [];
569
+ // Fork-specific wires (pipe wires targeting the fork's own instance)
570
+ const forkWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, target)) ?? [];
571
+ // Merge: base provides defaults, fork overrides
572
+ const bridgeWires = [...baseWires, ...forkWires];
573
+ // Look up ToolDef for this target
574
+ const toolName = this.getToolName(target);
575
+ const toolDef = this.resolveToolDefByName(toolName);
576
+ // Group wires by target path so that || (null-fallback) and ??
577
+ // (error-fallback) semantics are honoured via resolveWires().
578
+ const wireGroups = new Map();
579
+ for (const w of bridgeWires) {
580
+ const key = w.to.path.join(".");
581
+ let group = wireGroups.get(key);
582
+ if (!group) {
583
+ group = [];
584
+ wireGroups.set(key, group);
585
+ }
586
+ group.push(w);
587
+ }
588
+ // ── Async path: tool definition requires resolveToolWires + callTool ──
589
+ if (toolDef) {
590
+ return this.scheduleToolDef(toolName, toolDef, wireGroups, pullChain);
591
+ }
592
+ // ── Sync-capable path: no tool definition ──
593
+ // For __local bindings, __define_ pass-throughs, pipe forks backed by
594
+ // sync tools, and logic nodes — resolve bridge wires and return
595
+ // synchronously when all sources are already in state.
596
+ // See docs/performance.md (#12).
597
+ const groupEntries = Array.from(wireGroups.entries());
598
+ const nGroups = groupEntries.length;
599
+ const values = new Array(nGroups);
600
+ let hasAsync = false;
601
+ for (let i = 0; i < nGroups; i++) {
602
+ const v = this.resolveWires(groupEntries[i][1], pullChain);
603
+ values[i] = v;
604
+ if (!hasAsync && isPromise(v))
605
+ hasAsync = true;
606
+ }
607
+ if (!hasAsync) {
608
+ return this.scheduleFinish(target, toolName, groupEntries, values, baseTrunk);
609
+ }
610
+ return Promise.all(values).then((resolved) => this.scheduleFinish(target, toolName, groupEntries, resolved, baseTrunk));
611
+ }
612
+ /**
613
+ * Assemble input from resolved wire values and either invoke a direct tool
614
+ * function or return the data for pass-through targets (local/define/logic).
615
+ * Returns synchronously when the tool function (if any) returns sync.
616
+ * See docs/performance.md (#12).
617
+ */
618
+ scheduleFinish(target, toolName, groupEntries, resolvedValues, baseTrunk) {
619
+ const input = {};
620
+ const resolved = [];
621
+ for (let i = 0; i < groupEntries.length; i++) {
622
+ const path = groupEntries[i][1][0].to.path;
623
+ const value = resolvedValues[i];
624
+ resolved.push([path, value]);
625
+ if (path.length === 0 && value != null && typeof value === "object") {
626
+ Object.assign(input, value);
627
+ }
628
+ else {
629
+ setNested(input, path, value);
630
+ }
631
+ }
632
+ // Direct tool function lookup by name (simple or dotted).
633
+ // When the handle carries a @version tag, try the versioned key first
634
+ // (e.g. "std.str.toLowerCase@999.1") so user-injected overrides win.
635
+ // For pipe forks, fall back to the baseTrunk's version since forks
636
+ // use synthetic instance numbers (100000+).
637
+ const handleVersion = this.handleVersionMap.get(trunkKey(target)) ??
638
+ (baseTrunk ? this.handleVersionMap.get(trunkKey(baseTrunk)) : undefined);
639
+ let directFn = handleVersion
640
+ ? this.lookupToolFn(`${toolName}@${handleVersion}`)
641
+ : undefined;
642
+ if (!directFn) {
643
+ directFn = this.lookupToolFn(toolName);
644
+ }
645
+ if (directFn) {
646
+ return this.callTool(toolName, toolName, directFn, input);
647
+ }
648
+ // Define pass-through: synthetic trunks created by define inlining
649
+ // act as data containers — bridge wires set their values, no tool needed.
650
+ if (target.module.startsWith("__define_")) {
651
+ return input;
652
+ }
653
+ // Local binding or logic node: the wire resolves the source and stores
654
+ // the result — no tool call needed. For path=[] wires the resolved
655
+ // value may be a primitive (boolean from condAnd/condOr, string from
656
+ // a pipe tool like upperCase), so return the resolved value directly.
657
+ if (target.module === "__local" ||
658
+ target.field === "__and" ||
659
+ target.field === "__or") {
660
+ for (const [path, value] of resolved) {
661
+ if (path.length === 0)
662
+ return value;
663
+ }
664
+ return input;
665
+ }
666
+ throw new Error(`No tool found for "${toolName}"`);
667
+ }
668
+ /**
669
+ * Full async schedule path for targets backed by a ToolDef.
670
+ * Resolves tool wires, bridge wires, and invokes the tool function
671
+ * with error recovery support.
672
+ */
673
+ async scheduleToolDef(toolName, toolDef, wireGroups, pullChain) {
674
+ // Build input object: tool wires first (base), then bridge wires (override)
675
+ const input = {};
676
+ await this.resolveToolWires(toolDef, input);
677
+ // Resolve bridge wires and apply on top
678
+ const groupEntries = Array.from(wireGroups.entries());
679
+ const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
680
+ const value = await this.resolveWires(group, pullChain);
681
+ return [group[0].to.path, value];
682
+ }));
683
+ for (const [path, value] of resolved) {
684
+ if (path.length === 0 && value != null && typeof value === "object") {
685
+ Object.assign(input, value);
686
+ }
687
+ else {
688
+ setNested(input, path, value);
689
+ }
690
+ }
691
+ // Call ToolDef-backed tool function
692
+ const fn = this.lookupToolFn(toolDef.fn);
693
+ if (!fn)
694
+ throw new Error(`Tool function "${toolDef.fn}" not registered`);
695
+ // on error: wrap the tool call with fallback from onError wire
696
+ const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
697
+ try {
698
+ return await this.callTool(toolName, toolDef.fn, fn, input);
699
+ }
700
+ catch (err) {
701
+ if (!onErrorWire)
702
+ throw err;
703
+ if ("value" in onErrorWire)
704
+ return JSON.parse(onErrorWire.value);
705
+ return this.resolveToolSource(onErrorWire.source, toolDef);
706
+ }
707
+ }
708
+ /**
709
+ * Invoke a tool function, recording both an OpenTelemetry span and (when
710
+ * tracing is enabled) a ToolTrace entry. All three tool-call sites in the
711
+ * engine delegate here so instrumentation lives in exactly one place.
712
+ */
713
+ callTool(toolName, fnName, fnImpl, input) {
714
+ // Short-circuit before starting if externally aborted
715
+ if (this.signal?.aborted) {
716
+ throw new BridgeAbortError();
717
+ }
718
+ const tracer = this.tracer;
719
+ const logger = this.logger;
720
+ const toolContext = {
721
+ logger: logger ?? {},
722
+ signal: this.signal,
723
+ };
724
+ // ── Fast path: no instrumentation configured ──────────────────
725
+ // When there is no internal tracer, no logger, and OpenTelemetry
726
+ // has its default no-op provider, skip all instrumentation to
727
+ // avoid closure allocation, template-string building, and no-op
728
+ // metric calls. See docs/performance.md (#5).
729
+ if (!tracer && !logger && !isOtelActive()) {
730
+ return fnImpl(input, toolContext);
731
+ }
732
+ // ── Instrumented path ─────────────────────────────────────────
733
+ const traceStart = tracer?.now();
734
+ const metricAttrs = {
735
+ "bridge.tool.name": toolName,
736
+ "bridge.tool.fn": fnName,
737
+ };
738
+ return otelTracer.startActiveSpan(`bridge.tool.${toolName}.${fnName}`, { attributes: metricAttrs }, async (span) => {
739
+ const wallStart = performance.now();
740
+ try {
741
+ const result = await fnImpl(input, toolContext);
742
+ const durationMs = roundMs(performance.now() - wallStart);
743
+ toolCallCounter.add(1, metricAttrs);
744
+ toolDurationHistogram.record(durationMs, metricAttrs);
745
+ if (tracer && traceStart != null) {
746
+ tracer.record(tracer.entry({
747
+ tool: toolName,
748
+ fn: fnName,
749
+ input,
750
+ output: result,
751
+ durationMs: roundMs(tracer.now() - traceStart),
752
+ startedAt: traceStart,
753
+ }));
754
+ }
755
+ logger?.debug?.("[bridge] tool %s (%s) completed in %dms", toolName, fnName, durationMs);
756
+ return result;
757
+ }
758
+ catch (err) {
759
+ const durationMs = roundMs(performance.now() - wallStart);
760
+ toolCallCounter.add(1, metricAttrs);
761
+ toolDurationHistogram.record(durationMs, metricAttrs);
762
+ toolErrorCounter.add(1, metricAttrs);
763
+ if (tracer && traceStart != null) {
764
+ tracer.record(tracer.entry({
765
+ tool: toolName,
766
+ fn: fnName,
767
+ input,
768
+ error: err.message,
769
+ durationMs: roundMs(tracer.now() - traceStart),
770
+ startedAt: traceStart,
771
+ }));
772
+ }
773
+ span.recordException(err);
774
+ span.setStatus({
775
+ code: SpanStatusCode.ERROR,
776
+ message: err.message,
777
+ });
778
+ logger?.error?.("[bridge] tool %s (%s) failed: %s", toolName, fnName, err.message);
779
+ throw err;
780
+ }
781
+ finally {
782
+ span.end();
783
+ }
784
+ });
785
+ }
786
+ shadow() {
787
+ // Lightweight: bypass the constructor to avoid redundant work that
788
+ // re-derives data identical to the parent (bridge lookup, pipeHandleMap,
789
+ // handleVersionMap, constObj, toolFns spread). See docs/performance.md (#2).
790
+ const child = Object.create(ExecutionTree.prototype);
791
+ child.trunk = this.trunk;
792
+ child.document = this.document;
793
+ child.parent = this;
794
+ child.depth = this.depth + 1;
795
+ if (child.depth > MAX_EXECUTION_DEPTH) {
796
+ throw new BridgePanicError(`Maximum execution depth exceeded (${child.depth}) at ${trunkKey(this.trunk)}. Check for infinite recursion or circular array mappings.`);
797
+ }
798
+ child.state = {};
799
+ child.toolDepCache = new Map();
800
+ child.toolDefCache = new Map();
801
+ // Share read-only pre-computed data from parent
802
+ child.bridge = this.bridge;
803
+ child.pipeHandleMap = this.pipeHandleMap;
804
+ child.handleVersionMap = this.handleVersionMap;
805
+ child.toolFns = this.toolFns;
806
+ child.elementTrunkKey = this.elementTrunkKey;
807
+ child.tracer = this.tracer;
808
+ child.logger = this.logger;
809
+ child.signal = this.signal;
810
+ return child;
811
+ }
812
+ /** Returns collected traces (empty array when tracing is disabled). */
813
+ getTraces() {
814
+ return this.tracer?.traces ?? [];
815
+ }
816
+ /**
817
+ * Traverse `ref.path` on an already-resolved value, respecting null guards.
818
+ * Extracted from `pullSingle` so the sync and async paths can share logic.
819
+ */
820
+ applyPath(resolved, ref) {
821
+ if (!ref.path.length)
822
+ return resolved;
823
+ let result = resolved;
824
+ // Root-level null check
825
+ if (result == null) {
826
+ if (ref.rootSafe)
827
+ return undefined;
828
+ throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[0]}')`);
829
+ }
830
+ for (let i = 0; i < ref.path.length; i++) {
831
+ const segment = ref.path[i];
832
+ if (UNSAFE_KEYS.has(segment))
833
+ throw new Error(`Unsafe property traversal: ${segment}`);
834
+ if (Array.isArray(result) && !/^\d+$/.test(segment)) {
835
+ 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(".")}`);
836
+ }
837
+ result = result[segment];
838
+ if (result == null && i < ref.path.length - 1) {
839
+ const nextSafe = ref.pathSafe?.[i + 1] ?? false;
840
+ if (nextSafe)
841
+ return undefined;
842
+ throw new TypeError(`Cannot read properties of ${result} (reading '${ref.path[i + 1]}')`);
843
+ }
844
+ }
845
+ return result;
846
+ }
847
+ /**
848
+ * Pull a single value. Returns synchronously when already in state;
849
+ * returns a Promise only when the value is a pending tool call.
850
+ * See docs/performance.md (#10).
851
+ */
852
+ pullSingle(ref, pullChain = new Set()) {
853
+ // Cache trunkKey on the NodeRef to avoid repeated string allocation
854
+ // for the same AST node. See docs/performance.md (#11).
855
+ const key = (ref.__key ??= trunkKey(ref));
856
+ // ── Cycle detection ─────────────────────────────────────────────
857
+ if (pullChain.has(key)) {
858
+ throw new BridgePanicError(`Circular dependency detected: "${key}" depends on itself`);
859
+ }
860
+ // Walk the full parent chain — shadow trees may be nested multiple levels
861
+ let value = undefined;
862
+ let cursor = this;
863
+ while (cursor && value === undefined) {
864
+ value = cursor.state[key];
865
+ cursor = cursor.parent;
866
+ }
867
+ if (value === undefined) {
868
+ const nextChain = new Set(pullChain).add(key);
869
+ // ── Lazy define field resolution ────────────────────────────────
870
+ // For define trunks (__define_in_* / __define_out_*) with a specific
871
+ // field path, resolve ONLY the wire(s) targeting that field instead
872
+ // of scheduling the entire trunk. This avoids triggering unrelated
873
+ // dependency chains (e.g. requesting "city" should not fire the
874
+ // lat/lon coalesce chains that call the geo tool).
875
+ if (ref.path.length > 0 && ref.module.startsWith("__define_")) {
876
+ const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, ref) && pathEquals(w.to.path, ref.path)) ?? [];
877
+ if (fieldWires.length > 0) {
878
+ // resolveWires already delivers the value at ref.path — no applyPath.
879
+ return this.resolveWires(fieldWires, nextChain);
880
+ }
881
+ }
882
+ this.state[key] = this.schedule(ref, nextChain);
883
+ value = this.state[key]; // sync value or Promise (see #12)
884
+ }
885
+ // Sync fast path: value is already resolved (not a pending Promise).
886
+ if (!isPromise(value)) {
887
+ return this.applyPath(value, ref);
888
+ }
889
+ // Async: chain path traversal onto the pending promise.
890
+ return value.then((resolved) => this.applyPath(resolved, ref));
891
+ }
892
+ push(args) {
893
+ this.state[trunkKey(this.trunk)] = args;
894
+ }
895
+ /** Store the aggregated promise for critical forced handles so
896
+ * `response()` can await it exactly once per bridge execution. */
897
+ setForcedExecution(p) {
898
+ this.forcedExecution = p;
899
+ }
900
+ /** Return the critical forced-execution promise (if any). */
901
+ getForcedExecution() {
902
+ return this.forcedExecution;
903
+ }
904
+ /**
905
+ * Eagerly schedule tools targeted by `force <handle>` statements.
906
+ *
907
+ * Returns an array of promises for **critical** forced handles (those
908
+ * without `?? null`). Fire-and-forget handles (`catchError: true`) are
909
+ * scheduled but their errors are silently suppressed.
910
+ *
911
+ * Callers must `await Promise.all(...)` the returned promises so that a
912
+ * critical force failure propagates as a standard error.
913
+ */
914
+ executeForced() {
915
+ const forces = this.bridge?.forces;
916
+ if (!forces || forces.length === 0)
917
+ return [];
918
+ const critical = [];
919
+ const scheduled = new Set();
920
+ for (const f of forces) {
921
+ const trunk = {
922
+ module: f.module,
923
+ type: f.type,
924
+ field: f.field,
925
+ instance: f.instance,
926
+ };
927
+ const key = trunkKey(trunk);
928
+ if (scheduled.has(key) || this.state[key] !== undefined)
929
+ continue;
930
+ scheduled.add(key);
931
+ this.state[key] = this.schedule(trunk);
932
+ if (f.catchError) {
933
+ // Fire-and-forget: suppress unhandled rejection.
934
+ Promise.resolve(this.state[key]).catch(() => { });
935
+ }
936
+ else {
937
+ // Critical: caller must await and let failure propagate.
938
+ critical.push(Promise.resolve(this.state[key]));
939
+ }
940
+ }
941
+ return critical;
942
+ }
943
+ /**
944
+ * Resolve a set of matched wires.
945
+ *
946
+ * Architecture: two distinct resolution axes —
947
+ *
948
+ * **Falsy Gate** (`||`, within a wire): `falsyFallbackRefs` + `falsyFallback`
949
+ * → truthy check — falsy values (0, "", false) trigger fallback chain.
950
+ *
951
+ * **Overdefinition** (across wires): multiple wires target the same path
952
+ * → nullish check — only null/undefined falls through to the next wire.
953
+ *
954
+ * Per-wire layers:
955
+ * Layer 1 — Execution (pullSingle + safe modifier)
956
+ * Layer 2a — Falsy Gate (falsyFallbackRefs → falsyFallback / falsyControl)
957
+ * Layer 2b — Nullish Gate (nullishFallbackRef / nullishFallback / nullishControl)
958
+ * Layer 3 — Catch (catchFallbackRef / catchFallback / catchControl)
959
+ *
960
+ * After layers 1–2b, the overdefinition boundary (`!= null`) decides whether
961
+ * to return or continue to the next wire.
962
+ */
963
+ /**
964
+ * Resolve wires, returning synchronously when the hot path allows it.
965
+ *
966
+ * Fast path: single `from` wire with no fallback/catch modifiers, which is
967
+ * the common case for element field wires like `.id <- it.id`. Delegates to
968
+ * `resolveWiresAsync` for anything more complex.
969
+ * See docs/performance.md (#10).
970
+ */
971
+ resolveWires(wires, pullChain) {
972
+ if (wires.length === 1) {
973
+ const w = wires[0];
974
+ if ("value" in w)
975
+ return coerceConstant(w.value);
976
+ const ref = getSimplePullRef(w);
977
+ if (ref)
978
+ return this.pullSingle(ref, pullChain);
979
+ }
980
+ return this.resolveWiresAsync(wires, pullChain);
981
+ }
982
+ async resolveWiresAsync(wires, pullChain) {
983
+ let lastError;
984
+ for (const w of wires) {
985
+ // Constant wire — always wins, no modifiers
986
+ if ("value" in w)
987
+ return coerceConstant(w.value);
988
+ try {
989
+ // --- Layer 1: Execution ---
990
+ let resolvedValue;
991
+ if ("cond" in w) {
992
+ const condValue = await this.pullSingle(w.cond, pullChain);
993
+ if (condValue) {
994
+ if (w.thenRef !== undefined)
995
+ resolvedValue = await this.pullSingle(w.thenRef, pullChain);
996
+ else if (w.thenValue !== undefined)
997
+ resolvedValue = coerceConstant(w.thenValue);
998
+ }
999
+ else {
1000
+ if (w.elseRef !== undefined)
1001
+ resolvedValue = await this.pullSingle(w.elseRef, pullChain);
1002
+ else if (w.elseValue !== undefined)
1003
+ resolvedValue = coerceConstant(w.elseValue);
1004
+ }
1005
+ }
1006
+ else if ("condAnd" in w) {
1007
+ const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condAnd;
1008
+ const leftVal = isSafe
1009
+ ? await this.pullSingle(leftRef, pullChain).catch((e) => {
1010
+ if (isFatalError(e))
1011
+ throw e;
1012
+ return undefined;
1013
+ })
1014
+ : await this.pullSingle(leftRef, pullChain);
1015
+ if (!leftVal) {
1016
+ resolvedValue = false;
1017
+ }
1018
+ else if (rightRef !== undefined) {
1019
+ const rightVal = rightSafe
1020
+ ? await this.pullSingle(rightRef, pullChain).catch((e) => {
1021
+ if (isFatalError(e))
1022
+ throw e;
1023
+ return undefined;
1024
+ })
1025
+ : await this.pullSingle(rightRef, pullChain);
1026
+ resolvedValue = Boolean(rightVal);
1027
+ }
1028
+ else if (rightValue !== undefined) {
1029
+ resolvedValue = Boolean(coerceConstant(rightValue));
1030
+ }
1031
+ else {
1032
+ resolvedValue = Boolean(leftVal);
1033
+ }
1034
+ }
1035
+ else if ("condOr" in w) {
1036
+ const { leftRef, rightRef, rightValue, safe: isSafe, rightSafe, } = w.condOr;
1037
+ const leftVal = isSafe
1038
+ ? await this.pullSingle(leftRef, pullChain).catch((e) => {
1039
+ if (isFatalError(e))
1040
+ throw e;
1041
+ return undefined;
1042
+ })
1043
+ : await this.pullSingle(leftRef, pullChain);
1044
+ if (leftVal) {
1045
+ resolvedValue = true;
1046
+ }
1047
+ else if (rightRef !== undefined) {
1048
+ const rightVal = rightSafe
1049
+ ? await this.pullSingle(rightRef, pullChain).catch((e) => {
1050
+ if (isFatalError(e))
1051
+ throw e;
1052
+ return undefined;
1053
+ })
1054
+ : await this.pullSingle(rightRef, pullChain);
1055
+ resolvedValue = Boolean(rightVal);
1056
+ }
1057
+ else if (rightValue !== undefined) {
1058
+ resolvedValue = Boolean(coerceConstant(rightValue));
1059
+ }
1060
+ else {
1061
+ resolvedValue = Boolean(leftVal);
1062
+ }
1063
+ }
1064
+ else if ("from" in w) {
1065
+ if (w.safe) {
1066
+ try {
1067
+ resolvedValue = await this.pullSingle(w.from, pullChain);
1068
+ }
1069
+ catch (err) {
1070
+ if (isFatalError(err))
1071
+ throw err;
1072
+ resolvedValue = undefined;
1073
+ }
1074
+ }
1075
+ else {
1076
+ resolvedValue = await this.pullSingle(w.from, pullChain);
1077
+ }
1078
+ }
1079
+ else {
1080
+ continue;
1081
+ }
1082
+ // --- Layer 2a: Falsy Gate (||) ---
1083
+ if (!resolvedValue && w.falsyFallbackRefs?.length) {
1084
+ for (const ref of w.falsyFallbackRefs) {
1085
+ // Assign the fallback value regardless of whether it is truthy or falsy.
1086
+ // e.g. `false || 0` will correctly update resolvedValue to `0`.
1087
+ resolvedValue = await this.pullSingle(ref, pullChain);
1088
+ // If it is truthy, we are done! Short-circuit the || chain.
1089
+ if (resolvedValue)
1090
+ break;
1091
+ }
1092
+ }
1093
+ if (!resolvedValue) {
1094
+ if (w.falsyControl) {
1095
+ resolvedValue = applyControlFlow(w.falsyControl);
1096
+ }
1097
+ else if (w.falsyFallback != null) {
1098
+ resolvedValue = coerceConstant(w.falsyFallback);
1099
+ }
1100
+ }
1101
+ // --- Layer 2b: Nullish Gate (??) ---
1102
+ if (resolvedValue == null) {
1103
+ if (w.nullishControl) {
1104
+ resolvedValue = applyControlFlow(w.nullishControl);
1105
+ }
1106
+ else if (w.nullishFallbackRef) {
1107
+ resolvedValue = await this.pullSingle(w.nullishFallbackRef, pullChain);
1108
+ }
1109
+ else if (w.nullishFallback != null) {
1110
+ resolvedValue = coerceConstant(w.nullishFallback);
1111
+ }
1112
+ }
1113
+ // --- Overdefinition Boundary ---
1114
+ if (resolvedValue != null)
1115
+ return resolvedValue;
1116
+ }
1117
+ catch (err) {
1118
+ // --- Layer 3: Catch ---
1119
+ if (isFatalError(err))
1120
+ throw err;
1121
+ if (w.catchControl)
1122
+ return applyControlFlow(w.catchControl);
1123
+ if (w.catchFallbackRef)
1124
+ return this.pullSingle(w.catchFallbackRef, pullChain);
1125
+ if (w.catchFallback != null)
1126
+ return coerceConstant(w.catchFallback);
1127
+ lastError = err;
1128
+ }
1129
+ }
1130
+ if (lastError)
1131
+ throw lastError;
1132
+ return undefined;
1133
+ }
1134
+ /**
1135
+ * Resolve an output field by path for use outside of a GraphQL resolver.
1136
+ *
1137
+ * This is the non-GraphQL equivalent of what `response()` does per field:
1138
+ * it finds all wires targeting `this.trunk` at `path` and resolves them.
1139
+ *
1140
+ * Used by `executeBridge()` so standalone bridge execution does not need to
1141
+ * fabricate GraphQL Path objects to pull output data.
1142
+ *
1143
+ * @param path - Output field path, e.g. `["lat"]`. Pass `[]` for whole-output
1144
+ * array bridges (`o <- items[] as x { ... }`).
1145
+ * @param array - When `true` and the result is an array, wraps each element
1146
+ * in a shadow tree (mirrors `response()` array handling).
1147
+ */
1148
+ async pullOutputField(path, array = false) {
1149
+ const matches = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) && pathEquals(w.to.path, path)) ?? [];
1150
+ if (matches.length === 0)
1151
+ return undefined;
1152
+ const result = this.resolveWires(matches);
1153
+ if (!array)
1154
+ return result;
1155
+ const resolved = await result;
1156
+ if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
1157
+ return [];
1158
+ const items = resolved;
1159
+ const finalShadowTrees = [];
1160
+ for (const item of items) {
1161
+ if (item === BREAK_SYM)
1162
+ break;
1163
+ if (item === CONTINUE_SYM)
1164
+ continue;
1165
+ const s = this.shadow();
1166
+ s.state[this.elementTrunkKey] = item;
1167
+ finalShadowTrees.push(s);
1168
+ }
1169
+ return finalShadowTrees;
1170
+ }
1171
+ /**
1172
+ * Resolve pre-grouped wires on this shadow tree without re-filtering.
1173
+ * Called by the parent's `materializeShadows` to skip per-element wire
1174
+ * filtering. Returns synchronously when the wire resolves sync (hot path).
1175
+ * See docs/performance.md (#8, #10).
1176
+ */
1177
+ resolvePreGrouped(wires) {
1178
+ return this.resolveWires(wires);
1179
+ }
1180
+ /**
1181
+ * Materialise all output wires into a plain JS object.
1182
+ *
1183
+ * Used by the GraphQL adapter when a bridge field returns a scalar type
1184
+ * (e.g. `JSON`, `JSONObject`). In that case GraphQL won't call sub-field
1185
+ * resolvers, so we need to eagerly resolve every output wire and assemble
1186
+ * the result ourselves — the same logic `run()` uses for object output.
1187
+ */
1188
+ async collectOutput() {
1189
+ const bridge = this.bridge;
1190
+ if (!bridge)
1191
+ return undefined;
1192
+ const { type, field } = this.trunk;
1193
+ // Shadow tree (array element) — resolve element-level output fields.
1194
+ // For scalar arrays ([JSON!]) GraphQL won't call sub-field resolvers,
1195
+ // so we eagerly materialise each element here.
1196
+ if (this.parent) {
1197
+ const outputFields = new Set();
1198
+ for (const wire of bridge.wires) {
1199
+ if (wire.to.module === SELF_MODULE &&
1200
+ wire.to.type === type &&
1201
+ wire.to.field === field &&
1202
+ wire.to.path.length > 0) {
1203
+ outputFields.add(wire.to.path[0]);
1204
+ }
1205
+ }
1206
+ if (outputFields.size > 0) {
1207
+ const result = {};
1208
+ await Promise.all([...outputFields].map(async (name) => {
1209
+ result[name] = await this.pullOutputField([name]);
1210
+ }));
1211
+ return result;
1212
+ }
1213
+ // Passthrough: return stored element data directly
1214
+ return this.state[this.elementTrunkKey];
1215
+ }
1216
+ // Root wire (`o <- src`) — whole-object passthrough
1217
+ const hasRootWire = bridge.wires.some((w) => "from" in w &&
1218
+ w.to.module === SELF_MODULE &&
1219
+ w.to.type === type &&
1220
+ w.to.field === field &&
1221
+ w.to.path.length === 0);
1222
+ if (hasRootWire) {
1223
+ return this.pullOutputField([]);
1224
+ }
1225
+ // Object output — collect unique top-level field names
1226
+ const outputFields = new Set();
1227
+ for (const wire of bridge.wires) {
1228
+ if (wire.to.module === SELF_MODULE &&
1229
+ wire.to.type === type &&
1230
+ wire.to.field === field &&
1231
+ wire.to.path.length > 0) {
1232
+ outputFields.add(wire.to.path[0]);
1233
+ }
1234
+ }
1235
+ if (outputFields.size === 0)
1236
+ return undefined;
1237
+ const result = {};
1238
+ const resolveField = async (prefix) => {
1239
+ const exactWires = bridge.wires.filter((w) => w.to.module === SELF_MODULE &&
1240
+ w.to.type === type &&
1241
+ w.to.field === field &&
1242
+ pathEquals(w.to.path, prefix));
1243
+ if (exactWires.length > 0) {
1244
+ return this.resolveWires(exactWires);
1245
+ }
1246
+ const subFields = new Set();
1247
+ for (const wire of bridge.wires) {
1248
+ const p = wire.to.path;
1249
+ if (wire.to.module === SELF_MODULE &&
1250
+ wire.to.type === type &&
1251
+ wire.to.field === field &&
1252
+ p.length > prefix.length &&
1253
+ prefix.every((seg, i) => p[i] === seg)) {
1254
+ subFields.add(p[prefix.length]);
1255
+ }
1256
+ }
1257
+ if (subFields.size === 0)
1258
+ return undefined;
1259
+ const obj = {};
1260
+ await Promise.all([...subFields].map(async (sub) => {
1261
+ obj[sub] = await resolveField([...prefix, sub]);
1262
+ }));
1263
+ return obj;
1264
+ };
1265
+ await Promise.all([...outputFields].map(async (name) => {
1266
+ result[name] = await resolveField([name]);
1267
+ }));
1268
+ return result;
1269
+ }
1270
+ /**
1271
+ * Execute the bridge end-to-end without GraphQL.
1272
+ *
1273
+ * Injects `input` as the trunk arguments, runs forced wires, then pulls
1274
+ * and materialises every output field into a plain JS object (or array of
1275
+ * objects for array-mapped bridges).
1276
+ *
1277
+ * This is the single entry-point used by `executeBridge()`.
1278
+ */
1279
+ async run(input) {
1280
+ const bridge = this.bridge;
1281
+ if (!bridge) {
1282
+ throw new Error(`No bridge definition found for ${this.trunk.type}.${this.trunk.field}`);
1283
+ }
1284
+ this.push(input);
1285
+ const forcePromises = this.executeForced();
1286
+ const { type, field } = this.trunk;
1287
+ // Is there a root-level wire targeting the output with path []?
1288
+ const hasRootWire = bridge.wires.some((w) => "from" in w &&
1289
+ w.to.module === SELF_MODULE &&
1290
+ w.to.type === type &&
1291
+ w.to.field === field &&
1292
+ w.to.path.length === 0);
1293
+ // Array-mapped output (`o <- items[] as x { ... }`) has BOTH a root wire
1294
+ // AND element-level wires (from.element === true). A plain passthrough
1295
+ // (`o <- api.user`) only has the root wire.
1296
+ // Local bindings (from.__local) are also element-scoped.
1297
+ // Pipe fork output wires in element context (e.g. concat template strings)
1298
+ // may have to.element === true instead.
1299
+ const hasElementWires = bridge.wires.some((w) => "from" in w &&
1300
+ (w.from.element === true ||
1301
+ w.from.module === "__local" ||
1302
+ w.to.element === true) &&
1303
+ w.to.module === SELF_MODULE &&
1304
+ w.to.type === type &&
1305
+ w.to.field === field);
1306
+ if (hasRootWire && hasElementWires) {
1307
+ const [shadows] = await Promise.all([
1308
+ this.pullOutputField([], true),
1309
+ ...forcePromises,
1310
+ ]);
1311
+ return this.materializeShadows(shadows, []);
1312
+ }
1313
+ // Whole-object passthrough: `o <- api.user`
1314
+ if (hasRootWire) {
1315
+ const [result] = await Promise.all([
1316
+ this.pullOutputField([]),
1317
+ ...forcePromises,
1318
+ ]);
1319
+ return result;
1320
+ }
1321
+ // Object output — collect unique top-level field names
1322
+ const outputFields = new Set();
1323
+ for (const wire of bridge.wires) {
1324
+ if (wire.to.module === SELF_MODULE &&
1325
+ wire.to.type === type &&
1326
+ wire.to.field === field &&
1327
+ wire.to.path.length > 0) {
1328
+ outputFields.add(wire.to.path[0]);
1329
+ }
1330
+ }
1331
+ if (outputFields.size === 0) {
1332
+ throw new Error(`Bridge "${type}.${field}" has no output wires. ` +
1333
+ `Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).`);
1334
+ }
1335
+ const result = {};
1336
+ // Resolves a single output field at `prefix` — either via an exact-match
1337
+ // wire (leaf), or by collecting sub-fields from deeper wires (nested object).
1338
+ const resolveField = async (prefix) => {
1339
+ const exactWires = bridge.wires.filter((w) => w.to.module === SELF_MODULE &&
1340
+ w.to.type === type &&
1341
+ w.to.field === field &&
1342
+ pathEquals(w.to.path, prefix));
1343
+ if (exactWires.length > 0) {
1344
+ return this.resolveWires(exactWires);
1345
+ }
1346
+ // No exact wire — gather sub-field names from deeper-path wires
1347
+ // (e.g. `o.why { .temperature <- ... }` produces path ["why","temperature"])
1348
+ const subFields = new Set();
1349
+ for (const wire of bridge.wires) {
1350
+ const p = wire.to.path;
1351
+ if (wire.to.module === SELF_MODULE &&
1352
+ wire.to.type === type &&
1353
+ wire.to.field === field &&
1354
+ p.length > prefix.length &&
1355
+ prefix.every((seg, i) => p[i] === seg)) {
1356
+ subFields.add(p[prefix.length]);
1357
+ }
1358
+ }
1359
+ if (subFields.size === 0)
1360
+ return undefined;
1361
+ const obj = {};
1362
+ await Promise.all([...subFields].map(async (sub) => {
1363
+ obj[sub] = await resolveField([...prefix, sub]);
1364
+ }));
1365
+ return obj;
1366
+ };
1367
+ await Promise.all([
1368
+ ...[...outputFields].map(async (name) => {
1369
+ result[name] = await resolveField([name]);
1370
+ }),
1371
+ ...forcePromises,
1372
+ ]);
1373
+ return result;
1374
+ }
1375
+ /**
1376
+ * Recursively convert shadow trees into plain JS objects.
1377
+ *
1378
+ * Wire categories at each level (prefix = P):
1379
+ * Leaf — `to.path = [...P, name]`, no deeper paths → scalar
1380
+ * Array — direct wire AND deeper paths → pull as array, recurse
1381
+ * Nested object — only deeper paths, no direct wire → pull each
1382
+ * full path and assemble via setNested
1383
+ */
1384
+ async materializeShadows(items, pathPrefix) {
1385
+ const wires = this.bridge.wires;
1386
+ const { type, field } = this.trunk;
1387
+ const directFields = new Set();
1388
+ const deepPaths = new Map();
1389
+ // #8: Pre-group wires by exact path — eliminates per-element re-filtering.
1390
+ // Key: wire.to.path joined by \0 (null char is safe — field names are identifiers).
1391
+ const wireGroupsByPath = new Map();
1392
+ for (const wire of wires) {
1393
+ const p = wire.to.path;
1394
+ if (wire.to.module !== SELF_MODULE ||
1395
+ wire.to.type !== type ||
1396
+ wire.to.field !== field)
1397
+ continue;
1398
+ if (p.length <= pathPrefix.length)
1399
+ continue;
1400
+ if (!pathPrefix.every((seg, i) => p[i] === seg))
1401
+ continue;
1402
+ const name = p[pathPrefix.length];
1403
+ if (p.length === pathPrefix.length + 1) {
1404
+ directFields.add(name);
1405
+ const pathKey = p.join("\0");
1406
+ let group = wireGroupsByPath.get(pathKey);
1407
+ if (!group) {
1408
+ group = [];
1409
+ wireGroupsByPath.set(pathKey, group);
1410
+ }
1411
+ group.push(wire);
1412
+ }
1413
+ else {
1414
+ let arr = deepPaths.get(name);
1415
+ if (!arr) {
1416
+ arr = [];
1417
+ deepPaths.set(name, arr);
1418
+ }
1419
+ arr.push(p);
1420
+ }
1421
+ }
1422
+ // #9/#10: Fast path — no nested arrays, only direct fields.
1423
+ // Collect all (shadow × field) resolutions. When every value is already in
1424
+ // state (the hot case for element passthrough), resolvePreGrouped returns
1425
+ // synchronously and we skip Promise.all entirely.
1426
+ // See docs/performance.md (#9, #10).
1427
+ if (deepPaths.size === 0) {
1428
+ const directFieldArray = [...directFields];
1429
+ const nFields = directFieldArray.length;
1430
+ const nItems = items.length;
1431
+ // Pre-compute pathKeys and wire groups — only depend on j, not i.
1432
+ // See docs/performance.md (#11).
1433
+ const preGroups = new Array(nFields);
1434
+ for (let j = 0; j < nFields; j++) {
1435
+ const pathKey = [...pathPrefix, directFieldArray[j]].join("\0");
1436
+ preGroups[j] = wireGroupsByPath.get(pathKey);
1437
+ }
1438
+ const rawValues = new Array(nItems * nFields);
1439
+ let hasAsync = false;
1440
+ for (let i = 0; i < nItems; i++) {
1441
+ const shadow = items[i];
1442
+ for (let j = 0; j < nFields; j++) {
1443
+ const v = shadow.resolvePreGrouped(preGroups[j]);
1444
+ rawValues[i * nFields + j] = v;
1445
+ if (!hasAsync && isPromise(v))
1446
+ hasAsync = true;
1447
+ }
1448
+ }
1449
+ const flatValues = hasAsync
1450
+ ? await Promise.all(rawValues)
1451
+ : rawValues;
1452
+ const finalResults = [];
1453
+ for (let i = 0; i < items.length; i++) {
1454
+ const obj = {};
1455
+ let doBreak = false;
1456
+ let doSkip = false;
1457
+ for (let j = 0; j < nFields; j++) {
1458
+ const v = flatValues[i * nFields + j];
1459
+ if (v === BREAK_SYM) {
1460
+ doBreak = true;
1461
+ break;
1462
+ }
1463
+ if (v === CONTINUE_SYM) {
1464
+ doSkip = true;
1465
+ break;
1466
+ }
1467
+ obj[directFieldArray[j]] = v;
1468
+ }
1469
+ if (doBreak)
1470
+ break;
1471
+ if (doSkip)
1472
+ continue;
1473
+ finalResults.push(obj);
1474
+ }
1475
+ return finalResults;
1476
+ }
1477
+ // Slow path: deep paths (nested arrays) present.
1478
+ // Uses pre-grouped wires for direct fields (#8), original logic for the rest.
1479
+ const rawResults = await Promise.all(items.map(async (shadow) => {
1480
+ const obj = {};
1481
+ const tasks = [];
1482
+ for (const name of directFields) {
1483
+ const fullPath = [...pathPrefix, name];
1484
+ const hasDeeper = deepPaths.has(name);
1485
+ tasks.push((async () => {
1486
+ if (hasDeeper) {
1487
+ const children = await shadow.pullOutputField(fullPath, true);
1488
+ obj[name] = Array.isArray(children)
1489
+ ? await this.materializeShadows(children, fullPath)
1490
+ : children;
1491
+ }
1492
+ else {
1493
+ // #8: wireGroupsByPath is built in the same branch that populates
1494
+ // directFields, so the group is always present — no fallback needed.
1495
+ const pathKey = fullPath.join("\0");
1496
+ obj[name] = await shadow.resolvePreGrouped(wireGroupsByPath.get(pathKey));
1497
+ }
1498
+ })());
1499
+ }
1500
+ for (const [name, paths] of deepPaths) {
1501
+ if (directFields.has(name))
1502
+ continue;
1503
+ tasks.push((async () => {
1504
+ const nested = {};
1505
+ await Promise.all(paths.map(async (fullPath) => {
1506
+ const value = await shadow.pullOutputField(fullPath);
1507
+ setNested(nested, fullPath.slice(pathPrefix.length + 1), value);
1508
+ }));
1509
+ obj[name] = nested;
1510
+ })());
1511
+ }
1512
+ await Promise.all(tasks);
1513
+ // Check if any field resolved to a sentinel — propagate it
1514
+ for (const v of Object.values(obj)) {
1515
+ if (v === CONTINUE_SYM)
1516
+ return CONTINUE_SYM;
1517
+ if (v === BREAK_SYM)
1518
+ return BREAK_SYM;
1519
+ }
1520
+ return obj;
1521
+ }));
1522
+ // Filter sentinels from the final result
1523
+ const finalResults = [];
1524
+ for (const item of rawResults) {
1525
+ if (item === BREAK_SYM)
1526
+ break;
1527
+ if (item === CONTINUE_SYM)
1528
+ continue;
1529
+ finalResults.push(item);
1530
+ }
1531
+ return finalResults;
1532
+ }
1533
+ async response(ipath, array) {
1534
+ // Build path segments from GraphQL resolver info
1535
+ const pathSegments = [];
1536
+ let index = ipath;
1537
+ while (index.prev) {
1538
+ pathSegments.unshift(`${index.key}`);
1539
+ index = index.prev;
1540
+ }
1541
+ if (pathSegments.length === 0) {
1542
+ // Direct output for scalar/list return types (e.g. [String!])
1543
+ const directOutput = this.bridge?.wires.filter((w) => sameTrunk(w.to, this.trunk) &&
1544
+ w.to.path.length === 1 &&
1545
+ w.to.path[0] === this.trunk.field) ?? [];
1546
+ if (directOutput.length > 0) {
1547
+ return this.resolveWires(directOutput);
1548
+ }
1549
+ }
1550
+ // Strip numeric indices (array positions) from path for wire matching
1551
+ const cleanPath = pathSegments.filter((p) => !/^\d+$/.test(p));
1552
+ // Find wires whose target matches this trunk + path
1553
+ const matches = this.bridge?.wires.filter((w) => (w.to.element ? !!this.parent : true) &&
1554
+ sameTrunk(w.to, this.trunk) &&
1555
+ pathEquals(w.to.path, cleanPath)) ?? [];
1556
+ if (matches.length > 0) {
1557
+ // ── Lazy define resolution ──────────────────────────────────────
1558
+ // When ALL matches at the root object level (path=[]) are
1559
+ // whole-object wires sourced from define output modules, defer
1560
+ // resolution to field-by-field GraphQL traversal. This avoids
1561
+ // eagerly scheduling every tool inside the define block — only
1562
+ // fields actually requested by the query will trigger their
1563
+ // dependency chains.
1564
+ if (cleanPath.length === 0 &&
1565
+ !array &&
1566
+ matches.every((w) => "from" in w &&
1567
+ w.from.module.startsWith("__define_out_") &&
1568
+ w.from.path.length === 0)) {
1569
+ return this;
1570
+ }
1571
+ const response = this.resolveWires(matches);
1572
+ if (!array) {
1573
+ return response;
1574
+ }
1575
+ // Array: create shadow trees for per-element resolution
1576
+ const resolved = await response;
1577
+ if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
1578
+ return [];
1579
+ const items = resolved;
1580
+ const shadowTrees = [];
1581
+ for (const item of items) {
1582
+ if (item === BREAK_SYM)
1583
+ break;
1584
+ if (item === CONTINUE_SYM)
1585
+ continue;
1586
+ const s = this.shadow();
1587
+ s.state[this.elementTrunkKey] = item;
1588
+ shadowTrees.push(s);
1589
+ }
1590
+ return shadowTrees;
1591
+ }
1592
+ // ── Resolve field from deferred define ────────────────────────────
1593
+ // No direct wires for this field path — check whether a define
1594
+ // forward wire exists at the root level (`o <- defineHandle`) and
1595
+ // resolve only the matching field wire from the define's output.
1596
+ if (cleanPath.length > 0) {
1597
+ const defineFieldWires = this.findDefineFieldWires(cleanPath);
1598
+ if (defineFieldWires.length > 0) {
1599
+ const response = this.resolveWires(defineFieldWires);
1600
+ if (!array)
1601
+ return response;
1602
+ const resolved = await response;
1603
+ if (resolved === BREAK_SYM || resolved === CONTINUE_SYM)
1604
+ return [];
1605
+ const items = resolved;
1606
+ const shadowTrees = [];
1607
+ for (const item of items) {
1608
+ if (item === BREAK_SYM)
1609
+ break;
1610
+ if (item === CONTINUE_SYM)
1611
+ continue;
1612
+ const s = this.shadow();
1613
+ s.state[this.elementTrunkKey] = item;
1614
+ shadowTrees.push(s);
1615
+ }
1616
+ return shadowTrees;
1617
+ }
1618
+ }
1619
+ // Fallback: if this shadow tree has stored element data, resolve the
1620
+ // requested field directly from it. This handles passthrough arrays
1621
+ // where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
1622
+ // doesn't explicitly wire each scalar field on the element type.
1623
+ if (this.parent) {
1624
+ const elementData = this.state[this.elementTrunkKey];
1625
+ if (elementData != null &&
1626
+ typeof elementData === "object" &&
1627
+ !Array.isArray(elementData)) {
1628
+ const fieldName = cleanPath[cleanPath.length - 1];
1629
+ if (fieldName !== undefined && fieldName in elementData) {
1630
+ const value = elementData[fieldName];
1631
+ if (array && Array.isArray(value)) {
1632
+ // Nested array: wrap items in shadow trees so they can
1633
+ // resolve their own fields via this same fallback path.
1634
+ return value.map((item) => {
1635
+ const s = this.shadow();
1636
+ s.state[this.elementTrunkKey] = item;
1637
+ return s;
1638
+ });
1639
+ }
1640
+ return value;
1641
+ }
1642
+ }
1643
+ }
1644
+ // Return self to trigger downstream resolvers
1645
+ return this;
1646
+ }
1647
+ /**
1648
+ * Find define output wires for a specific field path.
1649
+ *
1650
+ * Looks for whole-object define forward wires (`o <- defineHandle`)
1651
+ * at path=[] for this trunk, then searches the define's output wires
1652
+ * for ones matching the requested field path.
1653
+ */
1654
+ findDefineFieldWires(cleanPath) {
1655
+ const forwards = this.bridge?.wires.filter((w) => "from" in w &&
1656
+ sameTrunk(w.to, this.trunk) &&
1657
+ w.to.path.length === 0 &&
1658
+ w.from.module.startsWith("__define_out_") &&
1659
+ w.from.path.length === 0) ?? [];
1660
+ if (forwards.length === 0)
1661
+ return [];
1662
+ const result = [];
1663
+ for (const fw of forwards) {
1664
+ const defOutTrunk = fw.from;
1665
+ const fieldWires = this.bridge?.wires.filter((w) => sameTrunk(w.to, defOutTrunk) && pathEquals(w.to.path, cleanPath)) ?? [];
1666
+ result.push(...fieldWires);
1667
+ }
1668
+ return result;
1669
+ }
1670
+ }