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:
@@ -55,3 +55,30 @@ test("(c) oversized linking (>45) shows cap notice", () => {
|
|||||||
screen.getByText(/troppi elementi per il grafico/i)
|
screen.getByText(/troppi elementi per il grafico/i)
|
||||||
).toBeInTheDocument();
|
).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());
|
||||||
|
});
|
||||||
|
|||||||
@@ -25,19 +25,23 @@ export interface SchemaLinking {
|
|||||||
open_questions: string[];
|
open_questions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFlowchart(candidates: Candidate[], joins: Join[]): string {
|
/** The inline "perché" comment for a candidate: evidence first, then signal keys. */
|
||||||
const promoted = candidates.filter((c) => c.decision === "promoted");
|
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 nodeId = (name: string) => name.replace(/[^a-zA-Z0-9]/g, "_");
|
||||||
|
|
||||||
const lines: string[] = ["flowchart TB"];
|
const lines: string[] = ["flowchart TB"];
|
||||||
|
|
||||||
for (const c of promoted) {
|
for (const c of promoted) {
|
||||||
const id = nodeId(c.name);
|
const id = nodeId(c.name);
|
||||||
const tooltip = c.evidence?.length
|
const reason = reasonFor(c);
|
||||||
? ` %% ${c.evidence[0]}`
|
const tooltip = reason ? ` %% ${reason}` : "";
|
||||||
: c.signals && Object.keys(c.signals).length
|
|
||||||
? ` %% ${Object.keys(c.signals).join(", ")}`
|
|
||||||
: "";
|
|
||||||
const shape = c.kind === "table" ? `[${c.name}]` : `(${c.name})`;
|
const shape = c.kind === "table" ? `[${c.name}]` : `(${c.name})`;
|
||||||
lines.push(` ${id}${shape}${tooltip}`);
|
lines.push(` ${id}${shape}${tooltip}`);
|
||||||
}
|
}
|
||||||
@@ -52,8 +56,7 @@ function buildFlowchart(candidates: Candidate[], joins: Join[]): string {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHierarchicalRows(candidates: Candidate[]) {
|
function buildHierarchicalRows(promoted: Candidate[]) {
|
||||||
const promoted = candidates.filter((c) => c.decision === "promoted");
|
|
||||||
const tables = promoted.filter((c) => c.kind === "table");
|
const tables = promoted.filter((c) => c.kind === "table");
|
||||||
const columns = promoted.filter((c) => c.kind === "column");
|
const columns = promoted.filter((c) => c.kind === "column");
|
||||||
|
|
||||||
@@ -75,7 +78,9 @@ function buildHierarchicalRows(candidates: Candidate[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
|
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">(
|
const [view, setView] = useState<"chart" | "table">(
|
||||||
oversized ? "table" : "chart"
|
oversized ? "table" : "chart"
|
||||||
);
|
);
|
||||||
@@ -84,16 +89,17 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view !== "chart" || oversized) return;
|
if (view !== "chart" || oversized) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const def = buildFlowchart(linking.candidates, linking.joins);
|
const def = buildFlowchart(promoted, linking.joins);
|
||||||
renderMermaid(def).then((s) => {
|
renderMermaid(def).then((s) => {
|
||||||
if (!cancelled) setSvg(s);
|
if (!cancelled) setSvg(s);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [view, linking, oversized]);
|
}, [view, linking, oversized]);
|
||||||
|
|
||||||
const { grouped, orphans } = buildHierarchicalRows(linking.candidates);
|
const { grouped, orphans } = buildHierarchicalRows(promoted);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="schema-linking-viewer">
|
<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 className="py-1 pl-1">{table.name}</td>
|
||||||
<td>tabella</td>
|
<td>tabella</td>
|
||||||
<td className="text-xs text-muted-foreground">
|
<td className="text-xs text-muted-foreground">
|
||||||
{table.evidence?.[0] ?? ""}
|
{reasonFor(table)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{cols.map((col) => (
|
{cols.map((col) => (
|
||||||
@@ -153,7 +159,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
|
|||||||
</td>
|
</td>
|
||||||
<td>colonna</td>
|
<td>colonna</td>
|
||||||
<td className="text-xs text-muted-foreground">
|
<td className="text-xs text-muted-foreground">
|
||||||
{col.evidence?.[0] ?? ""}
|
{reasonFor(col)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -166,7 +172,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
|
|||||||
</td>
|
</td>
|
||||||
<td>colonna</td>
|
<td>colonna</td>
|
||||||
<td className="text-xs text-muted-foreground">
|
<td className="text-xs text-muted-foreground">
|
||||||
{col.evidence?.[0] ?? ""}
|
{reasonFor(col)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user