Overview

1. Web‑based Corridor Viewer (React + D3)#

Goal: Load a .ctf, show the corridor as a living object: steps, Q‑metrics, status, rewinds.

1.1. High‑level layout#

  • Left: step list + status badges
  • Center: Q‑metric waveform (D3)
  • Right: step details + lineage mini‑graph
// components/CorridorViewer.tsx
import { useState } from "react";
import type { CorridorTrace, CorridorStepEvent } from "@/types/corridor";
import { QMetricWaveformPanel } from "./QMetricWaveformPanel";
import { CorridorTimeline } from "./CorridorTimeline";
import { StepDetails } from "./StepDetails";
 
export function CorridorViewer({ trace }: { trace: CorridorTrace }) {
  const [selectedStep, setSelectedStep] = useState<number | null>(null);
 
  return (
    <div className="grid grid-cols-3 gap-4 h-full">
      <CorridorTimeline
        steps={trace.steps}
        statuses={trace.statuses}
        selectedStep={selectedStep}
        onSelectStep={setSelectedStep}
      />
      <div className="flex flex-col gap-4 col-span-2">
        <QMetricWaveformPanel qHistory={trace.qHistory} />
        <StepDetails
          steps={trace.steps}
          qHistory={trace.qHistory}
          selectedStep={selectedStep}
        />
      </div>
    </div>
  );
}

2. Corridor Trace Inspector (timeline panel)#

Think “VSCode timeline” for a single corridor: events stacked over time.

2.1. Event timeline component#

// components/CorridorTimeline.tsx
import type {
  CorridorStepEvent,
  CorridorStatusEvent,
} from "@/types/corridor";
 
type Props = {
  steps: CorridorStepEvent[];
  statuses: CorridorStatusEvent[];
  selectedStep: number | null;
  onSelectStep: (id: number) => void;
};
 
export function CorridorTimeline({
  steps,
  statuses,
  selectedStep,
  onSelectStep,
}: Props) {
  const statusByStep = new Map(
    statuses.map((s) => [s.step_id, s.corridor_status])
  );
 
  return (
    <section className="border rounded p-2 overflow-y-auto">
      <h2 className="text-xs font-semibold mb-2">Corridor Timeline</h2>
      <ul className="space-y-1 text-xs">
        {steps.map((s) => {
          const status = statusByStep.get(s.step_id) ?? "stable";
          const isSelected = selectedStep === s.step_id;
          return (
            <li
              key={s.step_id}
              onClick={() => onSelectStep(s.step_id)}
              className={`cursor-pointer rounded px-2 py-1 flex justify-between ${
                isSelected ? "bg-blue-100" : "hover:bg-gray-100"
              }`}
            >
              <div>
                <div className="font-mono">
                  #{s.step_id} → {s.action}
                </div>
                <div className="text-gray-500">
                  tool: {s.tool ?? "n/a"} · {s.latency_ms ?? "?"} ms
                </div>
              </div>
              <span
                className={`text-[10px] px-1 py-0.5 rounded ${
                  status === "stable"
                    ? "bg-green-100 text-green-700"
                    : status === "warning"
                    ? "bg-yellow-100 text-yellow-700"
                    : "bg-red-100 text-red-700"
                }`}
              >
                {status}
              </span>
            </li>
          );
        })}
      </ul>
    </section>
  );
}

This is our “timeline panel”: scrollable, clickable, status‑aware.


3. Corridor Diff Tool (compare two .ctf files)#

Goal: Show where two runs diverge—by step, Q‑metric, or status.

3.1. Diff model#

// lib/corridorDiff.ts
import type { CorridorTrace } from "@/types/corridor";
 
export type CorridorDiffRow = {
  step: number;
  statusA?: string;
  statusB?: string;
  driftDelta?: number;
  entropyDelta?: number;
};
 
export function diffCorridors(a: CorridorTrace, b: CorridorTrace): CorridorDiffRow[] {
  const maxStep = Math.max(
    a.qHistory.length ? a.qHistory[a.qHistory.length - 1].step_id : 0,
    b.qHistory.length ? b.qHistory[b.qHistory.length - 1].step_id : 0
  );
 
  const qByStepA = new Map(a.qHistory.map((q) => [q.step_id, q]));
  const qByStepB = new Map(b.qHistory.map((q) => [q.step_id, q]));
 
  const statusByStepA = new Map(a.statuses.map((s) => [s.step_id, s.corridor_status]));
  const statusByStepB = new Map(b.statuses.map((s) => [s.step_id, s.corridor_status]));
 
  const rows: CorridorDiffRow[] = [];
  for (let step = 0; step <= maxStep; step++) {
    const qa = qByStepA.get(step);
    const qb = qByStepB.get(step);
    if (!qa && !qb) continue;
 
    rows.push({
      step,
      statusA: statusByStepA.get(step),
      statusB: statusByStepB.get(step),
      driftDelta:
        qa && qb ? qb.semantic_drift - qa.semantic_drift : undefined,
      entropyDelta:
        qa && qb ? qb.tool_entropy - qa.tool_entropy : undefined,
    });
  }
  return rows;
}

3.2. Diff view#

// components/CorridorDiffView.tsx
import type { CorridorTrace } from "@/types/corridor";
import { diffCorridors } from "@/lib/corridorDiff";
 
