fix(frontend): cap schema-linking graph on promoted count + signals in table Perché

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:47:29 +02:00
co-authored by Claude Opus 4.8
parent 2a23de6825
commit eb552331a6
2 changed files with 48 additions and 15 deletions
@@ -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(<SchemaLinkingViewer linking={mixedLinking} />);
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());
});
+21 -15
View File
@@ -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 (
<div className="schema-linking-viewer">
@@ -143,7 +149,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
<td className="py-1 pl-1">{table.name}</td>
<td>tabella</td>
<td className="text-xs text-muted-foreground">
{table.evidence?.[0] ?? ""}
{reasonFor(table)}
</td>
</tr>
{cols.map((col) => (
@@ -153,7 +159,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
</td>
<td>colonna</td>
<td className="text-xs text-muted-foreground">
{col.evidence?.[0] ?? ""}
{reasonFor(col)}
</td>
</tr>
))}
@@ -166,7 +172,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
</td>
<td>colonna</td>
<td className="text-xs text-muted-foreground">
{col.evidence?.[0] ?? ""}
{reasonFor(col)}
</td>
</tr>
))}