@stackables/bridge 1.4.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,45 @@ type Trunk = {
11
11
  field: string;
12
12
  instance?: number;
13
13
  };
14
+ /** Trace verbosity level */
15
+ export type TraceLevel = "basic" | "full";
16
+ /** A single recorded tool invocation. */
17
+ export type ToolTrace = {
18
+ /** Tool name as resolved (e.g. "hereGeo", "std.upperCase") */
19
+ tool: string;
20
+ /** The function that was called (e.g. "httpCall", "upperCase") */
21
+ fn: string;
22
+ /** Input object passed to the tool function (only in "full" level) */
23
+ input?: Record<string, any>;
24
+ /** Resolved output (only in "full" level, on success) */
25
+ output?: any;
26
+ /** Error message (present when the tool threw) */
27
+ error?: string;
28
+ /** Wall-clock duration in milliseconds */
29
+ durationMs: number;
30
+ /** Monotonic timestamp (ms) relative to the first trace in the request */
31
+ startedAt: number;
32
+ };
33
+ /** Shared trace collector — one per request, passed through the tree. */
34
+ export declare class TraceCollector {
35
+ readonly traces: ToolTrace[];
36
+ readonly level: TraceLevel;
37
+ private readonly epoch;
38
+ constructor(level?: TraceLevel);
39
+ /** Returns ms since the collector was created */
40
+ now(): number;
41
+ record(trace: ToolTrace): void;
42
+ /** Build a trace entry, omitting input/output for basic level. */
43
+ entry(base: {
44
+ tool: string;
45
+ fn: string;
46
+ startedAt: number;
47
+ durationMs: number;
48
+ input?: Record<string, any>;
49
+ output?: any;
50
+ error?: string;
51
+ }): ToolTrace;
52
+ }
14
53
  export declare class ExecutionTree {
15
54
  trunk: Trunk;
16
55
  private instructions;
@@ -22,6 +61,8 @@ export declare class ExecutionTree {
22
61
  private toolDepCache;
23
62
  private toolDefCache;
24
63
  private pipeHandleMap;
64
+ /** Shared trace collector — present only when tracing is enabled. */
65
+ tracer?: TraceCollector;
25
66
  constructor(trunk: Trunk, instructions: Instruction[], toolFns?: ToolMap | undefined, context?: Record<string, any> | undefined, parent?: ExecutionTree | undefined);
26
67
  /** Derive tool name from a trunk */
27
68
  private getToolName;
@@ -38,7 +79,15 @@ export declare class ExecutionTree {
38
79
  private resolveToolDep;
39
80
  schedule(target: Trunk): any;
40
81
  shadow(): ExecutionTree;
82
+ /** Returns collected traces (empty array when tracing is disabled). */
83
+ getTraces(): ToolTrace[];
41
84
  private pullSingle;
85
+ /**
86
+ * Infer the cost of resolving a NodeRef.
87
+ * Cost 0: memory reads (input, context, const) — no I/O.
88
+ * Cost 1: everything else (tool calls, pipes, defines) — may involve network.
89
+ */
90
+ private inferCost;
42
91
  pull(refs: NodeRef[]): Promise<any>;
43
92
  push(args: Record<string, any>): void;
44
93
  /** Eagerly schedule tools targeted by forced (<-!) wires. */
@@ -1 +1 @@
1
- {"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../src/ExecutionTree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EAEN,WAAW,EACX,OAAO,EAGP,OAAO,EAER,MAAM,YAAY,CAAC;AAGpB,gFAAgF;AAChF,UAAU,IAAI;IACZ,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,KAAK,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAsChF,qBAAa,aAAa;IAQf,KAAK,EAAE,KAAK;IACnB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO,CAAC;IAChB,OAAO,CAAC,OAAO,CAAC;IAChB,OAAO,CAAC,MAAM,CAAC;IAXjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAM;IAChC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,aAAa,CAAsE;gBAGlF,KAAK,EAAE,KAAK,EACX,YAAY,EAAE,WAAW,EAAE,EAC3B,OAAO,CAAC,EAAE,OAAO,YAAA,EACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YAAA,EAC7B,MAAM,CAAC,EAAE,aAAa,YAAA;IA8BhC,oCAAoC;IACpC,OAAO,CAAC,WAAW;IAKnB;uGACmG;IACnG,OAAO,CAAC,YAAY;IAsBpB,oEAAoE;IACpE,OAAO,CAAC,oBAAoB;IA8D5B,mEAAmE;YACrD,gBAAgB;IA0B9B,2EAA2E;YAC7D,iBAAiB;IAgC/B,kDAAkD;IAClD,OAAO,CAAC,cAAc;IAgCtB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,GAAG;IAmF5B,MAAM,IAAI,aAAa;YAUT,UAAU;IA4BlB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC;IAqCzC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAI9B,6DAA6D;IAC7D,aAAa,IAAI,IAAI;IAqBrB;;oFAEgF;IAChF,OAAO,CAAC,YAAY;IA8Cd,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;CAsD1D"}
1
+ {"version":3,"file":"ExecutionTree.d.ts","sourceRoot":"","sources":["../src/ExecutionTree.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,40 @@ function sameTrunk(a, b) {
17
17
  function pathEquals(a, b) {
18
18
  return a.length === b.length && a.every((v, i) => v === b[i]);
19
19
  }
20
+ /** Shared trace collector — one per request, passed through the tree. */
21
+ export class TraceCollector {
22
+ traces = [];
23
+ level;
24
+ epoch = performance.now();
25
+ constructor(level = "full") {
26
+ this.level = level;
27
+ }
28
+ /** Returns ms since the collector was created */
29
+ now() {
30
+ return Math.round((performance.now() - this.epoch) * 100) / 100;
31
+ }
32
+ record(trace) {
33
+ this.traces.push(trace);
34
+ }
35
+ /** Build a trace entry, omitting input/output for basic level. */
36
+ entry(base) {
37
+ if (this.level === "basic") {
38
+ const t = { tool: base.tool, fn: base.fn, durationMs: base.durationMs, startedAt: base.startedAt };
39
+ if (base.error)
40
+ t.error = base.error;
41
+ return t;
42
+ }
43
+ // full
44
+ const t = { tool: base.tool, fn: base.fn, durationMs: base.durationMs, startedAt: base.startedAt };
45
+ if (base.input)
46
+ t.input = structuredClone(base.input);
47
+ if (base.error)
48
+ t.error = base.error;
49
+ else if (base.output !== undefined)
50
+ t.output = base.output;
51
+ return t;
52
+ }
53
+ }
20
54
  /** Set a value at a nested path, creating intermediate objects/arrays as needed */
21
55
  function setNested(obj, path, value) {
22
56
  for (let i = 0; i < path.length - 1; i++) {
@@ -42,6 +76,8 @@ export class ExecutionTree {
42
76
  toolDepCache = new Map();
43
77
  toolDefCache = new Map();
44
78
  pipeHandleMap;
79
+ /** Shared trace collector — present only when tracing is enabled. */
80
+ tracer;
45
81
  constructor(trunk, instructions, toolFns, context, parent) {
46
82
  this.trunk = trunk;
47
83
  this.instructions = instructions;
@@ -186,10 +222,23 @@ export class ExecutionTree {
186
222
  throw new Error(`Unknown source "${handle}" in tool "${toolDef.name}"`);
187
223
  let value;
188
224
  if (dep.kind === "context") {
189
- value = this.context ?? this.parent?.context;
225
+ // Walk the full parent chain for context
226
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
227
+ let cursor = this;
228
+ while (cursor && value === undefined) {
229
+ value = cursor.context;
230
+ cursor = cursor.parent;
231
+ }
190
232
  }
191
233
  else if (dep.kind === "const") {
192
- value = this.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })] ?? this.parent?.state[trunkKey({ module: SELF_MODULE, type: "Const", field: "const" })];
234
+ // Walk the full parent chain for const state
235
+ const constKey = trunkKey({ module: SELF_MODULE, type: "Const", field: "const" });
236
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
237
+ let cursor = this;
238
+ while (cursor && value === undefined) {
239
+ value = cursor.state[constKey];
240
+ cursor = cursor.parent;
241
+ }
193
242
  }
194
243
  else if (dep.kind === "tool") {
195
244
  value = await this.resolveToolDep(dep.tool);
@@ -217,10 +266,19 @@ export class ExecutionTree {
217
266
  throw new Error(`Tool function "${toolDef.fn}" not registered`);
218
267
  // on error: wrap the tool call with fallback from onError wire
219
268
  const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
269
+ const tracer = this.tracer;
270
+ const traceStart = tracer?.now();
220
271
  try {
221
- return await fn(input);
272
+ const result = await fn(input);
273
+ if (tracer && traceStart != null) {
274
+ tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, output: result, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
275
+ }
276
+ return result;
222
277
  }
223
278
  catch (err) {
279
+ if (tracer && traceStart != null) {
280
+ tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, error: err.message, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
281
+ }
224
282
  if (!onErrorWire)
225
283
  throw err;
226
284
  if ("value" in onErrorWire)
@@ -258,10 +316,23 @@ export class ExecutionTree {
258
316
  if (toolDef) {
259
317
  await this.resolveToolWires(toolDef, input);
260
318
  }
261
- // Resolve bridge wires and apply on top
262
- const resolved = await Promise.all(bridgeWires.map(async (w) => {
263
- const value = "value" in w ? w.value : await this.pullSingle(w.from);
264
- return [w.to.path, value];
319
+ // Resolve bridge wires and apply on top.
320
+ // Group wires by target path so that || (null-fallback) and ??
321
+ // (error-fallback) semantics are honoured via resolveWires().
322
+ const wireGroups = new Map();
323
+ for (const w of bridgeWires) {
324
+ const key = w.to.path.join(".");
325
+ let group = wireGroups.get(key);
326
+ if (!group) {
327
+ group = [];
328
+ wireGroups.set(key, group);
329
+ }
330
+ group.push(w);
331
+ }
332
+ const groupEntries = Array.from(wireGroups.entries());
333
+ const resolved = await Promise.all(groupEntries.map(async ([, group]) => {
334
+ const value = await this.resolveWires(group);
335
+ return [group[0].to.path, value];
265
336
  }));
266
337
  for (const [path, value] of resolved) {
267
338
  if (path.length === 0 && value != null && typeof value === "object") {
@@ -278,10 +349,19 @@ export class ExecutionTree {
278
349
  throw new Error(`Tool function "${toolDef.fn}" not registered`);
279
350
  // on error: wrap the tool call with fallback from onError wire
280
351
  const onErrorWire = toolDef.wires.find((w) => w.kind === "onError");
352
+ const tracer = this.tracer;
353
+ const traceStart = tracer?.now();
281
354
  try {
282
- return await fn(input);
355
+ const result = await fn(input);
356
+ if (tracer && traceStart != null) {
357
+ tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, output: result, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
358
+ }
359
+ return result;
283
360
  }
284
361
  catch (err) {
362
+ if (tracer && traceStart != null) {
363
+ tracer.record(tracer.entry({ tool: toolName, fn: toolDef.fn, input, error: err.message, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
364
+ }
285
365
  if (!onErrorWire)
286
366
  throw err;
287
367
  if ("value" in onErrorWire)
@@ -292,7 +372,21 @@ export class ExecutionTree {
292
372
  // Direct tool function lookup by name (simple or dotted)
293
373
  const directFn = this.lookupToolFn(toolName);
294
374
  if (directFn) {
295
- return directFn(input);
375
+ const tracer = this.tracer;
376
+ const traceStart = tracer?.now();
377
+ try {
378
+ const result = await directFn(input);
379
+ if (tracer && traceStart != null) {
380
+ tracer.record(tracer.entry({ tool: toolName, fn: toolName, input, output: result, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
381
+ }
382
+ return result;
383
+ }
384
+ catch (err) {
385
+ if (tracer && traceStart != null) {
386
+ tracer.record(tracer.entry({ tool: toolName, fn: toolName, input, error: err.message, durationMs: Math.round((tracer.now() - traceStart) * 100) / 100, startedAt: traceStart }));
387
+ }
388
+ throw err;
389
+ }
296
390
  }
297
391
  // Define pass-through: synthetic trunks created by define inlining
298
392
  // act as data containers — bridge wires set their values, no tool needed.
@@ -303,11 +397,24 @@ export class ExecutionTree {
303
397
  })();
304
398
  }
305
399
  shadow() {
306
- return new ExecutionTree(this.trunk, this.instructions, this.toolFns, undefined, this);
400
+ const child = new ExecutionTree(this.trunk, this.instructions, this.toolFns, undefined, this);
401
+ child.tracer = this.tracer;
402
+ return child;
403
+ }
404
+ /** Returns collected traces (empty array when tracing is disabled). */
405
+ getTraces() {
406
+ return this.tracer?.traces ?? [];
307
407
  }
308
408
  async pullSingle(ref) {
309
409
  const key = trunkKey(ref);
310
- let value = this.state[key] ?? this.parent?.state[key];
410
+ // Walk the full parent chain — shadow trees may be nested multiple levels
411
+ let value = undefined;
412
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
413
+ let cursor = this;
414
+ while (cursor && value === undefined) {
415
+ value = cursor.state[key];
416
+ cursor = cursor.parent;
417
+ }
311
418
  if (value === undefined) {
312
419
  this.state[key] = this.schedule(ref);
313
420
  value = this.state[key];
@@ -326,37 +433,62 @@ export class ExecutionTree {
326
433
  }
327
434
  return result;
328
435
  }
436
+ /**
437
+ * Infer the cost of resolving a NodeRef.
438
+ * Cost 0: memory reads (input, context, const) — no I/O.
439
+ * Cost 1: everything else (tool calls, pipes, defines) — may involve network.
440
+ */
441
+ inferCost(ref) {
442
+ // Input args, context, and const live in the state map — free reads
443
+ if (ref.module === SELF_MODULE) {
444
+ const key = trunkKey(ref);
445
+ // Already resolved in state? Free.
446
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
447
+ let cursor = this;
448
+ while (cursor) {
449
+ if (cursor.state[key] !== undefined)
450
+ return 0;
451
+ cursor = cursor.parent;
452
+ }
453
+ // Input/context/const trunks are always cost 0
454
+ if (ref.type === "Context" || ref.type === "Const")
455
+ return 0;
456
+ // Input args trunk: _:Query:fieldName (same as this.trunk) — cost 0
457
+ if (ref.module === SELF_MODULE && ref.type === this.trunk.type && ref.field === this.trunk.field && !ref.element)
458
+ return 0;
459
+ }
460
+ return 1;
461
+ }
329
462
  async pull(refs) {
330
463
  if (refs.length === 1)
331
464
  return this.pullSingle(refs[0]);
332
- // Multiple sources: all start in parallel.
333
- // Return the first that resolves to a non-null/undefined value.
334
- // If all resolve to null/undefined resolve undefined (lets || fire).
335
- // If all reject throw AggregateError (lets ?? fire).
336
- return new Promise((resolve, reject) => {
337
- let remaining = refs.length;
338
- let hasValue = false;
339
- const errors = [];
340
- const settle = () => {
341
- if (--remaining === 0 && !hasValue) {
342
- if (errors.length === refs.length) {
343
- reject(new AggregateError(errors, "All sources failed"));
344
- }
345
- else {
346
- resolve(undefined); // all resolved to null/undefined
347
- }
348
- }
349
- };
350
- for (const ref of refs) {
351
- this.pullSingle(ref).then((value) => {
352
- if (!hasValue && value != null) {
353
- hasValue = true;
354
- resolve(value);
355
- }
356
- settle();
357
- }, (err) => { errors.push(err); settle(); });
465
+ // Cost-sorted sequential evaluation with short-circuit.
466
+ //
467
+ // Sort by inferred cost (stable preserves declaration order within
468
+ // the same cost tier). This means:
469
+ // • || chains (both sources are tools = same cost) left-to-right
470
+ // • Overdefinition with mixed costs → cheapest first
471
+ //
472
+ // Evaluate sequentially. Return the first non-null value.
473
+ // If all return null/undefined → return undefined (lets || fire).
474
+ // If all throw throw AggregateError (lets ?? fire).
475
+ const sorted = [...refs].sort((a, b) => this.inferCost(a) - this.inferCost(b));
476
+ const errors = [];
477
+ for (const ref of sorted) {
478
+ try {
479
+ const value = await this.pullSingle(ref);
480
+ if (value != null)
481
+ return value; // Short-circuit: found data
358
482
  }
359
- });
483
+ catch (err) {
484
+ errors.push(err);
485
+ }
486
+ }
487
+ // All resolved to null/undefined, or all threw
488
+ if (errors.length === refs.length) {
489
+ throw new AggregateError(errors, "All sources failed");
490
+ }
491
+ return undefined;
360
492
  }
361
493
  push(args) {
362
494
  this.state[trunkKey(this.trunk)] = args;
@@ -458,6 +590,30 @@ export class ExecutionTree {
458
590
  return s;
459
591
  });
460
592
  }
593
+ // Fallback: if this shadow tree has stored element data, resolve the
594
+ // requested field directly from it. This handles passthrough arrays
595
+ // where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
596
+ // doesn't explicitly wire each scalar field on the element type.
597
+ if (this.parent) {
598
+ const elementKey = trunkKey({ ...this.trunk, element: true });
599
+ const elementData = this.state[elementKey];
600
+ if (elementData != null && typeof elementData === "object" && !Array.isArray(elementData)) {
601
+ const fieldName = cleanPath[cleanPath.length - 1];
602
+ if (fieldName !== undefined && fieldName in elementData) {
603
+ const value = elementData[fieldName];
604
+ if (array && Array.isArray(value)) {
605
+ // Nested array: wrap items in shadow trees so they can
606
+ // resolve their own fields via this same fallback path.
607
+ return value.map((item) => {
608
+ const s = this.shadow();
609
+ s.state[elementKey] = item;
610
+ return s;
611
+ });
612
+ }
613
+ return value;
614
+ }
615
+ }
616
+ }
461
617
  // Return self to trigger downstream resolvers
462
618
  return this;
463
619
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-format.d.ts","sourceRoot":"","sources":["../src/bridge-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKR,WAAW,EAMd,MAAM,YAAY,CAAC;AA8CpB,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA0FvD;AAqwCD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAchD;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,CA8BnE"}
1
+ {"version":3,"file":"bridge-format.d.ts","sourceRoot":"","sources":["../src/bridge-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKR,WAAW,EAMd,MAAM,YAAY,CAAC;AA8CpB,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA0FvD;AAmxCD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAchD;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,CA8BnE"}
@@ -409,6 +409,7 @@ function parseBridgeBlock(block, lineOffset, previousInstructions) {
409
409
  const [, targetStr, quotedValue, unquotedValue] = constantMatch;
410
410
  const value = quotedValue ?? unquotedValue;
411
411
  const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
412
+ assertNoTargetIndices(toRef, ln(i));
412
413
  wires.push({ value, to: toRef });
413
414
  continue;
414
415
  }
@@ -427,6 +428,7 @@ function parseBridgeBlock(block, lineOffset, previousInstructions) {
427
428
  assertNotReserved(iterHandle, ln(i), "iterator handle");
428
429
  const fromRef = resolveAddress(fromClean, handleRes, bridgeType, bridgeField, ln(i));
429
430
  const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
431
+ assertNoTargetIndices(toRef, ln(i));
430
432
  wires.push({ from: fromRef, to: toRef });
431
433
  currentArrayToPath = toRef.path;
432
434
  currentIterHandle = iterHandle;
@@ -469,6 +471,7 @@ function parseBridgeBlock(block, lineOffset, previousInstructions) {
469
471
  fallbackInternalWires = wires.splice(preLen);
470
472
  }
471
473
  const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
474
+ assertNoTargetIndices(toRef, ln(i));
472
475
  for (let ci = 0; ci < sourceParts.length; ci++) {
473
476
  const isFirst = ci === 0;
474
477
  const isLast = ci === sourceParts.length - 1;
@@ -927,6 +930,14 @@ function resolveAddress(address, handles, bridgeType, bridgeField, lineNum) {
927
930
  throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared handle "${prefix}". ` +
928
931
  `Add 'with ${prefix}' or 'with ${prefix} as ${prefix}' to the bridge header.`);
929
932
  }
933
+ /** Reject explicit array indices on the target (LHS) of a wire. */
934
+ function assertNoTargetIndices(ref, lineNum) {
935
+ if (ref.path.some((seg) => /^\d+$/.test(seg))) {
936
+ throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}` +
937
+ `Explicit array index in wire target is not supported. ` +
938
+ `Use array mapping (\`[] as iter { }\`) instead.`);
939
+ }
940
+ }
930
941
  // ── Const block parser ──────────────────────────────────────────────────────
931
942
  /**
932
943
  * Parse `const` declarations into ConstDef instructions.
@@ -1,4 +1,5 @@
1
1
  import { type GraphQLSchema } from "graphql";
2
+ import { type ToolTrace, type TraceLevel } from "./ExecutionTree.js";
2
3
  import type { Instruction, ToolMap } from "./types.js";
3
4
  export type BridgeOptions = {
4
5
  /** Tool functions available to the engine.
@@ -9,8 +10,40 @@ export type BridgeOptions = {
9
10
  /** Optional function to reshape/restrict the GQL context before it reaches bridge files.
10
11
  * By default the full context is exposed via `with context`. */
11
12
  contextMapper?: (context: any) => Record<string, any>;
13
+ /** Enable tool-call tracing.
14
+ * - `true` or `"full"`: record tool, fn, input, output/error, timing
15
+ * - `"basic"`: record tool, fn, timing, error (no input/output) */
16
+ trace?: boolean | TraceLevel;
12
17
  };
13
18
  /** Instructions can be a static array or a function that selects per-request */
14
19
  export type InstructionSource = Instruction[] | ((context: any) => Instruction[]);
15
20
  export declare function bridgeTransform(schema: GraphQLSchema, instructions: InstructionSource, options?: BridgeOptions): GraphQLSchema;
21
+ /**
22
+ * Read traces that were collected during the current request.
23
+ * Pass the GraphQL context object; returns an empty array when tracing is
24
+ * disabled or no traces were recorded.
25
+ */
26
+ export declare function getBridgeTraces(context: any): ToolTrace[];
27
+ /**
28
+ * Envelop-compatible plugin for GraphQL Yoga (or any Envelop-based server).
29
+ * When bridge tracing is enabled, this plugin copies the recorded traces into
30
+ * the GraphQL response `extensions.traces` field.
31
+ *
32
+ * Usage:
33
+ * ```ts
34
+ * createYoga({ schema, plugins: [useBridgeTracing()] })
35
+ * ```
36
+ */
37
+ export declare function useBridgeTracing(): {
38
+ onExecute({ args }: {
39
+ args: {
40
+ contextValue: any;
41
+ };
42
+ }): {
43
+ onExecuteDone({ result, setResult, }: {
44
+ result: any;
45
+ setResult: (r: any) => void;
46
+ }): void;
47
+ };
48
+ };
16
49
  //# sourceMappingURL=bridge-transform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-transform.d.ts","sourceRoot":"","sources":["../src/bridge-transform.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;AAGjB,OAAO,KAAK,EAAE,WAAW,EAAc,OAAO,EAAE,MAAM,YAAY,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG;IAC1B;;;kDAG8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;qEACiE;IACjE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACvD,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB,WAAW,EAAE,GACb,CAAC,CAAC,OAAO,EAAE,GAAG,KAAK,WAAW,EAAE,CAAC,CAAC;AAEtC,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,iBAAiB,EAC/B,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CA0Ef"}
1
+ {"version":3,"file":"bridge-transform.d.ts","sourceRoot":"","sources":["../src/bridge-transform.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAiC,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEpG,OAAO,KAAK,EAAE,WAAW,EAAc,OAAO,EAAE,MAAM,YAAY,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG;IAC1B;;;kDAG8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;qEACiE;IACjE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtD;;wEAEoE;IACpE,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CAC9B,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB,WAAW,EAAE,GACb,CAAC,CAAC,OAAO,EAAE,GAAG,KAAK,WAAW,EAAE,CAAC,CAAC;AAEtC,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,iBAAiB,EAC/B,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CAuFf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,EAAE,CAEzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB;wBAER;QAAE,IAAI,EAAE;YAAE,YAAY,EAAE,GAAG,CAAA;SAAE,CAAA;KAAE;8CAK5C;YACD,MAAM,EAAE,GAAG,CAAC;YACZ,SAAS,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;SAC7B;;EAeR"}
@@ -1,11 +1,13 @@
1
1
  import { MapperKind, mapSchema } from "@graphql-tools/utils";
2
2
  import { GraphQLList, GraphQLNonNull, defaultFieldResolver, } from "graphql";
3
- import { ExecutionTree } from "./ExecutionTree.js";
3
+ import { ExecutionTree, TraceCollector } from "./ExecutionTree.js";
4
4
  import { builtinTools } from "./tools/index.js";
5
5
  import { SELF_MODULE } from "./types.js";
6
6
  export function bridgeTransform(schema, instructions, options) {
7
7
  const userTools = options?.tools;
8
8
  const contextMapper = options?.contextMapper;
9
+ const tracing = options?.trace ?? false;
10
+ const traceLevel = tracing === true ? "full" : tracing === false ? false : tracing;
9
11
  return mapSchema(schema, {
10
12
  [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
11
13
  let array = false;
@@ -36,6 +38,11 @@ export function bridgeTransform(schema, instructions, options) {
36
38
  ...(userTools ?? {}),
37
39
  };
38
40
  source = new ExecutionTree(trunk, activeInstructions, allTools, bridgeContext);
41
+ if (traceLevel) {
42
+ source.tracer = new TraceCollector(traceLevel);
43
+ // Stash tracer on GQL context so the tracing plugin can read it
44
+ context.__bridgeTracer = source.tracer;
45
+ }
39
46
  }
40
47
  if (source instanceof ExecutionTree &&
41
48
  args &&
@@ -44,6 +51,11 @@ export function bridgeTransform(schema, instructions, options) {
44
51
  }
45
52
  // Kick off forced wires (<-!) at the root entry point
46
53
  if (source instanceof ExecutionTree && !info.path.prev) {
54
+ // Ensure input state exists even with no args (prevents
55
+ // recursive scheduling of the input trunk → stack overflow).
56
+ if (!args || Object.keys(args).length === 0) {
57
+ source.push({});
58
+ }
47
59
  source.executeForced();
48
60
  }
49
61
  if (source instanceof ExecutionTree) {
@@ -55,3 +67,41 @@ export function bridgeTransform(schema, instructions, options) {
55
67
  },
56
68
  });
57
69
  }
70
+ /**
71
+ * Read traces that were collected during the current request.
72
+ * Pass the GraphQL context object; returns an empty array when tracing is
73
+ * disabled or no traces were recorded.
74
+ */
75
+ export function getBridgeTraces(context) {
76
+ return context?.__bridgeTracer?.traces ?? [];
77
+ }
78
+ /**
79
+ * Envelop-compatible plugin for GraphQL Yoga (or any Envelop-based server).
80
+ * When bridge tracing is enabled, this plugin copies the recorded traces into
81
+ * the GraphQL response `extensions.traces` field.
82
+ *
83
+ * Usage:
84
+ * ```ts
85
+ * createYoga({ schema, plugins: [useBridgeTracing()] })
86
+ * ```
87
+ */
88
+ export function useBridgeTracing() {
89
+ return {
90
+ onExecute({ args }) {
91
+ return {
92
+ onExecuteDone({ result, setResult, }) {
93
+ const traces = getBridgeTraces(args.contextValue);
94
+ if (traces.length > 0 && result && "data" in result) {
95
+ setResult({
96
+ ...result,
97
+ extensions: {
98
+ ...(result.extensions ?? {}),
99
+ traces,
100
+ },
101
+ });
102
+ }
103
+ },
104
+ };
105
+ },
106
+ };
107
+ }
package/build/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { parseBridge } from "./bridge-format.js";
2
- export { bridgeTransform } from "./bridge-transform.js";
2
+ export { bridgeTransform, getBridgeTraces, useBridgeTracing } from "./bridge-transform.js";
3
3
  export type { BridgeOptions, InstructionSource } from "./bridge-transform.js";
4
4
  export { builtinTools, std, createHttpCall } from "./tools/index.js";
5
+ export type { ToolTrace, TraceLevel } from "./ExecutionTree.js";
5
6
  export type { CacheStore, ConstDef, Instruction, ToolCallFn, ToolDef, ToolMap } from "./types.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
package/build/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { parseBridge } from "./bridge-format.js";
2
- export { bridgeTransform } from "./bridge-transform.js";
2
+ export { bridgeTransform, getBridgeTraces, useBridgeTracing } from "./bridge-transform.js";
3
3
  export { builtinTools, std, createHttpCall } from "./tools/index.js";
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@stackables/bridge",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "description": "Declarative dataflow for GraphQL",
5
5
  "main": "./build/index.js",
6
6
  "type": "module",
7
7
  "types": "./build/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "development": "./src/index.ts",
11
+ "import": "./build/index.js",
12
+ "types": "./build/index.d.ts"
13
+ }
14
+ },
8
15
  "files": [
9
16
  "build",
10
17
  "README.md"