feat(workbench-ui): wire Trace practice evidence tab
This commit is contained in:
parent
d3df22e24a
commit
a15a6f30e6
4 changed files with 328 additions and 0 deletions
|
|
@ -42,6 +42,8 @@ import type {
|
||||||
HealthStatus,
|
HealthStatus,
|
||||||
} from "../types/api";
|
} from "../types/api";
|
||||||
import type { ConstructionEvidence } from "../types/constructionEvidence";
|
import type { ConstructionEvidence } from "../types/constructionEvidence";
|
||||||
|
import type { PracticeEvidence } from "../types/practiceEvidence";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class WorkbenchApiError extends Error {
|
export class WorkbenchApiError extends Error {
|
||||||
|
|
@ -223,6 +225,13 @@ export async function fetchTraceConstruction(turnId: number): Promise<Constructi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchTracePractice(turnId: number): Promise<PracticeEvidence> {
|
||||||
|
return apiFetch<PracticeEvidence>(
|
||||||
|
`/trace/${encodeURIComponent(String(turnId))}/practice`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function fetchTour(): Promise<DeterminismTour> {
|
export async function fetchTour(): Promise<DeterminismTour> {
|
||||||
return apiFetch<DeterminismTour>("/tour");
|
return apiFetch<DeterminismTour>("/tour");
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import {
|
||||||
fetchTraceField,
|
fetchTraceField,
|
||||||
fetchTraceBundle,
|
fetchTraceBundle,
|
||||||
fetchTraceConstruction,
|
fetchTraceConstruction,
|
||||||
|
fetchTracePractice,
|
||||||
fetchTour,
|
fetchTour,
|
||||||
fetchLivedLife,
|
fetchLivedLife,
|
||||||
fetchTraceTurns,
|
fetchTraceTurns,
|
||||||
|
|
@ -92,6 +93,8 @@ import type {
|
||||||
MathRatifyResult,
|
MathRatifyResult,
|
||||||
} from "../types/api";
|
} from "../types/api";
|
||||||
import type { ConstructionEvidence } from "../types/constructionEvidence";
|
import type { ConstructionEvidence } from "../types/constructionEvidence";
|
||||||
|
import type { PracticeEvidence } from "../types/practiceEvidence";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export { QueryClientProvider };
|
export { QueryClientProvider };
|
||||||
|
|
@ -276,6 +279,17 @@ export function useTraceConstruction(turnId?: number | null) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTracePractice(turnId?: number | null) {
|
||||||
|
return useQuery<PracticeEvidence, WorkbenchApiError>({
|
||||||
|
queryKey: ["api", "trace", "practice", turnId ?? null],
|
||||||
|
queryFn: () => fetchTracePractice(turnId as number),
|
||||||
|
enabled: typeof turnId === "number",
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function useTour() {
|
export function useTour() {
|
||||||
return useQuery<DeterminismTour, WorkbenchApiError>({
|
return useQuery<DeterminismTour, WorkbenchApiError>({
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,11 @@ import {
|
||||||
useTraceTurn,
|
useTraceTurn,
|
||||||
useTraceTurns,
|
useTraceTurns,
|
||||||
useTraceConstruction,
|
useTraceConstruction,
|
||||||
|
useTracePractice,
|
||||||
} from "../../api/queries";
|
} from "../../api/queries";
|
||||||
import type { ConstructionEvidence } from "../../types/constructionEvidence";
|
import type { ConstructionEvidence } from "../../types/constructionEvidence";
|
||||||
|
import type { PracticeEvidence } from "../../types/practiceEvidence";
|
||||||
|
import { PracticeEvidencePanel } from "./PracticeEvidencePanel";
|
||||||
import {
|
import {
|
||||||
CONSTRUCTION_AUTHORITY_DISCLOSURES,
|
CONSTRUCTION_AUTHORITY_DISCLOSURES,
|
||||||
assessmentBlockerSummary,
|
assessmentBlockerSummary,
|
||||||
|
|
@ -59,6 +62,7 @@ const TRACE_TABS: readonly Tab[] = [
|
||||||
{ id: "field", label: "Field" },
|
{ id: "field", label: "Field" },
|
||||||
{ id: "bundle", label: "Bundle" },
|
{ id: "bundle", label: "Bundle" },
|
||||||
{ id: "construction", label: "Construction" },
|
{ id: "construction", label: "Construction" },
|
||||||
|
{ id: "practice", label: "Practice" },
|
||||||
{ id: "surfaces", label: "Surfaces" },
|
{ id: "surfaces", label: "Surfaces" },
|
||||||
{ id: "grounding", label: "Grounding" },
|
{ id: "grounding", label: "Grounding" },
|
||||||
{ id: "verdicts", label: "Verdicts" },
|
{ id: "verdicts", label: "Verdicts" },
|
||||||
|
|
@ -855,6 +859,9 @@ function TraceDetail({
|
||||||
constructionEvidence,
|
constructionEvidence,
|
||||||
constructionLoading,
|
constructionLoading,
|
||||||
constructionError,
|
constructionError,
|
||||||
|
practiceEvidence,
|
||||||
|
practiceLoading,
|
||||||
|
practiceError,
|
||||||
}: {
|
}: {
|
||||||
turn: TurnJournalEntry;
|
turn: TurnJournalEntry;
|
||||||
pipelineRecord?: CognitivePipelineRecord | null;
|
pipelineRecord?: CognitivePipelineRecord | null;
|
||||||
|
|
@ -869,6 +876,9 @@ function TraceDetail({
|
||||||
constructionEvidence?: ConstructionEvidence | null;
|
constructionEvidence?: ConstructionEvidence | null;
|
||||||
constructionLoading: boolean;
|
constructionLoading: boolean;
|
||||||
constructionError: unknown;
|
constructionError: unknown;
|
||||||
|
practiceEvidence?: PracticeEvidence | null;
|
||||||
|
practiceLoading: boolean;
|
||||||
|
practiceError: unknown;
|
||||||
}) {
|
}) {
|
||||||
const [activeTab, setActiveTab] = useState("pipeline");
|
const [activeTab, setActiveTab] = useState("pipeline");
|
||||||
return (
|
return (
|
||||||
|
|
@ -915,6 +925,15 @@ function TraceDetail({
|
||||||
turnId={turn.turn_id}
|
turnId={turn.turn_id}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{activeTab === "practice" ? (
|
||||||
|
<PracticeEvidencePanel
|
||||||
|
evidence={practiceEvidence}
|
||||||
|
isLoading={practiceLoading}
|
||||||
|
error={practiceError}
|
||||||
|
turnId={turn.turn_id}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{activeTab === "surfaces" ? <SurfacesTab turn={turn} /> : null}
|
{activeTab === "surfaces" ? <SurfacesTab turn={turn} /> : null}
|
||||||
{activeTab === "grounding" ? <GroundingTab turn={turn} /> : null}
|
{activeTab === "grounding" ? <GroundingTab turn={turn} /> : null}
|
||||||
{activeTab === "verdicts" ? <VerdictsTab turn={turn} /> : null}
|
{activeTab === "verdicts" ? <VerdictsTab turn={turn} /> : null}
|
||||||
|
|
@ -938,6 +957,7 @@ export function TraceRoute() {
|
||||||
const fieldQuery = useTraceField(selectedTurnId);
|
const fieldQuery = useTraceField(selectedTurnId);
|
||||||
const bundleQuery = useTraceBundle(selectedTurnId);
|
const bundleQuery = useTraceBundle(selectedTurnId);
|
||||||
const constructionQuery = useTraceConstruction(selectedTurnId);
|
const constructionQuery = useTraceConstruction(selectedTurnId);
|
||||||
|
const practiceQuery = useTracePractice(selectedTurnId);
|
||||||
|
|
||||||
const turns = turnsQuery.data ?? [];
|
const turns = turnsQuery.data ?? [];
|
||||||
const filteredTurns = useMemo(() => {
|
const filteredTurns = useMemo(() => {
|
||||||
|
|
@ -1057,6 +1077,9 @@ export function TraceRoute() {
|
||||||
constructionEvidence={constructionQuery.data}
|
constructionEvidence={constructionQuery.data}
|
||||||
constructionLoading={constructionQuery.isLoading}
|
constructionLoading={constructionQuery.isLoading}
|
||||||
constructionError={constructionQuery.isError ? constructionQuery.error : null}
|
constructionError={constructionQuery.isError ? constructionQuery.error : null}
|
||||||
|
practiceEvidence={practiceQuery.data}
|
||||||
|
practiceLoading={practiceQuery.isLoading}
|
||||||
|
practiceError={practiceQuery.isError ? practiceQuery.error : null}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
282
workbench-ui/src/app/trace/TraceRoutePractice.test.tsx
Normal file
282
workbench-ui/src/app/trace/TraceRoutePractice.test.tsx
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { createTestQueryClient } from "../../test/createTestQueryClient";
|
||||||
|
import { EvidenceProvider } from "../evidenceContext";
|
||||||
|
import { TraceRoute } from "./TraceRoute";
|
||||||
|
|
||||||
|
function jsonOk(data: unknown) {
|
||||||
|
return Promise.resolve({
|
||||||
|
json: () => Promise.resolve({ ok: true, generated_at: "now", data }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceSummary(turnId: number) {
|
||||||
|
return {
|
||||||
|
turn_id: turnId,
|
||||||
|
timestamp: "2026-06-23T20:00:00Z",
|
||||||
|
prompt_excerpt: "Lena has 3 marbles.",
|
||||||
|
surface_excerpt: "Trace surface",
|
||||||
|
trace_hash: "sha256:777777777777abcdef",
|
||||||
|
grounding_source: "pack",
|
||||||
|
trace_integrity: "pipeline_trace",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceEntry(turnId: number) {
|
||||||
|
const summary = traceSummary(turnId);
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
prompt: "Lena has 3 marbles.",
|
||||||
|
surface: "Trace surface",
|
||||||
|
articulation_surface: "Trace articulation",
|
||||||
|
walk_surface: "Trace walk",
|
||||||
|
epistemic_state: "evidenced",
|
||||||
|
normative_clearance: "cleared",
|
||||||
|
verdicts: {
|
||||||
|
identity: { outcome: "cleared", runtime_detail: "identity ok" },
|
||||||
|
safety: { outcome: "cleared", runtime_detail: "safety ok" },
|
||||||
|
ethics: { outcome: "cleared", runtime_detail: "ethics ok" },
|
||||||
|
},
|
||||||
|
refusal_emitted: false,
|
||||||
|
hedge_injected: false,
|
||||||
|
proposal_candidates: [],
|
||||||
|
turn_cost_ms: 12,
|
||||||
|
checkpoint_emitted: true,
|
||||||
|
journal_digest: "sha256:journal777abcdef",
|
||||||
|
pipeline_record: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pipelineRecord() {
|
||||||
|
return {
|
||||||
|
schema_version: "cognitive_pipeline_record_v1",
|
||||||
|
status: "missing_evidence",
|
||||||
|
missing_reason: "pipeline_record_not_persisted",
|
||||||
|
trace_hash: "sha256:777777777777abcdef",
|
||||||
|
versor_condition: null,
|
||||||
|
field_digest: null,
|
||||||
|
stages: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldEvidence() {
|
||||||
|
return {
|
||||||
|
schema_version: "field_evidence_v1",
|
||||||
|
status: "missing_evidence",
|
||||||
|
missing_reason: "field evidence was not persisted for this turn",
|
||||||
|
trace_hash: "sha256:777777777777abcdef",
|
||||||
|
versor_condition: null,
|
||||||
|
versor_condition_ceiling: 0.000001,
|
||||||
|
field_valid: null,
|
||||||
|
field_digest: null,
|
||||||
|
parent_field_digest: null,
|
||||||
|
transition_inner_product: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function evidenceBundle(turnId: number) {
|
||||||
|
return {
|
||||||
|
schema_version: "evidence_bundle_v1",
|
||||||
|
turn_id: turnId,
|
||||||
|
generated_from: "turn_journal",
|
||||||
|
trace_hash: "sha256:777777777777abcdef",
|
||||||
|
trace_integrity: "pipeline_trace",
|
||||||
|
prompt: "Lena has 3 marbles.",
|
||||||
|
surface: "Trace surface",
|
||||||
|
grounding_source: "pack",
|
||||||
|
epistemic_state: "evidenced",
|
||||||
|
normative_clearance: "cleared",
|
||||||
|
refusal_emitted: false,
|
||||||
|
journal_digest: "sha256:journal777abcdef",
|
||||||
|
pipeline_record: null,
|
||||||
|
field_evidence: null,
|
||||||
|
leeway_evidence: null,
|
||||||
|
replay_reproducer: `core replay turn ${turnId}`,
|
||||||
|
bundle_digest: "sha256:bundle777abcdef",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function missingPracticeEvidence(turnId: number) {
|
||||||
|
return {
|
||||||
|
schema_version: "practice_evidence_v1",
|
||||||
|
turn_id: turnId,
|
||||||
|
status: "missing_evidence",
|
||||||
|
missing_reason: "practice evidence was not persisted for this turn",
|
||||||
|
record_kind: null,
|
||||||
|
practice_disposition: null,
|
||||||
|
chain: [],
|
||||||
|
sealed_trace: null,
|
||||||
|
trace_refusal: null,
|
||||||
|
diagnostic_only: true,
|
||||||
|
serving_allowed: false,
|
||||||
|
mutation_allowed: false,
|
||||||
|
replay_execution_allowed: false,
|
||||||
|
replay_executed_by_workbench: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordedPracticeEvidence(turnId: number) {
|
||||||
|
return {
|
||||||
|
...missingPracticeEvidence(turnId),
|
||||||
|
status: "recorded",
|
||||||
|
missing_reason: null,
|
||||||
|
record_kind: "sealed_trace",
|
||||||
|
practice_disposition: "accepted",
|
||||||
|
chain: [
|
||||||
|
{
|
||||||
|
kind: "problem_frame",
|
||||||
|
status: "recorded",
|
||||||
|
refs: ["problem_frame.0"],
|
||||||
|
summary: "problem frame summary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sealed_trace: {
|
||||||
|
trace_id: "trace_123",
|
||||||
|
trace_policy_version: "v1",
|
||||||
|
input_digest: "sha256:input",
|
||||||
|
problem_frame_digest: "sha256:pf",
|
||||||
|
original_contract_assessment_id: "assessment_1",
|
||||||
|
residual_ids: [],
|
||||||
|
search_gate_decision_id: "decision_1",
|
||||||
|
compute_budget_id: "budget_1",
|
||||||
|
geometric_search_run_id: "run_1",
|
||||||
|
candidate_attempt_ids: [],
|
||||||
|
candidate_attempt_binding_ids: [],
|
||||||
|
replay_result_ids: [],
|
||||||
|
replay_refusal_ids: [],
|
||||||
|
upstream_identity_chain: [],
|
||||||
|
practice_disposition: "accepted",
|
||||||
|
trace_records: [],
|
||||||
|
evidence_spans: [
|
||||||
|
{
|
||||||
|
text: "Lena has 3 marbles.",
|
||||||
|
start: 0,
|
||||||
|
end: 18,
|
||||||
|
sentence_index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
created_by_policy: "policy_1",
|
||||||
|
explanation: "all good",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stubTraceFetch(practiceEvidence: unknown) {
|
||||||
|
const fetchMock = vi.fn((input: unknown) => {
|
||||||
|
const path = new URL(String(input)).pathname;
|
||||||
|
if (path === "/trace/turns") {
|
||||||
|
return jsonOk({ items: [traceSummary(7)] });
|
||||||
|
}
|
||||||
|
if (path === "/trace/7") {
|
||||||
|
return jsonOk(traceEntry(7));
|
||||||
|
}
|
||||||
|
if (path === "/trace/7/pipeline") {
|
||||||
|
return jsonOk(pipelineRecord());
|
||||||
|
}
|
||||||
|
if (path === "/trace/7/field") {
|
||||||
|
return jsonOk(fieldEvidence());
|
||||||
|
}
|
||||||
|
if (path === "/trace/7/bundle") {
|
||||||
|
return jsonOk(evidenceBundle(7));
|
||||||
|
}
|
||||||
|
if (path === "/trace/7/construction") {
|
||||||
|
return jsonOk({});
|
||||||
|
}
|
||||||
|
if (path === "/trace/7/practice") {
|
||||||
|
return jsonOk(practiceEvidence);
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
generated_at: "now",
|
||||||
|
error: { code: "not_found", message: `unexpected path ${path}` },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
return fetchMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoute() {
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={createTestQueryClient()}>
|
||||||
|
<MemoryRouter initialEntries={["/trace/7"]}>
|
||||||
|
<EvidenceProvider>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/trace/:turnId?" element={<TraceRoute />} />
|
||||||
|
</Routes>
|
||||||
|
</EvidenceProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetDescriptors = {
|
||||||
|
offsetHeight: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight"),
|
||||||
|
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("TraceRoute practice tab", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => 560,
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => 480,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (offsetDescriptors.offsetHeight) {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, "offsetHeight", offsetDescriptors.offsetHeight);
|
||||||
|
}
|
||||||
|
if (offsetDescriptors.offsetWidth) {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, "offsetWidth", offsetDescriptors.offsetWidth);
|
||||||
|
}
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches practice evidence through the trace practice endpoint", async () => {
|
||||||
|
const fetchMock = stubTraceFetch(missingPracticeEvidence(7));
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderRoute();
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole("tab", { name: "Practice" }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("/trace/7/practice"),
|
||||||
|
expect.any(Object),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const tab = await screen.findByTestId("practice-evidence-panel");
|
||||||
|
expect(within(tab).getAllByText("practice evidence was not persisted for this turn")[0]).toBeInTheDocument();
|
||||||
|
expect(within(tab).getByText("diagnostic_only")).toBeInTheDocument();
|
||||||
|
expect(within(tab).getByText("serving_allowed")).toBeInTheDocument();
|
||||||
|
expect(within(tab).getByText("curl /trace/7/practice")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders recorded practice evidence", async () => {
|
||||||
|
stubTraceFetch(recordedPracticeEvidence(7));
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderRoute();
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole("tab", { name: "Practice" }));
|
||||||
|
|
||||||
|
const tab = await screen.findByTestId("practice-evidence-panel");
|
||||||
|
expect(within(tab).getAllByText("problem_frame")[0]).toBeInTheDocument();
|
||||||
|
expect(within(tab).getByText("problem frame summary")).toBeInTheDocument();
|
||||||
|
expect(within(tab).getAllByText("trace_123")[0]).toBeInTheDocument();
|
||||||
|
expect(within(tab).getAllByText(/Lena has 3 marbles\./)[0]).toBeInTheDocument();
|
||||||
|
expect(within(tab).getByText("diagnostic_only")).toBeInTheDocument();
|
||||||
|
expect(within(tab).getByText("serving_allowed")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue