From 59b532c0622a13f99cb198834e77ce942daef122 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:43:48 +0000 Subject: [PATCH 01/11] Initial plan From 341191d1e09868b9784b41f80c9c8f1b88adee27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:54:17 +0000 Subject: [PATCH 02/11] feat: add execution trace bitmask to core engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bitIndex to TraversalEntry for bitmask encoding - Add buildTraversalManifest (alias for enumerateTraversalIds) - Add decodeExecutionTrace to decode bitmask back to entries - Add buildTraceBitsMap for runtime wire→bit lookup - Add traceBits/traceMask to TreeContext interface - Inject trace recording in resolveWires (primary/fallback/catch/then/else) - Add executionTrace to ExecuteBridgeResult - Propagate trace mask through shadow trees - All existing tests pass (133 execute-bridge + 64 resilience + 14 traversal) Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- .../bridge-compiler/src/execute-bridge.ts | 4 +- packages/bridge-core/src/ExecutionTree.ts | 31 +++++ .../bridge-core/src/enumerate-traversals.ts | 120 +++++++++++++++++- packages/bridge-core/src/execute-bridge.ts | 12 +- packages/bridge-core/src/index.ts | 9 +- packages/bridge-core/src/resolveWires.ts | 54 +++++++- packages/bridge-core/src/tree-types.ts | 12 ++ 7 files changed, 228 insertions(+), 14 deletions(-) diff --git a/packages/bridge-compiler/src/execute-bridge.ts b/packages/bridge-compiler/src/execute-bridge.ts index 04981eb5..ece3d915 100644 --- a/packages/bridge-compiler/src/execute-bridge.ts +++ b/packages/bridge-compiler/src/execute-bridge.ts @@ -83,6 +83,8 @@ export type ExecuteBridgeOptions = { export type ExecuteBridgeResult = { data: T; traces: ToolTrace[]; + /** Compact bitmask encoding which traversal paths were taken during execution. */ + executionTrace: number; }; // ── Cache ─────────────────────────────────────────────────────────────────── @@ -338,5 +340,5 @@ export async function executeBridge( } catch (err) { throw attachBridgeErrorDocumentContext(err, document); } - return { data: data as T, traces: tracer?.traces ?? [] }; + return { data: data as T, traces: tracer?.traces ?? [], executionTrace: 0 }; } diff --git a/packages/bridge-core/src/ExecutionTree.ts b/packages/bridge-core/src/ExecutionTree.ts index 430a55c7..3c986b7f 100644 --- a/packages/bridge-core/src/ExecutionTree.ts +++ b/packages/bridge-core/src/ExecutionTree.ts @@ -63,6 +63,8 @@ import { matchesRequestedFields, } from "./requested-fields.ts"; import { raceTimeout } from "./utils.ts"; +import type { TraceWireBits } from "./enumerate-traversals.ts"; +import { buildTraceBitsMap, enumerateTraversalIds } from "./enumerate-traversals.ts"; function stableMemoizeKey(value: unknown): string { if (value === undefined) { @@ -145,6 +147,16 @@ export class ExecutionTree implements TreeContext { private forcedExecution?: Promise; /** Shared trace collector — present only when tracing is enabled. */ tracer?: TraceCollector; + /** + * Per-wire bit positions for execution trace recording. + * Built once from the bridge manifest. Shared across shadow trees. + */ + traceBits?: Map; + /** + * Shared mutable trace bitmask — `[mask]`. Boxed in a single-element + * array so shadow trees can share the same mutable reference. + */ + traceMask?: [number]; /** Structured logger passed from BridgeOptions. Defaults to no-ops. */ logger?: Logger; /** External abort signal — cancels execution when triggered. */ @@ -726,6 +738,8 @@ export class ExecutionTree implements TreeContext { child.toolFns = this.toolFns; child.elementTrunkKey = this.elementTrunkKey; child.tracer = this.tracer; + child.traceBits = this.traceBits; + child.traceMask = this.traceMask; child.logger = this.logger; child.signal = this.signal; child.source = this.source; @@ -761,6 +775,23 @@ export class ExecutionTree implements TreeContext { return this.tracer?.traces ?? []; } + /** Returns the execution trace bitmask (0 when tracing is disabled). */ + getExecutionTrace(): number { + return this.traceMask?.[0] ?? 0; + } + + /** + * Enable execution trace recording. + * Builds the wire-to-bit map from the bridge manifest and initialises + * the shared mutable bitmask. Safe to call before `run()`. + */ + enableExecutionTrace(): void { + if (!this.bridge) return; + const manifest = enumerateTraversalIds(this.bridge); + this.traceBits = buildTraceBitsMap(this.bridge, manifest); + this.traceMask = [0]; + } + /** * Traverse `ref.path` on an already-resolved value, respecting null guards. * Extracted from `pullSingle` so the sync and async paths can share logic. diff --git a/packages/bridge-core/src/enumerate-traversals.ts b/packages/bridge-core/src/enumerate-traversals.ts index fcbe9fe4..0a2d7541 100644 --- a/packages/bridge-core/src/enumerate-traversals.ts +++ b/packages/bridge-core/src/enumerate-traversals.ts @@ -9,8 +9,10 @@ * `o <- i.arr[] as a { .data <- a.a ?? a.b }` → 3 traversals * (empty-array, primary for .data, nullish fallback for .data) * - * Used for complexity assessment and will integrate into the execution - * engine for monitoring. + * The traversal manifest is a static analysis result. At runtime, the + * execution engine produces a compact numeric `executionTrace` (bitmask) + * that records which traversal paths were actually taken. Use + * {@link decodeExecutionTrace} to map the bitmask back to entries. */ import type { Bridge, Wire, WireFallback } from "./types.ts"; @@ -40,6 +42,8 @@ export interface TraversalEntry { fallbackIndex?: number; /** Gate type (only when kind is `"fallback"`): `"falsy"` for `||`, `"nullish"` for `??`. */ gateType?: "falsy" | "nullish"; + /** Bit position in the execution trace bitmask (0-based). */ + bitIndex: number; } // ── Helpers ───────────────────────────────────────────────────────────────── @@ -93,6 +97,7 @@ function addFallbackEntries( kind: "fallback", fallbackIndex: i, gateType: fallbacks[i].type, + bitIndex: -1, // assigned after enumeration }); } } @@ -105,7 +110,7 @@ function addCatchEntry( w: Wire, ): void { if (hasCatch(w)) { - entries.push({ id: `${base}/catch`, wireIndex, target, kind: "catch" }); + entries.push({ id: `${base}/catch`, wireIndex, target, kind: "catch", bitIndex: -1 }); } } @@ -137,7 +142,7 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { // ── Constant wire ─────────────────────────────────────────────── if ("value" in w) { - entries.push({ id: `${base}/const`, wireIndex: i, target, kind: "const" }); + entries.push({ id: `${base}/const`, wireIndex: i, target, kind: "const", bitIndex: -1 }); continue; } @@ -151,6 +156,7 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { wireIndex: i, target, kind: "primary", + bitIndex: -1, }); addFallbackEntries(entries, base, i, target, w.fallbacks); addCatchEntry(entries, base, i, target, w); @@ -160,8 +166,8 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { // ── Conditional (ternary) wire ────────────────────────────────── if ("cond" in w) { - entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then" }); - entries.push({ id: `${base}/else`, wireIndex: i, target, kind: "else" }); + entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then", bitIndex: -1 }); + entries.push({ id: `${base}/else`, wireIndex: i, target, kind: "else", bitIndex: -1 }); addFallbackEntries(entries, base, i, target, w.fallbacks); addCatchEntry(entries, base, i, target, w); continue; @@ -173,6 +179,7 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { wireIndex: i, target, kind: "primary", + bitIndex: -1, }); if ("condAnd" in w) { addFallbackEntries(entries, base, i, target, w.fallbacks); @@ -194,9 +201,110 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { wireIndex: -1, target: key ? key.split(".") : [], kind: "empty-array", + bitIndex: -1, }); } } + // Assign sequential bit indices + for (let i = 0; i < entries.length; i++) { + entries[i].bitIndex = i; + } + return entries; } + +// ── New public API ────────────────────────────────────────────────────────── + +/** + * Build the static traversal manifest for a bridge. + * + * Alias for {@link enumerateTraversalIds} with the recommended naming. + * Returns the ordered array of {@link TraversalEntry} objects. Each entry + * carries a `bitIndex` that maps it to a bit position in the runtime + * execution trace bitmask. + */ +export const buildTraversalManifest = enumerateTraversalIds; + +/** + * Decode a runtime execution trace bitmask against a traversal manifest. + * + * Returns the subset of {@link TraversalEntry} objects whose bits are set + * in the trace — i.e. the paths that were actually taken during execution. + * + * @param manifest The static manifest from {@link buildTraversalManifest}. + * @param trace The numeric bitmask produced by the execution engine. + */ +export function decodeExecutionTrace( + manifest: TraversalEntry[], + trace: number, +): TraversalEntry[] { + const result: TraversalEntry[] = []; + for (const entry of manifest) { + if (trace & (1 << entry.bitIndex)) { + result.push(entry); + } + } + return result; +} + +// ── Runtime trace helpers ─────────────────────────────────────────────────── + +/** + * Per-wire bit positions used by the execution engine to record which + * traversal paths were taken. Built once per bridge from the manifest. + */ +export interface TraceWireBits { + /** Bit index for the primary / then / const path. */ + primary?: number; + /** Bit index for the else branch (conditional wires only). */ + else?: number; + /** Bit indices for each fallback gate (same order as `fallbacks` array). */ + fallbacks?: number[]; + /** Bit index for the catch path. */ + catch?: number; +} + +/** + * Build a lookup map from Wire objects to their trace bit positions. + * + * This is called once per bridge at setup time. The returned map is + * used by `resolveWires` to flip bits in the shared trace mask with + * minimal overhead (one Map.get + one bitwise OR per decision). + */ +export function buildTraceBitsMap( + bridge: Bridge, + manifest: TraversalEntry[], +): Map { + const map = new Map(); + for (const entry of manifest) { + if (entry.wireIndex < 0) continue; // synthetic entries (empty-array) + const wire = bridge.wires[entry.wireIndex]; + if (!wire) continue; + + let bits = map.get(wire); + if (!bits) { + bits = {}; + map.set(wire, bits); + } + + switch (entry.kind) { + case "primary": + case "then": + case "const": + bits.primary = entry.bitIndex; + break; + case "else": + bits.else = entry.bitIndex; + break; + case "fallback": + if (!bits.fallbacks) bits.fallbacks = []; + bits.fallbacks[entry.fallbackIndex ?? 0] = entry.bitIndex; + break; + case "catch": + bits.catch = entry.bitIndex; + break; + } + } + return map; +} diff --git a/packages/bridge-core/src/execute-bridge.ts b/packages/bridge-core/src/execute-bridge.ts index 235aeb62..73bc786d 100644 --- a/packages/bridge-core/src/execute-bridge.ts +++ b/packages/bridge-core/src/execute-bridge.ts @@ -78,6 +78,8 @@ export type ExecuteBridgeOptions = { export type ExecuteBridgeResult = { data: T; traces: ToolTrace[]; + /** Compact bitmask encoding which traversal paths were taken during execution. */ + executionTrace: number; }; /** @@ -159,6 +161,10 @@ export async function executeBridge( tree.tracer = new TraceCollector(traceLevel); } + // Always enable execution trace recording — the overhead is one + // Map.get + one bitwise OR per wire decision (negligible). + tree.enableExecutionTrace(); + let data: unknown; try { data = await tree.run(input, options.requestedFields); @@ -166,5 +172,9 @@ export async function executeBridge( throw attachBridgeErrorDocumentContext(err, doc); } - return { data: data as T, traces: tree.getTraces() }; + return { + data: data as T, + traces: tree.getTraces(), + executionTrace: tree.getExecutionTrace(), + }; } diff --git a/packages/bridge-core/src/index.ts b/packages/bridge-core/src/index.ts index 6753dc58..46fdaf71 100644 --- a/packages/bridge-core/src/index.ts +++ b/packages/bridge-core/src/index.ts @@ -84,8 +84,13 @@ export type { // ── Traversal enumeration ─────────────────────────────────────────────────── -export { enumerateTraversalIds } from "./enumerate-traversals.ts"; -export type { TraversalEntry } from "./enumerate-traversals.ts"; +export { + enumerateTraversalIds, + buildTraversalManifest, + decodeExecutionTrace, + buildTraceBitsMap, +} from "./enumerate-traversals.ts"; +export type { TraversalEntry, TraceWireBits } from "./enumerate-traversals.ts"; // ── Utilities ─────────────────────────────────────────────────────────────── diff --git a/packages/bridge-core/src/resolveWires.ts b/packages/bridge-core/src/resolveWires.ts index 7750e025..c33cac52 100644 --- a/packages/bridge-core/src/resolveWires.ts +++ b/packages/bridge-core/src/resolveWires.ts @@ -21,6 +21,7 @@ import { wrapBridgeRuntimeError, } from "./tree-types.ts"; import { coerceConstant, getSimplePullRef } from "./tree-utils.ts"; +import type { TraceWireBits } from "./enumerate-traversals.ts"; // ── Wire type helpers ──────────────────────────────────────────────────────── @@ -72,9 +73,13 @@ export function resolveWires( if (wires.length === 1) { const w = wires[0]!; - if ("value" in w) return coerceConstant(w.value); + if ("value" in w) { + recordPrimary(ctx, w); + return coerceConstant(w.value); + } const ref = getSimplePullRef(w); if (ref) { + recordPrimary(ctx, w); return ctx.pullSingle( ref, pullChain, @@ -121,7 +126,10 @@ async function resolveWiresAsync( if (ctx.signal?.aborted) throw new BridgeAbortError(); // Constant wire — always wins, no modifiers - if ("value" in w) return coerceConstant(w.value); + if ("value" in w) { + recordPrimary(ctx, w); + return coerceConstant(w.value); + } try { // Layer 1: Execution @@ -168,11 +176,13 @@ export async function applyFallbackGates( ): Promise { if (!w.fallbacks?.length) return value; - for (const fallback of w.fallbacks) { + for (let fi = 0; fi < w.fallbacks.length; fi++) { + const fallback = w.fallbacks[fi]; const isFalsyGateOpen = fallback.type === "falsy" && !value; const isNullishGateOpen = fallback.type === "nullish" && value == null; if (isFalsyGateOpen || isNullishGateOpen) { + recordFallback(ctx, w, fi); if (fallback.control) { return applyControlFlowWithLoc(fallback.control, fallback.loc ?? w.loc); } @@ -207,12 +217,17 @@ export async function applyCatchGate( pullChain?: Set, ): Promise { if (w.catchControl) { + recordCatch(ctx, w); return applyControlFlowWithLoc(w.catchControl, w.catchLoc ?? w.loc); } if (w.catchFallbackRef) { + recordCatch(ctx, w); return ctx.pullSingle(w.catchFallbackRef, pullChain, w.catchLoc ?? w.loc); } - if (w.catchFallback != null) return coerceConstant(w.catchFallback); + if (w.catchFallback != null) { + recordCatch(ctx, w); + return coerceConstant(w.catchFallback); + } return undefined; } @@ -256,11 +271,13 @@ async function evaluateWireSource( w.condLoc ?? w.loc, ); if (condValue) { + recordPrimary(ctx, w); // "then" branch → primary bit if (w.thenRef !== undefined) { return ctx.pullSingle(w.thenRef, pullChain, w.thenLoc ?? w.loc); } if (w.thenValue !== undefined) return coerceConstant(w.thenValue); } else { + recordElse(ctx, w); // "else" branch if (w.elseRef !== undefined) { return ctx.pullSingle(w.elseRef, pullChain, w.elseLoc ?? w.loc); } @@ -270,6 +287,7 @@ async function evaluateWireSource( } if ("condAnd" in w) { + recordPrimary(ctx, w); const { leftRef, rightRef, rightValue, safe, rightSafe } = w.condAnd; const leftVal = await pullSafe(ctx, leftRef, safe, pullChain, w.loc); if (!leftVal) return false; @@ -282,6 +300,7 @@ async function evaluateWireSource( } if ("condOr" in w) { + recordPrimary(ctx, w); const { leftRef, rightRef, rightValue, safe, rightSafe } = w.condOr; const leftVal = await pullSafe(ctx, leftRef, safe, pullChain, w.loc); if (leftVal) return true; @@ -294,6 +313,7 @@ async function evaluateWireSource( } if ("from" in w) { + recordPrimary(ctx, w); if (w.safe) { try { return await ctx.pullSingle(w.from, pullChain, w.fromLoc ?? w.loc); @@ -348,3 +368,29 @@ function pullSafe( return undefined; }); } + +// ── Trace recording helpers ───────────────────────────────────────────────── +// These are designed for minimal overhead: when `traceBits` is not set on the +// context (tracing disabled), the functions return immediately after a single +// falsy check. When enabled, one Map.get + one bitwise OR is the hot path. + +function recordPrimary(ctx: TreeContext, w: Wire): void { + const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; + if (bits?.primary != null) ctx.traceMask![0] |= 1 << bits.primary; +} + +function recordElse(ctx: TreeContext, w: Wire): void { + const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; + if (bits?.else != null) ctx.traceMask![0] |= 1 << bits.else; +} + +function recordFallback(ctx: TreeContext, w: Wire, index: number): void { + const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; + const fb = bits?.fallbacks; + if (fb && fb[index] != null) ctx.traceMask![0] |= 1 << fb[index]; +} + +function recordCatch(ctx: TreeContext, w: Wire): void { + const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; + if (bits?.catch != null) ctx.traceMask![0] |= 1 << bits.catch; +} diff --git a/packages/bridge-core/src/tree-types.ts b/packages/bridge-core/src/tree-types.ts index d236cde7..da480ef5 100644 --- a/packages/bridge-core/src/tree-types.ts +++ b/packages/bridge-core/src/tree-types.ts @@ -136,6 +136,18 @@ export interface TreeContext { classifyOverdefinitionWire?(wire: Wire): number; /** External abort signal — cancels execution when triggered. */ signal?: AbortSignal; + /** + * Per-wire bit positions for execution trace recording. + * Present only when execution tracing is enabled. Looked up by + * `resolveWires` to flip bits in `traceMask`. + */ + traceBits?: Map; + /** + * Shared mutable trace bitmask — `[mask]`. Boxed in a single-element + * array so shadow trees can share the same mutable reference without + * extra allocation. Present only when execution tracing is enabled. + */ + traceMask?: [number]; } /** Returns `true` when `value` is a thenable (Promise or Promise-like). */ From 62fce908733293595b4fdc51b548f12fb99ab329 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:55:29 +0000 Subject: [PATCH 03/11] test: add comprehensive tests for execution trace feature - Test buildTraversalManifest alias and bitIndex assignment - Test decodeExecutionTrace with empty/single/multiple/round-trip - Test end-to-end trace collection: primary, fallback, catch, then/else, const - All 27 tests pass Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- .../bridge/test/enumerate-traversals.test.ts | 305 +++++++++++++++++- 1 file changed, 303 insertions(+), 2 deletions(-) diff --git a/packages/bridge/test/enumerate-traversals.test.ts b/packages/bridge/test/enumerate-traversals.test.ts index 4885029f..9c26ed2f 100644 --- a/packages/bridge/test/enumerate-traversals.test.ts +++ b/packages/bridge/test/enumerate-traversals.test.ts @@ -1,8 +1,13 @@ import { describe, test } from "node:test"; import assert from "node:assert/strict"; import { parseBridge } from "@stackables/bridge-parser"; -import { enumerateTraversalIds } from "@stackables/bridge-core"; -import type { Bridge, TraversalEntry } from "@stackables/bridge-core"; +import { + enumerateTraversalIds, + buildTraversalManifest, + decodeExecutionTrace, + executeBridge, +} from "@stackables/bridge-core"; +import type { Bridge, TraversalEntry, BridgeDocument } from "@stackables/bridge-core"; function getBridge(source: string): Bridge { const doc = parseBridge(source); @@ -320,3 +325,299 @@ bridge Query.complex { ); }); }); + +// ── buildTraversalManifest ────────────────────────────────────────────────── + +describe("buildTraversalManifest", () => { + test("is an alias for enumerateTraversalIds", () => { + assert.strictEqual(buildTraversalManifest, enumerateTraversalIds); + }); + + test("entries have sequential bitIndex starting at 0", () => { + const bridge = getBridge(`version 1.5 +bridge Query.demo { + with a + with b + with input as i + with output as o + a.q <- i.q + b.q <- i.q + o.label <- a.label || b.label catch "none" + o.score <- a.score ?? 0 +}`); + const manifest = buildTraversalManifest(bridge); + for (let i = 0; i < manifest.length; i++) { + assert.equal(manifest[i].bitIndex, i, `entry ${i} should have bitIndex ${i}`); + } + }); +}); + +// ── decodeExecutionTrace ──────────────────────────────────────────────────── + +describe("decodeExecutionTrace", () => { + test("empty trace returns empty array", () => { + const bridge = getBridge(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.result <- api.label +}`); + const manifest = buildTraversalManifest(bridge); + const result = decodeExecutionTrace(manifest, 0); + assert.equal(result.length, 0); + }); + + test("single bit decodes to one entry", () => { + const bridge = getBridge(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.result <- api.label || "fallback" +}`); + const manifest = buildTraversalManifest(bridge); + const primary = manifest.find((e) => e.kind === "primary" && e.target.includes("result")); + assert.ok(primary); + const result = decodeExecutionTrace(manifest, 1 << primary.bitIndex); + assert.equal(result.length, 1); + assert.equal(result[0].id, primary.id); + }); + + test("multiple bits decode to multiple entries", () => { + const bridge = getBridge(`version 1.5 +bridge Query.demo { + with a + with b + with input as i + with output as o + a.q <- i.q + b.q <- i.q + o.label <- a.label || b.label catch "none" +}`); + const manifest = buildTraversalManifest(bridge); + const labelEntries = manifest.filter( + (e) => e.target.includes("label") && e.target.length === 1, + ); + assert.equal(labelEntries.length, 3); + + // Set all label bits + let mask = 0; + for (const e of labelEntries) { + mask |= 1 << e.bitIndex; + } + const decoded = decodeExecutionTrace(manifest, mask); + assert.equal(decoded.length, 3); + assert.deepEqual( + decoded.map((e) => e.kind), + ["primary", "fallback", "catch"], + ); + }); + + test("round-trip: build manifest, set bits, decode", () => { + const bridge = getBridge(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.label <- i.flag ? api.a : api.b +}`); + const manifest = buildTraversalManifest(bridge); + const thenEntry = manifest.find((e) => e.kind === "then"); + assert.ok(thenEntry); + const decoded = decodeExecutionTrace(manifest, 1 << thenEntry.bitIndex); + assert.equal(decoded.length, 1); + assert.equal(decoded[0].kind, "then"); + }); +}); + +// ── End-to-end execution trace ────────────────────────────────────────────── + +function getDoc(source: string): BridgeDocument { + const raw = parseBridge(source); + return JSON.parse(JSON.stringify(raw)) as BridgeDocument; +} + +describe("executionTrace: end-to-end", () => { + test("simple pull wire — primary bits are set", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.result <- api.label +}`); + const { executionTrace } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: { q: "test" }, + tools: { api: async () => ({ label: "Hello" }) }, + }); + + assert.ok(executionTrace > 0, "trace should have bits set"); + + // Decode and verify + const bridge = doc.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const manifest = buildTraversalManifest(bridge); + const decoded = decodeExecutionTrace(manifest, executionTrace); + const kinds = decoded.map((e) => e.kind); + assert.ok(kinds.includes("primary"), "should include primary paths"); + }); + + test("fallback fires — fallback bit is set", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.label <- api.label || "default" +}`); + const { executionTrace, data } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: { q: "test" }, + tools: { api: async () => ({ label: null }) }, + }); + + assert.equal((data as any).label, "default"); + + const bridge = doc.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const manifest = buildTraversalManifest(bridge); + const decoded = decodeExecutionTrace(manifest, executionTrace); + const kinds = decoded.map((e) => e.kind); + assert.ok(kinds.includes("fallback"), "should include fallback path"); + }); + + test("catch fires — catch bit is set", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.lat <- api.lat catch 0 +}`); + const { executionTrace, data } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: { q: "test" }, + tools: { api: async () => { throw new Error("boom"); } }, + }); + + assert.equal((data as any).lat, 0); + + const bridge = doc.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const manifest = buildTraversalManifest(bridge); + const decoded = decodeExecutionTrace(manifest, executionTrace); + const kinds = decoded.map((e) => e.kind); + assert.ok(kinds.includes("catch"), "should include catch path"); + }); + + test("ternary — then branch bit is set", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.label <- i.flag ? api.a : api.b +}`); + const { executionTrace } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: { q: "test", flag: true }, + tools: { api: async () => ({ a: "yes", b: "no" }) }, + }); + + const bridge = doc.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const manifest = buildTraversalManifest(bridge); + const decoded = decodeExecutionTrace(manifest, executionTrace); + const kinds = decoded.map((e) => e.kind); + assert.ok(kinds.includes("then"), "should include then path"); + assert.ok(!kinds.includes("else"), "should NOT include else path"); + }); + + test("ternary — else branch bit is set", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.label <- i.flag ? api.a : api.b +}`); + const { executionTrace } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: { q: "test", flag: false }, + tools: { api: async () => ({ a: "yes", b: "no" }) }, + }); + + const bridge = doc.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const manifest = buildTraversalManifest(bridge); + const decoded = decodeExecutionTrace(manifest, executionTrace); + const kinds = decoded.map((e) => e.kind); + assert.ok(kinds.includes("else"), "should include else path"); + assert.ok(!kinds.includes("then"), "should NOT include then path"); + }); + + test("constant wire — const bit is set", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with output as o + api.mode = "fast" + o.result <- api.label +}`); + const { executionTrace } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: {}, + tools: { api: async () => ({ label: "done" }) }, + }); + + const bridge = doc.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const manifest = buildTraversalManifest(bridge); + const decoded = decodeExecutionTrace(manifest, executionTrace); + const kinds = decoded.map((e) => e.kind); + assert.ok(kinds.includes("const"), "should include const path"); + }); + + test("executionTrace is a number suitable for hex encoding", async () => { + const doc = getDoc(`version 1.5 +bridge Query.demo { + with api + with input as i + with output as o + api.q <- i.q + o.result <- api.label +}`); + const { executionTrace } = await executeBridge({ + document: doc, + operation: "Query.demo", + input: { q: "Berlin" }, + tools: { api: async () => ({ label: "Berlin" }) }, + }); + + assert.equal(typeof executionTrace, "number"); + const hex = `0x${executionTrace.toString(16)}`; + assert.ok(hex.startsWith("0x"), "should be hex-encodable"); + }); +}); From e02f3c73c1aa7fef92c415817a4c0d6a5596d3fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:01:19 +0000 Subject: [PATCH 04/11] feat: add execution trace and manifest view to playground - Add executionTrace to RunResult type in engine - Pipe executionTrace through runBridgeStandalone - Add getTraversalManifest and decodeExecutionTrace helpers to engine - Show execution trace badge (hex) in ResultView - Add Manifest tab to Bridge DSL panel header - ManifestView shows all traversal entries with kind badges - Active paths highlighted with ring + dot based on execution trace - Works across mobile and desktop layouts Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/playground/src/Playground.tsx | 216 +++++++++++++++--- .../playground/src/components/ResultView.tsx | 13 +- packages/playground/src/engine.ts | 47 ++++ 3 files changed, 245 insertions(+), 31 deletions(-) diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index cdb6104c..439a22bd 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from "react"; +import { useState, useCallback, useRef, useMemo } from "react"; import { Panel, Group, @@ -218,12 +218,33 @@ function QueryTabBar({ } // ── bridge DSL panel header (label only) ───────────────────────────────────── -function BridgeDslHeader() { +function BridgeDslHeader({ + dslTab, + onDslTabChange, +}: { + dslTab: "dsl" | "manifest"; + onDslTabChange: (t: "dsl" | "manifest") => void; +}) { return ( -
- +
+ +
); } @@ -273,6 +294,106 @@ function SchemaHeader({ ); } +// ── manifest view ───────────────────────────────────────────────────────────── + +import { getTraversalManifest, decodeExecutionTrace } from "./engine"; + +function ManifestView({ + bridge, + operation, + executionTrace, + autoHeight = false, +}: { + bridge: string; + operation: string; + executionTrace?: number; + autoHeight?: boolean; +}) { + const manifest = useMemo( + () => getTraversalManifest(bridge, operation), + [bridge, operation], + ); + const activeIds = useMemo(() => { + if (executionTrace == null || executionTrace === 0 || manifest.length === 0) + return new Set(); + const decoded = decodeExecutionTrace(manifest, executionTrace); + return new Set(decoded.map((e) => e.id)); + }, [manifest, executionTrace]); + + if (!operation || manifest.length === 0) { + return ( +