export function CorridorDiffView({
  left,
  right,
}: {
  left: CorridorTrace;
  right: CorridorTrace;
}) {
  const rows = diffCorridors(left, right);
 
  return (
    <div className="border rounded p-2 text-xs">
      <h2 className="font-semibold mb-2">Corridor Diff</h2>
      <table className="w-full border-collapse">
        <thead>
          <tr className="border-b">
            <th className="text-left">Step</th>
            <th>A status</th>
            <th>B status</th>
            <th>Δ drift</th>
            <th>Δ entropy</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((r) => (
            <tr key={r.step} className="border-b">
              <td>#{r.step}</td>
              <td>{r.statusA ?? "-"}</td>
              <td>{r.statusB ?? "-"}</td>
              <td>{r.driftDelta?.toFixed(3) ?? "-"}</td>
              <td>{r.entropyDelta?.toFixed(3) ?? "-"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Now we can put two runs side‑by‑side and see where they structurally diverge.


4. VCD‑style trace viewer for agent corridors#

Goal: Treat Q‑metrics and status like digital signals over time.

We’ll render:

  • X: step index
  • Y: stacked “tracks”:
    • semantic_drift (analog line)
    • tool_entropy (analog line)
    • corridor_status (discrete bands: stable/warning/halted)

4.1. VCD‑style viewer component#

// components/CorridorVCDView.tsx
"use client";
 
import * as d3 from "d3";
import { useEffect, useRef } from "react";
import type { CorridorQEvent, CorridorStatusEvent } from "@/types/corridor";
 
type Props = {
  qHistory: CorridorQEvent[];
  statuses: CorridorStatusEvent[];
};
 
export function CorridorVCDView({ qHistory, statuses }: Props) {
  const ref = useRef<SVGSVGElement | null>(null);
 
  useEffect(() => {
    if (!ref.current || qHistory.length === 0) return;
 
    const svg = d3.select(ref.current);
    const width = Number(svg.attr("width")) || 800;
    const height = Number(svg.attr("height")) || 260;
    svg.selectAll("*").remove();
 
    const steps = qHistory.map((q) => q.step_id);
    const x = d3
      .scaleLinear()
      .domain(d3.extent(steps) as [number, number])
      .range([40, width - 10]);
 
    const yAnalog = d3.scaleLinear().domain([0, 1]).range([100, 20]);
    const yStatusTop = 140;
    const yStatusBottom = 240;
 
    const statusByStep = new Map(
      statuses.map((s) => [s.step_id, s.corridor_status])
    );
 
    // axes
    const xAxis = d3.axisBottom(x).ticks(8).tickFormat((d) => `#${d}`);
    svg
      .append("g")
      .attr("transform", `translate(0,${yStatusBottom})`)
      .call(xAxis as any)
      .selectAll("text")
      .style("font-size", "9px");
 
    const yAxis = d3.axisLeft(yAnalog).ticks(4);
    svg
      .append("g")
      .attr("transform", `translate(40,0)`)
      .call(yAxis as any)
      .selectAll("text")
      .style("font-size", "9px");
 
    // semantic_drift line
    const driftLine = d3
      .line<CorridorQEvent>()
      .x((d) => x(d.step_id))
      .y((d) => yAnalog(d.semantic_drift))
      .curve(d3.curveMonotoneX);
 
    svg
      .append("path")
      .datum(qHistory)
      .attr("fill", "none")
      .attr("stroke", "#1f77b4")
      .attr("stroke-width", 1.5)
      .attr("d", driftLine as any);
 
    // tool_entropy line
    const maxEntropy = d3.max(qHistory, (d) => d.tool_entropy) || 1;
    const entropyScale = d3.scaleLinear().domain([0, maxEntropy]).range([100, 20]);
 
    const entropyLine = d3
      .line<CorridorQEvent>()
      .x((d) => x(d.step_id))
      .y((d) => entropyScale(d.tool_entropy))
      .curve(d3.curveMonotoneX);
 
    svg
      .append("path")
      .datum(qHistory)
      .attr("fill", "none")
      .attr("stroke", "#ff7f0e")
      .attr("stroke-width", 1.2)
      .attr("d", entropyLine as any);
 
    // status bands (VCD‑style)
    const bandHeight = (yStatusBottom - yStatusTop) / 3;
    const statusColor = (s: string) =>
      s === "stable"
        ? "#e5f9e7"
        : s === "warning"
        ? "#fff7d6"
        : "#fde2e1";
 
    steps.forEach((step, i) => {
      const status = statusByStep.get(step) ?? "stable";
      const x0 = x(step);
      const x1 = i + 1 < steps.length ? x(steps[i + 1]) : x(step) + 8;
 
      svg
        .append("rect")
        .attr("x", x0)
        .attr("y", yStatusTop)
        .attr("width", x1 - x0)
        .attr("height", bandHeight)
        .attr("fill", statusColor(status))
        .attr("stroke", "#ddd")
        .attr("stroke-width", 0.5);
    });
 
    svg
      .append("text")
      .attr("x", 45)
      .attr("y", yStatusTop - 4)
      .text("corridor_status")
      .style("font-size", "9px")
      .style("fill", "#555");
  }, [qHistory, statuses]);
 
  return <svg ref={ref} width={800} height={260} className="border rounded" />;
}

This gives us a VCD‑like panel:

  • top: analog “signals” (drift, entropy)
  • bottom: discrete status bands over time

We now have, in blueprint form:

  • a web‑based Corridor Viewer (React shell)
  • a Trace Inspector (timeline panel)
  • a Diff Tool (structural comparison of two runs)
  • a VCD‑style trace viewer (Q + status as signals)