feat(workbench): CORE-Logos Studio LG-4 — Alignment tab + holonomy-absent guard
The trilingual resonance centerpiece, read-only over GET /logos/packs/{id}/alignment:
- deterministic DagViewer (he -> grc -> en longest-path) + selectable edge list;
alignmentToDag is a pure projection pinned by a golden-file layout test
- invalid alignment targets surfaced honestly (the undeclared en-collapse-*
anchors render a warning, never smoothed over)
- logos_alignment_edge evidence subject (logos_alignment_edge:<packId>/<edgeId>)
with RightInspector projection + EvidenceChainRail derivation
- holonomy stays absent: no tab, no proof card, no success element (guard test)
Full workbench-ui vitest: 516 passed, clean exit; build clean.
This commit is contained in:
parent
bcba7cba1e
commit
5474a15205
10 changed files with 579 additions and 3 deletions
|
|
@ -247,6 +247,30 @@ export function deriveStages(subject: EvidenceSubject): RailStage[] | null {
|
|||
stage("action", "dim", "proposal mode none"),
|
||||
];
|
||||
}
|
||||
case "logos_alignment_edge": {
|
||||
const d = subject.data;
|
||||
return [
|
||||
stage("intent", "dim", "not applicable to read-only Logos alignment inspection"),
|
||||
stage("subject", "lit", "selected CORE-Logos alignment edge"),
|
||||
stage(
|
||||
"provenance",
|
||||
d ? evidenceAny(...d.evidence_ids) : "hollow",
|
||||
"evidence_ids (cross-language attestations)",
|
||||
),
|
||||
stage(
|
||||
"admissibility",
|
||||
d ? (d.invalid_target ? "hollow" : evidenceOf(d.target_pack_id)) : "hollow",
|
||||
"target resolves to a declared lexicon entry (invalid_target = dangling)",
|
||||
),
|
||||
stage(
|
||||
"replay",
|
||||
d ? evidenceOf(d.relation) : "hollow",
|
||||
"relation + weight (deterministic resonance edge)",
|
||||
),
|
||||
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 [
|
||||
|
|
|
|||
|
|
@ -423,6 +423,48 @@ function LogosMorphologyInspector({
|
|||
);
|
||||
}
|
||||
|
||||
function LogosAlignmentEdgeInspector({
|
||||
subject,
|
||||
}: {
|
||||
subject: Extract<EvidenceSubject, { kind: "logos_alignment_edge" }>;
|
||||
}) {
|
||||
const { data } = subject;
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)]">
|
||||
CORE-Logos Alignment Edge
|
||||
</h3>
|
||||
<p className="m-0 font-mono text-xs text-[var(--color-text-primary)]">
|
||||
{subject.packId} · {subject.edgeId}
|
||||
</p>
|
||||
{data ? (
|
||||
<MetadataTable
|
||||
rows={[
|
||||
{ key: "source", value: data.source_id, mono: true },
|
||||
{ key: "target", value: data.target_id, mono: true },
|
||||
{ key: "relation", value: data.relation },
|
||||
{ key: "weight", value: data.weight.toFixed(2), mono: true },
|
||||
{
|
||||
key: "target_pack",
|
||||
value: data.target_pack_id ?? "unresolved",
|
||||
mono: data.target_pack_id !== null,
|
||||
},
|
||||
{
|
||||
key: "target",
|
||||
value: data.invalid_target ? "invalid (dangling)" : "resolved",
|
||||
},
|
||||
...(data.evidence_ids.length > 0
|
||||
? [{ key: "evidence", value: data.evidence_ids.join(", "), mono: true }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<DetailNotLoaded />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VaultEntryInspector({ subject }: { subject: Extract<EvidenceSubject, { kind: "vault_entry" }> }) {
|
||||
const { data } = subject;
|
||||
return (
|
||||
|
|
@ -565,6 +607,8 @@ function InspectorProjection({ subject }: { subject: EvidenceSubject }) {
|
|||
return <LogosGlossInspector subject={subject} />;
|
||||
case "logos_morphology":
|
||||
return <LogosMorphologyInspector subject={subject} />;
|
||||
case "logos_alignment_edge":
|
||||
return <LogosAlignmentEdgeInspector subject={subject} />;
|
||||
case "vault_entry":
|
||||
return <VaultEntryInspector subject={subject} />;
|
||||
case "audit_event":
|
||||
|
|
|
|||
|
|
@ -299,6 +299,10 @@ describe("logos sub-entity addresses (LG-3)", () => {
|
|||
subject: { kind: "logos_morphology", packId: PACK, morphologyId: "he-morph-001" },
|
||||
inspect: `logos_morphology:${PACK}/he-morph-001`,
|
||||
},
|
||||
{
|
||||
subject: { kind: "logos_alignment_edge", packId: PACK, edgeId: "a1b2c3d4e5f60718" },
|
||||
inspect: `logos_alignment_edge:${PACK}/a1b2c3d4e5f60718`,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(subEntities)(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type { EvidenceSubject } from "./evidenceContext";
|
|||
// logos_entry -> /logos/<packId>?inspect=logos_entry:<packId>/<entryId>
|
||||
// logos_gloss -> /logos/<packId>?inspect=logos_gloss:<packId>/<glossId>
|
||||
// logos_morphology -> /logos/<packId>?inspect=logos_morphology:<packId>/<morphologyId>
|
||||
// logos_alignment_edge -> /logos/<packId>?inspect=logos_alignment_edge:<packId>/<edgeId>
|
||||
// vault_entry -> /vault?inspect=vault:<entryIndex>
|
||||
// audit_event -> /audit?inspect=audit:<eventId>
|
||||
// calibration_class -> /calibration?inspect=calibration:<className>
|
||||
|
|
@ -69,6 +70,12 @@ export function sameIdentity(a: EvidenceSubject, b: EvidenceSubject): boolean {
|
|||
b.packId === a.packId &&
|
||||
b.morphologyId === a.morphologyId
|
||||
);
|
||||
case "logos_alignment_edge":
|
||||
return (
|
||||
b.kind === "logos_alignment_edge" &&
|
||||
b.packId === a.packId &&
|
||||
b.edgeId === a.edgeId
|
||||
);
|
||||
case "vault_entry":
|
||||
return b.kind === "vault_entry" && b.entryIndex === a.entryIndex;
|
||||
case "audit_event":
|
||||
|
|
@ -115,6 +122,7 @@ function subjectAddress(subject: AddressableSubject): SubjectAddress {
|
|||
case "logos_entry":
|
||||
case "logos_gloss":
|
||||
case "logos_morphology":
|
||||
case "logos_alignment_edge":
|
||||
{
|
||||
const params = emptyParams();
|
||||
params.set(INSPECT_PARAM, subjectToInspectValue(subject));
|
||||
|
|
@ -163,6 +171,8 @@ export function subjectToInspectValue(subject: AddressableSubject): string {
|
|||
return `logos_gloss:${subject.packId}/${subject.glossId}`;
|
||||
case "logos_morphology":
|
||||
return `logos_morphology:${subject.packId}/${subject.morphologyId}`;
|
||||
case "logos_alignment_edge":
|
||||
return `logos_alignment_edge:${subject.packId}/${subject.edgeId}`;
|
||||
case "vault_entry":
|
||||
return `vault:${subject.entryIndex}`;
|
||||
case "audit_event":
|
||||
|
|
@ -249,6 +259,16 @@ export function inspectValueToSubject(
|
|||
morphologyId: parts.subId,
|
||||
};
|
||||
}
|
||||
case "logos_alignment_edge": {
|
||||
const parts = splitPackScoped(id);
|
||||
return parts === null
|
||||
? null
|
||||
: {
|
||||
kind: "logos_alignment_edge",
|
||||
packId: parts.packId,
|
||||
edgeId: parts.subId,
|
||||
};
|
||||
}
|
||||
case "vault": {
|
||||
const entryIndex = parseTurnId(id);
|
||||
return entryIndex === null ? null : { kind: "vault_entry", entryIndex };
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type {
|
|||
LogosLexiconRow,
|
||||
LogosGlossRow,
|
||||
LogosMorphologyRow,
|
||||
LogosAlignmentRow,
|
||||
SafetyVerdict,
|
||||
} from "../types/api";
|
||||
|
||||
|
|
@ -105,6 +106,12 @@ export type EvidenceSubject =
|
|||
morphologyId: string;
|
||||
data?: LogosMorphologyRow;
|
||||
}
|
||||
| {
|
||||
kind: "logos_alignment_edge";
|
||||
packId: string;
|
||||
edgeId: string;
|
||||
data?: LogosAlignmentRow;
|
||||
}
|
||||
| { kind: "vault_entry"; entryIndex: number; data?: VaultEntrySubjectData }
|
||||
| { kind: "audit_event"; eventId: string; data?: AuditEventSubjectData }
|
||||
| { kind: "calibration_class"; className: string; data?: CalibrationClass }
|
||||
|
|
|
|||
73
workbench-ui/src/app/logos/LogosAlignment.layout.golden.json
Normal file
73
workbench-ui/src/app/logos/LogosAlignment.layout.golden.json
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "grc-023",
|
||||
"layer": 0,
|
||||
"row": 0,
|
||||
"x": 24,
|
||||
"y": 24
|
||||
},
|
||||
{
|
||||
"id": "he-001",
|
||||
"layer": 0,
|
||||
"row": 1,
|
||||
"x": 24,
|
||||
"y": 86
|
||||
},
|
||||
{
|
||||
"id": "en-collapse-breath",
|
||||
"layer": 1,
|
||||
"row": 0,
|
||||
"x": 240,
|
||||
"y": 24
|
||||
},
|
||||
{
|
||||
"id": "grc-001",
|
||||
"layer": 1,
|
||||
"row": 1,
|
||||
"x": 240,
|
||||
"y": 86
|
||||
},
|
||||
{
|
||||
"id": "en-024",
|
||||
"layer": 2,
|
||||
"row": 0,
|
||||
"x": 456,
|
||||
"y": 24
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"from": "grc-001",
|
||||
"to": "en-024",
|
||||
"points": {
|
||||
"x1": 384,
|
||||
"y1": 108,
|
||||
"x2": 456,
|
||||
"y2": 46
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": "grc-023",
|
||||
"to": "en-collapse-breath",
|
||||
"points": {
|
||||
"x1": 168,
|
||||
"y1": 46,
|
||||
"x2": 240,
|
||||
"y2": 46
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": "he-001",
|
||||
"to": "grc-001",
|
||||
"points": {
|
||||
"x1": 168,
|
||||
"y1": 108,
|
||||
"x2": 240,
|
||||
"y2": 108
|
||||
}
|
||||
}
|
||||
],
|
||||
"width": 624,
|
||||
"height": 154
|
||||
}
|
||||
114
workbench-ui/src/app/logos/LogosAlignmentTab.test.tsx
Normal file
114
workbench-ui/src/app/logos/LogosAlignmentTab.test.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { layoutDag } from "../../design/components/Dag/layout";
|
||||
import type { LogosAlignmentRow } from "../../types/api";
|
||||
import { AlignmentTab, alignmentToDag } from "./LogosAlignmentTab";
|
||||
import golden from "./LogosAlignment.layout.golden.json";
|
||||
|
||||
// Fixture mirrors the golden generator exactly: he -> grc -> en resonance plus
|
||||
// one undeclared en-collapse-* target (invalid).
|
||||
const FIXTURE: LogosAlignmentRow[] = [
|
||||
{
|
||||
edge_id: "edge-he-grc-1",
|
||||
source_id: "he-001",
|
||||
target_id: "grc-001",
|
||||
relation: "cross_lang.logos.utterance",
|
||||
weight: 0.95,
|
||||
evidence_ids: ["John1:1", "Gen1:1"],
|
||||
target_pack_id: "grc_logos_micro_v1",
|
||||
target_resolved: true,
|
||||
invalid_target: false,
|
||||
},
|
||||
{
|
||||
edge_id: "edge-grc-en-1",
|
||||
source_id: "grc-001",
|
||||
target_id: "en-024",
|
||||
relation: "cross_lang.logos.utterance.en",
|
||||
weight: 0.95,
|
||||
evidence_ids: ["John1:1"],
|
||||
target_pack_id: "en_core_cognition_v1",
|
||||
target_resolved: true,
|
||||
invalid_target: false,
|
||||
},
|
||||
{
|
||||
edge_id: "edge-grc-collapse-1",
|
||||
source_id: "grc-023",
|
||||
target_id: "en-collapse-breath",
|
||||
relation: "cross_lang.no_english_collapse",
|
||||
weight: 0.0,
|
||||
evidence_ids: ["adr-0073c"],
|
||||
target_pack_id: null,
|
||||
target_resolved: false,
|
||||
invalid_target: true,
|
||||
},
|
||||
];
|
||||
|
||||
function compactLayout(rows: readonly LogosAlignmentRow[]) {
|
||||
const { nodes, edges } = alignmentToDag(rows);
|
||||
const layout = layoutDag(nodes, edges);
|
||||
return {
|
||||
nodes: layout.nodes.map(({ id, layer, row, x, y }) => ({ id, layer, row, x, y })),
|
||||
edges: layout.edges.map(({ from, to, points }) => ({ from, to, points })),
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
};
|
||||
}
|
||||
|
||||
describe("alignmentToDag", () => {
|
||||
it("matches the committed golden deterministic layout", () => {
|
||||
expect(compactLayout(FIXTURE)).toEqual(golden);
|
||||
});
|
||||
|
||||
it("is order-independent in node placement (sorted) and re-runs identically", () => {
|
||||
const reversed = [...FIXTURE].reverse();
|
||||
// Nodes are sorted, so node placement is invariant to edge order.
|
||||
expect(compactLayout(reversed).nodes).toEqual(compactLayout(FIXTURE).nodes);
|
||||
// And the projection itself is referentially deterministic.
|
||||
expect(alignmentToDag(FIXTURE)).toEqual(alignmentToDag(FIXTURE));
|
||||
});
|
||||
|
||||
it("derives one node per distinct id and one edge per row", () => {
|
||||
const { nodes, edges } = alignmentToDag(FIXTURE);
|
||||
expect(nodes.map((n) => n.id)).toEqual([
|
||||
"en-024",
|
||||
"en-collapse-breath",
|
||||
"grc-001",
|
||||
"grc-023",
|
||||
"he-001",
|
||||
]);
|
||||
expect(edges).toHaveLength(FIXTURE.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AlignmentTab", () => {
|
||||
it("renders the resonance graph, edge cards, and an honest invalid-target warning", () => {
|
||||
render(<AlignmentTab rows={FIXTURE} selectedEdgeId={null} onSelect={() => {}} />);
|
||||
|
||||
expect(screen.getByLabelText("CORE-Logos cross-language alignment graph")).toBeInTheDocument();
|
||||
expect(screen.getByText("3 alignment edges")).toBeInTheDocument();
|
||||
// The undeclared collapse anchor surfaces as invalid — not smoothed over.
|
||||
expect(screen.getByText("1 invalid target")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/invalid target — en-collapse-breath resolves to no declared lexicon entry/),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText("John1:1").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Gen1:1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onSelect with the row when an edge card is activated", async () => {
|
||||
const onSelect = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<AlignmentTab rows={FIXTURE} selectedEdgeId={null} onSelect={onSelect} />);
|
||||
|
||||
await user.click(screen.getByText("cross_lang.logos.utterance"));
|
||||
expect(onSelect).toHaveBeenCalledWith(FIXTURE[0]);
|
||||
});
|
||||
|
||||
it("renders an honest empty state when the pack declares no edges", () => {
|
||||
render(<AlignmentTab rows={[]} selectedEdgeId={null} onSelect={() => {}} />);
|
||||
expect(
|
||||
screen.getByText("This pack declares no cross-language alignment edges."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
168
workbench-ui/src/app/logos/LogosAlignmentTab.tsx
Normal file
168
workbench-ui/src/app/logos/LogosAlignmentTab.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { useMemo } from "react";
|
||||
import { DagViewer } from "../../design/components/Dag/Dag";
|
||||
import type { DagEdgeInput, DagNodeInput } from "../../design/components/Dag/layout";
|
||||
import type { LogosAlignmentRow } from "../../types/api";
|
||||
|
||||
// Read-only CORE-Logos alignment tab (LG-4). The trilingual resonance graph
|
||||
// (he -> grc -> en) renders through the deterministic Dag primitive; the edge
|
||||
// list carries selection + honest invalid-target warnings. Nothing is
|
||||
// recomputed — every value is a field from GET /logos/packs/{id}/alignment.
|
||||
|
||||
/**
|
||||
* Pure, deterministic projection of alignment rows into Dag inputs. Exported so
|
||||
* the golden-file layout test can pin the geometry. Node order is sorted (not
|
||||
* row order) so the layout is stable regardless of edge ordering; edges follow
|
||||
* row order (the endpoint is already deterministic).
|
||||
*/
|
||||
export function alignmentToDag(rows: readonly LogosAlignmentRow[]): {
|
||||
nodes: DagNodeInput[];
|
||||
edges: DagEdgeInput[];
|
||||
} {
|
||||
const ids = new Set<string>();
|
||||
for (const row of rows) {
|
||||
ids.add(row.source_id);
|
||||
ids.add(row.target_id);
|
||||
}
|
||||
const nodes: DagNodeInput[] = Array.from(ids)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((id) => ({ id, label: id }));
|
||||
const edges: DagEdgeInput[] = rows.map((row) => ({
|
||||
from: row.source_id,
|
||||
to: row.target_id,
|
||||
label: row.relation,
|
||||
}));
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
function languageOf(id: string): string {
|
||||
if (id.startsWith("he-")) return "Hebrew";
|
||||
if (id.startsWith("grc-")) return "Koine Greek";
|
||||
if (id.startsWith("en-")) return "English";
|
||||
return "—";
|
||||
}
|
||||
|
||||
function EdgeCard({
|
||||
row,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
row: LogosAlignmentRow;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-current={selected ? "true" : undefined}
|
||||
onClick={onSelect}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
onSelect();
|
||||
}}
|
||||
className={`grid gap-1 rounded-md border px-3 py-2 text-left transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)] ${
|
||||
row.invalid_target
|
||||
? "border-[var(--color-state-warning-border)] bg-[var(--color-state-warning-bg)]"
|
||||
: selected
|
||||
? "border-[var(--color-selected-border)] bg-[var(--color-selected-bg)]"
|
||||
: "border-[var(--color-border-subtle)] hover:bg-[var(--color-surface-inset)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 font-mono text-xs text-[var(--color-text-primary)]">
|
||||
<span className="truncate">
|
||||
{row.source_id} <span aria-hidden className="text-[var(--color-text-muted)]">→</span> {row.target_id}
|
||||
</span>
|
||||
<span className="text-[var(--color-text-secondary)]">{row.weight.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[var(--color-text-secondary)]">
|
||||
<span>{row.relation}</span>
|
||||
<span aria-hidden className="text-[var(--color-text-muted)]">·</span>
|
||||
<span>
|
||||
{languageOf(row.source_id)} → {languageOf(row.target_id)}
|
||||
</span>
|
||||
{row.target_pack_id ? (
|
||||
<>
|
||||
<span aria-hidden className="text-[var(--color-text-muted)]">·</span>
|
||||
<span className="font-mono">{row.target_pack_id}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{row.evidence_ids.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{row.evidence_ids.map((e) => (
|
||||
<span
|
||||
key={e}
|
||||
className="inline-flex h-5 items-center rounded border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] px-1.5 text-[10px] font-mono text-[var(--color-text-secondary)]"
|
||||
>
|
||||
{e}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{row.invalid_target ? (
|
||||
<p className="m-0 text-[11px] text-[var(--color-state-warning-text)]">
|
||||
invalid target — {row.target_id} resolves to no declared lexicon entry
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlignmentTab({
|
||||
rows,
|
||||
selectedEdgeId,
|
||||
onSelect,
|
||||
}: {
|
||||
rows: readonly LogosAlignmentRow[];
|
||||
selectedEdgeId: string | null;
|
||||
onSelect: (row: LogosAlignmentRow) => void;
|
||||
}) {
|
||||
const { nodes, edges } = useMemo(() => alignmentToDag(rows), [rows]);
|
||||
const invalidCount = useMemo(
|
||||
() => rows.filter((r) => r.invalid_target).length,
|
||||
[rows],
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
|
||||
This pack declares no cross-language alignment edges.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid min-h-0 gap-4">
|
||||
<section>
|
||||
<h3 className="m-0 mb-2 text-xs font-semibold text-[var(--color-text-secondary)]">
|
||||
Resonance graph (deterministic)
|
||||
</h3>
|
||||
<DagViewer
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
ariaLabel="CORE-Logos cross-language alignment graph"
|
||||
showInspector={false}
|
||||
/>
|
||||
</section>
|
||||
<section className="grid gap-2">
|
||||
<div className="flex items-center justify-between text-xs text-[var(--color-text-secondary)]">
|
||||
<span>{rows.length} alignment edges</span>
|
||||
{invalidCount > 0 ? (
|
||||
<span className="text-[var(--color-state-warning-text)]">
|
||||
{invalidCount} invalid target{invalidCount === 1 ? "" : "s"}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{rows.map((row) => (
|
||||
<EdgeCard
|
||||
key={row.edge_id}
|
||||
row={row}
|
||||
selected={row.edge_id === selectedEdgeId}
|
||||
onSelect={() => onSelect(row)}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestQueryClient } from "../../test/createTestQueryClient";
|
||||
import type {
|
||||
LogosAlignmentRow,
|
||||
LogosPackContents,
|
||||
LogosPackOverview,
|
||||
LogosPackSummary,
|
||||
|
|
@ -174,6 +175,34 @@ function contentsFor(packId: string): LogosPackContents {
|
|||
};
|
||||
}
|
||||
|
||||
function alignmentFor(packId: string): LogosAlignmentRow[] {
|
||||
const he = packId.includes("he");
|
||||
return [
|
||||
{
|
||||
edge_id: "edge-1",
|
||||
source_id: he ? "he-001" : "grc-001",
|
||||
target_id: he ? "grc-001" : "en-024",
|
||||
relation: "cross_lang.logos.utterance",
|
||||
weight: 0.95,
|
||||
evidence_ids: ["John1:1", "Gen1:1"],
|
||||
target_pack_id: he ? "grc_logos_micro_v1" : "en_core_cognition_v1",
|
||||
target_resolved: true,
|
||||
invalid_target: false,
|
||||
},
|
||||
{
|
||||
edge_id: "edge-collapse",
|
||||
source_id: he ? "he-023" : "grc-023",
|
||||
target_id: "en-collapse-breath",
|
||||
relation: "cross_lang.no_english_collapse",
|
||||
weight: 0.0,
|
||||
evidence_ids: ["adr-0073c"],
|
||||
target_pack_id: null,
|
||||
target_resolved: false,
|
||||
invalid_target: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function okEnvelope(data: unknown) {
|
||||
return { ok: true, generated_at: "2026-06-14T00:00:00Z", data };
|
||||
}
|
||||
|
|
@ -202,6 +231,13 @@ function stubLogosFetch() {
|
|||
json: async () => okEnvelope(contentsFor(packId)),
|
||||
});
|
||||
}
|
||||
const alignmentMatch = path.match(/^\/logos\/packs\/([^/]+)\/alignment$/);
|
||||
if (alignmentMatch) {
|
||||
const packId = decodeURIComponent(alignmentMatch[1]);
|
||||
return Promise.resolve({
|
||||
json: async () => okEnvelope({ items: alignmentFor(packId) }),
|
||||
});
|
||||
}
|
||||
const overviewMatch = path.match(/^\/logos\/packs\/([^/]+)$/);
|
||||
if (overviewMatch) {
|
||||
const packId = decodeURIComponent(overviewMatch[1]);
|
||||
|
|
@ -298,7 +334,7 @@ describe("LogosRoute", () => {
|
|||
expect(screen.queryByText(/Draft proposal/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selects a pack, renders Overview, projects logos_pack evidence, and defers only the alignment endpoint", async () => {
|
||||
it("selects a pack, renders Overview, projects logos_pack evidence, and fetches contents + alignment", async () => {
|
||||
const { paths } = stubLogosFetch();
|
||||
const user = userEvent.setup();
|
||||
renderRoute();
|
||||
|
|
@ -316,9 +352,9 @@ describe("LogosRoute", () => {
|
|||
expect(screen.queryByText(/proof card/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText("CORE-Logos Pack")).toBeInTheDocument();
|
||||
expect(screen.getByText("0 / missing_evidence")).toBeInTheDocument();
|
||||
// LG-3 fetches contents eagerly on pack-select; /alignment stays deferred to LG-4.
|
||||
// LG-3 + LG-4 fetch contents and alignment eagerly on pack-select.
|
||||
expect(paths).toContain("/logos/packs/he_logos_micro_v1/contents");
|
||||
expect(paths).not.toContain("/logos/packs/he_logos_micro_v1/alignment");
|
||||
expect(paths).toContain("/logos/packs/he_logos_micro_v1/alignment");
|
||||
});
|
||||
|
||||
it("renders Identity passport fields and the raw live overview projection", async () => {
|
||||
|
|
@ -411,4 +447,44 @@ describe("LogosRoute", () => {
|
|||
await user.click(screen.getByText("word, matter, or spoken thing"));
|
||||
expect(await screen.findByText("CORE-Logos Gloss")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the Alignment tab, surfaces invalid targets, and projects a logos_alignment_edge subject", async () => {
|
||||
stubLogosFetch();
|
||||
const user = userEvent.setup();
|
||||
renderRoute("/logos/he_logos_micro_v1");
|
||||
|
||||
await user.click(await screen.findByRole("tab", { name: "Alignment" }));
|
||||
|
||||
// Real edges from /alignment, deterministic graph, honest invalid-target warning.
|
||||
expect(
|
||||
await screen.findByLabelText("CORE-Logos cross-language alignment graph"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("2 alignment edges")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 invalid target")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/invalid target — en-collapse-breath resolves to no declared lexicon entry/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText("cross_lang.logos.utterance"));
|
||||
|
||||
expect(await screen.findByText("CORE-Logos Alignment Edge")).toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("location")).toHaveTextContent(
|
||||
"inspect=logos_alignment_edge%3Ahe_logos_micro_v1%2Fedge-1",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("never renders a holonomy tab or proof/success element, even with alignment present", async () => {
|
||||
stubLogosFetch();
|
||||
const user = userEvent.setup();
|
||||
renderRoute("/logos/he_logos_micro_v1");
|
||||
|
||||
await user.click(await screen.findByRole("tab", { name: "Alignment" }));
|
||||
await screen.findByLabelText("CORE-Logos cross-language alignment graph");
|
||||
|
||||
expect(screen.queryByRole("tab", { name: /holonomy/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/proof card/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/holonomy proof/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WorkbenchApiError } from "../../api/client";
|
||||
import {
|
||||
useLogosPackAlignment,
|
||||
useLogosPackContents,
|
||||
useLogosPackOverview,
|
||||
useLogosPacks,
|
||||
|
|
@ -21,6 +22,7 @@ import {
|
|||
SafetyVerdictBadge,
|
||||
} from "../../design/components/badges";
|
||||
import type {
|
||||
LogosAlignmentRow,
|
||||
LogosAlignmentTargetIssue,
|
||||
LogosGlossRow,
|
||||
LogosLexiconRow,
|
||||
|
|
@ -36,6 +38,7 @@ import { pushRecentItem } from "../commandRegistry";
|
|||
import { subjectToUrl } from "../evidenceAddress";
|
||||
import { useEvidenceSubject, type EvidenceSubject } from "../evidenceContext";
|
||||
import { GlossesTab, LexiconTab, MorphologyTab } from "./LogosContentsTabs";
|
||||
import { AlignmentTab } from "./LogosAlignmentTab";
|
||||
|
||||
const LOGOS_TABS: readonly Tab[] = [
|
||||
{ id: "overview", label: "Overview" },
|
||||
|
|
@ -43,6 +46,7 @@ const LOGOS_TABS: readonly Tab[] = [
|
|||
{ id: "lexicon", label: "Lexicon" },
|
||||
{ id: "glosses", label: "Glosses" },
|
||||
{ id: "morphology", label: "Morphology" },
|
||||
{ id: "alignment", label: "Alignment" },
|
||||
{ id: "safety", label: "Safety" },
|
||||
];
|
||||
|
||||
|
|
@ -586,10 +590,12 @@ interface ContentsSelection {
|
|||
entryId: string | null;
|
||||
glossId: string | null;
|
||||
morphologyId: string | null;
|
||||
edgeId: string | null;
|
||||
danglingEntryIds: ReadonlySet<string>;
|
||||
onSelectEntry: (row: LogosLexiconRow) => void;
|
||||
onSelectGloss: (row: LogosGlossRow) => void;
|
||||
onSelectMorphology: (row: LogosMorphologyRow) => void;
|
||||
onSelectEdge: (row: LogosAlignmentRow) => void;
|
||||
}
|
||||
|
||||
function ContentsGate({
|
||||
|
|
@ -658,6 +664,9 @@ function Workspace({
|
|||
contents,
|
||||
contentsLoading,
|
||||
contentsError,
|
||||
alignment,
|
||||
alignmentLoading,
|
||||
alignmentError,
|
||||
selection,
|
||||
}: {
|
||||
selectedPackId: string | null;
|
||||
|
|
@ -670,6 +679,9 @@ function Workspace({
|
|||
contents?: LogosPackContents;
|
||||
contentsLoading: boolean;
|
||||
contentsError: unknown;
|
||||
alignment?: LogosAlignmentRow[];
|
||||
alignmentLoading: boolean;
|
||||
alignmentError: unknown;
|
||||
selection: ContentsSelection;
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
|
|
@ -721,6 +733,24 @@ function Workspace({
|
|||
selection={selection}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === "alignment" ? (
|
||||
alignmentLoading ? (
|
||||
<LoadingState label="Loading CORE-Logos alignment..." />
|
||||
) : alignmentError ? (
|
||||
<ErrorState
|
||||
whatFailed={errorMessage(alignmentError)}
|
||||
mutationStatus="No Logos mutation occurred."
|
||||
reproducer={`curl /logos/packs/${selectedPackId}/alignment`}
|
||||
retrySafety="Retry: safe"
|
||||
/>
|
||||
) : alignment ? (
|
||||
<AlignmentTab
|
||||
rows={alignment}
|
||||
selectedEdgeId={selection.edgeId}
|
||||
onSelect={selection.onSelectEdge}
|
||||
/>
|
||||
) : null
|
||||
) : null}
|
||||
{activeTab === "safety" ? (
|
||||
safetyLoading ? (
|
||||
<LoadingState label="Loading CORE-Logos safety..." />
|
||||
|
|
@ -750,6 +780,7 @@ export function LogosRoute() {
|
|||
const overviewQuery = useLogosPackOverview(selectedPackId);
|
||||
const safetyQuery = useLogosPackSafety(selectedPackId);
|
||||
const contentsQuery = useLogosPackContents(selectedPackId);
|
||||
const alignmentQuery = useLogosPackAlignment(selectedPackId);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPackId === null) return;
|
||||
|
|
@ -782,6 +813,7 @@ export function LogosRoute() {
|
|||
entryId: subject.kind === "logos_entry" ? subject.entryId : null,
|
||||
glossId: subject.kind === "logos_gloss" ? subject.glossId : null,
|
||||
morphologyId: subject.kind === "logos_morphology" ? subject.morphologyId : null,
|
||||
edgeId: subject.kind === "logos_alignment_edge" ? subject.edgeId : null,
|
||||
danglingEntryIds: new Set(
|
||||
safetyQuery.data?.dangling_morphology_links.map((issue) => issue.entry_id) ?? [],
|
||||
),
|
||||
|
|
@ -808,6 +840,17 @@ export function LogosRoute() {
|
|||
data: row,
|
||||
},
|
||||
),
|
||||
onSelectEdge: (row) =>
|
||||
selectSubEntity(
|
||||
selectedPackId === null
|
||||
? { kind: "none" }
|
||||
: {
|
||||
kind: "logos_alignment_edge",
|
||||
packId: selectedPackId,
|
||||
edgeId: row.edge_id,
|
||||
data: row,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
if (packsQuery.isLoading) {
|
||||
|
|
@ -856,6 +899,9 @@ export function LogosRoute() {
|
|||
contents={contentsQuery.data}
|
||||
contentsLoading={contentsQuery.isLoading}
|
||||
contentsError={contentsQuery.isError ? contentsQuery.error : null}
|
||||
alignment={alignmentQuery.data}
|
||||
alignmentLoading={alignmentQuery.isLoading}
|
||||
alignmentError={alignmentQuery.isError ? alignmentQuery.error : null}
|
||||
selection={selection}
|
||||
/>
|
||||
</section>
|
||||
|
|
|
|||
Loading…
Reference in a new issue