Routing
Aug 22, 2023198 min read

Route Transcripts: Deep Dive #72

AV
Ariel Voss

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)

  1. Every row has requested and granted.
  2. Every row has a human-meaningful cause.
  3. Every row has a stable stepId.
  4. Rows are rendered with stable data-* attributes.
  5. 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:

  • intentId lets you group rows per transition.
  • stepId lets 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:

  1. A click publishes an intent (from #71).
  2. The shell requests a step (writes a requested row).
  3. The shell grants that step (writes a granted row).
  4. The panel renders both rows as stable DOM.
  5. 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 navigation
  • shell:* for policy decisions
  • boundary:* for failure and recovery

When you keep causes consistent, transcripts become searchable artifacts - not just runtime logs.

Checklist

  • Schema includes intentId and stepId.
  • Render transcript rows into the DOM.
  • Make request/grant pairing the default.
  • Assert behavior by reading transcript DOM.