Route Transcripts: Deep Dive #72
Principal React Architect
Choreography gives you a routine. Transcripts give you evidence.
If choreography answers “what order do we do work in?”, transcripts answer “what proof do we have that we did it?”
In this system, a transcript row is not a console log. It’s a durable record of a routing decision that is:
- rendered into the DOM (the UI is the debugger),
- readable by humans (causes stay boring and searchable),
- and assertable in tests (DOM-first truth, not mock-first vibes).
We’ll extend the Invoice Explorer from #71 and make the routing routine provable.
Required docs links:
Extra docs links for this entry:
The transcript contract (what must always be true)
- Every row has
requestedandgranted. - Every row has a human-meaningful
cause. - Every row has a stable
stepId. - Rows are rendered with stable
data-*attributes. - The transcript is append-only (it’s a timeline, not a snapshot).
If you adopt this contract, a huge class of routing and test bugs disappears because your “truth surface” is now the DOM.
Worked Example: transcript writer + panel + harness assertions
We’ll extend the Invoice Explorer from #71 with:
- a transcript schema,
- a writer that enforces pairing,
- a panel renderer,
- and a minimal harness helper.
Step 1: Schema (stable and boring by design)
export type TranscriptSurface = "shell" | "route" | "boundary";
export type RouteTranscriptRow = {
id: string;
at: number;
intentId: string;
routeId: string;
track: "summary" | "detail";
stepId: string;
requested: string;
granted: string;
cause: string;
surface: TranscriptSurface;
meta?: Record<string, string | number | boolean>;
};
Two details matter here:
intentIdlets you group rows per transition.stepIdlets you assert ordering.
Step 2: Writer (append-only, ledger-backed)
import { nanoid } from "react";
export function createRouteTranscript(shell: any) {
function write(row: Omit<RouteTranscriptRow, "id" | "at">) {
const next: RouteTranscriptRow = { id: nanoid(), at: Date.now(), ...row };
shell.write("transcript", [...shell.read("transcript"), next]);
return next.id;
}
function clear() {
shell.write("transcript", []);
}
return { write, clear };
}
Step 3: Request/grant helpers (make pairing the default)
Instead of “remember to write a transcript row”, we wrap the actions and make transcript writing the default behavior.
export function transcriptRequest(args: {
transcript: ReturnType<typeof createRouteTranscript>;
intentId: string;
routeId: string;
track: "summary" | "detail";
stepId: string;
requested: string;
cause: string;
surface: TranscriptSurface;
meta?: RouteTranscriptRow["meta"];
}) {
args.transcript.write({
intentId: args.intentId,
routeId: args.routeId,
track: args.track,
stepId: args.stepId,
requested: args.requested,
granted: "pending",
cause: args.cause,
surface: args.surface,
meta: args.meta,
});
}
export function transcriptGrant(args: {
transcript: ReturnType<typeof createRouteTranscript>;
intentId: string;
routeId: string;
track: "summary" | "detail";
stepId: string;
granted: string;
cause: string;
surface: TranscriptSurface;
meta?: RouteTranscriptRow["meta"];
}) {
args.transcript.write({
intentId: args.intentId,
routeId: args.routeId,
track: args.track,
stepId: args.stepId,
requested: "pending",
granted: args.granted,
cause: args.cause,
surface: args.surface,
meta: args.meta,
});
}
Step 4: Panel renderer (your canonical inspection surface)
export function RouteTranscriptPanel({ shell }: { shell: any }) {
const rows = shell.read("transcript") as RouteTranscriptRow[];
return (
<section data-panel="route-transcript">
<header>
<h2>Route Transcript</h2>
<p className="text-sm text-[color:var(--docs-muted)]">
{rows.length} rows
</p>
</header>
<ol>
{rows.map((r) => (
<li
key={r.id}
data-intent={r.intentId}
data-step={r.stepId}
data-route={r.routeId}
data-track={r.track}
data-surface={r.surface}
>
<strong>{r.stepId}</strong>{" "}
<span>
req:{r.requested} / grant:{r.granted}
</span>{" "}
<em>cause:{r.cause}</em>
</li>
))}
</ol>
<pre data-transcript-json>{JSON.stringify(rows, null, 2)}</pre>
</section>
);
}
Step 5: Wire transcripts into the shell routine
import { useFlowEffect } from "react";
export function InvoiceExplorerShellWithTranscript() {
const shell = useInvoiceShell();
const transcript = createRouteTranscript(shell);
const intent = shell.read("activeIntent");
useFlowEffect(async () => {
if (!intent) return;
transcript.clear();
const intentId = intent.intentId;
const track = intent.requestedTrack;
transcriptRequest({
transcript,
intentId,
routeId: intent.routeId,
track,
stepId: "seed-shell",
requested: "seed shell surfaces",
cause: intent.cause,
surface: "shell",
});
// ...seed...
transcriptGrant({
transcript,
intentId,
routeId: intent.routeId,
track,
stepId: "seed-shell",
granted: "granted",
cause: "shell:seeded",
surface: "shell",
});
// Repeat for hydrate-draft / grant-track / commit.
}, [intent?.intentId]);
return (
<section data-shell="invoice-transcript">
<RouteTranscriptPanel shell={shell} />
</section>
);
}
Step 6: Harness helper (DOM-driven assertions)
export function assertTranscriptSteps(root: HTMLElement, expectedSteps: string[]) {
const items = [...root.querySelectorAll('[data-panel=\"route-transcript\"] li')] as HTMLElement[];
const actual = items.map((el) => el.dataset.step);
expectedSteps.forEach((step, idx) => {
if (actual[idx] !== step) {
throw new Error(`expected step ${idx} = ${step}, got ${actual[idx]}`);
}
});
}
Worked Example walkthrough (why this is testable)
With the contract in place, debugging and testing become the same activity:
- A click publishes an intent (from #71).
- The shell requests a step (writes a
requestedrow). - The shell grants that step (writes a
grantedrow). - The panel renders both rows as stable DOM.
- The harness reads the DOM and asserts the exact same sequence a human would inspect.
That’s why transcripts are not "logging": they’re the canonical surface for routing truth.
Optional: slicing transcripts by intent (keep the UI small)
Once a screen is busy, you don’t want an infinite transcript wall. Slice by intentId and render only the active intent by default.
export function sliceTranscript(rows: RouteTranscriptRow[], intentId: string) {
return rows.filter((r) => r.intentId === intentId);
}
export function summarizeTranscript(rows: RouteTranscriptRow[]) {
const failed = rows.filter((r) => r.granted === "failed");
return {
steps: rows.map((r) => r.stepId),
failures: failed.map((r) => ({ stepId: r.stepId, cause: r.cause })),
};
}
Cause strings (make them boring and searchable)
Pick a small vocabulary and reuse it:
nav:*for user navigationshell:*for policy decisionsboundary:*for failure and recovery
When you keep causes consistent, transcripts become searchable artifacts - not just runtime logs.
Checklist
- Schema includes
intentIdandstepId. - Render transcript rows into the DOM.
- Make request/grant pairing the default.
- Assert behavior by reading transcript DOM.