+ No bridge operation selected. +

+ ); + } + + const kindColors: Record = { + primary: "bg-sky-900/50 text-sky-300 border-sky-700/50", + fallback: "bg-amber-900/50 text-amber-300 border-amber-700/50", + catch: "bg-red-900/50 text-red-300 border-red-700/50", + "empty-array": "bg-slate-700/50 text-slate-400 border-slate-600/50", + then: "bg-emerald-900/50 text-emerald-300 border-emerald-700/50", + else: "bg-orange-900/50 text-orange-300 border-orange-700/50", + const: "bg-violet-900/50 text-violet-300 border-violet-700/50", + }; + + return ( +
+
+ {manifest.map((entry) => { + const isActive = activeIds.has(entry.id); + return ( +
+ + {entry.kind} + + + {entry.target.length > 0 ? entry.target.join(".") : "*"} + + {entry.gateType && ( + + {entry.gateType === "falsy" ? "||" : "??"} + + )} + + bit {entry.bitIndex} + + {isActive && ( + + )} +
+ ); + })} +
+
+ ); +} + // ── panel wrapper ───────────────────────────────────────────────────────────── function PanelBox({ children }: { children: React.ReactNode }) { return ( @@ -359,6 +480,14 @@ export function Playground({ const activeQuery = queries.find((q) => q.id === activeTabId); const isStandalone = mode === "standalone"; + const [dslTab, setDslTab] = useState<"dsl" | "manifest">("dsl"); + + // Determine which operation to use for manifest + const manifestOperation = useMemo(() => { + if (isStandalone && activeQuery?.operation) return activeQuery.operation; + if (bridgeOperations.length > 0) return bridgeOperations[0].label; + return ""; + }, [isStandalone, activeQuery?.operation, bridgeOperations]); return ( <> @@ -395,16 +524,25 @@ export function Playground({ {/* Bridge DSL panel */}
- +
- + {dslTab === "dsl" ? ( + + ) : ( + + )}
@@ -489,6 +627,7 @@ export function Playground({ loading={displayRunning} traces={displayResult?.traces} logs={displayResult?.logs} + executionTrace={displayResult?.executionTrace} onClearCache={clearHttpCache} autoHeight /> @@ -519,15 +658,23 @@ export function Playground({
)} - +
- + {dslTab === "dsl" ? ( + + ) : ( + + )}
@@ -563,15 +710,23 @@ export function Playground({ {/* Bridge DSL panel */} - +
- + {dslTab === "dsl" ? ( + + ) : ( + + )}
@@ -665,6 +820,7 @@ export function Playground({ loading={displayRunning} traces={displayResult?.traces} logs={displayResult?.logs} + executionTrace={displayResult?.executionTrace} onClearCache={clearHttpCache} /> diff --git a/packages/playground/src/components/ResultView.tsx b/packages/playground/src/components/ResultView.tsx index 0362293d..3d6832ce 100644 --- a/packages/playground/src/components/ResultView.tsx +++ b/packages/playground/src/components/ResultView.tsx @@ -15,6 +15,7 @@ type Props = { loading: boolean; traces?: ToolTrace[]; logs?: LogEntry[]; + executionTrace?: number; onClearCache?: () => void; /** When true the result view sizes itself to its content instead of filling the parent. */ autoHeight?: boolean; @@ -26,6 +27,7 @@ export function ResultView({ loading, traces, logs, + executionTrace, onClearCache, autoHeight = false, }: Props) { @@ -37,6 +39,7 @@ export function ResultView({ const hasTraces = traces && traces.length > 0; const hasLogs = logs && logs.length > 0; + const hasExecutionTrace = executionTrace != null && executionTrace > 0; function toggle(panel: "traces" | "logs") { setActivePanel((v) => (v === panel ? null : panel)); @@ -92,9 +95,17 @@ export function ResultView({ {/* Badges row + expanded panel pinned to bottom */} - {(hasTraces || hasLogs || onClearCache) && ( + {(hasTraces || hasLogs || hasExecutionTrace || onClearCache) && (
+ {hasExecutionTrace && ( + + trace 0x{executionTrace.toString(16)} + + )} {hasTraces && ( 0 ? result.traces : undefined, logs: logs.length > 0 ? logs : undefined, + executionTrace: result.executionTrace, }; } catch (err: unknown) { return { @@ -674,3 +680,44 @@ export async function runBridgeStandalone( _onCacheHit = null; } } + +// ── Traversal manifest helpers ────────────────────────────────────────────── + +export type { TraversalEntry }; + +/** + * Build the static traversal manifest for a bridge operation. + * + * Returns the ordered array of TraversalEntry objects describing every + * possible execution path through the bridge's wires. + */ +export function getTraversalManifest( + bridgeText: string, + operation: string, +): TraversalEntry[] { + try { + const { document } = parseBridgeDiagnostics(bridgeText, { + filename: "playground.bridge", + }); + const [type, field] = operation.split("."); + if (!type || !field) return []; + + const bridge = document.instructions.find( + (i): i is Bridge => + i.kind === "bridge" && i.type === type && i.field === field, + ); + if (!bridge) return []; + + return buildTraversalManifest(bridge); + } catch { + return []; + } +} + +/** + * Decode a runtime execution trace bitmask against a traversal manifest. + * + * Returns the subset of TraversalEntry objects whose bits are set in + * the trace — i.e. the paths that were actually taken during execution. + */ +export { decodeExecutionTrace }; From c2ee99a0ad123685a96d9229729b6dcbd099e5c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:10:14 +0000 Subject: [PATCH 05/11] refactor: address code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename dslTab → activeDslTab for clarity - Rename fi → fallbackIndex in applyFallbackGates loop - Add comment explaining bitmask check in decodeExecutionTrace - Add INVARIANT comment for traceMask/traceBits coupling - Document bitIndex assignment lifecycle in enumerateTraversalIds Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/bridge-core/src/enumerate-traversals.ts | 6 ++++++ packages/bridge-core/src/resolveWires.ts | 9 ++++++--- packages/playground/src/Playground.tsx | 14 +++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/bridge-core/src/enumerate-traversals.ts b/packages/bridge-core/src/enumerate-traversals.ts index 0a2d7541..bc7a5e3e 100644 --- a/packages/bridge-core/src/enumerate-traversals.ts +++ b/packages/bridge-core/src/enumerate-traversals.ts @@ -122,6 +122,10 @@ function addCatchEntry( * Returns a flat list of {@link TraversalEntry} objects, one per * unique code-path through the bridge's wires. The total length * of the returned array is a useful proxy for bridge complexity. + * + * `bitIndex` is initially set to `-1` during construction and + * assigned sequentially (0, 1, 2, …) at the end. No entry is + * exposed with `bitIndex === -1`. */ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { const entries: TraversalEntry[] = []; @@ -241,6 +245,8 @@ export function decodeExecutionTrace( ): TraversalEntry[] { const result: TraversalEntry[] = []; for (const entry of manifest) { + // Check if the bit at position `entry.bitIndex` is set in the trace, + // indicating this path was taken during execution. if (trace & (1 << entry.bitIndex)) { result.push(entry); } diff --git a/packages/bridge-core/src/resolveWires.ts b/packages/bridge-core/src/resolveWires.ts index c33cac52..8313ab04 100644 --- a/packages/bridge-core/src/resolveWires.ts +++ b/packages/bridge-core/src/resolveWires.ts @@ -176,13 +176,13 @@ export async function applyFallbackGates( ): Promise { if (!w.fallbacks?.length) return value; - for (let fi = 0; fi < w.fallbacks.length; fi++) { - const fallback = w.fallbacks[fi]; + for (let fallbackIndex = 0; fallbackIndex < w.fallbacks.length; fallbackIndex++) { + const fallback = w.fallbacks[fallbackIndex]; const isFalsyGateOpen = fallback.type === "falsy" && !value; const isNullishGateOpen = fallback.type === "nullish" && value == null; if (isFalsyGateOpen || isNullishGateOpen) { - recordFallback(ctx, w, fi); + recordFallback(ctx, w, fallbackIndex); if (fallback.control) { return applyControlFlowWithLoc(fallback.control, fallback.loc ?? w.loc); } @@ -373,6 +373,9 @@ function pullSafe( // These are designed for minimal overhead: when `traceBits` is not set on the // context (tracing disabled), the functions return immediately after a single // falsy check. When enabled, one Map.get + one bitwise OR is the hot path. +// +// INVARIANT: `traceMask` is always set when `traceBits` is set — both are +// initialised together by `ExecutionTree.enableExecutionTrace()`. function recordPrimary(ctx: TreeContext, w: Wire): void { const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index 439a22bd..74a821d4 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -480,7 +480,7 @@ export function Playground({ const activeQuery = queries.find((q) => q.id === activeTabId); const isStandalone = mode === "standalone"; - const [dslTab, setDslTab] = useState<"dsl" | "manifest">("dsl"); + const [activeDslTab, setActiveDslTab] = useState<"dsl" | "manifest">("dsl"); // Determine which operation to use for manifest const manifestOperation = useMemo(() => { @@ -524,9 +524,9 @@ export function Playground({ {/* Bridge DSL panel */}
- +
- {dslTab === "dsl" ? ( + {activeDslTab === "dsl" ? ( )} - +
- {dslTab === "dsl" ? ( + {activeDslTab === "dsl" ? ( - +
- {dslTab === "dsl" ? ( + {activeDslTab === "dsl" ? ( Date: Mon, 9 Mar 2026 08:45:08 +0000 Subject: [PATCH 06/11] feat: BigInt trace mask, manifest grouping/filtering, scroll fix - Switch traceMask from number to bigint for unlimited entry support - Update all recording helpers to use BigInt bitwise ops - Update decodeExecutionTrace, ExecuteBridgeResult, TreeContext - Update playground engine, ResultView, Playground types - ManifestView: group entries by wire, show group headers for alternatives - ManifestView: add "Show alternatives only" toggle filter - ManifestView: fix scrolling with h-full / max-h-[60vh] - Update tests to use bigint assertions - All 1143 tests pass Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- .../bridge-compiler/src/execute-bridge.ts | 4 +- packages/bridge-core/src/ExecutionTree.ts | 11 +- .../bridge-core/src/enumerate-traversals.ts | 6 +- packages/bridge-core/src/execute-bridge.ts | 2 +- packages/bridge-core/src/resolveWires.ts | 8 +- packages/bridge-core/src/tree-types.ts | 3 +- .../bridge/test/enumerate-traversals.test.ts | 16 +- packages/playground/src/Playground.tsx | 198 +++++++++++++----- .../playground/src/components/ResultView.tsx | 4 +- packages/playground/src/engine.ts | 2 +- 10 files changed, 172 insertions(+), 82 deletions(-) diff --git a/packages/bridge-compiler/src/execute-bridge.ts b/packages/bridge-compiler/src/execute-bridge.ts index ece3d915..408ca1ca 100644 --- a/packages/bridge-compiler/src/execute-bridge.ts +++ b/packages/bridge-compiler/src/execute-bridge.ts @@ -84,7 +84,7 @@ export type ExecuteBridgeResult = { data: T; traces: ToolTrace[]; /** Compact bitmask encoding which traversal paths were taken during execution. */ - executionTrace: number; + executionTrace: bigint; }; // ── Cache ─────────────────────────────────────────────────────────────────── @@ -340,5 +340,5 @@ export async function executeBridge( } catch (err) { throw attachBridgeErrorDocumentContext(err, document); } - return { data: data as T, traces: tracer?.traces ?? [], executionTrace: 0 }; + return { data: data as T, traces: tracer?.traces ?? [], executionTrace: 0n }; } diff --git a/packages/bridge-core/src/ExecutionTree.ts b/packages/bridge-core/src/ExecutionTree.ts index 3c986b7f..82629915 100644 --- a/packages/bridge-core/src/ExecutionTree.ts +++ b/packages/bridge-core/src/ExecutionTree.ts @@ -155,8 +155,9 @@ export class ExecutionTree implements TreeContext { /** * Shared mutable trace bitmask — `[mask]`. Boxed in a single-element * array so shadow trees can share the same mutable reference. + * Uses `bigint` to support manifests with more than 31 entries. */ - traceMask?: [number]; + traceMask?: [bigint]; /** Structured logger passed from BridgeOptions. Defaults to no-ops. */ logger?: Logger; /** External abort signal — cancels execution when triggered. */ @@ -775,9 +776,9 @@ export class ExecutionTree implements TreeContext { return this.tracer?.traces ?? []; } - /** Returns the execution trace bitmask (0 when tracing is disabled). */ - getExecutionTrace(): number { - return this.traceMask?.[0] ?? 0; + /** Returns the execution trace bitmask (0n when tracing is disabled). */ + getExecutionTrace(): bigint { + return this.traceMask?.[0] ?? 0n; } /** @@ -789,7 +790,7 @@ export class ExecutionTree implements TreeContext { if (!this.bridge) return; const manifest = enumerateTraversalIds(this.bridge); this.traceBits = buildTraceBitsMap(this.bridge, manifest); - this.traceMask = [0]; + this.traceMask = [0n]; } /** diff --git a/packages/bridge-core/src/enumerate-traversals.ts b/packages/bridge-core/src/enumerate-traversals.ts index bc7a5e3e..538d2010 100644 --- a/packages/bridge-core/src/enumerate-traversals.ts +++ b/packages/bridge-core/src/enumerate-traversals.ts @@ -237,17 +237,17 @@ export const buildTraversalManifest = enumerateTraversalIds; * in the trace — i.e. the paths that were actually taken during execution. * * @param manifest The static manifest from {@link buildTraversalManifest}. - * @param trace The numeric bitmask produced by the execution engine. + * @param trace The bigint bitmask produced by the execution engine. */ export function decodeExecutionTrace( manifest: TraversalEntry[], - trace: number, + trace: bigint, ): TraversalEntry[] { const result: TraversalEntry[] = []; for (const entry of manifest) { // Check if the bit at position `entry.bitIndex` is set in the trace, // indicating this path was taken during execution. - if (trace & (1 << entry.bitIndex)) { + if (trace & (1n << BigInt(entry.bitIndex))) { result.push(entry); } } diff --git a/packages/bridge-core/src/execute-bridge.ts b/packages/bridge-core/src/execute-bridge.ts index 73bc786d..a6fde106 100644 --- a/packages/bridge-core/src/execute-bridge.ts +++ b/packages/bridge-core/src/execute-bridge.ts @@ -79,7 +79,7 @@ export type ExecuteBridgeResult = { data: T; traces: ToolTrace[]; /** Compact bitmask encoding which traversal paths were taken during execution. */ - executionTrace: number; + executionTrace: bigint; }; /** diff --git a/packages/bridge-core/src/resolveWires.ts b/packages/bridge-core/src/resolveWires.ts index 8313ab04..81e12efc 100644 --- a/packages/bridge-core/src/resolveWires.ts +++ b/packages/bridge-core/src/resolveWires.ts @@ -379,21 +379,21 @@ function pullSafe( function recordPrimary(ctx: TreeContext, w: Wire): void { const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.primary != null) ctx.traceMask![0] |= 1 << bits.primary; + if (bits?.primary != null) ctx.traceMask![0] |= 1n << BigInt(bits.primary); } function recordElse(ctx: TreeContext, w: Wire): void { const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.else != null) ctx.traceMask![0] |= 1 << bits.else; + if (bits?.else != null) ctx.traceMask![0] |= 1n << BigInt(bits.else); } function recordFallback(ctx: TreeContext, w: Wire, index: number): void { const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; const fb = bits?.fallbacks; - if (fb && fb[index] != null) ctx.traceMask![0] |= 1 << fb[index]; + if (fb && fb[index] != null) ctx.traceMask![0] |= 1n << BigInt(fb[index]); } function recordCatch(ctx: TreeContext, w: Wire): void { const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.catch != null) ctx.traceMask![0] |= 1 << bits.catch; + if (bits?.catch != null) ctx.traceMask![0] |= 1n << BigInt(bits.catch); } diff --git a/packages/bridge-core/src/tree-types.ts b/packages/bridge-core/src/tree-types.ts index da480ef5..95137997 100644 --- a/packages/bridge-core/src/tree-types.ts +++ b/packages/bridge-core/src/tree-types.ts @@ -146,8 +146,9 @@ export interface TreeContext { * Shared mutable trace bitmask — `[mask]`. Boxed in a single-element * array so shadow trees can share the same mutable reference without * extra allocation. Present only when execution tracing is enabled. + * Uses `bigint` to support manifests with more than 31 entries. */ - traceMask?: [number]; + traceMask?: [bigint]; } /** Returns `true` when `value` is a thenable (Promise or Promise-like). */ diff --git a/packages/bridge/test/enumerate-traversals.test.ts b/packages/bridge/test/enumerate-traversals.test.ts index 9c26ed2f..f53fce15 100644 --- a/packages/bridge/test/enumerate-traversals.test.ts +++ b/packages/bridge/test/enumerate-traversals.test.ts @@ -365,7 +365,7 @@ bridge Query.demo { o.result <- api.label }`); const manifest = buildTraversalManifest(bridge); - const result = decodeExecutionTrace(manifest, 0); + const result = decodeExecutionTrace(manifest, 0n); assert.equal(result.length, 0); }); @@ -381,7 +381,7 @@ bridge Query.demo { const manifest = buildTraversalManifest(bridge); const primary = manifest.find((e) => e.kind === "primary" && e.target.includes("result")); assert.ok(primary); - const result = decodeExecutionTrace(manifest, 1 << primary.bitIndex); + const result = decodeExecutionTrace(manifest, 1n << BigInt(primary.bitIndex)); assert.equal(result.length, 1); assert.equal(result[0].id, primary.id); }); @@ -404,9 +404,9 @@ bridge Query.demo { assert.equal(labelEntries.length, 3); // Set all label bits - let mask = 0; + let mask = 0n; for (const e of labelEntries) { - mask |= 1 << e.bitIndex; + mask |= 1n << BigInt(e.bitIndex); } const decoded = decodeExecutionTrace(manifest, mask); assert.equal(decoded.length, 3); @@ -428,7 +428,7 @@ bridge Query.demo { const manifest = buildTraversalManifest(bridge); const thenEntry = manifest.find((e) => e.kind === "then"); assert.ok(thenEntry); - const decoded = decodeExecutionTrace(manifest, 1 << thenEntry.bitIndex); + const decoded = decodeExecutionTrace(manifest, 1n << BigInt(thenEntry.bitIndex)); assert.equal(decoded.length, 1); assert.equal(decoded[0].kind, "then"); }); @@ -458,7 +458,7 @@ bridge Query.demo { tools: { api: async () => ({ label: "Hello" }) }, }); - assert.ok(executionTrace > 0, "trace should have bits set"); + assert.ok(executionTrace > 0n, "trace should have bits set"); // Decode and verify const bridge = doc.instructions.find( @@ -600,7 +600,7 @@ bridge Query.demo { assert.ok(kinds.includes("const"), "should include const path"); }); - test("executionTrace is a number suitable for hex encoding", async () => { + test("executionTrace is a bigint suitable for hex encoding", async () => { const doc = getDoc(`version 1.5 bridge Query.demo { with api @@ -616,7 +616,7 @@ bridge Query.demo { tools: { api: async () => ({ label: "Berlin" }) }, }); - assert.equal(typeof executionTrace, "number"); + assert.equal(typeof executionTrace, "bigint"); const hex = `0x${executionTrace.toString(16)}`; assert.ok(hex.startsWith("0x"), "should be hex-encodable"); }); diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index 74a821d4..a2017cac 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -297,6 +297,49 @@ function SchemaHeader({ // ── manifest view ───────────────────────────────────────────────────────────── import { getTraversalManifest, decodeExecutionTrace } from "./engine"; +import type { TraversalEntry } from "./engine"; + +/** Group entries by target path (wireIndex) for visual grouping. */ +type ManifestGroup = { + label: string; + entries: TraversalEntry[]; + hasAlternatives: boolean; +}; + +function buildGroups(manifest: TraversalEntry[]): ManifestGroup[] { + const byWire = new Map(); + const order: number[] = []; + for (const e of manifest) { + const key = e.wireIndex; + let group = byWire.get(key); + if (!group) { + group = []; + byWire.set(key, group); + order.push(key); + } + group.push(e); + } + return order.map((key) => { + const entries = byWire.get(key)!; + const label = + entries[0].target.length > 0 ? entries[0].target.join(".") : "*"; + return { + label, + entries, + hasAlternatives: entries.length > 1, + }; + }); +} + +const kindColors: Record = { + primary: "bg-sky-900/50 text-sky-300 border-sky-700/50", + fallback: "bg-amber-900/50 text-amber-300 border-amber-700/50", + catch: "bg-red-900/50 text-red-300 border-red-700/50", + "empty-array": "bg-slate-700/50 text-slate-400 border-slate-600/50", + then: "bg-emerald-900/50 text-emerald-300 border-emerald-700/50", + else: "bg-orange-900/50 text-orange-300 border-orange-700/50", + const: "bg-violet-900/50 text-violet-300 border-violet-700/50", +}; function ManifestView({ bridge, @@ -306,7 +349,7 @@ function ManifestView({ }: { bridge: string; operation: string; - executionTrace?: number; + executionTrace?: bigint; autoHeight?: boolean; }) { const manifest = useMemo( @@ -314,12 +357,16 @@ function ManifestView({ [bridge, operation], ); const activeIds = useMemo(() => { - if (executionTrace == null || executionTrace === 0 || manifest.length === 0) + if (executionTrace == null || executionTrace === 0n || manifest.length === 0) return new Set(); const decoded = decodeExecutionTrace(manifest, executionTrace); return new Set(decoded.map((e) => e.id)); }, [manifest, executionTrace]); + const groups = useMemo(() => buildGroups(manifest), [manifest]); + const hasAnyAlternatives = groups.some((g) => g.hasAlternatives); + const [showAllPaths, setShowAllPaths] = useState(true); + if (!operation || manifest.length === 0) { return (

@@ -328,67 +375,108 @@ function ManifestView({ ); } - const kindColors: Record = { - primary: "bg-sky-900/50 text-sky-300 border-sky-700/50", - fallback: "bg-amber-900/50 text-amber-300 border-amber-700/50", - catch: "bg-red-900/50 text-red-300 border-red-700/50", - "empty-array": "bg-slate-700/50 text-slate-400 border-slate-600/50", - then: "bg-emerald-900/50 text-emerald-300 border-emerald-700/50", - else: "bg-orange-900/50 text-orange-300 border-orange-700/50", - const: "bg-violet-900/50 text-violet-300 border-violet-700/50", - }; + const visibleGroups = showAllPaths + ? groups + : groups.filter((g) => g.hasAlternatives); return (

-
- {manifest.map((entry) => { - const isActive = activeIds.has(entry.id); - return ( -
- - {entry.kind} - - - {entry.target.length > 0 ? entry.target.join(".") : "*"} - - {entry.gateType && ( - - {entry.gateType === "falsy" ? "||" : "??"} - - )} - - bit {entry.bitIndex} - - {isActive && ( - - )} + {/* Filter toggle */} + {hasAnyAlternatives && ( +
+ + + {visibleGroups.length}/{groups.length} groups + +
+ )} + +
+ {visibleGroups.map((group) => ( +
+ {/* Group header — target path (only for groups with alternatives) */} + {group.hasAlternatives && ( +
+ {group.label} +
+ )} + + {/* Entries */} +
+ {group.entries.map((entry) => { + const isActive = activeIds.has(entry.id); + return ( +
+ + {entry.kind} + + {/* Show target label for entries without group header */} + {!group.hasAlternatives && ( + + {entry.target.length > 0 + ? entry.target.join(".") + : "*"} + + )} + {entry.gateType && ( + + {entry.gateType === "falsy" ? "||" : "??"} + + )} + {isActive && ( + + )} +
+ ); + })}
- ); - })} +
+ ))}
); diff --git a/packages/playground/src/components/ResultView.tsx b/packages/playground/src/components/ResultView.tsx index 3d6832ce..b3408447 100644 --- a/packages/playground/src/components/ResultView.tsx +++ b/packages/playground/src/components/ResultView.tsx @@ -15,7 +15,7 @@ type Props = { loading: boolean; traces?: ToolTrace[]; logs?: LogEntry[]; - executionTrace?: number; + executionTrace?: bigint; onClearCache?: () => void; /** When true the result view sizes itself to its content instead of filling the parent. */ autoHeight?: boolean; @@ -39,7 +39,7 @@ export function ResultView({ const hasTraces = traces && traces.length > 0; const hasLogs = logs && logs.length > 0; - const hasExecutionTrace = executionTrace != null && executionTrace > 0; + const hasExecutionTrace = executionTrace != null && executionTrace > 0n; function toggle(panel: "traces" | "logs") { setActivePanel((v) => (v === panel ? null : panel)); diff --git a/packages/playground/src/engine.ts b/packages/playground/src/engine.ts index 9ccca411..8b72888c 100644 --- a/packages/playground/src/engine.ts +++ b/packages/playground/src/engine.ts @@ -81,7 +81,7 @@ export type RunResult = { traces?: ToolTrace[]; logs?: LogEntry[]; /** Compact bitmask encoding which traversal paths were taken during execution. */ - executionTrace?: number; + executionTrace?: bigint; }; export type DiagnosticResult = { From 5790b1d1cac56a33d664994d944bbd47a7c5f24f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:47:18 +0000 Subject: [PATCH 07/11] fix: add aria-label to manifest group count for accessibility Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/playground/src/Playground.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index a2017cac..2ecaee96 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -400,7 +400,10 @@ function ManifestView({ > {showAllPaths ? "Show alternatives only" : "Show all paths"} - + {visibleGroups.length}/{groups.length} groups
From e0cc512e37c9d047c5480da8aeba1c6fac1506ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:24:53 +0000 Subject: [PATCH 08/11] feat: source descriptions, alias labels, empty-array grouping, always-on toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `description` field to TraversalEntry with human-readable source info (e.g., "api.username", "|| \"Anonymous\"", "catch continue", "= \"SBB\"") - Fix alias wires showing as `*` — use `to.field` for `__local` module targets - Fix empty-array entries grouped under same `*` — assign unique wireIndex per scope - Handle map resolves tool, input, context, pipe, and alias refs to handle aliases - Filter toggle always visible regardless of whether alternatives exist - Consistent group headers shown for all entries (not just alternatives) - Empty-array entries show iterator variable in description (e.g., "c[] empty") - All 1143 tests pass Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- .../bridge-core/src/enumerate-traversals.ts | 168 +++++++++++++++--- packages/playground/src/Playground.tsx | 104 ++++++----- 2 files changed, 195 insertions(+), 77 deletions(-) diff --git a/packages/bridge-core/src/enumerate-traversals.ts b/packages/bridge-core/src/enumerate-traversals.ts index 538d2010..05498118 100644 --- a/packages/bridge-core/src/enumerate-traversals.ts +++ b/packages/bridge-core/src/enumerate-traversals.ts @@ -15,7 +15,7 @@ * {@link decodeExecutionTrace} to map the bitmask back to entries. */ -import type { Bridge, Wire, WireFallback } from "./types.ts"; +import type { Bridge, Wire, WireFallback, NodeRef, ControlFlowInstruction } from "./types.ts"; // ── Public types ──────────────────────────────────────────────────────────── @@ -44,6 +44,11 @@ export interface TraversalEntry { gateType?: "falsy" | "nullish"; /** Bit position in the execution trace bitmask (0-based). */ bitIndex: number; + /** + * Human-readable description of the source for this path. + * Examples: `"api.username"`, `"|| \"Anonymous\""`, `"catch continue"`, `"= \"SBB\""`. + */ + description?: string; } // ── Helpers ───────────────────────────────────────────────────────────────── @@ -81,12 +86,96 @@ function isPlainArraySourceWire( return !w.fallbacks?.length && !hasCatch(w); } +// ── Description helpers ──────────────────────────────────────────────────── + +/** Map from ref type+field → handle alias for readable ref descriptions. */ +function buildHandleMap(bridge: Bridge): Map { + const map = new Map(); + for (const h of bridge.handles) { + if (h.kind === "tool" || h.kind === "define") { + // Tool/define refs use type="Tools" and field=tool name. + map.set(`Tools:${h.name}`, h.handle); + } else if (h.kind === "input") { + map.set("input", h.handle); + } else if (h.kind === "context") { + map.set("context", h.handle); + } + } + // Pipe handles use a non-"_" module (e.g., "std.str") with type="Query". + if (bridge.pipeHandles) { + for (const ph of bridge.pipeHandles) { + map.set(`pipe:${ph.baseTrunk.module}`, ph.handle); + } + } + return map; +} + +function refLabel(ref: NodeRef, hmap: Map): string { + if (ref.element) { + return ref.path.length > 0 ? ref.path.join(".") : "element"; + } + // __local refs are alias variables — use the field name (alias name) directly. + if (ref.module === "__local") { + return ref.path.length > 0 ? `${ref.field}.${ref.path.join(".")}` : ref.field; + } + let alias: string | undefined; + if (ref.type === "Tools") { + alias = hmap.get(`Tools:${ref.field}`); + } else if (ref.module !== "_") { + // Pipe handle — look up by module name. + alias = hmap.get(`pipe:${ref.module}`); + } else { + alias = hmap.get("input") ?? hmap.get("context"); + } + alias ??= ref.field; + return ref.path.length > 0 ? `${alias}.${ref.path.join(".")}` : alias; +} + +function controlLabel(ctrl: ControlFlowInstruction): string { + const n = ctrl.kind === "continue" || ctrl.kind === "break" + ? (ctrl.levels != null && ctrl.levels > 1 ? ` ${ctrl.levels}` : "") + : ""; + if (ctrl.kind === "throw" || ctrl.kind === "panic") { + return `${ctrl.kind} "${ctrl.message}"`; + } + return `${ctrl.kind}${n}`; +} + +function fallbackDescription(fb: WireFallback, hmap: Map): string { + const gate = fb.type === "falsy" ? "||" : "??"; + if (fb.value != null) return `${gate} ${fb.value}`; + if (fb.ref) return `${gate} ${refLabel(fb.ref, hmap)}`; + if (fb.control) return `${gate} ${controlLabel(fb.control)}`; + return gate; +} + +function catchDescription(w: Wire, hmap: Map): string { + if ("value" in w) return "catch"; + if (w.catchFallback != null) return `catch ${w.catchFallback}`; + if (w.catchFallbackRef) return `catch ${refLabel(w.catchFallbackRef, hmap)}`; + if (w.catchControl) return `catch ${controlLabel(w.catchControl)}`; + return "catch"; +} + +/** + * Compute the effective target path for a wire. + * For `__local` module wires (aliases), use `to.field` as the target + * since `to.path` is always empty for alias wires. + */ +function effectiveTarget(w: Wire): string[] { + if (w.to.path.length === 0 && w.to.module === "__local") { + return [w.to.field]; + } + return w.to.path; +} + function addFallbackEntries( entries: TraversalEntry[], base: string, wireIndex: number, target: string[], fallbacks: WireFallback[] | undefined, + hmap: Map, ): void { if (!fallbacks) return; for (let i = 0; i < fallbacks.length; i++) { @@ -98,6 +187,7 @@ function addFallbackEntries( fallbackIndex: i, gateType: fallbacks[i].type, bitIndex: -1, // assigned after enumeration + description: fallbackDescription(fallbacks[i], hmap), }); } } @@ -108,9 +198,17 @@ function addCatchEntry( wireIndex: number, target: string[], w: Wire, + hmap: Map, ): void { if (hasCatch(w)) { - entries.push({ id: `${base}/catch`, wireIndex, target, kind: "catch", bitIndex: -1 }); + entries.push({ + id: `${base}/catch`, + wireIndex, + target, + kind: "catch", + bitIndex: -1, + description: catchDescription(w, hmap), + }); } } @@ -129,6 +227,7 @@ function addCatchEntry( */ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { const entries: TraversalEntry[] = []; + const hmap = buildHandleMap(bridge); // Track per-target occurrence counts for disambiguation when // multiple wires write to the same target (overdefinition). @@ -136,7 +235,7 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { for (let i = 0; i < bridge.wires.length; i++) { const w = bridge.wires[i]; - const target = w.to.path; + const target = effectiveTarget(w); const tKey = pathKey(target); // Disambiguate overdefined targets (same target written by >1 wire). @@ -146,7 +245,14 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { // ── Constant wire ─────────────────────────────────────────────── if ("value" in w) { - entries.push({ id: `${base}/const`, wireIndex: i, target, kind: "const", bitIndex: -1 }); + entries.push({ + id: `${base}/const`, + wireIndex: i, + target, + kind: "const", + bitIndex: -1, + description: `= ${w.value}`, + }); continue; } @@ -161,51 +267,65 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { target, kind: "primary", bitIndex: -1, + description: refLabel(w.from, hmap), }); - addFallbackEntries(entries, base, i, target, w.fallbacks); - addCatchEntry(entries, base, i, target, w); + addFallbackEntries(entries, base, i, target, w.fallbacks, hmap); + addCatchEntry(entries, base, i, target, w, hmap); } continue; } // ── Conditional (ternary) wire ────────────────────────────────── if ("cond" in w) { - entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then", bitIndex: -1 }); - entries.push({ id: `${base}/else`, wireIndex: i, target, kind: "else", bitIndex: -1 }); - addFallbackEntries(entries, base, i, target, w.fallbacks); - addCatchEntry(entries, base, i, target, w); + const thenDesc = w.thenRef ? `? ${refLabel(w.thenRef, hmap)}` : w.thenValue != null ? `? ${w.thenValue}` : "then"; + const elseDesc = w.elseRef ? `: ${refLabel(w.elseRef, hmap)}` : w.elseValue != null ? `: ${w.elseValue}` : "else"; + entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then", bitIndex: -1, description: thenDesc }); + entries.push({ id: `${base}/else`, wireIndex: i, target, kind: "else", bitIndex: -1, description: elseDesc }); + addFallbackEntries(entries, base, i, target, w.fallbacks, hmap); + addCatchEntry(entries, base, i, target, w, hmap); continue; } // ── condAnd / condOr (logical binary) ─────────────────────────── - entries.push({ - id: `${base}/primary`, - wireIndex: i, - target, - kind: "primary", - bitIndex: -1, - }); if ("condAnd" in w) { - addFallbackEntries(entries, base, i, target, w.fallbacks); - addCatchEntry(entries, base, i, target, w); + const desc = w.condAnd.rightRef + ? `${refLabel(w.condAnd.leftRef, hmap)} && ${refLabel(w.condAnd.rightRef, hmap)}` + : w.condAnd.rightValue != null + ? `${refLabel(w.condAnd.leftRef, hmap)} && ${w.condAnd.rightValue}` + : refLabel(w.condAnd.leftRef, hmap); + entries.push({ id: `${base}/primary`, wireIndex: i, target, kind: "primary", bitIndex: -1, description: desc }); + addFallbackEntries(entries, base, i, target, w.fallbacks, hmap); + addCatchEntry(entries, base, i, target, w, hmap); } else { // condOr const wo = w as Extract; - addFallbackEntries(entries, base, i, target, wo.fallbacks); - addCatchEntry(entries, base, i, target, w); + const desc = wo.condOr.rightRef + ? `${refLabel(wo.condOr.leftRef, hmap)} || ${refLabel(wo.condOr.rightRef, hmap)}` + : wo.condOr.rightValue != null + ? `${refLabel(wo.condOr.leftRef, hmap)} || ${wo.condOr.rightValue}` + : refLabel(wo.condOr.leftRef, hmap); + entries.push({ id: `${base}/primary`, wireIndex: i, target, kind: "primary", bitIndex: -1, description: desc }); + addFallbackEntries(entries, base, i, target, wo.fallbacks, hmap); + addCatchEntry(entries, base, i, target, w, hmap); } } // ── Array iterators — each scope adds an "empty-array" path ───── if (bridge.arrayIterators) { + let emptyIdx = 0; for (const key of Object.keys(bridge.arrayIterators)) { - const id = key ? `${key}/empty-array` : "*/empty-array"; + const iterName = bridge.arrayIterators[key]; + const target = key ? key.split(".") : []; + const label = key || "(root)"; + const id = `${label}/empty-array`; entries.push({ id, - wireIndex: -1, - target: key ? key.split(".") : [], + // Use unique negative wireIndex per empty-array so they don't group together. + wireIndex: -(++emptyIdx), + target, kind: "empty-array", bitIndex: -1, + description: `${iterName}[] empty`, }); } } diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index 2ecaee96..e84c75d8 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -299,7 +299,7 @@ function SchemaHeader({ import { getTraversalManifest, decodeExecutionTrace } from "./engine"; import type { TraversalEntry } from "./engine"; -/** Group entries by target path (wireIndex) for visual grouping. */ +/** Group entries by wireIndex for visual grouping. */ type ManifestGroup = { label: string; entries: TraversalEntry[]; @@ -307,22 +307,24 @@ type ManifestGroup = { }; function buildGroups(manifest: TraversalEntry[]): ManifestGroup[] { - const byWire = new Map(); + // Group by wireIndex; each unique wireIndex forms one group. + // Negative wireIndex values (empty-array entries) are already unique per scope. + const byKey = new Map(); const order: number[] = []; for (const e of manifest) { const key = e.wireIndex; - let group = byWire.get(key); + let group = byKey.get(key); if (!group) { group = []; - byWire.set(key, group); + byKey.set(key, group); order.push(key); } group.push(e); } return order.map((key) => { - const entries = byWire.get(key)!; + const entries = byKey.get(key)!; const label = - entries[0].target.length > 0 ? entries[0].target.join(".") : "*"; + entries[0].target.length > 0 ? entries[0].target.join(".") : "(root)"; return { label, entries, @@ -364,7 +366,6 @@ function ManifestView({ }, [manifest, executionTrace]); const groups = useMemo(() => buildGroups(manifest), [manifest]); - const hasAnyAlternatives = groups.some((g) => g.hasAlternatives); const [showAllPaths, setShowAllPaths] = useState(true); if (!operation || manifest.length === 0) { @@ -386,49 +387,55 @@ function ManifestView({ autoHeight ? "max-h-[60vh]" : "h-full", )} > - {/* Filter toggle */} - {hasAnyAlternatives && ( -
- - - {visibleGroups.length}/{groups.length} groups - -
- )} + {/* Filter toggle — always visible so users can switch freely */} +
+ + + {visibleGroups.length}/{groups.length} groups + +
+ {visibleGroups.length === 0 && ( +

+ No entries match the current filter. +

+ )} {visibleGroups.map((group) => (
- {/* Group header — target path (only for groups with alternatives) */} - {group.hasAlternatives && ( -
- {group.label} -
- )} + {/* Group header — always show target path */} +
+ {group.label} +
{/* Entries */} -
+
{group.entries.map((entry) => { const isActive = activeIds.has(entry.id); return ( @@ -438,9 +445,7 @@ function ManifestView({ "flex items-center gap-2 px-2.5 py-1 rounded-md text-[12px] font-mono transition-all", isActive ? "bg-slate-700/80 ring-1 ring-indigo-500/50" - : group.hasAlternatives - ? "bg-slate-900/40 opacity-60" - : "bg-slate-900/20 opacity-50", + : "bg-slate-900/40 opacity-60", )} > {entry.kind} - {/* Show target label for entries without group header */} - {!group.hasAlternatives && ( + {/* Source description */} + {entry.description && ( - {entry.target.length > 0 - ? entry.target.join(".") - : "*"} - - )} - {entry.gateType && ( - - {entry.gateType === "falsy" ? "||" : "??"} + {entry.description} )} {isActive && ( From 760e7cfff6e73efc53631eb4892834d6f87a4d62 Mon Sep 17 00:00:00 2001 From: Aarne Laur Date: Mon, 9 Mar 2026 10:53:34 +0100 Subject: [PATCH 09/11] UI tweaks --- packages/playground/src/Playground.tsx | 94 ++++++++++---------------- 1 file changed, 37 insertions(+), 57 deletions(-) diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index e84c75d8..7b4b4183 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -231,7 +231,9 @@ function BridgeDslHeader({ onClick={() => onDslTabChange("dsl")} className={cn( "text-[11px] font-bold uppercase tracking-widest transition-colors", - dslTab === "dsl" ? "text-slate-200" : "text-slate-500 hover:text-slate-300", + dslTab === "dsl" + ? "text-slate-200" + : "text-slate-500 hover:text-slate-300", )} > Bridge DSL @@ -240,10 +242,12 @@ function BridgeDslHeader({ onClick={() => onDslTabChange("manifest")} className={cn( "text-[11px] font-bold uppercase tracking-widest transition-colors", - dslTab === "manifest" ? "text-slate-200" : "text-slate-500 hover:text-slate-300", + dslTab === "manifest" + ? "text-slate-200" + : "text-slate-500 hover:text-slate-300", )} > - Manifest + Trace Manifest
); @@ -328,7 +332,6 @@ function buildGroups(manifest: TraversalEntry[]): ManifestGroup[] { return { label, entries, - hasAlternatives: entries.length > 1, }; }); } @@ -359,14 +362,17 @@ function ManifestView({ [bridge, operation], ); const activeIds = useMemo(() => { - if (executionTrace == null || executionTrace === 0n || manifest.length === 0) + if ( + executionTrace == null || + executionTrace === 0n || + manifest.length === 0 + ) return new Set(); const decoded = decodeExecutionTrace(manifest, executionTrace); return new Set(decoded.map((e) => e.id)); }, [manifest, executionTrace]); const groups = useMemo(() => buildGroups(manifest), [manifest]); - const [showAllPaths, setShowAllPaths] = useState(true); if (!operation || manifest.length === 0) { return ( @@ -376,66 +382,29 @@ function ManifestView({ ); } - const visibleGroups = showAllPaths - ? groups - : groups.filter((g) => g.hasAlternatives); - return (
- {/* Filter toggle — always visible so users can switch freely */} -
- - - {visibleGroups.length}/{groups.length} groups - -
-
- {visibleGroups.length === 0 && ( -

- No entries match the current filter. -

- )} - {visibleGroups.map((group) => ( + {groups.map((group) => (
{/* Group header — always show target path */} -
+
{group.label}
{/* Entries */} -
+
{group.entries.map((entry) => { const isActive = activeIds.has(entry.id); return ( @@ -444,7 +413,7 @@ function ManifestView({ className={cn( "flex items-center gap-2 px-2.5 py-1 rounded-md text-[12px] font-mono transition-all", isActive - ? "bg-slate-700/80 ring-1 ring-indigo-500/50" + ? "bg-slate-700/80" : "bg-slate-900/40 opacity-60", )} > @@ -470,7 +439,9 @@ function ManifestView({ )} {isActive && ( - + + on + )}
); @@ -613,7 +584,10 @@ export function Playground({ {/* Bridge DSL panel */}
- +
{activeDslTab === "dsl" ? ( )} - +
{activeDslTab === "dsl" ? ( - +
{activeDslTab === "dsl" ? ( Date: Mon, 9 Mar 2026 12:24:26 +0100 Subject: [PATCH 10/11] LSP for dead code in playground --- .../playground-dead-code-trace-manifest.md | 6 + .../bridge-core/src/enumerate-traversals.ts | 114 +++++- packages/bridge-core/src/execute-bridge.ts | 4 + .../test/traversal-manifest-locations.test.ts | 114 ++++++ packages/bridge-parser/src/parser/parser.ts | 100 +++-- packages/bridge/test/source-locations.test.ts | 54 +++ packages/playground/src/Playground.tsx | 358 +++++++----------- .../playground/src/codemirror/dead-code.ts | 59 +++ packages/playground/src/codemirror/theme.ts | 67 ++-- packages/playground/src/components/Editor.tsx | 20 + .../playground/src/components/ResultView.tsx | 13 +- packages/playground/src/engine.ts | 5 + packages/playground/src/usePlaygroundState.ts | 13 + 13 files changed, 599 insertions(+), 328 deletions(-) create mode 100644 .changeset/playground-dead-code-trace-manifest.md create mode 100644 packages/bridge-core/test/traversal-manifest-locations.test.ts create mode 100644 packages/playground/src/codemirror/dead-code.ts diff --git a/.changeset/playground-dead-code-trace-manifest.md b/.changeset/playground-dead-code-trace-manifest.md new file mode 100644 index 00000000..1ecbca35 --- /dev/null +++ b/.changeset/playground-dead-code-trace-manifest.md @@ -0,0 +1,6 @@ +--- +"@stackables/bridge": patch +"@stackables/bridge-core": patch +--- + +Bridge Trace IDs - The engine now returns a compact Trace ID alongside your data (e.g., 0x2a). This ID can be decoded into an exact execution map showing precisely which wires, fallbacks, and conditions activated. Because every bridge has a finite number of execution paths, these IDs are perfect for zero-PII monitoring and bucketing telemetry data. diff --git a/packages/bridge-core/src/enumerate-traversals.ts b/packages/bridge-core/src/enumerate-traversals.ts index 05498118..e1117599 100644 --- a/packages/bridge-core/src/enumerate-traversals.ts +++ b/packages/bridge-core/src/enumerate-traversals.ts @@ -15,7 +15,14 @@ * {@link decodeExecutionTrace} to map the bitmask back to entries. */ -import type { Bridge, Wire, WireFallback, NodeRef, ControlFlowInstruction } from "./types.ts"; +import type { + Bridge, + Wire, + WireFallback, + NodeRef, + ControlFlowInstruction, + SourceLocation, +} from "./types.ts"; // ── Public types ──────────────────────────────────────────────────────────── @@ -44,6 +51,10 @@ export interface TraversalEntry { gateType?: "falsy" | "nullish"; /** Bit position in the execution trace bitmask (0-based). */ bitIndex: number; + /** Source span for the specific traversal branch, when known. */ + loc?: SourceLocation; + /** Source span covering the entire wire (full line), when known. */ + wireLoc?: SourceLocation; /** * Human-readable description of the source for this path. * Examples: `"api.username"`, `"|| \"Anonymous\""`, `"catch continue"`, `"= \"SBB\""`. @@ -116,7 +127,9 @@ function refLabel(ref: NodeRef, hmap: Map): string { } // __local refs are alias variables — use the field name (alias name) directly. if (ref.module === "__local") { - return ref.path.length > 0 ? `${ref.field}.${ref.path.join(".")}` : ref.field; + return ref.path.length > 0 + ? `${ref.field}.${ref.path.join(".")}` + : ref.field; } let alias: string | undefined; if (ref.type === "Tools") { @@ -132,16 +145,22 @@ function refLabel(ref: NodeRef, hmap: Map): string { } function controlLabel(ctrl: ControlFlowInstruction): string { - const n = ctrl.kind === "continue" || ctrl.kind === "break" - ? (ctrl.levels != null && ctrl.levels > 1 ? ` ${ctrl.levels}` : "") - : ""; + const n = + ctrl.kind === "continue" || ctrl.kind === "break" + ? ctrl.levels != null && ctrl.levels > 1 + ? ` ${ctrl.levels}` + : "" + : ""; if (ctrl.kind === "throw" || ctrl.kind === "panic") { return `${ctrl.kind} "${ctrl.message}"`; } return `${ctrl.kind}${n}`; } -function fallbackDescription(fb: WireFallback, hmap: Map): string { +function fallbackDescription( + fb: WireFallback, + hmap: Map, +): string { const gate = fb.type === "falsy" ? "||" : "??"; if (fb.value != null) return `${gate} ${fb.value}`; if (fb.ref) return `${gate} ${refLabel(fb.ref, hmap)}`; @@ -169,14 +188,21 @@ function effectiveTarget(w: Wire): string[] { return w.to.path; } +function primaryLoc(w: Wire): SourceLocation | undefined { + if ("value" in w) return w.loc; + if ("from" in w) return w.fromLoc ?? w.loc; + return w.loc; +} + function addFallbackEntries( entries: TraversalEntry[], base: string, wireIndex: number, target: string[], - fallbacks: WireFallback[] | undefined, + w: Wire, hmap: Map, ): void { + const fallbacks = "fallbacks" in w ? w.fallbacks : undefined; if (!fallbacks) return; for (let i = 0; i < fallbacks.length; i++) { entries.push({ @@ -187,6 +213,8 @@ function addFallbackEntries( fallbackIndex: i, gateType: fallbacks[i].type, bitIndex: -1, // assigned after enumeration + loc: fallbacks[i].loc, + wireLoc: w.loc, description: fallbackDescription(fallbacks[i], hmap), }); } @@ -207,6 +235,8 @@ function addCatchEntry( target, kind: "catch", bitIndex: -1, + loc: "catchLoc" in w ? w.catchLoc : undefined, + wireLoc: w.loc, description: catchDescription(w, hmap), }); } @@ -251,6 +281,8 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { target, kind: "const", bitIndex: -1, + loc: w.loc, + wireLoc: w.loc, description: `= ${w.value}`, }); continue; @@ -267,9 +299,11 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { target, kind: "primary", bitIndex: -1, + loc: primaryLoc(w), + wireLoc: w.loc, description: refLabel(w.from, hmap), }); - addFallbackEntries(entries, base, i, target, w.fallbacks, hmap); + addFallbackEntries(entries, base, i, target, w, hmap); addCatchEntry(entries, base, i, target, w, hmap); } continue; @@ -277,11 +311,37 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { // ── Conditional (ternary) wire ────────────────────────────────── if ("cond" in w) { - const thenDesc = w.thenRef ? `? ${refLabel(w.thenRef, hmap)}` : w.thenValue != null ? `? ${w.thenValue}` : "then"; - const elseDesc = w.elseRef ? `: ${refLabel(w.elseRef, hmap)}` : w.elseValue != null ? `: ${w.elseValue}` : "else"; - entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then", bitIndex: -1, description: thenDesc }); - entries.push({ id: `${base}/else`, wireIndex: i, target, kind: "else", bitIndex: -1, description: elseDesc }); - addFallbackEntries(entries, base, i, target, w.fallbacks, hmap); + const thenDesc = w.thenRef + ? `? ${refLabel(w.thenRef, hmap)}` + : w.thenValue != null + ? `? ${w.thenValue}` + : "then"; + const elseDesc = w.elseRef + ? `: ${refLabel(w.elseRef, hmap)}` + : w.elseValue != null + ? `: ${w.elseValue}` + : "else"; + entries.push({ + id: `${base}/then`, + wireIndex: i, + target, + kind: "then", + bitIndex: -1, + loc: w.thenLoc ?? w.loc, + wireLoc: w.loc, + description: thenDesc, + }); + entries.push({ + id: `${base}/else`, + wireIndex: i, + target, + kind: "else", + bitIndex: -1, + loc: w.elseLoc ?? w.loc, + wireLoc: w.loc, + description: elseDesc, + }); + addFallbackEntries(entries, base, i, target, w, hmap); addCatchEntry(entries, base, i, target, w, hmap); continue; } @@ -293,8 +353,17 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { : w.condAnd.rightValue != null ? `${refLabel(w.condAnd.leftRef, hmap)} && ${w.condAnd.rightValue}` : refLabel(w.condAnd.leftRef, hmap); - entries.push({ id: `${base}/primary`, wireIndex: i, target, kind: "primary", bitIndex: -1, description: desc }); - addFallbackEntries(entries, base, i, target, w.fallbacks, hmap); + entries.push({ + id: `${base}/primary`, + wireIndex: i, + target, + kind: "primary", + bitIndex: -1, + loc: primaryLoc(w), + wireLoc: w.loc, + description: desc, + }); + addFallbackEntries(entries, base, i, target, w, hmap); addCatchEntry(entries, base, i, target, w, hmap); } else { // condOr @@ -304,8 +373,17 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { : wo.condOr.rightValue != null ? `${refLabel(wo.condOr.leftRef, hmap)} || ${wo.condOr.rightValue}` : refLabel(wo.condOr.leftRef, hmap); - entries.push({ id: `${base}/primary`, wireIndex: i, target, kind: "primary", bitIndex: -1, description: desc }); - addFallbackEntries(entries, base, i, target, wo.fallbacks, hmap); + entries.push({ + id: `${base}/primary`, + wireIndex: i, + target, + kind: "primary", + bitIndex: -1, + loc: primaryLoc(wo), + wireLoc: wo.loc, + description: desc, + }); + addFallbackEntries(entries, base, i, target, wo, hmap); addCatchEntry(entries, base, i, target, w, hmap); } } @@ -321,7 +399,7 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { entries.push({ id, // Use unique negative wireIndex per empty-array so they don't group together. - wireIndex: -(++emptyIdx), + wireIndex: -++emptyIdx, target, kind: "empty-array", bitIndex: -1, diff --git a/packages/bridge-core/src/execute-bridge.ts b/packages/bridge-core/src/execute-bridge.ts index a6fde106..ea570942 100644 --- a/packages/bridge-core/src/execute-bridge.ts +++ b/packages/bridge-core/src/execute-bridge.ts @@ -169,6 +169,10 @@ export async function executeBridge( try { data = await tree.run(input, options.requestedFields); } catch (err) { + if (err && typeof err === "object") { + (err as { executionTrace?: bigint }).executionTrace = + tree.getExecutionTrace(); + } throw attachBridgeErrorDocumentContext(err, doc); } diff --git a/packages/bridge-core/test/traversal-manifest-locations.test.ts b/packages/bridge-core/test/traversal-manifest-locations.test.ts new file mode 100644 index 00000000..837c7b0b --- /dev/null +++ b/packages/bridge-core/test/traversal-manifest-locations.test.ts @@ -0,0 +1,114 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { parseBridgeChevrotain } from "../../bridge-parser/src/index.ts"; +import { + buildTraversalManifest, + type Bridge, + type SourceLocation, + type TraversalEntry, + type Wire, +} from "../src/index.ts"; + +function getBridge(text: string): Bridge { + const document = parseBridgeChevrotain(text); + const bridge = document.instructions.find( + (instruction): instruction is Bridge => instruction.kind === "bridge", + ); + assert.ok(bridge, "expected a bridge instruction"); + return bridge; +} + +function assertLoc( + entry: TraversalEntry | undefined, + expected: SourceLocation | undefined, +): void { + assert.ok(entry, "expected traversal entry to exist"); + assert.deepEqual(entry.loc, expected); +} + +function isPullWire(wire: Wire): wire is Extract { + return "from" in wire; +} + +function isTernaryWire(wire: Wire): wire is Extract { + return "cond" in wire; +} + +describe("buildTraversalManifest source locations", () => { + it("maps pull, fallback, and catch entries to granular source spans", () => { + const bridge = getBridge(`version 1.5 +bridge Query.test { + with input as i + with output as o + alias i.empty.array.error catch i.empty.array.error as clean + o.message <- i.empty.array?.error ?? i.empty.array.error catch clean +}`); + + const pullWires = bridge.wires.filter(isPullWire); + const aliasWire = pullWires.find((wire) => wire.to.field === "clean"); + const messageWire = pullWires.find( + (wire) => wire.to.path.join(".") === "message", + ); + + assert.ok(aliasWire); + assert.ok(messageWire); + + const manifest = buildTraversalManifest(bridge); + assertLoc( + manifest.find((entry) => entry.id === "message/primary"), + messageWire.fromLoc, + ); + assertLoc( + manifest.find((entry) => entry.id === "message/fallback:0"), + messageWire.fallbacks?.[0]?.loc, + ); + assertLoc( + manifest.find((entry) => entry.id === "message/catch"), + messageWire.catchLoc, + ); + assertLoc( + manifest.find((entry) => entry.id === "clean/primary"), + aliasWire.fromLoc, + ); + assertLoc( + manifest.find((entry) => entry.id === "clean/catch"), + aliasWire.catchLoc, + ); + }); + + it("maps ternary branches to then/else spans", () => { + const bridge = getBridge(`version 1.5 +bridge Query.test { + with input as i + with output as o + o.name <- i.user ? i.user.name : "Anonymous" +}`); + + const ternaryWire = bridge.wires.find(isTernaryWire); + assert.ok(ternaryWire); + + const manifest = buildTraversalManifest(bridge); + assertLoc( + manifest.find((entry) => entry.id === "name/then"), + ternaryWire.thenLoc, + ); + assertLoc( + manifest.find((entry) => entry.id === "name/else"), + ternaryWire.elseLoc, + ); + }); + + it("maps constant entries to the wire span", () => { + const bridge = getBridge(`version 1.5 +bridge Query.test { + with output as o + o.name = "Ada" +}`); + + const manifest = buildTraversalManifest(bridge); + assertLoc( + manifest.find((entry) => entry.id === "name/const"), + bridge.wires[0]?.loc, + ); + }); +}); diff --git a/packages/bridge-parser/src/parser/parser.ts b/packages/bridge-parser/src/parser/parser.ts index 83398ab2..cdede099 100644 --- a/packages/bridge-parser/src/parser/parser.ts +++ b/packages/bridge-parser/src/parser/parser.ts @@ -2620,18 +2620,23 @@ function processElementScopeLines( const actualNode = pipeNodes.length > 0 ? pipeNodes[pipeNodes.length - 1]! : headNode; const { safe: spreadSafe } = extractAddressPath(actualNode); - wires.push({ - from: fromRef, - to: { - module: SELF_MODULE, - type: bridgeType, - field: bridgeField, - element: true, - path: spreadToPath, - }, - spread: true as const, - ...(spreadSafe ? { safe: true as const } : {}), - }); + wires.push( + withLoc( + { + from: fromRef, + to: { + module: SELF_MODULE, + type: bridgeType, + field: bridgeField, + element: true, + path: spreadToPath, + }, + spread: true as const, + ...(spreadSafe ? { safe: true as const } : {}), + }, + locFromNode(spreadLine), + ), + ); } processElementScopeLines( nestedScopeLines, @@ -2657,16 +2662,21 @@ function processElementScopeLines( // ── Constant wire: .field = value ── if (sc.scopeEquals) { const value = extractBareValue(sub(scopeLine, "scopeValue")!); - wires.push({ - value, - to: { - module: SELF_MODULE, - type: bridgeType, - field: bridgeField, - element: true, - path: elemToPath, - }, - }); + wires.push( + withLoc( + { + value, + to: { + module: SELF_MODULE, + type: bridgeType, + field: bridgeField, + element: true, + path: elemToPath, + }, + }, + scopeLineLoc, + ), + ); continue; } @@ -2895,24 +2905,29 @@ function processElementScopeLines( catchFallbackInternalWires = wires.splice(preLen); } } - wires.push({ - cond: condRef, - ...(condLoc ? { condLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - to: elemToRef, - }); + wires.push( + withLoc( + { + cond: condRef, + ...(condLoc ? { condLoc } : {}), + thenLoc: thenBranch.loc, + ...(thenBranch.kind === "ref" + ? { thenRef: thenBranch.ref } + : { thenValue: thenBranch.value }), + elseLoc: elseBranch.loc, + ...(elseBranch.kind === "ref" + ? { elseRef: elseBranch.ref } + : { elseValue: elseBranch.value }), + ...(fallbacks.length > 0 ? { fallbacks } : {}), + ...(catchLoc ? { catchLoc } : {}), + ...(catchFallback !== undefined ? { catchFallback } : {}), + ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), + ...(catchControl ? { catchControl } : {}), + to: elemToRef, + }, + scopeLineLoc, + ), + ); wires.push(...fallbackInternalWires); wires.push(...catchFallbackInternalWires); continue; @@ -2959,13 +2974,16 @@ function processElementScopeLines( const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0]; const wireAttrs = { ...(isPipe ? { pipe: true as const } : {}), + ...(condLoc ? { fromLoc: condLoc } : {}), ...(fallbacks.length > 0 ? { fallbacks } : {}), ...(catchLoc ? { catchLoc } : {}), ...(catchFallback !== undefined ? { catchFallback } : {}), ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), ...(catchControl ? { catchControl } : {}), }; - wires.push({ from: fromRef, to: elemToRef, ...wireAttrs }); + wires.push( + withLoc({ from: fromRef, to: elemToRef, ...wireAttrs }, scopeLineLoc), + ); wires.push(...fallbackInternalWires); wires.push(...catchFallbackInternalWires); } diff --git a/packages/bridge/test/source-locations.test.ts b/packages/bridge/test/source-locations.test.ts index cc820944..574c386d 100644 --- a/packages/bridge/test/source-locations.test.ts +++ b/packages/bridge/test/source-locations.test.ts @@ -106,4 +106,58 @@ bridge Query.test { assert.equal(messageWire.catchLoc?.startLine, 6); assert.equal(messageWire.catchLoc?.startColumn, 66); }); + + it("element scope wires in nested blocks carry source locations", () => { + const bridge = getBridge(`version 1.5 +bridge Query.test { + with input as i + with output as o + o.legs <- i.legs[] as s { + .destination { + .station { + .id <- s.arrival.station.id + .name <- s.arrival.station.name + } + .plannedTime <- s.arrival.arrival + .delayMinutes <- s.arrival.delay || 0 + } + } +}`); + + const destinationIdWire = bridge.wires.find( + (wire) => + "to" in wire && + wire.to.path.join(".") === "legs.destination.station.id", + ); + assertLoc(destinationIdWire, 8, 9); + assert.ok(destinationIdWire && "from" in destinationIdWire); + assert.equal(destinationIdWire.fromLoc?.startLine, 8); + assert.equal(destinationIdWire.fromLoc?.startColumn, 16); + + const destinationPlannedTimeWire = bridge.wires.find( + (wire) => + "to" in wire && + wire.to.path.join(".") === "legs.destination.plannedTime", + ); + assertLoc(destinationPlannedTimeWire, 11, 7); + assert.ok( + destinationPlannedTimeWire && "from" in destinationPlannedTimeWire, + ); + assert.equal(destinationPlannedTimeWire.fromLoc?.startLine, 11); + assert.equal(destinationPlannedTimeWire.fromLoc?.startColumn, 23); + + const destinationDelayWire = bridge.wires.find( + (wire) => + "to" in wire && + wire.to.path.join(".") === "legs.destination.delayMinutes", + ); + assert.ok( + destinationDelayWire && + "from" in destinationDelayWire && + "fallbacks" in destinationDelayWire, + ); + assertLoc(destinationDelayWire, 12, 7); + assert.equal(destinationDelayWire.fallbacks?.[0]?.loc?.startLine, 12); + assert.equal(destinationDelayWire.fallbacks?.[0]?.loc?.startColumn, 43); + }); }); diff --git a/packages/playground/src/Playground.tsx b/packages/playground/src/Playground.tsx index 7b4b4183..b158e95d 100644 --- a/packages/playground/src/Playground.tsx +++ b/packages/playground/src/Playground.tsx @@ -11,6 +11,7 @@ import { StandaloneQueryPanel } from "./components/StandaloneQueryPanel"; import { clearHttpCache } from "./engine"; import type { RunResult, BridgeOperation, OutputFieldNode } from "./engine"; import type { GraphQLSchema } from "graphql"; +import type { SourceLocation } from "@stackables/bridge"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { PlaygroundMode } from "./share"; @@ -22,8 +23,8 @@ function ResizeHandle({ direction }: { direction: "horizontal" | "vertical" }) { className={cn( "shrink-0 outline-none", direction === "horizontal" - ? "w-2 cursor-[col-resize]" - : "h-2 cursor-[row-resize]", + ? "w-2 cursor-col-resize" + : "h-2 cursor-row-resize", )} /> ); @@ -217,38 +218,47 @@ function QueryTabBar({ ); } -// ── bridge DSL panel header (label only) ───────────────────────────────────── +// ── bridge DSL header with optional trace badge ───────────────────────────── function BridgeDslHeader({ - dslTab, - onDslTabChange, + executionTrace, + onClearExecutionTrace, }: { - dslTab: "dsl" | "manifest"; - onDslTabChange: (t: "dsl" | "manifest") => void; + executionTrace?: bigint; + onClearExecutionTrace?: () => void; }) { + const hasTrace = executionTrace != null && executionTrace > 0n; return (
- - + + {hasTrace && ( + + trace-id 0x{executionTrace.toString(16)} + {onClearExecutionTrace && ( + + )} + + )}
); } @@ -298,160 +308,65 @@ function SchemaHeader({ ); } -// ── manifest view ───────────────────────────────────────────────────────────── - import { getTraversalManifest, decodeExecutionTrace } from "./engine"; -import type { TraversalEntry } from "./engine"; -/** Group entries by wireIndex for visual grouping. */ -type ManifestGroup = { - label: string; - entries: TraversalEntry[]; - hasAlternatives: boolean; -}; +function getInactiveTraversalLocations( + bridge: string, + operation: string, + executionTrace?: bigint, +): SourceLocation[] { + if (!operation || executionTrace == null || executionTrace === 0n) { + return []; + } + + const manifest = getTraversalManifest(bridge, operation); + if (manifest.length === 0) return []; -function buildGroups(manifest: TraversalEntry[]): ManifestGroup[] { - // Group by wireIndex; each unique wireIndex forms one group. - // Negative wireIndex values (empty-array entries) are already unique per scope. - const byKey = new Map(); - const order: number[] = []; - for (const e of manifest) { - const key = e.wireIndex; - let group = byKey.get(key); + const activeIds = new Set( + decodeExecutionTrace(manifest, executionTrace).map((entry) => entry.id), + ); + + // Group entries by wire (wireIndex). + const wireGroups = new Map(); + for (const entry of manifest) { + let group = wireGroups.get(entry.wireIndex); if (!group) { group = []; - byKey.set(key, group); - order.push(key); + wireGroups.set(entry.wireIndex, group); } - group.push(e); + group.push(entry); } - return order.map((key) => { - const entries = byKey.get(key)!; - const label = - entries[0].target.length > 0 ? entries[0].target.join(".") : "(root)"; - return { - label, - entries, - }; - }); -} -const kindColors: Record = { - primary: "bg-sky-900/50 text-sky-300 border-sky-700/50", - fallback: "bg-amber-900/50 text-amber-300 border-amber-700/50", - catch: "bg-red-900/50 text-red-300 border-red-700/50", - "empty-array": "bg-slate-700/50 text-slate-400 border-slate-600/50", - then: "bg-emerald-900/50 text-emerald-300 border-emerald-700/50", - else: "bg-orange-900/50 text-orange-300 border-orange-700/50", - const: "bg-violet-900/50 text-violet-300 border-violet-700/50", -}; - -function ManifestView({ - bridge, - operation, - executionTrace, - autoHeight = false, -}: { - bridge: string; - operation: string; - executionTrace?: bigint; - autoHeight?: boolean; -}) { - const manifest = useMemo( - () => getTraversalManifest(bridge, operation), - [bridge, operation], - ); - const activeIds = useMemo(() => { - if ( - executionTrace == null || - executionTrace === 0n || - manifest.length === 0 - ) - return new Set(); - const decoded = decodeExecutionTrace(manifest, executionTrace); - return new Set(decoded.map((e) => e.id)); - }, [manifest, executionTrace]); - - const groups = useMemo(() => buildGroups(manifest), [manifest]); - - if (!operation || manifest.length === 0) { - return ( -

- No bridge operation selected. -

- ); + const seen = new Set(); + const result: SourceLocation[] = []; + + for (const entries of wireGroups.values()) { + const allDead = entries.every((e) => !activeIds.has(e.id)); + + if (allDead) { + // When all branches of a wire are dead, use the full wire span + // so the entire line (including the target on the left of <-) is dimmed. + const wl = entries[0]?.wireLoc ?? entries[0]?.loc; + if (wl) { + const key = `${wl.startLine}:${wl.startColumn}:${wl.endLine}:${wl.endColumn}`; + if (!seen.has(key)) { + seen.add(key); + result.push(wl); + } + } + } else { + // Only some branches are dead — use individual entry locs. + for (const entry of entries) { + if (activeIds.has(entry.id) || !entry.loc) continue; + const key = `${entry.loc.startLine}:${entry.loc.startColumn}:${entry.loc.endLine}:${entry.loc.endColumn}`; + if (seen.has(key)) continue; + seen.add(key); + result.push(entry.loc); + } + } } - return ( -
-
- {groups.map((group) => ( -
- {/* Group header — always show target path */} -
- {group.label} -
- - {/* Entries */} -
- {group.entries.map((entry) => { - const isActive = activeIds.has(entry.id); - return ( -
- - {entry.kind} - - {/* Source description */} - {entry.description && ( - - {entry.description} - - )} - {isActive && ( - - on - - )} -
- ); - })} -
-
- ))} -
-
- ); + return result; } // ── panel wrapper ───────────────────────────────────────────────────────────── @@ -504,6 +419,7 @@ export type PlaygroundProps = { bridgeOperations: BridgeOperation[]; availableOutputFields: OutputFieldNode[]; hideGqlSwitch?: boolean; + onClearExecutionTrace?: () => void; }; export function Playground({ @@ -533,6 +449,7 @@ export function Playground({ bridgeOperations, availableOutputFields, hideGqlSwitch, + onClearExecutionTrace, }: PlaygroundProps) { const hLayout = useDefaultLayout({ id: "bridge-playground-h" }); const leftVLayout = useDefaultLayout({ id: "bridge-playground-left-v" }); @@ -540,7 +457,6 @@ export function Playground({ const activeQuery = queries.find((q) => q.id === activeTabId); const isStandalone = mode === "standalone"; - const [activeDslTab, setActiveDslTab] = useState<"dsl" | "manifest">("dsl"); // Determine which operation to use for manifest const manifestOperation = useMemo(() => { @@ -549,6 +465,16 @@ export function Playground({ return ""; }, [isStandalone, activeQuery?.operation, bridgeOperations]); + const inactiveTraversalLocations = useMemo( + () => + getInactiveTraversalLocations( + bridge, + manifestOperation, + displayResult?.executionTrace, + ), + [bridge, manifestOperation, displayResult?.executionTrace], + ); + return ( <> {/* ── Mobile layout: vertical scrollable stack ── */} @@ -585,27 +511,19 @@ export function Playground({ {/* Bridge DSL panel */}
- {activeDslTab === "dsl" ? ( - - ) : ( - - )} +
@@ -690,7 +608,6 @@ export function Playground({ loading={displayRunning} traces={displayResult?.traces} logs={displayResult?.logs} - executionTrace={displayResult?.executionTrace} onClearCache={clearHttpCache} autoHeight /> @@ -722,25 +639,18 @@ export function Playground({ )}
- {activeDslTab === "dsl" ? ( - - ) : ( - - )} +
@@ -777,25 +687,18 @@ export function Playground({
- {activeDslTab === "dsl" ? ( - - ) : ( - - )} +
@@ -889,7 +792,6 @@ export function Playground({ loading={displayRunning} traces={displayResult?.traces} logs={displayResult?.logs} - executionTrace={displayResult?.executionTrace} onClearCache={clearHttpCache} />
diff --git a/packages/playground/src/codemirror/dead-code.ts b/packages/playground/src/codemirror/dead-code.ts new file mode 100644 index 00000000..d428936b --- /dev/null +++ b/packages/playground/src/codemirror/dead-code.ts @@ -0,0 +1,59 @@ +import { RangeSetBuilder, type Extension } from "@codemirror/state"; +import { Decoration, EditorView } from "@codemirror/view"; +import type { SourceLocation } from "@stackables/bridge"; + +function toOffsets(state: EditorView["state"], loc: SourceLocation) { + if (state.doc.lines === 0) return null; + + const startLine = Math.min(Math.max(loc.startLine, 1), state.doc.lines); + const endLine = Math.min(Math.max(loc.endLine, startLine), state.doc.lines); + const startInfo = state.doc.line(startLine); + const endInfo = state.doc.line(endLine); + + const from = + startInfo.from + + Math.min(Math.max(loc.startColumn - 1, 0), startInfo.length); + let to = endInfo.from + Math.min(Math.max(loc.endColumn, 0), endInfo.length); + + if (to <= from) { + to = Math.min(from + 1, state.doc.length); + } + + if (to <= from) return null; + return { from, to }; +} + +export function deadCodeRangesExtension( + locations: SourceLocation[], +): Extension { + if (locations.length === 0) return []; + + const mark = Decoration.mark({ class: "cm-dead-code-range" }); + + return EditorView.decorations.compute([], (state) => { + // Convert to offsets and sort by start position. + const ranges: { from: number; to: number }[] = []; + for (const location of locations) { + const offsets = toOffsets(state, location); + if (offsets) ranges.push(offsets); + } + ranges.sort((a, b) => a.from - b.from || a.to - b.to); + + // Merge overlapping ranges to prevent stacking decorations. + const merged: { from: number; to: number }[] = []; + for (const r of ranges) { + const last = merged[merged.length - 1]; + if (last && r.from <= last.to) { + last.to = Math.max(last.to, r.to); + } else { + merged.push({ from: r.from, to: r.to }); + } + } + + const builder = new RangeSetBuilder(); + for (const { from, to } of merged) { + builder.add(from, to, mark); + } + return builder.finish(); + }); +} diff --git a/packages/playground/src/codemirror/theme.ts b/packages/playground/src/codemirror/theme.ts index 78958830..88678b64 100644 --- a/packages/playground/src/codemirror/theme.ts +++ b/packages/playground/src/codemirror/theme.ts @@ -20,27 +20,31 @@ import { tags } from "@lezer/highlight"; const theme = EditorView.theme( { "&": { - color: "#cbd5e1", // slate-300 + color: "#cbd5e1", // slate-300 backgroundColor: "transparent", fontSize: "13px", - fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", + fontFamily: + "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", }, ".cm-content": { - caretColor: "#38bdf8", // sky-400 + caretColor: "#38bdf8", // sky-400 lineHeight: "1.625", padding: "8px 0", }, "&.cm-focused .cm-cursor": { borderLeftColor: "#38bdf8" }, "&.cm-focused .cm-selectionBackground, .cm-selectionBackground": { - backgroundColor: "#334155", // slate-700 + backgroundColor: "#334155", // slate-700 }, "&.cm-focused": { outline: "none" }, ".cm-gutters": { backgroundColor: "transparent", borderRight: "none", - color: "#475569", // slate-600 + color: "#475569", // slate-600 + }, + ".cm-activeLineGutter": { + backgroundColor: "transparent", + color: "#64748b", }, - ".cm-activeLineGutter": { backgroundColor: "transparent", color: "#64748b" }, ".cm-activeLine": { backgroundColor: "rgba(51, 65, 85, 0.3)" }, ".cm-matchingBracket": { backgroundColor: "rgba(56, 189, 248, 0.15)", @@ -51,73 +55,78 @@ const theme = EditorView.theme( padding: "3px 6px 3px 8px", marginLeft: "-1px", fontSize: "12px", - fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", + fontFamily: + "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", }, ".cm-diagnostic-error": { - borderLeftColor: "#f87171", // red-400 - color: "#fca5a5", // red-300 + borderLeftColor: "#f87171", // red-400 + color: "#fca5a5", // red-300 backgroundColor: "#1e1215", }, ".cm-diagnostic-warning": { - borderLeftColor: "#facc15", // yellow-400 - color: "#fde68a", // amber-200 + borderLeftColor: "#facc15", // yellow-400 + color: "#fde68a", // amber-200 backgroundColor: "#1a1a0e", }, ".cm-lint-marker-error": { content: "'●'", color: "#f87171" }, ".cm-lint-marker-warning": { content: "'●'", color: "#facc15" }, ".cm-tooltip-lint": { - backgroundColor: "#0f172a", // slate-900 - border: "1px solid #334155", // slate-700 + backgroundColor: "#0f172a", // slate-900 + border: "1px solid #334155", // slate-700 }, // ── Autocomplete ───────────────────────────────────────────────── ".cm-tooltip-autocomplete": { - backgroundColor: "#0f172a", // slate-900 - border: "1px solid #334155", // slate-700 + backgroundColor: "#0f172a", // slate-900 + border: "1px solid #334155", // slate-700 }, ".cm-tooltip-autocomplete ul li": { - color: "#cbd5e1", // slate-300 + color: "#cbd5e1", // slate-300 }, ".cm-tooltip-autocomplete ul li[aria-selected]": { - backgroundColor: "#1e293b", // slate-800 - color: "#f1f5f9", // slate-100 + backgroundColor: "#1e293b", // slate-800 + color: "#f1f5f9", // slate-100 }, ".cm-completionLabel": { fontSize: "13px", }, ".cm-completionDetail": { - color: "#64748b", // slate-500 + color: "#64748b", // slate-500 fontStyle: "italic", }, + ".cm-dead-code-range": { + color: "#64748b", + opacity: "0.45", + }, }, { dark: true }, ); const highlights = HighlightStyle.define([ // Keywords: bridge, tool, with, const, define, version, as, from, on error - { tag: tags.keyword, color: "#38bdf8" }, // sky-400 + { tag: tags.keyword, color: "#38bdf8" }, // sky-400 // Types: Query, Mutation - { tag: tags.typeName, color: "#34d399" }, // emerald-400 + { tag: tags.typeName, color: "#34d399" }, // emerald-400 // Definitions: field names, tool names, handle aliases { tag: tags.definition(tags.variableName), color: "#fbbf24" }, // amber-400 // General variables / identifiers - { tag: tags.variableName, color: "#cbd5e1" }, // slate-300 + { tag: tags.variableName, color: "#cbd5e1" }, // slate-300 // Built-in handles: input, output, context { tag: tags.standard(tags.variableName), color: "#a78bfa" }, // violet-400 // Properties: .baseUrl, .headers.Authorization - { tag: tags.propertyName, color: "#fdba74" }, // orange-300 + { tag: tags.propertyName, color: "#fdba74" }, // orange-300 // Operators: <-, <-!, =, ||, ??, : - { tag: tags.operator, color: "#f472b6" }, // pink-400 + { tag: tags.operator, color: "#f472b6" }, // pink-400 // Atoms: true, false, null, GET, POST, … - { tag: tags.atom, color: "#c084fc" }, // purple-400 + { tag: tags.atom, color: "#c084fc" }, // purple-400 // Numbers - { tag: tags.number, color: "#fb923c" }, // orange-400 + { tag: tags.number, color: "#fb923c" }, // orange-400 // Strings - { tag: tags.string, color: "#4ade80" }, // green-400 + { tag: tags.string, color: "#4ade80" }, // green-400 { tag: tags.special(tags.string), color: "#86efac" }, // green-300 (url paths) // Comments - { tag: tags.comment, color: "#475569" }, // slate-600 + { tag: tags.comment, color: "#475569" }, // slate-600 // Brackets - { tag: tags.bracket, color: "#64748b" }, // slate-500 + { tag: tags.bracket, color: "#64748b" }, // slate-500 ]); export const playgroundTheme = [theme, syntaxHighlighting(highlights)]; diff --git a/packages/playground/src/components/Editor.tsx b/packages/playground/src/components/Editor.tsx index c59f78b1..3626ee8c 100644 --- a/packages/playground/src/components/Editor.tsx +++ b/packages/playground/src/components/Editor.tsx @@ -7,10 +7,12 @@ import { diagnosticCount, lintGutter } from "@codemirror/lint"; import { json } from "@codemirror/lang-json"; import { graphql, graphqlLanguageSupport, updateSchema } from "cm6-graphql"; import type { GraphQLSchema } from "graphql"; +import type { SourceLocation } from "@stackables/bridge"; import { Paintbrush } from "lucide-react"; import { bridgeLanguage } from "@/codemirror/bridge-lang"; import { bridgeLinter } from "@/codemirror/bridge-lint"; import { bridgeAutocomplete } from "@/codemirror/bridge-completion"; +import { deadCodeRangesExtension } from "@/codemirror/dead-code"; import { graphqlSchemaLinter } from "@/codemirror/graphql-schema-lint"; import { playgroundTheme } from "@/codemirror/theme"; import { cn } from "@/lib/utils"; @@ -43,6 +45,8 @@ type Props = { graphqlSchema?: GraphQLSchema; /** Optional callback to format the code. When provided, shows a format button. */ onFormat?: () => void; + /** Source ranges to render as dimmed dead code. */ + deadCodeLocations?: SourceLocation[]; }; function languageExtension( @@ -72,6 +76,7 @@ export function Editor({ autoHeight = false, graphqlSchema, onFormat, + deadCodeLocations = [], }: Props) { const containerRef = useRef(null); const viewRef = useRef(null); @@ -95,6 +100,7 @@ export function Editor({ // Compartment lets us toggle readOnly after creation (e.g. when result arrives) const readOnlyCompartment = useRef(new Compartment()); + const deadCodeCompartment = useRef(new Compartment()); // Create the editor once useEffect(() => { @@ -112,6 +118,9 @@ export function Editor({ EditorState.readOnly.of(readOnly), EditorView.editable.of(!readOnly), ]), + deadCodeCompartment.current.of( + deadCodeRangesExtension(deadCodeLocations), + ), ], }); @@ -144,6 +153,17 @@ export function Editor({ } }, [value]); + useEffect(() => { + const view = viewRef.current; + if (!view) return; + + view.dispatch({ + effects: deadCodeCompartment.current.reconfigure( + deadCodeRangesExtension(deadCodeLocations), + ), + }); + }, [deadCodeLocations]); + return (
{label && ( diff --git a/packages/playground/src/components/ResultView.tsx b/packages/playground/src/components/ResultView.tsx index b3408447..0362293d 100644 --- a/packages/playground/src/components/ResultView.tsx +++ b/packages/playground/src/components/ResultView.tsx @@ -15,7 +15,6 @@ type Props = { loading: boolean; traces?: ToolTrace[]; logs?: LogEntry[]; - executionTrace?: bigint; onClearCache?: () => void; /** When true the result view sizes itself to its content instead of filling the parent. */ autoHeight?: boolean; @@ -27,7 +26,6 @@ export function ResultView({ loading, traces, logs, - executionTrace, onClearCache, autoHeight = false, }: Props) { @@ -39,7 +37,6 @@ export function ResultView({ const hasTraces = traces && traces.length > 0; const hasLogs = logs && logs.length > 0; - const hasExecutionTrace = executionTrace != null && executionTrace > 0n; function toggle(panel: "traces" | "logs") { setActivePanel((v) => (v === panel ? null : panel)); @@ -95,17 +92,9 @@ export function ResultView({
{/* Badges row + expanded panel pinned to bottom */} - {(hasTraces || hasLogs || hasExecutionTrace || onClearCache) && ( + {(hasTraces || hasLogs || onClearCache) && (
- {hasExecutionTrace && ( - - trace 0x{executionTrace.toString(16)} - - )} {hasTraces && ( { + if (!displayQueryId) return; + setResults((prev) => { + const cur = prev[displayQueryId]; + if (!cur) return prev; + return { + ...prev, + [displayQueryId]: { ...cur, executionTrace: undefined }, + }; + }); + }, [displayQueryId]); + const activeQuery = queries.find((q) => q.id === activeTabId); const selectExample = useCallback( @@ -401,6 +413,7 @@ export function usePlaygroundState( hasErrors, isActiveRunning, onRun: handleRun, + onClearExecutionTrace: clearExecutionTrace, graphqlSchema, bridgeOperations, availableOutputFields, From 0ad7859c1617457b503cb3e9755f092f9acb97a2 Mon Sep 17 00:00:00 2001 From: Aarne Laur Date: Mon, 9 Mar 2026 12:33:36 +0100 Subject: [PATCH 11/11] Docs --- .../src/content/docs/advanced/trace-id.mdx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/docs-site/src/content/docs/advanced/trace-id.mdx diff --git a/packages/docs-site/src/content/docs/advanced/trace-id.mdx b/packages/docs-site/src/content/docs/advanced/trace-id.mdx new file mode 100644 index 00000000..86ed40c6 --- /dev/null +++ b/packages/docs-site/src/content/docs/advanced/trace-id.mdx @@ -0,0 +1,97 @@ +--- +title: Execution Trace IDs +description: Deterministic, zero-data representation of a bridge's execution path. +--- + +import { Aside } from "@astrojs/starlight/components"; + +Trace IDs provide a deterministic, zero-data representation of a bridge's execution path. Returned as a compact integer or hex string (e.g., `0x2a`), the ID encodes every control flow decision—such as fallbacks, ternaries, and error catches—made during a request. + +Because Trace IDs contain only topological data (which code branches executed) and zero runtime values, they are completely free of Personally Identifiable Information (PII) and are safe to log to external observability platforms. + +## Architecture & Mechanics + +Bridge utilizes a statically analyzable, pull-based graph. The Trace ID relies on the separation of a **Static Manifest** and a **Runtime Bitmask**. + +### 1. The Static Manifest + +At build time, the compiler evaluates the AST of a `.bridge` file and assigns a strict, zero-indexed integer to every possible branching path (e.g., `primary`, `fallback`, `catch`, `then`, `else`). + +For example, a ternary operation generates two distinct indices in the manifest: + +```bridge +o.price <- i.isPro ? i.proPrice : i.basicPrice +# Index 4: i.proPrice (then) +# Index 5: i.basicPrice (else) + +``` + +### 2. The Runtime Bitmask + +During execution, the `ExecutionTree` maintains a shared numeric mask initialized to `0`. As the engine resolves wires, it performs a bitwise `OR` operation to flip the bit corresponding to the executed path index. + +At the end of the request, the final integer is returned as the Trace ID. + +### 3. O(1) Array Coverage Masking + +For array iterations (`items[] as item`), the trace acts as a **Coverage Bitmask**. Instead of appending a new trace for every element, the engine simply records which paths were taken *at least once* across the entire array. This guarantees that processing an array of 10,000 items produces a Trace ID of the exact same byte size as an array of 1 item. + +--- + +## Runtime Usage + +The Trace ID is returned alongside your standard data payload from the `executeBridge` function. + +```typescript +import { executeBridge } from "@stackables/bridge-core"; + +const { data, traceId } = await executeBridge({ + bridge: Query.pricing, + input: { isPro: false } +}); + +// data: { tier: "basic", discount: 5, price: 9.99 } +// traceId: 42 (or "0x2a") + +// Safe to log without scrubbing PII +logger.info("Bridge executed", { + operation: "Query.pricing", + trace: traceId +}); + +``` + +### Telemetry Bucketing + +Because every `.bridge` file has a finite, mathematically bounded number of execution paths, Trace IDs serve as perfect bucketing keys in platforms like Datadog or Sentry. You can group logs by Trace ID to instantly monitor the distribution of "happy path" requests versus fallback/error paths. + +### API Reference: Decoding Traces + +If you are building custom tooling or UI visualizers, you can decode a trace programmatically using the core library: + +```typescript +import { + enumerateTraversalIds, + decodeExecutionTrace +} from "@stackables/bridge-core"; + +// 1. Generate the static map of all branches +const manifest = enumerateTraversalIds(bridgeAst); + +// 2. Decode the runtime trace against the manifest +const activePaths = decodeExecutionTrace(manifest, 42); + +console.log(activePaths); +/* Returns: +[ + { target: ["tier"], kind: "else", label: '"basic"' }, + { target: ["discount"], kind: "else", label: '5' }, + ... +] +*/ + +``` + +