From b563c279ab0554b5280ec8eae5b5e5a24c9f867e Mon Sep 17 00:00:00 2001 From: even-wei Date: Wed, 1 Apr 2026 11:29:03 +0800 Subject: [PATCH 1/3] feat(lineage): add Body Changes view mode to filter config-only changes Add a new "Body Changes" mode to the lineage view Mode dropdown that uses state:modified.body + state:modified.macros + state:modified.contract selectors. This filters out config-only YAML changes (tags, descriptions, deprecation dates, etc.) while keeping data-impacting changes visible. The existing "Changed Models" and "All" modes remain unchanged. Resolves DRC-3047 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: even-wei --- js/packages/ui/src/api/lineagecheck.ts | 2 +- js/packages/ui/src/api/schemacheck.ts | 2 +- js/packages/ui/src/api/select.ts | 2 +- js/packages/ui/src/api/types/run.ts | 4 ++-- .../ui/src/components/check/SchemaDiffView.tsx | 2 +- .../lineage/topbar/LineageViewTopBar.tsx | 15 ++++++++++++++- .../lineage/__tests__/LineageViewTopBar.test.tsx | 14 ++++++++++++++ recce/adapter/base.py | 2 +- recce/adapter/dbt_adapter/__init__.py | 10 ++++++++-- recce/mcp_server.py | 8 ++++---- recce/server.py | 2 +- recce/tasks/core.py | 2 +- recce/tasks/lineage.py | 2 +- recce/tasks/rowcount.py | 2 +- recce/tasks/schema.py | 2 +- 15 files changed, 52 insertions(+), 19 deletions(-) diff --git a/js/packages/ui/src/api/lineagecheck.ts b/js/packages/ui/src/api/lineagecheck.ts index d94b60abe..404657761 100644 --- a/js/packages/ui/src/api/lineagecheck.ts +++ b/js/packages/ui/src/api/lineagecheck.ts @@ -13,7 +13,7 @@ import type { CllInput } from "./cll"; // ============================================================================ export interface LineageDiffViewOptions { - view_mode?: "changed_models" | "all"; + view_mode?: "changed_models" | "all" | "body_changes"; node_ids?: string[]; packages?: string[]; select?: string; diff --git a/js/packages/ui/src/api/schemacheck.ts b/js/packages/ui/src/api/schemacheck.ts index d9a3e9fd5..0c9077bc7 100644 --- a/js/packages/ui/src/api/schemacheck.ts +++ b/js/packages/ui/src/api/schemacheck.ts @@ -15,7 +15,7 @@ export interface SchemaDiffViewParams { node_id?: string | string[]; select?: string; exclude?: string; - view_mode?: "all" | "changed_models"; + view_mode?: "all" | "changed_models" | "body_changes"; packages?: string[]; } diff --git a/js/packages/ui/src/api/select.ts b/js/packages/ui/src/api/select.ts index e1bbcd89a..bd7cf14dd 100644 --- a/js/packages/ui/src/api/select.ts +++ b/js/packages/ui/src/api/select.ts @@ -6,7 +6,7 @@ export interface SelectInput { select?: string; exclude?: string; packages?: string[]; - view_mode?: "all" | "changed_models"; + view_mode?: "all" | "changed_models" | "body_changes"; } export interface SelectOutput { diff --git a/js/packages/ui/src/api/types/run.ts b/js/packages/ui/src/api/types/run.ts index 9e363a46f..2a0f21251 100644 --- a/js/packages/ui/src/api/types/run.ts +++ b/js/packages/ui/src/api/types/run.ts @@ -120,7 +120,7 @@ export interface SchemaDiffParams { /** Package names to include */ packages?: string[]; /** View mode - show all models or only changed ones */ - view_mode?: "all" | "changed_models"; + view_mode?: "all" | "changed_models" | "body_changes"; } /** @@ -135,7 +135,7 @@ export interface LineageDiffParams { /** Package names to include */ packages?: string[]; /** View mode - show all models or only changed ones */ - view_mode?: "all" | "changed_models"; + view_mode?: "all" | "changed_models" | "body_changes"; } // ============================================================================ diff --git a/js/packages/ui/src/components/check/SchemaDiffView.tsx b/js/packages/ui/src/components/check/SchemaDiffView.tsx index 275cf589e..c6e0210c4 100644 --- a/js/packages/ui/src/components/check/SchemaDiffView.tsx +++ b/js/packages/ui/src/components/check/SchemaDiffView.tsx @@ -28,7 +28,7 @@ export interface SchemaDiffParams { node_id?: string | string[]; select?: string; exclude?: string; - view_mode?: "all" | "changed_models"; + view_mode?: "all" | "changed_models" | "body_changes"; packages?: string[]; } diff --git a/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx b/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx index 7b39a7ff1..26a6b97a0 100644 --- a/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx +++ b/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx @@ -209,7 +209,12 @@ const ViewModeSelectMenu = ({ const open = Boolean(anchorEl); const viewMode = viewOptions.view_mode ?? "changed_models"; - const label = viewMode === "changed_models" ? "Changed Models" : "All"; + const label = + viewMode === "body_changes" + ? "Body Changes" + : viewMode === "changed_models" + ? "Changed Models" + : "All"; const handleClick = (event: MouseEvent) => { setAnchorEl(event.currentTarget); @@ -256,6 +261,14 @@ const ViewModeSelectMenu = ({ sx={{ m: 0 }} /> + handleSelect("body_changes")}> + } + label="Body Changes" + sx={{ m: 0 }} + /> + handleSelect("all")}> { expect(modeButton).toBeDefined(); }); + it("shows Body Changes when view mode is body_changes", () => { + mockUseLineageViewContextSafe.mockReturnValue( + createMockLineageViewContext({ + viewOptions: { view_mode: "body_changes" }, + }), + ); + + render(); + + expect( + screen.getByRole("button", { name: /Body Changes/i }), + ).toBeInTheDocument(); + }); + it("opens mode menu when clicked", async () => { render(); diff --git a/recce/adapter/base.py b/recce/adapter/base.py index 9cbd41d42..e17b70fa7 100644 --- a/recce/adapter/base.py +++ b/recce/adapter/base.py @@ -54,7 +54,7 @@ def select_nodes( select: Optional[str] = None, exclude: Optional[str] = None, packages: Optional[list[str]] = None, - view_mode: Optional[Literal["all", "changed_models"]] = None, + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None, ) -> Set[str]: raise NotImplementedError() diff --git a/recce/adapter/dbt_adapter/__init__.py b/recce/adapter/dbt_adapter/__init__.py index c4a109c34..e7fc21a79 100644 --- a/recce/adapter/dbt_adapter/__init__.py +++ b/recce/adapter/dbt_adapter/__init__.py @@ -1852,7 +1852,7 @@ def select_nodes( select: Optional[str] = None, exclude: Optional[str] = None, packages: Optional[list[str]] = None, - view_mode: Optional[Literal["all", "changed_models"]] = None, + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None, ) -> Set[str]: import dbt.compilation from dbt.compilation import Compiler @@ -1886,8 +1886,14 @@ def _parse_difference(include, exclude): if packages is not None: package_spec = SelectionUnion([_parse_difference([f"package:{p}"], None) for p in packages]) specs.append(package_spec) - if view_mode and view_mode == "changed_models": + if view_mode == "changed_models": specs.append(_parse_difference(["1+state:modified+"], None)) + elif view_mode == "body_changes": + specs.append( + _parse_difference( + ["1+state:modified.body+", "1+state:modified.macros+", "1+state:modified.contract+"], None + ) + ) spec = SelectionIntersection(specs) manifest = Manifest() diff --git a/recce/mcp_server.py b/recce/mcp_server.py index 39726b1b3..def772660 100644 --- a/recce/mcp_server.py +++ b/recce/mcp_server.py @@ -241,9 +241,9 @@ async def list_tools() -> List[Tool]: }, "view_mode": { "type": "string", - "enum": ["changed_models", "all"], + "enum": ["changed_models", "all", "body_changes"], "default": "changed_models", - "description": "View mode: 'changed_models' for only changed models (default), 'all' for all models", + "description": "View mode: 'changed_models' for only changed models (default), 'all' for all models, 'body_changes' for SQL body/macro/contract changes only", }, }, }, @@ -362,8 +362,8 @@ async def list_tools() -> List[Tool]: }, "view_mode": { "type": "string", - "enum": ["all", "changed_models"], - "description": "View mode: 'all' for all models, 'changed_models' for only changed models (optional)", + "enum": ["all", "changed_models", "body_changes"], + "description": "View mode: 'all' for all models, 'changed_models' for only changed models (optional), 'body_changes' for SQL body/macro/contract changes only", }, }, }, diff --git a/recce/server.py b/recce/server.py index 45ee6a416..e42a10479 100644 --- a/recce/server.py +++ b/recce/server.py @@ -663,7 +663,7 @@ class SelectNodesInput(BaseModel): select: Optional[str] = None exclude: Optional[str] = None packages: Optional[list[str]] = None - view_mode: Optional[Literal["all", "changed_models"]] = None + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None class SelectNodesOutput(BaseModel): diff --git a/recce/tasks/core.py b/recce/tasks/core.py index 77f2c5af1..d472cbc11 100644 --- a/recce/tasks/core.py +++ b/recce/tasks/core.py @@ -77,7 +77,7 @@ def get_node_ids_by_selector( select: Optional[str] = None, exclude: Optional[str] = None, packages: Optional[list[str]] = None, - view_mode: Optional[Literal["all", "changed_models"]] = None, + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None, ) -> List[str]: nodes = default_context().adapter.select_nodes( select=select, exclude=exclude, packages=packages, view_mode=view_mode diff --git a/recce/tasks/lineage.py b/recce/tasks/lineage.py index f27f177cc..8132a9f1c 100644 --- a/recce/tasks/lineage.py +++ b/recce/tasks/lineage.py @@ -10,7 +10,7 @@ class LineageDiffParams(BaseModel): select: Optional[str] = None exclude: Optional[str] = None packages: Optional[list[str]] = None - view_mode: Optional[Literal["all", "changed_models"]] = None + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None class LineageDiffCheckValidator(CheckValidator): diff --git a/recce/tasks/rowcount.py b/recce/tasks/rowcount.py index 5d4b7b678..811acb7ef 100644 --- a/recce/tasks/rowcount.py +++ b/recce/tasks/rowcount.py @@ -253,7 +253,7 @@ class RowCountDiffParams(BaseModel): select: Optional[str] = None exclude: Optional[str] = None packages: Optional[list[str]] = None - view_mode: Optional[Literal["all", "changed_models"]] = None + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None class RowCountDiffTask(Task, QueryMixin): diff --git a/recce/tasks/schema.py b/recce/tasks/schema.py index 74a419fd6..359068373 100644 --- a/recce/tasks/schema.py +++ b/recce/tasks/schema.py @@ -56,7 +56,7 @@ class SchemaDiffParams(BaseModel): select: Optional[str] = None exclude: Optional[str] = None packages: Optional[list[str]] = None - view_mode: Optional[Literal["all", "changed_models"]] = None + view_mode: Optional[Literal["all", "changed_models", "body_changes"]] = None class SchemaDiffCheckValidator(CheckValidator): From 9fbdd183b000829ac86d35f50e5f08a648c41fd4 Mon Sep 17 00:00:00 2001 From: even-wei Date: Wed, 1 Apr 2026 12:09:23 +0800 Subject: [PATCH 2/3] fix: address review findings for body_changes mode - Add missing body_changes to LineageView.tsx and LineageDiffView.tsx types - Handle body_changes in Cloud LineageView node filtering (was falling through to "all") - Update stale comments in run.ts, LineageViewTopBar docstring, and 3 test files - Add Body Changes radio to dropdown assertion in existing test - Add interaction test for switching to body_changes mode Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: even-wei --- js/packages/ui/src/api/types/run.ts | 4 +-- .../src/components/check/LineageDiffView.tsx | 2 +- .../ui/src/components/lineage/LineageView.tsx | 9 ++++-- .../lineage/topbar/LineageViewTopBar.tsx | 2 +- .../__tests__/LineageViewTopBar.test.tsx | 31 +++++++++++++++++++ tests/tasks/test_lineage.py | 2 +- tests/tasks/test_row_count.py | 2 +- tests/tasks/test_schema.py | 2 +- 8 files changed, 44 insertions(+), 10 deletions(-) diff --git a/js/packages/ui/src/api/types/run.ts b/js/packages/ui/src/api/types/run.ts index 2a0f21251..c46c8ee72 100644 --- a/js/packages/ui/src/api/types/run.ts +++ b/js/packages/ui/src/api/types/run.ts @@ -119,7 +119,7 @@ export interface SchemaDiffParams { exclude?: string; /** Package names to include */ packages?: string[]; - /** View mode - show all models or only changed ones */ + /** View mode - 'all', 'changed_models', or 'body_changes' (SQL body/macro/contract only) */ view_mode?: "all" | "changed_models" | "body_changes"; } @@ -134,7 +134,7 @@ export interface LineageDiffParams { exclude?: string; /** Package names to include */ packages?: string[]; - /** View mode - show all models or only changed ones */ + /** View mode - 'all', 'changed_models', or 'body_changes' (SQL body/macro/contract only) */ view_mode?: "all" | "changed_models" | "body_changes"; } diff --git a/js/packages/ui/src/components/check/LineageDiffView.tsx b/js/packages/ui/src/components/check/LineageDiffView.tsx index 3bf7d5ff2..b2b419c0a 100644 --- a/js/packages/ui/src/components/check/LineageDiffView.tsx +++ b/js/packages/ui/src/components/check/LineageDiffView.tsx @@ -10,7 +10,7 @@ import { LineageView, type LineageViewRef } from "../lineage/LineageView"; * View options for lineage diff checks */ export interface LineageDiffViewOptions { - view_mode?: "changed_models" | "all"; + view_mode?: "changed_models" | "all" | "body_changes"; node_ids?: string[]; select?: string; exclude?: string; diff --git a/js/packages/ui/src/components/lineage/LineageView.tsx b/js/packages/ui/src/components/lineage/LineageView.tsx index 21b3a4f9b..8e6043421 100644 --- a/js/packages/ui/src/components/lineage/LineageView.tsx +++ b/js/packages/ui/src/components/lineage/LineageView.tsx @@ -43,7 +43,7 @@ export interface LineageViewProps { * View options for lineage diff visualization */ viewOptions?: { - view_mode?: "changed_models" | "all"; + view_mode?: "changed_models" | "all" | "body_changes"; node_ids?: string[]; select?: string; exclude?: string; @@ -201,8 +201,11 @@ export const LineageView = forwardRef( if (viewOptions?.node_ids && viewOptions.node_ids.length > 0) { // Explicit node selection selectedNodeIds = viewOptions.node_ids; - } else if (viewOptions?.view_mode === "changed_models") { - // Only changed models + } else if ( + viewOptions?.view_mode === "changed_models" || + viewOptions?.view_mode === "body_changes" + ) { + // Changed models or body changes (filtering done server-side via selector) selectedNodeIds = lineageGraph.modifiedSet; } diff --git a/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx b/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx index 26a6b97a0..4540512f9 100644 --- a/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx +++ b/js/packages/ui/src/components/lineage/topbar/LineageViewTopBar.tsx @@ -572,7 +572,7 @@ const DefaultSetupConnectionPopover = ({ * LineageViewTopBar Component * * Top toolbar for the lineage view providing: - * - View mode selection (Changed Models vs All) + * - View mode selection (Changed Models, Body Changes, All) * - Package filtering * - Node selector filters (Select, Exclude) * - Actions menu for diff operations and checklist additions diff --git a/js/src/components/lineage/__tests__/LineageViewTopBar.test.tsx b/js/src/components/lineage/__tests__/LineageViewTopBar.test.tsx index 12e5b56f9..86d74c625 100644 --- a/js/src/components/lineage/__tests__/LineageViewTopBar.test.tsx +++ b/js/src/components/lineage/__tests__/LineageViewTopBar.test.tsx @@ -409,12 +409,43 @@ describe("LineageViewTopBar", () => { expect( screen.getByRole("radio", { name: /Changed Models/i }), ).toBeInTheDocument(); + expect( + screen.getByRole("radio", { name: /Body Changes/i }), + ).toBeInTheDocument(); expect( screen.getByRole("radio", { name: /^All$/i }), ).toBeInTheDocument(); }); }); + it("calls onViewOptionsChanged when mode is changed to body_changes", async () => { + const mockOnViewOptionsChanged = vi.fn(); + mockUseLineageViewContextSafe.mockReturnValue( + createMockLineageViewContext({ + viewOptions: { view_mode: "changed_models" }, + onViewOptionsChanged: mockOnViewOptionsChanged, + }), + ); + + render(); + + const modeButton = screen.getByRole("button", { + name: /Changed Models/i, + }); + fireEvent.click(modeButton); + + await waitFor(() => { + const bodyChangesMenuItem = screen.getByRole("menuitem", { + name: /Body Changes/i, + }); + fireEvent.click(bodyChangesMenuItem); + }); + + expect(mockOnViewOptionsChanged).toHaveBeenCalledWith( + expect.objectContaining({ view_mode: "body_changes" }), + ); + }); + it("calls onViewOptionsChanged when mode is changed to all", async () => { const mockOnViewOptionsChanged = vi.fn(); mockUseLineageViewContextSafe.mockReturnValue( diff --git a/tests/tasks/test_lineage.py b/tests/tasks/test_lineage.py index 3d5d305fb..167745b69 100644 --- a/tests/tasks/test_lineage.py +++ b/tests/tasks/test_lineage.py @@ -36,7 +36,7 @@ def validate(params: dict): } ) - # view_mode should be 'all' or 'changed_models' + # view_mode should be 'all', 'changed_models', or 'body_changes' validate( { "view_mode": None, diff --git a/tests/tasks/test_row_count.py b/tests/tasks/test_row_count.py index 2e59e7f4a..99becca3c 100644 --- a/tests/tasks/test_row_count.py +++ b/tests/tasks/test_row_count.py @@ -130,7 +130,7 @@ def validate(params: dict): } ) - # view_mode should be 'all' or 'changed_models' + # view_mode should be 'all', 'changed_models', or 'body_changes' validate( { "view_mode": None, diff --git a/tests/tasks/test_schema.py b/tests/tasks/test_schema.py index edf799e16..c4e04b667 100644 --- a/tests/tasks/test_schema.py +++ b/tests/tasks/test_schema.py @@ -56,7 +56,7 @@ def validate(params: dict): } ) - # view_mode should be 'all' or 'changed_models' + # view_mode should be 'all', 'changed_models', or 'body_changes' validate( { "view_mode": None, From b06c3ed341e4e07f0396453c470ba5a148273809 Mon Sep 17 00:00:00 2001 From: even-wei Date: Wed, 1 Apr 2026 12:33:03 +0800 Subject: [PATCH 3/3] test: add selector test for body_changes view mode Cover the body_changes branch in select_nodes to fix patch coverage. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: even-wei --- tests/adapter/dbt_adapter/test_selector.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/adapter/dbt_adapter/test_selector.py b/tests/adapter/dbt_adapter/test_selector.py index 2f4fd6402..0a5cf7a9a 100644 --- a/tests/adapter/dbt_adapter/test_selector.py +++ b/tests/adapter/dbt_adapter/test_selector.py @@ -190,6 +190,10 @@ def test_select_with_pacakage_mode_include_exclude(dbt_test_helper): node_ids = adapter.select_nodes(view_mode="changed_models") assert len(node_ids) == 3 + # body_changes: only body/macros/contract changes (same result here since the change is a body change) + node_ids = adapter.select_nodes(view_mode="body_changes") + assert len(node_ids) == 3 + node_ids = adapter.select_nodes(view_mode="changed_models", packages=["other_package"]) assert len(node_ids) == 1