From af0b4f707ba239a4e1e814c1ca2158b14828d347 Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 14 Jun 2026 12:12:47 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(workbench):=20CORE-Logos=20Studio=20LG?= =?UTF-8?q?-2=20=E2=80=94=20/logos=20route=20shell=20+=20Overview/Identity?= =?UTF-8?q?/Safety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read-only /logos frontend route (Wave M CORE-Logos read-only wave, LG-2): - route registered via WORKBENCH_ROUTES + lazy ROUTE_ELEMENTS (Substrate section) - Overview / Identity / Safety tabs over live /logos/packs, /{id}, /safety only (no /contents or /alignment — those are LG-3 / LG-4) - 10 Logos* TS interfaces mirrored; NOT_YET_MIRRORED shrunk back to empty - SafetyVerdict UI badge + dump-enums.py source + enum-snapshot + coverage test - logos_pack evidence subject (logos:) + round-trip - bottom strip "proposal mode: none — read-only"; holonomy rendered as missing_evidence (no tab, no proof card); invalid alignment targets surfaced honestly in Safety; verdict never mapped to "clear" for warning/unknown Depends on LG-1 (#737). No mutation endpoints, no engine math in the UI. --- scripts/dump-enums.py | 1 + workbench-ui/enum-snapshot.json | 6 + workbench-ui/src/api/client.ts | 18 + workbench-ui/src/api/queries.ts | 35 + workbench-ui/src/app/App.tsx | 4 + workbench-ui/src/app/EvidenceChainRail.tsx | 24 + workbench-ui/src/app/RightInspector.tsx | 73 ++ workbench-ui/src/app/evidenceAddress.test.ts | 8 + workbench-ui/src/app/evidenceAddress.ts | 14 + workbench-ui/src/app/evidenceContext.tsx | 24 + .../src/app/logos/LogosRoute.test.tsx | 252 ++++++ workbench-ui/src/app/logos/LogosRoute.tsx | 733 ++++++++++++++++++ .../src/app/routeConformance.test.tsx | 10 + workbench-ui/src/app/routes.ts | 13 + .../components/badges/enumCoverage.test.ts | 9 + .../src/design/components/badges/index.tsx | 7 + .../src/design/components/badges/mappings.ts | 8 + .../src/design/components/badges/types.ts | 7 + .../src/design/doctrine/schemaDrift.test.ts | 15 +- workbench-ui/src/types/api.ts | 125 +++ 20 files changed, 1372 insertions(+), 14 deletions(-) create mode 100644 workbench-ui/src/app/logos/LogosRoute.test.tsx create mode 100644 workbench-ui/src/app/logos/LogosRoute.tsx diff --git a/scripts/dump-enums.py b/scripts/dump-enums.py index f0dda58c..f3da4537 100644 --- a/scripts/dump-enums.py +++ b/scripts/dump-enums.py @@ -55,6 +55,7 @@ snapshot = { "GroundingSource": literal_values(ROOT / "core" / "epistemic_state.py", "GroundingSource"), "NormativeClearance": enum_values(ROOT / "core" / "epistemic_state.py", "NormativeClearance"), "ReviewState": literal_values(ROOT / "teaching" / "proposals.py", "ReviewState"), + "SafetyVerdict": enum_values(ROOT / "workbench" / "schemas.py", "SafetyVerdict"), } print(json.dumps(snapshot, indent=2, sort_keys=True)) diff --git a/workbench-ui/enum-snapshot.json b/workbench-ui/enum-snapshot.json index 7d2e04a9..9ebab01d 100644 --- a/workbench-ui/enum-snapshot.json +++ b/workbench-ui/enum-snapshot.json @@ -35,5 +35,11 @@ "accepted", "rejected", "withdrawn" + ], + "SafetyVerdict": [ + "clear", + "warning", + "failed", + "unknown" ] } diff --git a/workbench-ui/src/api/client.ts b/workbench-ui/src/api/client.ts index e9fc2530..1ddae855 100644 --- a/workbench-ui/src/api/client.ts +++ b/workbench-ui/src/api/client.ts @@ -17,6 +17,9 @@ import type { RunDetail, PackSummary, PackDetail, + LogosPackSummary, + LogosPackOverview, + LogosSafetyReport, VaultSummary, VaultEntry, CalibrationClass, @@ -298,6 +301,21 @@ export async function fetchPack(packId: string): Promise { return apiFetch(`/packs/${encodeURIComponent(packId)}`); } +export async function fetchLogosPacks(): Promise { + const envelope = await apiFetch>("/logos/packs"); + return envelope.items; +} + +export async function fetchLogosPackOverview(packId: string): Promise { + return apiFetch(`/logos/packs/${encodeURIComponent(packId)}`); +} + +export async function fetchLogosPackSafety(packId: string): Promise { + return apiFetch( + `/logos/packs/${encodeURIComponent(packId)}/safety`, + ); +} + export async function fetchVaultSummary(): Promise { return apiFetch("/vault/summary"); } diff --git a/workbench-ui/src/api/queries.ts b/workbench-ui/src/api/queries.ts index 59dd620e..58c47d3a 100644 --- a/workbench-ui/src/api/queries.ts +++ b/workbench-ui/src/api/queries.ts @@ -20,6 +20,9 @@ import { fetchRun, fetchPacks, fetchPack, + fetchLogosPacks, + fetchLogosPackOverview, + fetchLogosPackSafety, fetchVaultSummary, fetchVaultEntries, fetchCalibrationClasses, @@ -53,6 +56,9 @@ import type { RunDetail, PackSummary, PackDetail, + LogosPackSummary, + LogosPackOverview, + LogosSafetyReport, VaultSummary, VaultEntry, CalibrationClass, @@ -399,6 +405,35 @@ export function usePack(packId?: string | null) { }); } +export function useLogosPacks() { + return useQuery({ + queryKey: ["api", "logos", "packs"], + queryFn: fetchLogosPacks, + staleTime: 30_000, + refetchOnWindowFocus: false, + }); +} + +export function useLogosPackOverview(packId?: string | null) { + return useQuery({ + queryKey: ["api", "logos", "pack", packId ?? null], + queryFn: () => fetchLogosPackOverview(packId as string), + enabled: typeof packId === "string" && packId.length > 0, + staleTime: 30_000, + refetchOnWindowFocus: false, + }); +} + +export function useLogosPackSafety(packId?: string | null) { + return useQuery({ + queryKey: ["api", "logos", "pack", packId ?? null, "safety"], + queryFn: () => fetchLogosPackSafety(packId as string), + enabled: typeof packId === "string" && packId.length > 0, + staleTime: 30_000, + refetchOnWindowFocus: false, + }); +} + export function useVaultSummary() { return useQuery({ queryKey: ["api", "vault", "summary"], diff --git a/workbench-ui/src/app/App.tsx b/workbench-ui/src/app/App.tsx index b71f4b91..19b4250f 100644 --- a/workbench-ui/src/app/App.tsx +++ b/workbench-ui/src/app/App.tsx @@ -54,6 +54,9 @@ const CalibrationRoute = lazy(() => const PacksRoute = lazy(() => import("./packs/PacksRoute").then((module) => ({ default: module.PacksRoute })), ); +const LogosRoute = lazy(() => + import("./logos/LogosRoute").then((module) => ({ default: module.LogosRoute })), +); const SettingsRoute = lazy(() => import("./settings/SettingsRoute").then((module) => ({ default: module.SettingsRoute, @@ -89,6 +92,7 @@ export const ROUTE_ELEMENTS: RouteElementMap = { evals: lazyRoute(), calibration: lazyRoute(), packs: lazyRoute(), + logos: lazyRoute(), settings: lazyRoute(), }; diff --git a/workbench-ui/src/app/EvidenceChainRail.tsx b/workbench-ui/src/app/EvidenceChainRail.tsx index f04a16c1..7580994a 100644 --- a/workbench-ui/src/app/EvidenceChainRail.tsx +++ b/workbench-ui/src/app/EvidenceChainRail.tsx @@ -159,6 +159,30 @@ export function deriveStages(subject: EvidenceSubject): RailStage[] | null { stage("action", "dim", "not applicable to pack metadata"), ]; } + case "logos_pack": { + const d = subject.data; + return [ + stage("intent", "dim", "not applicable to read-only Logos pack inspection"), + stage("subject", "lit", "selected CORE-Logos pack"), + stage( + "provenance", + d ? evidenceAny(d.manifest_digest, d.manifest_path) : "hollow", + "manifest_digest / manifest_path", + ), + stage( + "admissibility", + d ? evidenceAny(d.safety_status, d.checksum_status) : "hollow", + "safety_status / checksum_status", + ), + stage( + "replay", + d && d.holonomy_case_count && d.holonomy_case_count > 0 ? "lit" : "hollow", + "holonomy_case_count (0 means missing_evidence)", + ), + stage("authority", "dim", "read-only Logos Studio has no mutation authority"), + stage("action", "dim", "proposal mode none"), + ]; + } case "vault_entry": { const d = subject.data; return [ diff --git a/workbench-ui/src/app/RightInspector.tsx b/workbench-ui/src/app/RightInspector.tsx index a55b1ecf..da623f87 100644 --- a/workbench-ui/src/app/RightInspector.tsx +++ b/workbench-ui/src/app/RightInspector.tsx @@ -8,9 +8,11 @@ import { EpistemicStateBadge, GroundingSourceBadge, NormativeClearanceBadge, + SafetyVerdictBadge, type EpistemicState, type GroundingSource, type NormativeClearance, + type SafetyVerdict, } from "../design/components/badges"; import type { LeewayEvidence } from "../types/api"; @@ -238,6 +240,75 @@ function PackInspector({ subject }: { subject: Extract; +}) { + const { data } = subject; + return ( +
+

+ CORE-Logos Pack +

+

+ {subject.packId} +

+ {data ? ( + + ), + }, + ] + : []), + ...(data.checksum_status + ? [ + { + key: "checksum", + value: ( + + ), + }, + ] + : []), + ...(data.manifest_digest + ? [ + { + key: "manifest", + value: data.manifest_digest, + mono: true, + copyable: true, + }, + ] + : []), + ...(data.role ? [{ key: "role", value: data.role }] : []), + ...(data.language ? [{ key: "language", value: data.language }] : []), + ...(typeof data.holonomy_case_count === "number" + ? [ + { + key: "holonomy", + value: + data.holonomy_case_count === 0 + ? "0 / missing_evidence" + : String(data.holonomy_case_count), + }, + ] + : []), + ]} + /> + ) : ( + + )} +
+ ); +} + function VaultEntryInspector({ subject }: { subject: Extract }) { const { data } = subject; return ( @@ -372,6 +443,8 @@ function InspectorProjection({ subject }: { subject: EvidenceSubject }) { return ; case "pack": return ; + case "logos_pack": + return ; case "vault_entry": return ; case "audit_event": diff --git a/workbench-ui/src/app/evidenceAddress.test.ts b/workbench-ui/src/app/evidenceAddress.test.ts index 39994070..26f62d80 100644 --- a/workbench-ui/src/app/evidenceAddress.test.ts +++ b/workbench-ui/src/app/evidenceAddress.test.ts @@ -26,6 +26,8 @@ function routerParamsFor(url: string): Record { return segment === undefined ? {} : { sessionId: segment }; case "packs": return segment === undefined ? {} : { packId: segment }; + case "logos": + return segment === undefined ? {} : { logosPackId: segment }; case "evals": return segment === undefined ? {} : { laneId: segment }; case "replay": @@ -68,6 +70,11 @@ const KINDS: Array<{ name: string; subject: AddressableSubject; path: string }> subject: { kind: "pack", packId: "en/core pack" }, path: "/packs/en%2Fcore%20pack", }, + { + name: "logos_pack", + subject: { kind: "logos_pack", packId: "he_logos_micro_v1" }, + path: "/logos/he_logos_micro_v1", + }, ]; const INSPECT_ONLY_KINDS: Array<{ name: string; subject: AddressableSubject; path: string }> = [ @@ -192,6 +199,7 @@ describe("urlToSubject malformed input", () => { ["empty proposal id", { proposalId: "" }], ["empty session id", { sessionId: "" }], ["empty pack id", { packId: "" }], + ["empty logos pack id", { logosPackId: "" }], ["empty lane id", { laneId: "" }], ["empty artifact id", { artifactId: "" }], ["no params at all", {}], diff --git a/workbench-ui/src/app/evidenceAddress.ts b/workbench-ui/src/app/evidenceAddress.ts index 1ba0eda8..8e0c6691 100644 --- a/workbench-ui/src/app/evidenceAddress.ts +++ b/workbench-ui/src/app/evidenceAddress.ts @@ -10,6 +10,7 @@ import type { EvidenceSubject } from "./evidenceContext"; // artifact -> /replay/ // run -> /runs/ // pack -> /packs/ +// logos_pack -> /logos/ (?inspect uses logos:) // vault_entry -> /vault?inspect=vault: // audit_event -> /audit?inspect=audit: // calibration_class -> /calibration?inspect=calibration: @@ -45,6 +46,8 @@ export function sameIdentity(a: EvidenceSubject, b: EvidenceSubject): boolean { return b.kind === "run" && b.sessionId === a.sessionId; case "pack": return b.kind === "pack" && b.packId === a.packId; + case "logos_pack": + return b.kind === "logos_pack" && b.packId === a.packId; case "vault_entry": return b.kind === "vault_entry" && b.entryIndex === a.entryIndex; case "audit_event": @@ -86,6 +89,8 @@ function subjectAddress(subject: AddressableSubject): SubjectAddress { return { path: `/runs/${encodeURIComponent(subject.sessionId)}`, params: emptyParams() }; case "pack": return { path: `/packs/${encodeURIComponent(subject.packId)}`, params: emptyParams() }; + case "logos_pack": + return { path: `/logos/${encodeURIComponent(subject.packId)}`, params: emptyParams() }; case "vault_entry": { const params = emptyParams(); @@ -121,6 +126,8 @@ export function subjectToInspectValue(subject: AddressableSubject): string { return `run:${subject.sessionId}`; case "pack": return `pack:${subject.packId}`; + case "logos_pack": + return `logos:${subject.packId}`; case "vault_entry": return `vault:${subject.entryIndex}`; case "audit_event": @@ -172,6 +179,8 @@ export function inspectValueToSubject( return { kind: "run", sessionId: id }; case "pack": return { kind: "pack", packId: id }; + case "logos": + return { kind: "logos_pack", packId: id }; case "vault": { const entryIndex = parseTurnId(id); return entryIndex === null ? null : { kind: "vault_entry", entryIndex }; @@ -211,6 +220,11 @@ function routeParamsToSubject( ? null : { kind: "pack", packId: params.packId }; } + if (params.logosPackId !== undefined) { + return params.logosPackId === "" + ? null + : { kind: "logos_pack", packId: params.logosPackId }; + } if (params.laneId !== undefined) { return params.laneId === "" ? null diff --git a/workbench-ui/src/app/evidenceContext.tsx b/workbench-ui/src/app/evidenceContext.tsx index 3c04fd10..1752d224 100644 --- a/workbench-ui/src/app/evidenceContext.tsx +++ b/workbench-ui/src/app/evidenceContext.tsx @@ -14,6 +14,8 @@ import type { ArtifactDetail, EvalRunResult, CalibrationClass, + LogosPackOverview, + SafetyVerdict, } from "../types/api"; export type ProposalSubjectDomain = "cognition" | "math"; @@ -33,6 +35,27 @@ export interface PackSubjectData { determinism_class?: string | null; } +export interface LogosPackSubjectData + extends Partial< + Pick< + LogosPackOverview, + | "pack_id" + | "language" + | "role" + | "script" + | "version" + | "determinism_class" + | "gate_engaged" + | "oov_policy" + | "safety_status" + | "manifest_digest" + | "manifest_path" + | "holonomy_case_count" + > + > { + checksum_status?: SafetyVerdict | null; +} + export interface VaultEntrySubjectData { entry_index?: number; epistemic_state?: string; @@ -60,6 +83,7 @@ export type EvidenceSubject = | { kind: "eval_result"; lane: string; data?: EvalRunResult } | { kind: "run"; sessionId: string; data?: RunSubjectData } | { kind: "pack"; packId: string; data?: PackSubjectData } + | { kind: "logos_pack"; packId: string; data?: LogosPackSubjectData } | { kind: "vault_entry"; entryIndex: number; data?: VaultEntrySubjectData } | { kind: "audit_event"; eventId: string; data?: AuditEventSubjectData } | { kind: "calibration_class"; className: string; data?: CalibrationClass } diff --git a/workbench-ui/src/app/logos/LogosRoute.test.tsx b/workbench-ui/src/app/logos/LogosRoute.test.tsx new file mode 100644 index 00000000..9db98ac9 --- /dev/null +++ b/workbench-ui/src/app/logos/LogosRoute.test.tsx @@ -0,0 +1,252 @@ +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, useLocation } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createTestQueryClient } from "../../test/createTestQueryClient"; +import type { + LogosPackOverview, + LogosPackSummary, + LogosSafetyReport, + SafetyVerdict, +} from "../../types/api"; +import { EvidenceProvider } from "../evidenceContext"; +import { RightInspector } from "../RightInspector"; +import { LogosRoute } from "./LogosRoute"; + +const summaries: LogosPackSummary[] = [ + { + pack_id: "he_logos_micro_v1", + language: "he", + role: "depth_root", + script: "Hebrew", + version: "1.0.0", + determinism_class: "D0", + gate_engaged: true, + oov_policy: "fail_closed", + lexicon_count: 3, + gloss_count: 2, + morphology_count: 3, + frame_count: 0, + composition_count: 0, + alignment_edge_count: 4, + holonomy_case_count: 0, + safety_status: "unknown", + manifest_digest: "sha256:aaaaaaaaaaaaaaaa", + manifest_path: "language_packs/data/he_logos_micro_v1/manifest.json", + }, + { + pack_id: "grc_logos_cognition_v1", + language: "grc", + role: "depth_relation", + script: "Greek", + version: "1.0.0", + determinism_class: "D0", + gate_engaged: true, + oov_policy: "fail_closed", + lexicon_count: 5, + gloss_count: 5, + morphology_count: 5, + frame_count: 0, + composition_count: 0, + alignment_edge_count: 8, + holonomy_case_count: 0, + safety_status: "warning", + manifest_digest: "sha256:bbbbbbbbbbbbbbbb", + manifest_path: "language_packs/data/grc_logos_cognition_v1/manifest.json", + }, +]; + +function overviewFor(packId: string): LogosPackOverview { + const summary = summaries.find((item) => item.pack_id === packId) ?? summaries[0]; + return { + ...summary, + schema_version: "logos_pack_overview_v1", + normalization_policy: "NFC", + source_manifest: "logos-source-manifest.json", + known_gaps: packId.includes("grc") ? ["undeclared English collapse anchors"] : [], + }; +} + +function safetyFor( + packId: string, + verdict: SafetyVerdict = packId.includes("grc") ? "warning" : "unknown", +): LogosSafetyReport { + return { + schema_version: "logos_safety_report_v1", + pack_id: packId, + checksum_status: verdict === "warning" ? "warning" : "unknown", + checksum_errors: verdict === "warning" ? ["alignment checksum pending review"] : [], + domain_contract: { valid: verdict !== "failed", domain_id: "hebrew_greek_textual_reasoning" }, + domain_contract_status: verdict === "failed" ? "failed" : "unknown", + oov_policy_ok: true, + gate_policy_ok: true, + path_safety_ok: true, + dangling_morphology_links: verdict === "warning" + ? [{ entry_id: "logos.entry.1", morphology_id: "missing-morph" }] + : [], + invalid_alignment_targets: verdict === "warning" + ? [ + { + edge_id: "edge-warning-1", + source_id: "grc-logos", + target_id: "en-collapse-logos", + relation: "logos-collapse", + target_pack_id: "en_core_cognition_v1", + }, + ] + : [], + missing_holonomy_refs: "unknown", + epistemic_status_counts: { speculative: 5, contested: verdict === "warning" ? 1 : 0 }, + speculative_entries: ["logos.entry.1", "logos.entry.2"], + contested_entries: verdict === "warning" ? ["logos.entry.3"] : [], + falsified_entries: [], + known_gaps: verdict === "warning" ? ["no holonomy refs"] : [], + verdict, + }; +} + +function okEnvelope(data: unknown) { + return { ok: true, generated_at: "2026-06-14T00:00:00Z", data }; +} + +function stubLogosFetch() { + const paths: string[] = []; + const fetchMock = vi.fn((input: unknown) => { + const path = new URL(String(input)).pathname; + paths.push(path); + if (path === "/logos/packs") { + return Promise.resolve({ + json: async () => okEnvelope({ items: summaries }), + }); + } + const safetyMatch = path.match(/^\/logos\/packs\/([^/]+)\/safety$/); + if (safetyMatch) { + const packId = decodeURIComponent(safetyMatch[1]); + return Promise.resolve({ + json: async () => okEnvelope(safetyFor(packId)), + }); + } + const overviewMatch = path.match(/^\/logos\/packs\/([^/]+)$/); + if (overviewMatch) { + const packId = decodeURIComponent(overviewMatch[1]); + return Promise.resolve({ + json: async () => okEnvelope(overviewFor(packId)), + }); + } + return Promise.resolve({ + json: async () => ({ + ok: false, + generated_at: "2026-06-14T00:00:00Z", + error: { code: "not_found", message: `unexpected path ${path}` }, + }), + }); + }); + vi.stubGlobal("fetch", fetchMock); + return { paths, fetchMock }; +} + +function LocationProbe() { + const location = useLocation(); + return {`${location.pathname}${location.search}`}; +} + +function renderRoute(initialEntry = "/logos") { + return render( + + + + + +
+ + +
+ + + } + /> +
+
+
+
, + ); +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + localStorage.clear(); +}); + +describe("LogosRoute", () => { + it("renders the grouped Pack Universe and persistent read-only status", async () => { + stubLogosFetch(); + renderRoute(); + + expect(await screen.findByText("Pack Universe")).toBeInTheDocument(); + expect(screen.getAllByText("Depth root").length).toBeGreaterThan(0); + expect(screen.getByText("Logos cognition")).toBeInTheDocument(); + expect(screen.getByText("he_logos_micro_v1")).toBeInTheDocument(); + expect(screen.getByText("grc_logos_cognition_v1")).toBeInTheDocument(); + expect(screen.getByText("proposal mode: none — read-only")).toBeInTheDocument(); + expect(screen.getByText("no pack selected")).toBeInTheDocument(); + expect(screen.queryByText(/Draft proposal/i)).not.toBeInTheDocument(); + }); + + it("selects a pack, renders Overview, projects logos_pack evidence, and avoids deferred endpoints", async () => { + const { paths } = stubLogosFetch(); + const user = userEvent.setup(); + renderRoute(); + + await user.click(await screen.findByText("he_logos_micro_v1")); + + await waitFor(() => + expect(screen.getByTestId("location")).toHaveTextContent("/logos/he_logos_micro_v1"), + ); + expect(await screen.findByText("depth-root compression")).toBeInTheDocument(); + expect(screen.getByText("operational articulation")).toBeInTheDocument(); + expect(screen.getByText("depth-relation precision")).toBeInTheDocument(); + expect(screen.getByText("holonomy_cases · missing_evidence")).toBeInTheDocument(); + expect(screen.queryByRole("tab", { name: /holonomy/i })).not.toBeInTheDocument(); + expect(screen.queryByText(/proof card/i)).not.toBeInTheDocument(); + expect(screen.getByText("CORE-Logos Pack")).toBeInTheDocument(); + expect(screen.getByText("0 / missing_evidence")).toBeInTheDocument(); + expect(paths).not.toContain("/logos/packs/he_logos_micro_v1/contents"); + expect(paths).not.toContain("/logos/packs/he_logos_micro_v1/alignment"); + }); + + it("renders Identity passport fields and the raw live overview projection", async () => { + stubLogosFetch(); + const user = userEvent.setup(); + renderRoute("/logos/he_logos_micro_v1"); + + await user.click(await screen.findByRole("tab", { name: "Identity" })); + + expect(await screen.findByText("manifest_digest")).toBeInTheDocument(); + expect(screen.getByText("source_manifest")).toBeInTheDocument(); + expect(screen.getByText("Raw overview projection")).toBeInTheDocument(); + expect(screen.getByText(/\/manifest_digest/)).toBeInTheDocument(); + expect(screen.getByText(/\/schema_version/)).toBeInTheDocument(); + }); + + it("renders Safety warnings and unknown holonomy without mapping either to clear", async () => { + stubLogosFetch(); + const user = userEvent.setup(); + renderRoute("/logos/grc_logos_cognition_v1"); + + await user.click(await screen.findByRole("tab", { name: "Safety" })); + + expect(await screen.findByText("Invalid alignment targets")).toBeInTheDocument(); + expect(screen.getByText("en-collapse-logos")).toBeInTheDocument(); + expect(screen.getByText("Dangling morphology links")).toBeInTheDocument(); + expect(screen.getByText("missing-morph")).toBeInTheDocument(); + expect(screen.getByText("missing_holonomy_refs")).toBeInTheDocument(); + expect(screen.getAllByText("Warning").length).toBeGreaterThan(0); + expect(screen.getAllByText("Unknown").length).toBeGreaterThan(0); + expect(screen.queryByText("Clear")).not.toBeInTheDocument(); + }); +}); diff --git a/workbench-ui/src/app/logos/LogosRoute.tsx b/workbench-ui/src/app/logos/LogosRoute.tsx new file mode 100644 index 00000000..0f047667 --- /dev/null +++ b/workbench-ui/src/app/logos/LogosRoute.tsx @@ -0,0 +1,733 @@ +import { useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { WorkbenchApiError } from "../../api/client"; +import { + useLogosPackOverview, + useLogosPacks, + useLogosPackSafety, +} from "../../api/queries"; +import { MetadataTable } from "../../design/components/MetadataTable/MetadataTable"; +import { Panel } from "../../design/components/Panel/Panel"; +import { SplitPane } from "../../design/components/SplitPane/SplitPane"; +import { StableJsonViewer } from "../../design/components/StableJsonViewer"; +import { TabBar, type Tab } from "../../design/components/TabBar/TabBar"; +import { EmptyState } from "../../design/components/states/EmptyState"; +import { ErrorState } from "../../design/components/states/ErrorState"; +import { LoadingState } from "../../design/components/states/LoadingState"; +import { + SafetyVerdict as BadgeSafetyVerdict, + SafetyVerdictBadge, +} from "../../design/components/badges"; +import type { + LogosAlignmentTargetIssue, + LogosMorphologyLinkIssue, + LogosPackOverview, + LogosPackSummary, + LogosSafetyReport, + SafetyVerdict, +} from "../../types/api"; +import { pushRecentItem } from "../commandRegistry"; +import { subjectToUrl } from "../evidenceAddress"; +import { useEvidenceSubject } from "../evidenceContext"; + +const LOGOS_TABS: readonly Tab[] = [ + { id: "overview", label: "Overview" }, + { id: "identity", label: "Identity" }, + { id: "safety", label: "Safety" }, +]; + +const ROLE_ORDER = ["depth_root", "depth_relation", "logos-cognition", "other"] as const; + +const ROLE_LABELS: Record = { + depth_root: "Depth root", + depth_relation: "Depth relation", + "logos-cognition": "Logos cognition", + other: "Other Logos packs", +}; + +const TRI_LANGUAGE = [ + { + id: "en", + label: "English", + role: "operational articulation", + }, + { + id: "he", + label: "Hebrew", + role: "depth-root compression", + }, + { + id: "grc", + label: "Koine Greek", + role: "depth-relation precision", + }, +] as const; + +function errorMessage(error: unknown) { + return error instanceof WorkbenchApiError + ? error.message + : "CORE-Logos request failed."; +} + +function safetyVerdict(value: SafetyVerdict): BadgeSafetyVerdict { + return value as BadgeSafetyVerdict; +} + +function roleKey(pack: LogosPackSummary): (typeof ROLE_ORDER)[number] { + if (pack.role === "logos-cognition" || pack.pack_id.includes("cognition")) { + return "logos-cognition"; + } + if (pack.role === "depth_root" || pack.role === "depth_relation") { + return pack.role; + } + return "other"; +} + +function roleLabel(role: string | null): string { + if (!role) return "not declared"; + return ROLE_LABELS[role] ?? role.replaceAll("_", " "); +} + +function languageLabel(language: string | null): string { + if (language === "he") return "Hebrew"; + if (language === "grc") return "Koine Greek"; + if (language === "en") return "English"; + return language ?? "not declared"; +} + +function CountBadge({ label, value }: { label: string; value: number }) { + return ( + + {value} + {label} + + ); +} + +function StatusPill({ + tone, + children, +}: { + tone: "neutral" | "warning" | "danger"; + children: ReactNode; +}) { + const className = + tone === "warning" + ? "border-[var(--color-state-warning-border)] bg-[var(--color-state-warning-bg)] text-[var(--color-state-warning-text)]" + : tone === "danger" + ? "border-[var(--color-state-danger-border)] bg-[var(--color-state-danger-bg)] text-[var(--color-state-danger-text)]" + : "border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] text-[var(--color-text-secondary)]"; + return ( + + {children} + + ); +} + +function PackRow({ + pack, + selected, + onSelect, +}: { + pack: LogosPackSummary; + selected: boolean; + onSelect: () => void; +}) { + return ( +
{ + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + 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 + ? "border-l-2 border-l-[var(--color-selected-border)] bg-[var(--color-selected-bg)] pl-[10px]" + : "border-l-2 border-l-transparent pl-[10px]" + }`} + > + + + {pack.pack_id} + + + {roleLabel(pack.role)} + + · + + {languageLabel(pack.language)} + {pack.script ? {pack.script} : null} + + + + + + + + + + event.stopPropagation()} + > + + +
+ ); +} + +function PackUniverse({ + packs, + selectedPackId, + onSelect, +}: { + packs: readonly LogosPackSummary[]; + selectedPackId: string | null; + onSelect: (pack: LogosPackSummary) => void; +}) { + const groups = useMemo(() => { + const byRole = new Map<(typeof ROLE_ORDER)[number], LogosPackSummary[]>( + ROLE_ORDER.map((role) => [role, []]), + ); + for (const pack of packs) byRole.get(roleKey(pack))!.push(pack); + return ROLE_ORDER.map((role) => ({ + role, + packs: byRole.get(role)!.toSorted((a, b) => a.pack_id.localeCompare(b.pack_id)), + })).filter((group) => group.packs.length > 0); + }, [packs]); + + return ( + +
+ {groups.map((group) => ( +
+
+

+ {ROLE_LABELS[group.role]} +

+ +
+
+ {group.packs.map((pack) => ( + onSelect(pack)} + /> + ))} +
+
+ ))} +
+
+ ); +} + +function OverviewTab({ overview }: { overview: LogosPackOverview }) { + const counts = [ + ["lexicon", overview.lexicon_count], + ["glosses", overview.gloss_count], + ["morphology", overview.morphology_count], + ["frames", overview.frame_count], + ["compositions", overview.composition_count], + ["alignment_edges", overview.alignment_edge_count], + ] as const; + + return ( +
+
+ {TRI_LANGUAGE.map((item) => { + const active = + overview.language === item.id || + (item.id === "he" && overview.role === "depth_root") || + (item.id === "grc" && overview.role === "depth_relation"); + return ( +
+

+ {item.label} +

+

+ {item.role} +

+
+ ); + })} +
+ + , + }, + ]} + /> + +
+ {counts.map(([label, value]) => ( +
+
+ {value} +
+
{label}
+
+ ))} +
+
+ {overview.holonomy_case_count} +
+
+ holonomy_cases · missing_evidence +
+
+
+
+ ); +} + +function IdentityTab({ overview }: { overview: LogosPackOverview }) { + return ( +
+ +
+

+ Raw overview projection +

+ +
+
+ ); +} + +function IssueList({ + title, + empty, + items, +}: { + title: string; + empty: string; + items: readonly ReactNode[]; +}) { + return ( +
+

+ {title} +

+ {items.length > 0 ? ( +
{items}
+ ) : ( +

{empty}

+ )} +
+ ); +} + +function MorphologyIssue({ issue }: { issue: LogosMorphologyLinkIssue }) { + return ( +
+ {issue.entry_id} + references missing morphology + {issue.morphology_id} +
+ ); +} + +function AlignmentIssue({ issue }: { issue: LogosAlignmentTargetIssue }) { + return ( +
+ {issue.edge_id} + invalid target + {issue.target_id} + via {issue.relation} + {issue.target_pack_id ? ( + + {" "} + in {issue.target_pack_id} + + ) : null} +
+ ); +} + +function SafetyTab({ report }: { report: LogosSafetyReport }) { + const epistemicRows = Object.entries(report.epistemic_status_counts).sort(([a], [b]) => + a.localeCompare(b), + ); + return ( +
+ , + }, + { + key: "checksum", + value: , + }, + { + key: "domain_contract", + value: ( + + ), + }, + { + key: "missing_holonomy_refs", + value: ( + + ), + }, + { + key: "OOV policy", + value: report.oov_policy_ok ? ( + ok + ) : ( + failed + ), + }, + { + key: "gate policy", + value: report.gate_policy_ok ? ( + ok + ) : ( + failed + ), + }, + { + key: "path safety", + value: report.path_safety_ok ? ( + ok + ) : ( + failed + ), + }, + ]} + /> + +
+ ( +
+ {item} +
+ ))} + /> + ( +
+ {item} +
+ ))} + /> + ( + + ))} + /> + ( + + ))} + /> +
+ +
+
+

+ Epistemic counts +

+ {epistemicRows.length === 0 ? ( +

none recorded

+ ) : ( + ({ + key, + value: String(value), + mono: true, + }))} + /> + )} +
+
+

+ Epistemic entry sets +

+ +
+
+
+ ); +} + +function StatusStrip({ + selectedPackId, + overview, + safety, +}: { + selectedPackId: string | null; + overview?: LogosPackOverview; + safety?: LogosSafetyReport; +}) { + const selected = overview?.pack_id ?? selectedPackId ?? "no pack selected"; + const checksum = safety?.checksum_status ?? "unknown"; + const gateOov = overview + ? `gate ${overview.gate_engaged ? "engaged" : "not engaged"} / OOV ${ + overview.oov_policy ?? "unknown" + }` + : "gate/OOV unknown"; + return ( +
+ {selected} + · + checksum {checksum} + · + {gateOov} + · + proposal mode: none — read-only +
+ ); +} + +function Workspace({ + selectedPackId, + overview, + overviewLoading, + overviewError, + safety, + safetyLoading, + safetyError, +}: { + selectedPackId: string | null; + overview?: LogosPackOverview; + overviewLoading: boolean; + overviewError: unknown; + safety?: LogosSafetyReport; + safetyLoading: boolean; + safetyError: unknown; +}) { + const [activeTab, setActiveTab] = useState("overview"); + + if (selectedPackId === null) { + return ( + " }} + /> + ); + } + + if (overviewLoading) { + return ; + } + + if (overviewError) { + return ( + + ); + } + + if (!overview) return null; + + return ( + } + > + + {activeTab === "overview" ? : null} + {activeTab === "identity" ? : null} + {activeTab === "safety" ? ( + safetyLoading ? ( + + ) : safetyError ? ( + + ) : safety ? ( + + ) : null + ) : null} + + + ); +} + +export function LogosRoute() { + const { logosPackId } = useParams(); + const selectedPackId = logosPackId && logosPackId.length > 0 ? logosPackId : null; + const navigate = useNavigate(); + const { setSubject } = useEvidenceSubject(); + + const packsQuery = useLogosPacks(); + const overviewQuery = useLogosPackOverview(selectedPackId); + const safetyQuery = useLogosPackSafety(selectedPackId); + + useEffect(() => { + if (selectedPackId === null) return; + setSubject({ + kind: "logos_pack", + packId: selectedPackId, + data: overviewQuery.data + ? { + ...overviewQuery.data, + checksum_status: safetyQuery.data?.checksum_status ?? null, + } + : undefined, + }); + }, [selectedPackId, setSubject, overviewQuery.data, safetyQuery.data]); + + function selectPack(pack: LogosPackSummary) { + const subject = { kind: "logos_pack" as const, packId: pack.pack_id }; + const path = subjectToUrl(subject); + navigate(path, { replace: true }); + pushRecentItem({ label: pack.pack_id, path }); + } + + if (packsQuery.isLoading) { + return ; + } + + if (packsQuery.isError) { + return ( + + ); + } + + const packs = packsQuery.data ?? []; + if (packs.length === 0) { + return ( + " }} + /> + ); + } + + return ( +
+
+ + +
+ +
+
+
+ +
+ ); +} diff --git a/workbench-ui/src/app/routeConformance.test.tsx b/workbench-ui/src/app/routeConformance.test.tsx index 3e8ce74e..ed371721 100644 --- a/workbench-ui/src/app/routeConformance.test.tsx +++ b/workbench-ui/src/app/routeConformance.test.tsx @@ -16,6 +16,7 @@ import { ReplayRoute } from "./replay/ReplayRoute"; import { DemoTheaterRoute } from "./demos/DemoTheaterRoute"; import { RunsRoute } from "./runs/RunsRoute"; import { PacksRoute } from "./packs/PacksRoute"; +import { LogosRoute } from "./logos/LogosRoute"; import { VaultRoute } from "./vault/VaultRoute"; import { CalibrationRoute } from "./calibration/CalibrationRoute"; import { SettingsRoute } from "./settings/SettingsRoute"; @@ -194,6 +195,15 @@ const MOUNT_ROUTES: MountRouteSpec[] = [ emptyStatement: "No packs discovered.", emptyCommand: "core pack validate ", }, + { + name: "CORE-Logos", + element: , + path: "/logos/:logosPackId?", + initialEntry: "/logos", + loadingLabel: "Loading CORE-Logos packs...", + emptyStatement: "No CORE-Logos packs discovered.", + emptyCommand: "core pack validate ", + }, { name: "Vault", element: , diff --git a/workbench-ui/src/app/routes.ts b/workbench-ui/src/app/routes.ts index e4e261a0..7febf5be 100644 --- a/workbench-ui/src/app/routes.ts +++ b/workbench-ui/src/app/routes.ts @@ -235,6 +235,19 @@ export const WORKBENCH_ROUTES: readonly WorkbenchRoute[] = [ keyboardDigit: "7", routeConformanceRequired: true, }, + { + id: "logos", + path: "/logos", + routePattern: "logos/:logosPackId?", + label: "CORE-Logos", + description: "Inspect CORE-Logos pack identity and safety.", + section: "Substrate", + leftNavVisible: true, + commandPaletteVisible: true, + landingRouteAllowed: true, + keyboardDigit: null, + routeConformanceRequired: true, + }, { id: "settings", path: "/settings", diff --git a/workbench-ui/src/design/components/badges/enumCoverage.test.ts b/workbench-ui/src/design/components/badges/enumCoverage.test.ts index 52d1ae8b..795dcdd0 100644 --- a/workbench-ui/src/design/components/badges/enumCoverage.test.ts +++ b/workbench-ui/src/design/components/badges/enumCoverage.test.ts @@ -5,6 +5,7 @@ import { groundingSourceMeta, normativeClearanceMeta, reviewStateMeta, + safetyVerdictMeta, } from "./mappings"; function expectExactCoverage(name: string, engineValues: string[], uiValues: string[]) { @@ -43,4 +44,12 @@ describe("build-time enum coverage", () => { Object.keys(groundingSourceMeta), ); }); + + it("tracks every ratified SafetyVerdict value exactly once", () => { + expectExactCoverage( + "SafetyVerdict", + snapshot.SafetyVerdict, + Object.keys(safetyVerdictMeta), + ); + }); }); diff --git a/workbench-ui/src/design/components/badges/index.tsx b/workbench-ui/src/design/components/badges/index.tsx index 32237818..ed2985d2 100644 --- a/workbench-ui/src/design/components/badges/index.tsx +++ b/workbench-ui/src/design/components/badges/index.tsx @@ -3,6 +3,7 @@ import { groundingSourceMeta, normativeClearanceMeta, reviewStateMeta, + safetyVerdictMeta, } from "./mappings"; import { InfoBadge } from "./Badge"; import { @@ -10,6 +11,7 @@ import { GroundingSource, NormativeClearance, ReviewState, + SafetyVerdict, } from "./types"; export { @@ -17,6 +19,7 @@ export { GroundingSource, NormativeClearance, ReviewState, + SafetyVerdict, }; export function EpistemicStateBadge({ value }: { value: EpistemicState }) { @@ -35,6 +38,10 @@ export function GroundingSourceBadge({ value }: { value: GroundingSource }) { return ; } +export function SafetyVerdictBadge({ value }: { value: SafetyVerdict }) { + return ; +} + export function TraceHashBadge({ value, truncate = 12, diff --git a/workbench-ui/src/design/components/badges/mappings.ts b/workbench-ui/src/design/components/badges/mappings.ts index 2f7f1a4d..93b077e9 100644 --- a/workbench-ui/src/design/components/badges/mappings.ts +++ b/workbench-ui/src/design/components/badges/mappings.ts @@ -3,6 +3,7 @@ import { GroundingSource, NormativeClearance, ReviewState, + SafetyVerdict, type BadgeMeta, } from "./types"; @@ -46,3 +47,10 @@ export const groundingSourceMeta = { [GroundingSource.OOV]: { label: "OOV", colorToken: "--color-grounding-oov", meaning: "Out-of-vocabulary grounding was encountered.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is oov." }, [GroundingSource.NONE]: { label: "None", colorToken: "--color-grounding-none", meaning: "No grounding source was present.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is none." }, } satisfies BadgeMeta; + +export const safetyVerdictMeta = { + [SafetyVerdict.CLEAR]: { label: "Clear", colorToken: "--color-state-verified", meaning: "The reported Logos safety check has no recorded warning or failure.", adr: "ADR-0162 / CORE-Logos LG-2", evidence: "SafetyVerdict is clear." }, + [SafetyVerdict.WARNING]: { label: "Warning", colorToken: "--color-state-warning-text", meaning: "The reported Logos safety check found reviewable gaps or link issues; this is not clear.", adr: "ADR-0162 / CORE-Logos LG-2", evidence: "SafetyVerdict is warning." }, + [SafetyVerdict.FAILED]: { label: "Failed", colorToken: "--color-state-danger-text", meaning: "The reported Logos safety check failed a blocking contract.", adr: "ADR-0162 / CORE-Logos LG-2", evidence: "SafetyVerdict is failed." }, + [SafetyVerdict.UNKNOWN]: { label: "Unknown", colorToken: "--color-state-undetermined", meaning: "The reported Logos safety check lacks proof evidence; this is not clear.", adr: "ADR-0162 / CORE-Logos LG-2", evidence: "SafetyVerdict is unknown." }, +} satisfies BadgeMeta; diff --git a/workbench-ui/src/design/components/badges/types.ts b/workbench-ui/src/design/components/badges/types.ts index 7b895820..f47c7799 100644 --- a/workbench-ui/src/design/components/badges/types.ts +++ b/workbench-ui/src/design/components/badges/types.ts @@ -39,6 +39,13 @@ export enum GroundingSource { NONE = "none", } +export enum SafetyVerdict { + CLEAR = "clear", + WARNING = "warning", + FAILED = "failed", + UNKNOWN = "unknown", +} + export type BadgeMeta = Record< T, { diff --git a/workbench-ui/src/design/doctrine/schemaDrift.test.ts b/workbench-ui/src/design/doctrine/schemaDrift.test.ts index 520d3a2d..aefaaa1b 100644 --- a/workbench-ui/src/design/doctrine/schemaDrift.test.ts +++ b/workbench-ui/src/design/doctrine/schemaDrift.test.ts @@ -21,20 +21,7 @@ import { describe, expect, it } from "vitest"; // Shrink-only debt: backend schemas shipped ahead of their TS mirror. A class // gaining a TS interface while still listed here fails the gate. -// CORE-Logos read models (LG-1): backend readers shipped without the /logos -// frontend route; LG-2 adds the TS interfaces and removes each entry here. -const NOT_YET_MIRRORED = new Set([ - "LogosPackSummary", - "LogosPackOverview", - "LogosPackContents", - "LogosLexiconRow", - "LogosGlossRow", - "LogosMorphologyRow", - "LogosAlignmentRow", - "LogosMorphologyLinkIssue", - "LogosAlignmentTargetIssue", - "LogosSafetyReport", -]); +const NOT_YET_MIRRORED = new Set([]); const UI_ROOT = join(__dirname, "..", "..", ".."); const snapshot: Record = JSON.parse( diff --git a/workbench-ui/src/types/api.ts b/workbench-ui/src/types/api.ts index 7614f4a9..ae5bccdf 100644 --- a/workbench-ui/src/types/api.ts +++ b/workbench-ui/src/types/api.ts @@ -302,6 +302,7 @@ export interface AuditEvent { } export type PackSource = "language_pack" | "runtime_pack"; +export type SafetyVerdict = "clear" | "warning" | "failed" | "unknown"; export interface PackSummary { pack_id: string; @@ -320,6 +321,130 @@ export interface PackDetail extends PackSummary { manifest: Record; } +export interface LogosPackSummary { + pack_id: string; + language: string | null; + role: string | null; + script: string | null; + version: string | null; + determinism_class: string | null; + gate_engaged: boolean; + oov_policy: string | null; + lexicon_count: number; + gloss_count: number; + morphology_count: number; + frame_count: number; + composition_count: number; + alignment_edge_count: number; + holonomy_case_count: number; + safety_status: SafetyVerdict; + manifest_digest: string; + manifest_path: string; +} + +export interface LogosPackOverview extends LogosPackSummary { + schema_version: "logos_pack_overview_v1"; + normalization_policy: string | null; + source_manifest: string | null; + known_gaps: string[]; +} + +export interface LogosLexiconRow { + entry_id: string; + surface: string; + lemma: string; + language: string; + part_of_speech: string | null; + pos: string | null; + morphology_id: string | null; + morphology_tags: string[]; + semantic_domains: string[]; + provenance_ids: string[]; + epistemic_status: string; +} + +export interface LogosGlossRow { + gloss_id: string; + lemma: string; + gloss: string; + pos: string | null; + entry_ids: string[]; + provenance_ids: string[]; + epistemic_status: string | null; + raw: Record; +} + +export interface LogosMorphologyRow { + morphology_id: string; + surface: string; + lemma: string; + language: string; + root: string | null; + prefix_chain: string[]; + stem: string | null; + inflection: Record; + suffix_chain: string[]; +} + +export interface LogosAlignmentRow { + edge_id: string; + source_id: string; + target_id: string; + relation: string; + weight: number; + evidence_ids: string[]; + target_pack_id: string | null; + target_resolved: boolean; + invalid_target: boolean; +} + +export interface LogosPackContents { + schema_version: "logos_pack_contents_v1"; + pack_id: string; + manifest: Record; + lexicon: LogosLexiconRow[]; + glosses: LogosGlossRow[]; + morphology: LogosMorphologyRow[]; + frames: Record[]; + compositions: Record[]; + alignment_edges: LogosAlignmentRow[]; + holonomy_cases: Record[]; +} + +export interface LogosMorphologyLinkIssue { + entry_id: string; + morphology_id: string; +} + +export interface LogosAlignmentTargetIssue { + edge_id: string; + source_id: string; + target_id: string; + relation: string; + target_pack_id: string | null; +} + +export interface LogosSafetyReport { + schema_version: "logos_safety_report_v1"; + pack_id: string; + checksum_status: SafetyVerdict; + checksum_errors: string[]; + domain_contract: Record; + domain_contract_status: SafetyVerdict; + oov_policy_ok: boolean; + gate_policy_ok: boolean; + path_safety_ok: boolean; + dangling_morphology_links: LogosMorphologyLinkIssue[]; + invalid_alignment_targets: LogosAlignmentTargetIssue[]; + missing_holonomy_refs: SafetyVerdict; + epistemic_status_counts: Record; + speculative_entries: string[]; + contested_entries: string[]; + falsified_entries: string[]; + known_gaps: string[]; + verdict: SafetyVerdict; +} + export type ArtifactKind = | "trace" | "eval_result" From 5ac6dbe3c510f6507346c21cc6f0e99afdc104df Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 14 Jun 2026 12:20:11 -0700 Subject: [PATCH 2/2] fix(workbench): sync route-count assertions + UI/UX guide for /logos (15 routes) Registering /logos bumped the registry to 15 routes. Update the stale hardcoded counts and the registry-driven guide doc: - Shell.test.tsx: LeftNav 14 -> 15 + CORE-Logos in section-grouped order - CommandPalette.keyboard.test.tsx: palette nav items 14 -> 15 - docs/workbench/UI-UX-GUIDE.md: route count 15 + Substrate CORE-Logos row Full workbench-ui vitest suite: 494 passed, exits clean. --- docs/workbench/UI-UX-GUIDE.md | 3 ++- workbench-ui/src/app/Shell.test.tsx | 5 +++-- .../primitives/CommandPalette.keyboard.test.tsx | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/workbench/UI-UX-GUIDE.md b/docs/workbench/UI-UX-GUIDE.md index 2295d4f5..ac51ab63 100644 --- a/docs/workbench/UI-UX-GUIDE.md +++ b/docs/workbench/UI-UX-GUIDE.md @@ -59,7 +59,7 @@ successful proof. ## 4. Current Route Map The route registry in `workbench-ui/src/app/routes.ts` is the source of truth. -Current route count: 14. +Current route count: 15. | Section | Route | Path | Shortcut | Purpose | |---|---|---|---|---| @@ -76,6 +76,7 @@ Current route count: 14. | Discipline | Evals | `/evals` | `⌘5` | Run/read allowlisted eval lanes and wrong=0 ledgers. | | Discipline | Calibration | `/calibration` | Palette | Inspect practice-class reliability and license verdicts. | | Substrate | Packs | `/packs` | `⌘7` | Browse language/runtime pack metadata. | +| Substrate | CORE-Logos | `/logos` | Palette | Inspect CORE-Logos pack identity and safety. | | Settings | Settings | `/settings` | `⌘0` | Manage local UI preferences; engine config remains CLI-only. | Pinned route shortcuts cover Chat through Settings. All routes are searchable in diff --git a/workbench-ui/src/app/Shell.test.tsx b/workbench-ui/src/app/Shell.test.tsx index 89b0a324..07f19fdd 100644 --- a/workbench-ui/src/app/Shell.test.tsx +++ b/workbench-ui/src/app/Shell.test.tsx @@ -85,11 +85,11 @@ describe("Shell", () => { expect(document.querySelector('[data-density="compact"]')).toBeInTheDocument(); }); - it("LeftNav has exactly 14 items in section-grouped order", () => { + it("LeftNav has exactly 15 items in section-grouped order", () => { renderShell(); const nav = document.querySelector('[data-region="leftnav"]')!; const links = nav.querySelectorAll("a"); - expect(links).toHaveLength(14); + expect(links).toHaveLength(15); const labels = Array.from(links).map((l) => l.textContent); // Grouped by section (Converse → Cognition → Determinism → Evidence → // Discipline → Substrate → Settings), derived from the route registry. @@ -107,6 +107,7 @@ describe("Shell", () => { "Evals", "Calibration", "Packs", + "CORE-Logos", "Settings", ]); }); diff --git a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx index 0a84a182..4dea19fe 100644 --- a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx +++ b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx @@ -52,11 +52,11 @@ describe("CommandPalette keyboard contract", () => { expect(screen.getByRole("button", { name: "Open Trace" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Open Replay" })).toBeInTheDocument(); - // One Navigate command per palette-visible route (14), derived from the - // route registry — Demos, Calibration, Contemplation, and Tour are included - // (the prior hand-maintained list of 10 dropped them). + // One Navigate command per palette-visible route (15), derived from the + // route registry — Demos, Calibration, Contemplation, Tour, and CORE-Logos + // are included (the prior hand-maintained list of 10 dropped them). const items = dialog.querySelectorAll('[role="option"]'); - expect(items.length).toBe(14); + expect(items.length).toBe(15); const lastIndex = items.length - 1; // Initially first item (index 0) is focused — check aria-selected