From eb552331a60254f5d834934349a800ac34a0676d Mon Sep 17 00:00:00 2001 From: mptyl Date: Sun, 28 Jun 2026 12:47:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20cap=20schema-linking=20graph?= =?UTF-8?q?=20on=20promoted=20count=20+=20signals=20in=20table=20Perch?= =?UTF-8?q?=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cap counts promoted candidates only (was all candidates); reuse promoted list (DRY) - New test: 10 promoted + 50 excluded must not cap, graph stays available - Extract reasonFor() helper; table Perché falls back to signal keys like the flowchart Co-Authored-By: Claude Opus 4.8 --- .../src/viewers/SchemaLinkingViewer.test.tsx | 27 ++++++++++++++ frontend/src/viewers/SchemaLinkingViewer.tsx | 36 +++++++++++-------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/frontend/src/viewers/SchemaLinkingViewer.test.tsx b/frontend/src/viewers/SchemaLinkingViewer.test.tsx index 681e9034..7651d2a9 100644 --- a/frontend/src/viewers/SchemaLinkingViewer.test.tsx +++ b/frontend/src/viewers/SchemaLinkingViewer.test.tsx @@ -55,3 +55,30 @@ test("(c) oversized linking (>45) shows cap notice", () => { screen.getByText(/troppi elementi per il grafico/i) ).toBeInTheDocument(); }); + +test("(d) ~10 promoted + ~50 excluded does NOT cap; graph is available", async () => { + const mixedLinking = { + question: "caso misto", + candidates: [ + ...Array.from({ length: 10 }, (_, i) => ({ + kind: "table" as const, + name: `kept_${i}`, + decision: "promoted" as const, + })), + ...Array.from({ length: 50 }, (_, i) => ({ + kind: "table" as const, + name: `dropped_${i}`, + decision: "excluded" as const, + })), + ], + joins: [], + excluded: [], + open_questions: [], + }; + render(); + expect( + screen.queryByText(/troppi elementi per il grafico/i) + ).not.toBeInTheDocument(); + // Grafico (mermaid) view is the default and renders the mocked svg. + await waitFor(() => expect(screen.getByTestId("mm")).toBeInTheDocument()); +}); diff --git a/frontend/src/viewers/SchemaLinkingViewer.tsx b/frontend/src/viewers/SchemaLinkingViewer.tsx index d1c02bb2..c0b9ea24 100644 --- a/frontend/src/viewers/SchemaLinkingViewer.tsx +++ b/frontend/src/viewers/SchemaLinkingViewer.tsx @@ -25,19 +25,23 @@ export interface SchemaLinking { open_questions: string[]; } -function buildFlowchart(candidates: Candidate[], joins: Join[]): string { - const promoted = candidates.filter((c) => c.decision === "promoted"); +/** The inline "perché" comment for a candidate: evidence first, then signal keys. */ +function reasonFor(c: Candidate): string { + if (c.evidence?.length) return c.evidence[0]; + if (c.signals && Object.keys(c.signals).length) + return Object.keys(c.signals).join(", "); + return ""; +} + +function buildFlowchart(promoted: Candidate[], joins: Join[]): string { const nodeId = (name: string) => name.replace(/[^a-zA-Z0-9]/g, "_"); const lines: string[] = ["flowchart TB"]; for (const c of promoted) { const id = nodeId(c.name); - const tooltip = c.evidence?.length - ? ` %% ${c.evidence[0]}` - : c.signals && Object.keys(c.signals).length - ? ` %% ${Object.keys(c.signals).join(", ")}` - : ""; + const reason = reasonFor(c); + const tooltip = reason ? ` %% ${reason}` : ""; const shape = c.kind === "table" ? `[${c.name}]` : `(${c.name})`; lines.push(` ${id}${shape}${tooltip}`); } @@ -52,8 +56,7 @@ function buildFlowchart(candidates: Candidate[], joins: Join[]): string { return lines.join("\n"); } -function buildHierarchicalRows(candidates: Candidate[]) { - const promoted = candidates.filter((c) => c.decision === "promoted"); +function buildHierarchicalRows(promoted: Candidate[]) { const tables = promoted.filter((c) => c.kind === "table"); const columns = promoted.filter((c) => c.kind === "column"); @@ -75,7 +78,9 @@ function buildHierarchicalRows(candidates: Candidate[]) { } export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) { - const oversized = linking.candidates.length > CAP; + const promoted = linking.candidates.filter((c) => c.decision === "promoted"); + // The graph only contains promoted candidates, so cap on that count. + const oversized = promoted.length > CAP; const [view, setView] = useState<"chart" | "table">( oversized ? "table" : "chart" ); @@ -84,16 +89,17 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) { useEffect(() => { if (view !== "chart" || oversized) return; let cancelled = false; - const def = buildFlowchart(linking.candidates, linking.joins); + const def = buildFlowchart(promoted, linking.joins); renderMermaid(def).then((s) => { if (!cancelled) setSvg(s); }); return () => { cancelled = true; }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [view, linking, oversized]); - const { grouped, orphans } = buildHierarchicalRows(linking.candidates); + const { grouped, orphans } = buildHierarchicalRows(promoted); return (
@@ -143,7 +149,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) { {table.name} tabella - {table.evidence?.[0] ?? ""} + {reasonFor(table)} {cols.map((col) => ( @@ -153,7 +159,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) { colonna - {col.evidence?.[0] ?? ""} + {reasonFor(col)} ))} @@ -166,7 +172,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) { colonna - {col.evidence?.[0] ?? ""} + {reasonFor(col)} ))}