feat(workbench): Vault route — fail-closed epistemic memory evidence (Wave R2)
Replaces VaultRoutePlaceholder with the real route over the R2-B /vault read substrate (#712). - TS mirrors VaultSummary/VaultEntry; both removed from NOT_YET_MIRRORED. Also fixes a latent ErrorCode drift: adds 'evidence_unavailable' to the TS union (present in Python schemas, missing in TS) — the route branches on it. - fail-closed is the design: a missing/unpersisted vault (the 501 evidence_unavailable signal, or persisted=false / entry_count=0) renders the honest absence card as the PRIMARY state, naming the opt-in RuntimeConfig.persist_session_state pointer — not a generic error, no skeleton theater - with data: summary strip (entry_count, store_count, reproject_interval, max_entries, source_path) + VirtualizedList of entries with epistemic_status / epistemic_state pills and versor_digest DigestBadge - exact-recall doctrine: renders only backend fields; never computes or displays a similarity/relevance proxy (runtime recall is exact cga_inner) - selection publishes the vault_entry subject (inspect-param only, via setSubject + setInspectorOpen; EvidenceUrlSync writes ?inspect=vault:N); a URL-restored identity-only subject is hydrated once entries load - Vault row added to routeConformance MOUNT_ROUTES — the fail-closed absence asserted as the primary contract - tests: fail-closed card (not error), summary+entries, no-similarity-score doctrine, vault_entry subject publication, j/k spine Token-only styling (hexScan green).
This commit is contained in:
parent
19f8d5e3bd
commit
ebb95a0541
9 changed files with 484 additions and 15 deletions
|
|
@ -15,6 +15,8 @@ import type {
|
|||
RunDetail,
|
||||
PackSummary,
|
||||
PackDetail,
|
||||
VaultSummary,
|
||||
VaultEntry,
|
||||
TurnJournalEntry,
|
||||
TurnJournalSummary,
|
||||
MathProposalSummary,
|
||||
|
|
@ -224,3 +226,18 @@ export async function fetchPacks(limit?: number, offset?: number): Promise<PackS
|
|||
export async function fetchPack(packId: string): Promise<PackDetail> {
|
||||
return apiFetch<PackDetail>(`/packs/${encodeURIComponent(packId)}`);
|
||||
}
|
||||
|
||||
export async function fetchVaultSummary(): Promise<VaultSummary> {
|
||||
return apiFetch<VaultSummary>("/vault/summary");
|
||||
}
|
||||
|
||||
export async function fetchVaultEntries(limit?: number, offset?: number): Promise<VaultEntry[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (limit !== undefined) params.set("limit", String(limit));
|
||||
if (offset !== undefined) params.set("offset", String(offset));
|
||||
const query = params.toString();
|
||||
const envelope = await apiFetch<ItemsEnvelope<VaultEntry>>(
|
||||
query ? `/vault/entries?${query}` : "/vault/entries",
|
||||
);
|
||||
return envelope.items;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
fetchRun,
|
||||
fetchPacks,
|
||||
fetchPack,
|
||||
fetchVaultSummary,
|
||||
fetchVaultEntries,
|
||||
fetchTraceTurn,
|
||||
fetchTraceTurns,
|
||||
fetchMathProposals,
|
||||
|
|
@ -39,6 +41,8 @@ import type {
|
|||
RunDetail,
|
||||
PackSummary,
|
||||
PackDetail,
|
||||
VaultSummary,
|
||||
VaultEntry,
|
||||
TurnJournalEntry,
|
||||
TurnJournalSummary,
|
||||
EvalLaneSummary,
|
||||
|
|
@ -295,3 +299,24 @@ export function usePack(packId?: string | null) {
|
|||
});
|
||||
}
|
||||
|
||||
export function useVaultSummary() {
|
||||
return useQuery<VaultSummary, WorkbenchApiError>({
|
||||
queryKey: ["api", "vault", "summary"],
|
||||
queryFn: () => fetchVaultSummary(),
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useVaultEntries(enabled: boolean, limit?: number, offset?: number) {
|
||||
return useQuery<VaultEntry[], WorkbenchApiError>({
|
||||
queryKey: ["api", "vault", "entries", limit ?? null, offset ?? null],
|
||||
queryFn: () => fetchVaultEntries(limit, offset),
|
||||
enabled,
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { ReplayRoute } from "./replay/ReplayRoute";
|
|||
import { EvalsRoute } from "./evals/EvalsRoute";
|
||||
import { RunsRoute } from "./runs/RunsRoute";
|
||||
import { PacksRoute } from "./packs/PacksRoute";
|
||||
import { VaultRoutePlaceholder } from "../routes/VaultRoutePlaceholder";
|
||||
import { VaultRoute } from "./vault/VaultRoute";
|
||||
import { SettingsRoutePlaceholder } from "../routes/SettingsRoutePlaceholder";
|
||||
|
||||
export function App() {
|
||||
|
|
@ -28,7 +28,7 @@ export function App() {
|
|||
<Route path="evals/:laneId?" element={<EvalsRoute />} />
|
||||
<Route path="runs/:sessionId?" element={<RunsRoute />} />
|
||||
<Route path="packs/:packId?" element={<PacksRoute />} />
|
||||
<Route path="vault" element={<VaultRoutePlaceholder />} />
|
||||
<Route path="vault" element={<VaultRoute />} />
|
||||
<Route path="audit" element={<AuditRoute />} />
|
||||
<Route path="settings" element={<SettingsRoutePlaceholder />} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { EvalsRoute } from "./evals/EvalsRoute";
|
|||
import { ReplayRoute } from "./replay/ReplayRoute";
|
||||
import { RunsRoute } from "./runs/RunsRoute";
|
||||
import { PacksRoute } from "./packs/PacksRoute";
|
||||
import { VaultRoute } from "./vault/VaultRoute";
|
||||
|
||||
/**
|
||||
* ADR-0162 §6 route conformance — executable, not aspirational.
|
||||
|
|
@ -171,6 +172,17 @@ const MOUNT_ROUTES: MountRouteSpec[] = [
|
|||
emptyStatement: "No packs discovered.",
|
||||
emptyCommand: "core pack validate <path>",
|
||||
},
|
||||
{
|
||||
name: "Vault",
|
||||
element: <VaultRoute />,
|
||||
path: "/vault",
|
||||
initialEntry: "/vault",
|
||||
loadingLabel: "Loading vault...",
|
||||
// Fail-closed: absence of a persisted vault is the primary contract.
|
||||
emptyStatement:
|
||||
"No persisted vault. Session memory is held in-process and discarded on exit; persistence is opt-in via RuntimeConfig.persist_session_state.",
|
||||
emptyCommand: "Set RuntimeConfig.persist_session_state = true",
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(MOUNT_ROUTES)("route conformance: $name", (spec) => {
|
||||
|
|
|
|||
185
workbench-ui/src/app/vault/VaultRoute.test.tsx
Normal file
185
workbench-ui/src/app/vault/VaultRoute.test.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen, waitFor } 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 type { VaultEntry, VaultSummary } from "../../types/api";
|
||||
import { EvidenceProvider, useEvidenceSubject } from "../evidenceContext";
|
||||
import { VaultRoute } from "./VaultRoute";
|
||||
|
||||
const summary: VaultSummary = {
|
||||
source_path: "engine_state/session_state.json",
|
||||
entry_count: 2,
|
||||
store_count: 1,
|
||||
reproject_interval: 64,
|
||||
max_entries: 4096,
|
||||
persisted: true,
|
||||
};
|
||||
|
||||
const entries: VaultEntry[] = [
|
||||
{
|
||||
entry_index: 0,
|
||||
epistemic_status: "coherent",
|
||||
epistemic_state: "verified",
|
||||
metadata: { concept: "truth" },
|
||||
versor_digest: "sha256:aaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
entry_index: 1,
|
||||
epistemic_status: "speculative",
|
||||
epistemic_state: "inferred",
|
||||
metadata: { concept: "beauty" },
|
||||
versor_digest: "sha256:bbbbbbbbbbbbbbbbbbbb",
|
||||
},
|
||||
];
|
||||
|
||||
function SubjectProbe() {
|
||||
const { subject } = useEvidenceSubject();
|
||||
return (
|
||||
<span data-testid="subject">
|
||||
{subject.kind === "vault_entry" ? `vault_entry:${subject.entryIndex}` : subject.kind}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRoute() {
|
||||
return render(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MemoryRouter initialEntries={["/vault"]}>
|
||||
<EvidenceProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/vault"
|
||||
element={
|
||||
<>
|
||||
<VaultRoute />
|
||||
<SubjectProbe />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</EvidenceProvider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function okBody(data: unknown) {
|
||||
return { ok: true, generated_at: "now", data };
|
||||
}
|
||||
|
||||
function evidenceUnavailable(message: string) {
|
||||
return { ok: false, generated_at: "now", error: { code: "evidence_unavailable", message } };
|
||||
}
|
||||
|
||||
function stubVault({ persisted = true }: { persisted?: boolean } = {}) {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn((input: unknown) => {
|
||||
const path = new URL(String(input)).pathname;
|
||||
if (path === "/vault/summary") {
|
||||
return persisted
|
||||
? Promise.resolve({ json: () => Promise.resolve(okBody(summary)) })
|
||||
: Promise.resolve({
|
||||
json: () =>
|
||||
Promise.resolve(evidenceUnavailable("vault evidence unavailable: not persisted")),
|
||||
});
|
||||
}
|
||||
if (path === "/vault/entries") {
|
||||
return Promise.resolve({ json: () => Promise.resolve(okBody({ items: entries })) });
|
||||
}
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve(evidenceUnavailable(`unexpected ${path}`)),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const offsetDescriptors = {
|
||||
offsetHeight: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight"),
|
||||
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
|
||||
};
|
||||
|
||||
describe("VaultRoute", () => {
|
||||
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("fail-closed: an unpersisted vault renders the honest absence card, not an error", async () => {
|
||||
stubVault({ persisted: false });
|
||||
renderRoute();
|
||||
|
||||
expect(
|
||||
await screen.findByText(/No persisted vault\./),
|
||||
).toBeInTheDocument();
|
||||
// both the statement and the config-pointer action name the opt-in flag
|
||||
expect(
|
||||
screen.getAllByText(/RuntimeConfig\.persist_session_state/).length,
|
||||
).toBeGreaterThan(0);
|
||||
// it is NOT the generic error contract
|
||||
expect(screen.queryByText("What failed")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the summary strip and entries when persisted", async () => {
|
||||
stubVault();
|
||||
renderRoute();
|
||||
|
||||
expect(await screen.findByText("entry_count")).toBeInTheDocument();
|
||||
expect(screen.getByText("reproject_interval")).toBeInTheDocument();
|
||||
expect(screen.getByText("coherent")).toBeInTheDocument();
|
||||
expect(screen.getByText("verified")).toBeInTheDocument();
|
||||
expect(screen.getByText("speculative")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never invents a similarity / relevance score (exact-recall doctrine)", async () => {
|
||||
stubVault();
|
||||
renderRoute();
|
||||
|
||||
await screen.findByText("entry_count");
|
||||
expect(screen.queryByText(/similarity|relevance|score/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selecting an entry publishes the vault_entry subject", async () => {
|
||||
stubVault();
|
||||
const user = userEvent.setup();
|
||||
renderRoute();
|
||||
|
||||
await user.click(await screen.findByText("speculative"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("subject")).toHaveTextContent("vault_entry:1"),
|
||||
);
|
||||
});
|
||||
|
||||
it("moves entry focus with j/k through the VirtualizedList spine", async () => {
|
||||
stubVault();
|
||||
const user = userEvent.setup();
|
||||
renderRoute();
|
||||
|
||||
const list = await screen.findByRole("listbox", { name: "Vault entries" });
|
||||
list.focus();
|
||||
expect(screen.getAllByRole("option")[0]).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await user.keyboard("j");
|
||||
expect(screen.getAllByRole("option")[1]).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
});
|
||||
225
workbench-ui/src/app/vault/VaultRoute.tsx
Normal file
225
workbench-ui/src/app/vault/VaultRoute.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { WorkbenchApiError } from "../../api/client";
|
||||
import { useVaultEntries, useVaultSummary } from "../../api/queries";
|
||||
import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
|
||||
import { MetadataTable } from "../../design/components/MetadataTable/MetadataTable";
|
||||
import { Panel } from "../../design/components/Panel/Panel";
|
||||
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
|
||||
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
|
||||
import { EmptyState } from "../../design/components/states/EmptyState";
|
||||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
import { LoadingState } from "../../design/components/states/LoadingState";
|
||||
import type { VaultEntry, VaultSummary } from "../../types/api";
|
||||
import { useEvidenceSubject } from "../evidenceContext";
|
||||
|
||||
// Vault persistence is opt-in; absence is the common, honest state. This is
|
||||
// the fail-closed primary contract, not an error.
|
||||
const FAIL_CLOSED_STATEMENT =
|
||||
"No persisted vault. Session memory is held in-process and discarded on exit; persistence is opt-in via RuntimeConfig.persist_session_state.";
|
||||
const FAIL_CLOSED_ACTION = "Set RuntimeConfig.persist_session_state = true";
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
return error instanceof WorkbenchApiError ? error.message : "Vault request failed.";
|
||||
}
|
||||
|
||||
function digestPayload(value: string | null | undefined): string | null {
|
||||
if (!value) return null;
|
||||
return value.replace(/^sha256:/, "");
|
||||
}
|
||||
|
||||
function StatusPill({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<span className="inline-flex h-6 items-center rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] px-2 text-xs text-[var(--color-text-secondary)]">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function VaultRow({
|
||||
entry,
|
||||
selected,
|
||||
focused,
|
||||
onSelect,
|
||||
}: {
|
||||
entry: VaultEntry;
|
||||
selected: boolean;
|
||||
focused: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const digest = digestPayload(entry.versor_digest);
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-current={selected ? "true" : undefined}
|
||||
onClick={onSelect}
|
||||
className={`grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-3 border-b border-[var(--color-border-subtle)] px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-inset)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[var(--color-focus-ring)] ${
|
||||
selected ? "bg-[var(--color-selected-bg)]" : ""
|
||||
} ${
|
||||
selected
|
||||
? "border-l-2 border-l-[var(--color-selected-border)] pl-[10px]"
|
||||
: focused
|
||||
? "border-l-2 border-l-[var(--color-focus-ring)] pl-[10px]"
|
||||
: "border-l-2 border-l-transparent pl-[10px]"
|
||||
}`}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block font-mono text-xs text-[var(--color-text-muted)]">
|
||||
#{entry.entry_index}
|
||||
</span>
|
||||
<span className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<StatusPill>{entry.epistemic_status}</StatusPill>
|
||||
<StatusPill>{entry.epistemic_state}</StatusPill>
|
||||
</span>
|
||||
</span>
|
||||
<span className="justify-self-end">
|
||||
{digest ? (
|
||||
<DigestBadge digest={digest} truncate={12} />
|
||||
) : (
|
||||
<span className="font-mono text-xs text-[var(--color-text-muted)]">no versor</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VaultSummaryStrip({ summary }: { summary: VaultSummary }) {
|
||||
// Render only fields the backend returns. Recall is exact CGA (cga_inner);
|
||||
// the UI must never invent an approximate similarity/relevance proxy.
|
||||
return (
|
||||
<MetadataTable
|
||||
rows={[
|
||||
{ key: "entry_count", value: String(summary.entry_count), mono: true },
|
||||
{ key: "store_count", value: String(summary.store_count), mono: true },
|
||||
{ key: "reproject_interval", value: String(summary.reproject_interval), mono: true },
|
||||
{
|
||||
key: "max_entries",
|
||||
value: summary.max_entries === null ? "unbounded" : String(summary.max_entries),
|
||||
mono: true,
|
||||
},
|
||||
{ key: "source_path", value: summary.source_path, mono: true },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FailClosed() {
|
||||
return (
|
||||
<EmptyState statement={FAIL_CLOSED_STATEMENT} nextAction={FAIL_CLOSED_ACTION} />
|
||||
);
|
||||
}
|
||||
|
||||
export function VaultRoute() {
|
||||
const { subject, setSubject, setInspectorOpen } = useEvidenceSubject();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const summaryQuery = useVaultSummary();
|
||||
const summary = summaryQuery.data;
|
||||
const hasEntries = !!summary && summary.persisted && summary.entry_count > 0;
|
||||
const entriesQuery = useVaultEntries(hasEntries);
|
||||
|
||||
const selectedIndex =
|
||||
subject.kind === "vault_entry" ? subject.entryIndex : null;
|
||||
|
||||
const entries = entriesQuery.data ?? [];
|
||||
const filteredEntries = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) return entries;
|
||||
return entries.filter(
|
||||
(entry) =>
|
||||
entry.epistemic_status.toLowerCase().includes(q) ||
|
||||
entry.epistemic_state.toLowerCase().includes(q) ||
|
||||
String(entry.entry_index).includes(q),
|
||||
);
|
||||
}, [search, entries]);
|
||||
|
||||
// Hydrate a URL-restored (identity-only) vault_entry subject with its data.
|
||||
useEffect(() => {
|
||||
if (subject.kind !== "vault_entry" || subject.data) return;
|
||||
const match = entries.find((entry) => entry.entry_index === subject.entryIndex);
|
||||
if (match) {
|
||||
setSubject({ kind: "vault_entry", entryIndex: match.entry_index, data: match });
|
||||
}
|
||||
}, [subject, entries, setSubject]);
|
||||
|
||||
function selectEntry(entry: VaultEntry) {
|
||||
setSubject({ kind: "vault_entry", entryIndex: entry.entry_index, data: entry });
|
||||
setInspectorOpen(true);
|
||||
}
|
||||
|
||||
if (summaryQuery.isLoading) {
|
||||
return <LoadingState label="Loading vault..." />;
|
||||
}
|
||||
|
||||
// Fail-closed: no persisted vault is the expected primary state, not an error.
|
||||
if (summaryQuery.isError) {
|
||||
if (summaryQuery.error.code === "evidence_unavailable") {
|
||||
return <FailClosed />;
|
||||
}
|
||||
return (
|
||||
<ErrorState
|
||||
whatFailed={errorMessage(summaryQuery.error)}
|
||||
mutationStatus="No vault mutation occurred."
|
||||
reproducer="curl /vault/summary"
|
||||
retrySafety="Retry: safe"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasEntries) {
|
||||
return <FailClosed />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="Vault"
|
||||
toolbar={
|
||||
<span className="font-mono text-xs text-[var(--color-text-muted)]">
|
||||
{summary.entry_count} entries
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="grid min-h-0 gap-3">
|
||||
<VaultSummaryStrip summary={summary} />
|
||||
<SearchInput
|
||||
placeholder="Filter by epistemic status, state, or index"
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
/>
|
||||
{entriesQuery.isLoading ? (
|
||||
<LoadingState label="Loading vault entries..." />
|
||||
) : entriesQuery.isError ? (
|
||||
<ErrorState
|
||||
whatFailed={errorMessage(entriesQuery.error)}
|
||||
mutationStatus="No vault mutation occurred."
|
||||
reproducer="curl /vault/entries"
|
||||
retrySafety="Retry: safe"
|
||||
/>
|
||||
) : filteredEntries.length === 0 ? (
|
||||
<EmptyState
|
||||
statement="No vault entries match this filter."
|
||||
nextAction={FAIL_CLOSED_ACTION}
|
||||
/>
|
||||
) : (
|
||||
<VirtualizedList
|
||||
ariaLabel="Vault entries"
|
||||
estimateSize={72}
|
||||
getKey={(entry) => String(entry.entry_index)}
|
||||
height="calc(100vh - 18rem)"
|
||||
initialRect={{ width: 480, height: 560 }}
|
||||
items={filteredEntries}
|
||||
onActivate={(entry) => selectEntry(entry)}
|
||||
renderItem={(entry, _index, focused) => (
|
||||
<VaultRow
|
||||
entry={entry}
|
||||
selected={entry.entry_index === selectedIndex}
|
||||
focused={focused}
|
||||
onSelect={() => selectEntry(entry)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,9 +20,6 @@ import { describe, expect, it } from "vitest";
|
|||
*/
|
||||
|
||||
const NOT_YET_MIRRORED = new Set([
|
||||
// R2-B backend read substrate (#712) — TS mirrors land with each R2 route:
|
||||
"VaultSummary",
|
||||
"VaultEntry",
|
||||
// Wave R3 sealed turn replay backend — TS mirrors land with the frontend
|
||||
// Replay Moment PR (which also retires the W-026 artifact-keyed
|
||||
// ReplayComparison/ReplayDivergence pair on both sides):
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { EmptyState } from "../design/components/states/EmptyState";
|
||||
|
||||
export function VaultRoutePlaceholder() {
|
||||
return (
|
||||
<EmptyState
|
||||
statement="Vault — no data loaded yet."
|
||||
nextAction="Pending W-029"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
export type ErrorCode =
|
||||
| "bad_request"
|
||||
| "evidence_unavailable"
|
||||
| "not_found"
|
||||
| "unsupported"
|
||||
| "read_error"
|
||||
|
|
@ -263,6 +264,23 @@ export interface ReplayComparison {
|
|||
divergences: ReplayDivergence[];
|
||||
}
|
||||
|
||||
export interface VaultSummary {
|
||||
source_path: string;
|
||||
entry_count: number;
|
||||
store_count: number;
|
||||
reproject_interval: number;
|
||||
max_entries: number | null;
|
||||
persisted: boolean;
|
||||
}
|
||||
|
||||
export interface VaultEntry {
|
||||
entry_index: number;
|
||||
epistemic_status: string;
|
||||
epistemic_state: string;
|
||||
metadata: Record<string, unknown>;
|
||||
versor_digest: string | null;
|
||||
}
|
||||
|
||||
// API envelope types
|
||||
export interface ApiOk<T> {
|
||||
ok: true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue