From 5fe34a6813a022ddc1f06f462f10f0b42cee194b Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 14 Aug 2026 11:11:55 +0200 Subject: [PATCH 1/2] fix(lineage): surface the ambiguity warning on the diagram surfaces too (#584) #579 gave `lineage show` and the JSON routes an ambiguity warning, but the mermaid renderer had no way to carry one: a diagram has no metadata channel, so all three callers dropped it. A Web UI or --format mermaid user therefore still got one project's answer looking like the whole picture -- the exact shape of #568, just on a different surface. render_mermaid now takes an optional `warnings` list and emits each as a standalone, deliberately unconnected note node, styled as a warning so it does not read as part of the dependency graph. All three callers pass what the query result carries: the CLI's --format mermaid/html, the `lineage server` browser, and GET /lineage/mermaid. Warnings go through the same label escaping as every other API-derived string (issue #269 sec-05), covered by a test. Also removes _find_node, which #579 left behind with no callers. Leaving it invites a future caller to bypass candidate resolution entirely, which is the bug #568 reported. --- src/keboola_agent_cli/commands/lineage.py | 5 ++- .../server/routers/lineage.py | 2 +- .../services/deep_lineage_service.py | 18 +++++++-- tests/test_deep_lineage_service.py | 40 +++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/keboola_agent_cli/commands/lineage.py b/src/keboola_agent_cli/commands/lineage.py index 9350f1c8..730571f6 100644 --- a/src/keboola_agent_cli/commands/lineage.py +++ b/src/keboola_agent_cli/commands/lineage.py @@ -401,7 +401,9 @@ def _output_mermaid_or_html( typer.echo(er_code) return - mermaid_code = DeepLineageService.render_mermaid(edges, graph, direction, node_fqn) + mermaid_code = DeepLineageService.render_mermaid( + edges, graph, direction, node_fqn, warnings=query_result.get("warnings") + ) if output_format == "mermaid": typer.echo(mermaid_code) @@ -1368,6 +1370,7 @@ def _handle_mermaid(self, parsed) -> None: direction, node, show_columns=show_cols, + warnings=result.get("warnings"), ) self._serve(mermaid_code, "text/plain") diff --git a/src/keboola_agent_cli/server/routers/lineage.py b/src/keboola_agent_cli/server/routers/lineage.py index 138ab615..c46f57a2 100644 --- a/src/keboola_agent_cli/server/routers/lineage.py +++ b/src/keboola_agent_cli/server/routers/lineage.py @@ -221,6 +221,6 @@ def mermaid( code = DeepLineageService.render_er_diagram(edges, graph, node, show_columns=show_cols) else: code = DeepLineageService.render_mermaid( - edges, graph, direction, node, show_columns=show_cols + edges, graph, direction, node, show_columns=show_cols, warnings=result.get("warnings") ) return PlainTextResponse(code) diff --git a/src/keboola_agent_cli/services/deep_lineage_service.py b/src/keboola_agent_cli/services/deep_lineage_service.py index 85e579b8..f7394e19 100644 --- a/src/keboola_agent_cli/services/deep_lineage_service.py +++ b/src/keboola_agent_cli/services/deep_lineage_service.py @@ -1063,10 +1063,6 @@ def _graph_from_dict(data: dict) -> LineageGraph: ) return graph - def _find_node(self, graph: LineageGraph, identifier: str, project: str = "") -> str | None: - candidates = self._find_node_candidates(graph, identifier, project) - return candidates[0] if candidates else None - def _find_node_candidates( self, graph: LineageGraph, identifier: str, project: str = "" ) -> list[str]: @@ -1120,6 +1116,7 @@ def render_mermaid( direction: str, node_fqn: str, show_columns: bool = False, + warnings: list[str] | None = None, ) -> str: """Render lineage edges as a mermaid flowchart. @@ -1129,6 +1126,12 @@ def render_mermaid( direction: "upstream" or "downstream". node_fqn: The FQN of the queried node. show_columns: If True, include column names in table labels. + warnings: Non-fatal notes to render into the diagram itself, + typically the ambiguity warning from an unqualified id (#568). + A diagram carries no metadata channel the way the JSON shapes + do, so a caller that has warnings and drops them leaves the + viewer with one project's answer looking like the whole + picture -- which is the bug, not a cosmetic omission. Returns: Mermaid flowchart source code. @@ -1139,6 +1142,13 @@ def render_mermaid( graph_dir = "RL" if direction == "upstream" else "LR" lines: list[str] = [f"graph {graph_dir}"] + # Standalone nodes -- deliberately unconnected, so they read as a note + # on the diagram rather than as part of the dependency graph. + for index, warning in enumerate(warnings or []): + warning_id = f"kbagentNote{index}" + lines.append(f' {warning_id}["⚠ {escape(warning)}"]') + lines.append(f" style {warning_id} fill:#fff3cd,stroke:#856404,color:#856404") + # Determine the root node's project for cross-project detection root_project = node_fqn.split(":")[0] if ":" in node_fqn else "" diff --git a/tests/test_deep_lineage_service.py b/tests/test_deep_lineage_service.py index ac529a42..baacc250 100644 --- a/tests/test_deep_lineage_service.py +++ b/tests/test_deep_lineage_service.py @@ -683,6 +683,46 @@ def test_name_only_match_suggests_a_retry_that_resolves(self, tmp_path: Path) -> assert "error" not in retry assert retry["node"] == suggested + def test_mermaid_carries_the_warning_into_the_diagram(self, tmp_path: Path) -> None: + """A diagram has no metadata channel, so the note has to be a node (#584).""" + service = self._service(tmp_path) + graph = _shared_table_graph() + result = service.query_downstream(graph, SHARED_TABLE) + + code = DeepLineageService.render_mermaid( + result["edges"], + graph, + "downstream", + result["node"], + warnings=result.get("warnings"), + ) + + assert "kbagentNote0" in code + assert "2 projects" in code + # Standalone: the note must not become part of the dependency graph. + assert "kbagentNote0 --" not in code and "--> kbagentNote0" not in code + + def test_mermaid_without_warnings_gains_no_note(self, tmp_path: Path) -> None: + service = self._service(tmp_path) + graph = _shared_table_graph() + result = service.query_downstream(graph, f"beta:{SHARED_TABLE}") + + code = DeepLineageService.render_mermaid( + result["edges"], graph, "downstream", result["node"], warnings=result.get("warnings") + ) + + assert "kbagentNote" not in code + + def test_mermaid_warning_is_escaped(self) -> None: + """Warnings reach the diagram as text; sec-05 escaping must apply to them too.""" + code = DeepLineageService.render_mermaid( + [], LineageGraph(), "downstream", "alpha:in.c-a.t", warnings=[''] + ) + + assert "'] + ) + + assert '"hi"' not in code + assert ""hi"" in code + assert "