# Business Domain Knowledge Extraction — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a `/understand-domain` skill that extracts business domain knowledge from codebases and renders it as an interactive horizontal flow graph in the dashboard. **Architecture:** Separate `domain-graph.json` file using extended `KnowledgeGraph` schema (3 new node types, 4 new edge types, optional `domainMeta` field). Two analysis paths: lightweight scan (no existing graph) or derivation from existing graph. Dashboard shows domain view by default when available, with pill toggle to switch to structural view. **Tech Stack:** TypeScript, Zod, React Flow (dagre LR layout), Zustand, Vitest, web-tree-sitter **Design Spec:** `docs/plans/2026-04-01-business-domain-knowledge-design.md` --- ## Task 1: Extend Core Types **Files:** - Modify: `understand-anything-plugin/packages/core/src/types.ts` - [ ] **Step 1: Write failing test for new node types** Create test file: ```typescript // understand-anything-plugin/packages/core/src/__tests__/domain-types.test.ts import { describe, it, expect } from "vitest"; import { validateGraph } from "../schema.js"; import type { KnowledgeGraph } from "../types.js"; const domainGraph: KnowledgeGraph = { version: "1.0.0", project: { name: "test-project", languages: ["typescript"], frameworks: [], description: "A test project", analyzedAt: "2026-04-01T00:00:00.000Z", gitCommitHash: "abc123", }, nodes: [ { id: "domain:order-management", type: "domain", name: "Order Management", summary: "Handles order lifecycle", tags: ["core"], complexity: "complex", }, { id: "flow:create-order", type: "flow", name: "Create Order", summary: "Customer submits a new order", tags: ["write-path"], complexity: "moderate", domainMeta: { entryPoint: "POST /api/orders", entryType: "http", }, }, { id: "step:create-order:validate", type: "step", name: "Validate Input", summary: "Checks request body", tags: ["validation"], complexity: "simple", filePath: "src/validators/order.ts", lineRange: [10, 30], }, ], edges: [ { source: "domain:order-management", target: "flow:create-order", type: "contains_flow", direction: "forward", weight: 1.0, }, { source: "flow:create-order", target: "step:create-order:validate", type: "flow_step", direction: "forward", weight: 0.1, }, ], layers: [], tour: [], }; describe("domain graph types", () => { it("validates a domain graph with domain/flow/step node types", () => { const result = validateGraph(domainGraph); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.data!.nodes).toHaveLength(3); expect(result.data!.edges).toHaveLength(2); }); it("validates contains_flow edge type", () => { const result = validateGraph(domainGraph); expect(result.success).toBe(true); expect(result.data!.edges[0].type).toBe("contains_flow"); }); it("validates flow_step edge type", () => { const result = validateGraph(domainGraph); expect(result.success).toBe(true); expect(result.data!.edges[1].type).toBe("flow_step"); }); it("validates cross_domain edge type", () => { const graph = structuredClone(domainGraph); graph.nodes.push({ id: "domain:logistics", type: "domain", name: "Logistics", summary: "Handles shipping", tags: [], complexity: "moderate", }); graph.edges.push({ source: "domain:order-management", target: "domain:logistics", type: "cross_domain", direction: "forward", description: "Triggers on order confirmed", weight: 0.6, }); const result = validateGraph(graph); expect(result.success).toBe(true); }); it("normalizes domain type aliases", () => { const graph = structuredClone(domainGraph); (graph.nodes[0] as any).type = "business_domain"; (graph.nodes[1] as any).type = "workflow"; (graph.nodes[2] as any).type = "action"; const result = validateGraph(graph); expect(result.success).toBe(true); expect(result.data!.nodes[0].type).toBe("domain"); expect(result.data!.nodes[1].type).toBe("flow"); expect(result.data!.nodes[2].type).toBe("step"); }); it("normalizes domain edge type aliases", () => { const graph = structuredClone(domainGraph); (graph.edges[0] as any).type = "has_flow"; (graph.edges[1] as any).type = "next_step"; const result = validateGraph(graph); expect(result.success).toBe(true); expect(result.data!.edges[0].type).toBe("contains_flow"); expect(result.data!.edges[1].type).toBe("flow_step"); }); it("preserves domainMeta on nodes through validation", () => { const result = validateGraph(domainGraph); expect(result.success).toBe(true); // domainMeta is passthrough — schema uses .passthrough() const flowNode = result.data!.nodes.find((n) => n.id === "flow:create-order"); expect((flowNode as any).domainMeta).toEqual({ entryPoint: "POST /api/orders", entryType: "http", }); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/domain-types.test.ts` Expected: FAIL — "domain" is not a valid NodeType enum value - [ ] **Step 3: Add domain/flow/step to NodeType union** In `understand-anything-plugin/packages/core/src/types.ts`, update the `NodeType` union (lines 1-5): ```typescript // Node types (16 total: 5 code + 8 non-code + 3 domain) export type NodeType = | "file" | "function" | "class" | "module" | "concept" | "config" | "document" | "service" | "table" | "endpoint" | "pipeline" | "schema" | "resource" | "domain" | "flow" | "step"; ``` Update `EdgeType` union (lines 7-15): ```typescript // Edge types (30 total in 7 categories) export type EdgeType = | "imports" | "exports" | "contains" | "inherits" | "implements" // Structural | "calls" | "subscribes" | "publishes" | "middleware" // Behavioral | "reads_from" | "writes_to" | "transforms" | "validates" // Data flow | "depends_on" | "tested_by" | "configures" // Dependencies | "related" | "similar_to" // Semantic | "deploys" | "serves" | "provisions" | "triggers" // Infrastructure | "migrates" | "documents" | "routes" | "defines_schema" // Schema/Data | "contains_flow" | "flow_step" | "cross_domain"; // Domain ``` Add `DomainMeta` interface after `GraphNode` (after line 28): ```typescript // Optional domain metadata for domain/flow/step nodes export interface DomainMeta { // For domain nodes entities?: string[]; businessRules?: string[]; crossDomainInteractions?: string[]; // For flow nodes entryPoint?: string; entryType?: "http" | "cli" | "event" | "cron" | "manual"; } ``` - [ ] **Step 4: Update Zod schemas in schema.ts** In `understand-anything-plugin/packages/core/src/schema.ts`: Update `EdgeTypeSchema` (lines 4-12) to add the 4 new edge types: ```typescript export const EdgeTypeSchema = z.enum([ "imports", "exports", "contains", "inherits", "implements", "calls", "subscribes", "publishes", "middleware", "reads_from", "writes_to", "transforms", "validates", "depends_on", "tested_by", "configures", "related", "similar_to", "deploys", "serves", "provisions", "triggers", "migrates", "documents", "routes", "defines_schema", "contains_flow", "flow_step", "cross_domain", ]); ``` Add domain aliases to `NODE_TYPE_ALIASES` (after line 52): ```typescript // Domain aliases business_domain: "domain", process: "flow", workflow: "flow", action: "step", task: "step", ``` Note: This overwrites the existing `workflow: "pipeline"` and `action: "pipeline"` mappings. Since domain extraction is the newer, higher-priority feature, the domain aliases take precedence. The LLM prompt for structural analysis already uses `"pipeline"` directly. Add domain edge aliases to `EDGE_TYPE_ALIASES` (after line 81): ```typescript // Domain edge aliases has_flow: "contains_flow", next_step: "flow_step", interacts_with: "cross_domain", implemented_by: "implements", ``` Update `GraphNodeSchema` (lines 310-324) to add domain types and use `.passthrough()`: ```typescript export const GraphNodeSchema = z.object({ id: z.string(), type: z.enum([ "file", "function", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step", ]), name: z.string(), filePath: z.string().optional(), lineRange: z.tuple([z.number(), z.number()]).optional(), summary: z.string(), tags: z.array(z.string()), complexity: z.enum(["simple", "moderate", "complex"]), languageNotes: z.string().optional(), }).passthrough(); ``` The `.passthrough()` allows `domainMeta` and other extra fields to survive validation without needing to define them in Zod (keeps the schema simple and forward-compatible). - [ ] **Step 5: Run tests to verify they pass** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/domain-types.test.ts` Expected: All 7 tests PASS - [ ] **Step 6: Run existing tests to verify no regressions** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run` Expected: All existing tests PASS - [ ] **Step 7: Commit** ```bash git add understand-anything-plugin/packages/core/src/types.ts \ understand-anything-plugin/packages/core/src/schema.ts \ understand-anything-plugin/packages/core/src/__tests__/domain-types.test.ts git commit -m "feat(core): add domain/flow/step node types and domain edge types for business domain knowledge" ``` --- ## Task 2: Add Domain Graph Persistence **Files:** - Modify: `understand-anything-plugin/packages/core/src/persistence/index.ts` - Modify: `understand-anything-plugin/packages/core/src/index.ts` - [ ] **Step 1: Write failing test for domain graph persistence** ```typescript // understand-anything-plugin/packages/core/src/__tests__/domain-persistence.test.ts import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdirSync, rmSync, existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { saveDomainGraph, loadDomainGraph } from "../persistence/index.js"; import type { KnowledgeGraph } from "../types.js"; const testRoot = join(tmpdir(), "ua-domain-persist-test"); const domainGraph: KnowledgeGraph = { version: "1.0.0", project: { name: "test", languages: ["typescript"], frameworks: [], description: "test", analyzedAt: "2026-04-01T00:00:00.000Z", gitCommitHash: "abc123", }, nodes: [ { id: "domain:orders", type: "domain" as any, name: "Orders", summary: "Order management", tags: [], complexity: "moderate", }, ], edges: [], layers: [], tour: [], }; describe("domain graph persistence", () => { beforeEach(() => { if (existsSync(testRoot)) rmSync(testRoot, { recursive: true }); mkdirSync(testRoot, { recursive: true }); }); afterEach(() => { if (existsSync(testRoot)) rmSync(testRoot, { recursive: true }); }); it("saves and loads domain graph", () => { saveDomainGraph(testRoot, domainGraph); const loaded = loadDomainGraph(testRoot); expect(loaded).not.toBeNull(); expect(loaded!.nodes[0].id).toBe("domain:orders"); }); it("returns null when no domain graph exists", () => { const loaded = loadDomainGraph(testRoot); expect(loaded).toBeNull(); }); it("saves to domain-graph.json, not knowledge-graph.json", () => { saveDomainGraph(testRoot, domainGraph); const domainPath = join(testRoot, ".understand-anything", "domain-graph.json"); const structuralPath = join(testRoot, ".understand-anything", "knowledge-graph.json"); expect(existsSync(domainPath)).toBe(true); expect(existsSync(structuralPath)).toBe(false); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/domain-persistence.test.ts` Expected: FAIL — `saveDomainGraph` is not exported - [ ] **Step 3: Implement saveDomainGraph and loadDomainGraph** Add to `understand-anything-plugin/packages/core/src/persistence/index.ts` (after `loadConfig` function, before end of file): ```typescript const DOMAIN_GRAPH_FILE = "domain-graph.json"; export function saveDomainGraph(projectRoot: string, graph: KnowledgeGraph): void { const dir = ensureDir(projectRoot); const sanitised = sanitiseFilePaths(graph, projectRoot); writeFileSync( join(dir, DOMAIN_GRAPH_FILE), JSON.stringify(sanitised, null, 2), "utf-8", ); } export function loadDomainGraph( projectRoot: string, options?: { validate?: boolean }, ): KnowledgeGraph | null { const filePath = join(projectRoot, UA_DIR, DOMAIN_GRAPH_FILE); if (!existsSync(filePath)) return null; const data = JSON.parse(readFileSync(filePath, "utf-8")); if (options?.validate !== false) { const result = validateGraph(data); if (!result.success) { throw new Error( `Invalid domain graph: ${result.fatal ?? "unknown error"}`, ); } return result.data as KnowledgeGraph; } return data as KnowledgeGraph; } ``` - [ ] **Step 4: Export from core index** Add to `understand-anything-plugin/packages/core/src/index.ts` (after the existing persistence re-exports on line 2): The existing line 2 is `export * from "./persistence/index.js";` which will auto-export the new functions. No change needed — the wildcard export picks up `saveDomainGraph` and `loadDomainGraph` automatically. - [ ] **Step 5: Run tests to verify they pass** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/domain-persistence.test.ts` Expected: All 3 tests PASS - [ ] **Step 6: Run all core tests for regressions** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run` Expected: All tests PASS - [ ] **Step 7: Commit** ```bash git add understand-anything-plugin/packages/core/src/persistence/index.ts \ understand-anything-plugin/packages/core/src/__tests__/domain-persistence.test.ts git commit -m "feat(core): add saveDomainGraph/loadDomainGraph persistence functions" ``` --- ## Task 3: Update Normalize Graph for Domain ID Prefixes **Files:** - Modify: `understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts` - [ ] **Step 1: Write failing test for domain ID normalization** ```typescript // understand-anything-plugin/packages/core/src/__tests__/domain-normalize.test.ts import { describe, it, expect } from "vitest"; import { normalizeNodeId } from "../analyzer/normalize-graph.js"; describe("domain node ID normalization", () => { it("normalizes domain node IDs", () => { const result = normalizeNodeId("domain:order-management", { type: "domain", name: "Order Management", }); expect(result).toBe("domain:order-management"); }); it("normalizes flow node IDs", () => { const result = normalizeNodeId("flow:create-order", { type: "flow", name: "Create Order", }); expect(result).toBe("flow:create-order"); }); it("normalizes step node IDs with filePath", () => { const result = normalizeNodeId("step:create-order:validate", { type: "step", name: "Validate", filePath: "src/validators/order.ts", }); expect(result).toBe("step:src/validators/order.ts:validate"); }); it("normalizes step node IDs without filePath", () => { const result = normalizeNodeId("step:validate", { type: "step", name: "Validate", }); expect(result).toBe("step:validate"); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/domain-normalize.test.ts` Expected: FAIL — "domain" is not a valid prefix in `VALID_PREFIXES` - [ ] **Step 3: Add domain prefixes to normalize-graph.ts** In `understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts`: Add `"domain"`, `"flow"`, `"step"` to `VALID_PREFIXES` (lines 1-5): ```typescript const VALID_PREFIXES = new Set([ "file", "func", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step", ]); ``` Add to `TYPE_TO_PREFIX` map (lines 7-21): ```typescript domain: "domain", flow: "flow", step: "step", ``` - [ ] **Step 4: Run tests to verify they pass** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/domain-normalize.test.ts` Expected: All 4 tests PASS - [ ] **Step 5: Run all core tests** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run` Expected: All tests PASS - [ ] **Step 6: Commit** ```bash git add understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts \ understand-anything-plugin/packages/core/src/__tests__/domain-normalize.test.ts git commit -m "feat(core): add domain/flow/step prefixes to node ID normalization" ``` --- ## Task 4: Build Core Package and Verify **Files:** - None (build verification) - [ ] **Step 1: Build core package** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` Expected: Build succeeds with no errors - [ ] **Step 2: Run full test suite** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run` Expected: All tests PASS - [ ] **Step 3: Commit (if any build config changes were needed)** Only commit if build required changes. Otherwise skip. --- ## Task 5: Dashboard Store — Add Domain State **Files:** - Modify: `understand-anything-plugin/packages/dashboard/src/store.ts` - [ ] **Step 1: Add ViewMode type and domain state to store** In `understand-anything-plugin/packages/dashboard/src/store.ts`: Add `ViewMode` type after the existing type definitions (after line 14): ```typescript export type ViewMode = "structural" | "domain"; ``` Update `NodeType` (line 12) to include domain types: ```typescript export type NodeType = "file" | "function" | "class" | "module" | "concept" | "config" | "document" | "service" | "table" | "endpoint" | "pipeline" | "schema" | "resource" | "domain" | "flow" | "step"; ``` Update `ALL_NODE_TYPES` (line 23): ```typescript export const ALL_NODE_TYPES: NodeType[] = ["file", "function", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step"]; ``` Add domain edge category to `EDGE_CATEGORY_MAP` (after line 33): ```typescript export const EDGE_CATEGORY_MAP: Record = { structural: ["imports", "exports", "contains", "inherits", "implements"], behavioral: ["calls", "subscribes", "publishes", "middleware"], "data-flow": ["reads_from", "writes_to", "transforms", "validates"], dependencies: ["depends_on", "tested_by", "configures"], semantic: ["related", "similar_to"], }; export const DOMAIN_EDGE_TYPES = ["contains_flow", "flow_step", "cross_domain"]; ``` Add to `DashboardStore` interface (after line 93): ```typescript // Domain view viewMode: ViewMode; domainGraph: KnowledgeGraph | null; activeDomainId: string | null; setDomainGraph: (graph: KnowledgeGraph) => void; setViewMode: (mode: ViewMode) => void; navigateToDomain: (domainId: string) => void; ``` - [ ] **Step 2: Implement domain state in the create() block** In the `create()` call (after line 183): ```typescript viewMode: "structural", domainGraph: null, activeDomainId: null, setDomainGraph: (graph) => { set({ domainGraph: graph, viewMode: "domain" }); }, setViewMode: (mode) => { set({ viewMode: mode, selectedNodeId: null, focusNodeId: null, codeViewerOpen: false, codeViewerNodeId: null, }); }, navigateToDomain: (domainId) => { set({ activeDomainId: domainId, selectedNodeId: null, focusNodeId: null, }); }, ``` - [ ] **Step 3: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 4: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/store.ts git commit -m "feat(dashboard): add domain view state to store (viewMode, domainGraph, activeDomainId)" ``` --- ## Task 6: Dashboard — View Mode Toggle **Files:** - Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` - [ ] **Step 1: Add domain graph loading to Dashboard component** In `understand-anything-plugin/packages/dashboard/src/App.tsx`, add store selectors (after line 81): ```typescript const viewMode = useDashboardStore((s) => s.viewMode); const setViewMode = useDashboardStore((s) => s.setViewMode); const domainGraph = useDashboardStore((s) => s.domainGraph); const setDomainGraph = useDashboardStore((s) => s.setDomainGraph); ``` Add a `useEffect` to load `domain-graph.json` (after the diff-overlay useEffect, ~line 265): ```typescript useEffect(() => { fetch(tokenUrl("/domain-graph.json", accessToken)) .then((res) => { if (!res.ok) return null; return res.json(); }) .then((data: unknown) => { if (!data) return; const result = validateGraph(data); if (result.success && result.data) { setDomainGraph(result.data); } }) .catch(() => { // Silently ignore — domain graph is optional }); }, [setDomainGraph]); ``` - [ ] **Step 2: Add view mode toggle pill to header** In the header left section (after PersonaSelector, around line 290), add the toggle pill. Only show when both graphs exist: ```typescript {graph && domainGraph && ( <>
)} ``` - [ ] **Step 3: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 4: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/App.tsx git commit -m "feat(dashboard): add domain graph loading and view mode toggle pill" ``` --- ## Task 7: Dashboard — Domain Cluster Node Component **Files:** - Create: `understand-anything-plugin/packages/dashboard/src/components/DomainClusterNode.tsx` - [ ] **Step 1: Create the DomainClusterNode component** ```typescript // understand-anything-plugin/packages/dashboard/src/components/DomainClusterNode.tsx import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { Node, NodeProps } from "@xyflow/react"; import { useDashboardStore } from "../store"; export interface DomainClusterData { label: string; summary: string; entities?: string[]; flowCount: number; businessRules?: string[]; domainId: string; } export type DomainClusterFlowNode = Node; function DomainClusterNode({ data, id }: NodeProps) { const navigateToDomain = useDashboardStore((s) => s.navigateToDomain); const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); const selectNode = useDashboardStore((s) => s.selectNode); const isSelected = selectedNodeId === data.domainId; return (
selectNode(data.domainId)} onDoubleClick={() => navigateToDomain(data.domainId)} >
{data.label}
{data.summary}
{data.entities && data.entities.length > 0 && (
Entities
{data.entities.slice(0, 5).map((e) => ( {e} ))} {data.entities.length > 5 && ( +{data.entities.length - 5} )}
)}
{data.flowCount} flow{data.flowCount !== 1 ? "s" : ""}
); } export default memo(DomainClusterNode); ``` - [ ] **Step 2: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 3: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/components/DomainClusterNode.tsx git commit -m "feat(dashboard): add DomainClusterNode component for domain view" ``` --- ## Task 8: Dashboard — Flow and Step Node Components **Files:** - Create: `understand-anything-plugin/packages/dashboard/src/components/FlowNode.tsx` - Create: `understand-anything-plugin/packages/dashboard/src/components/StepNode.tsx` - [ ] **Step 1: Create the FlowNode component** ```typescript // understand-anything-plugin/packages/dashboard/src/components/FlowNode.tsx import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { Node, NodeProps } from "@xyflow/react"; import { useDashboardStore } from "../store"; export interface FlowNodeData { label: string; summary: string; entryPoint?: string; entryType?: string; stepCount: number; flowId: string; } export type FlowFlowNode = Node; function FlowNode({ data }: NodeProps) { const selectNode = useDashboardStore((s) => s.selectNode); const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); const isSelected = selectedNodeId === data.flowId; return (
selectNode(data.flowId)} > {data.entryPoint && (
{data.entryPoint}
)}
{data.label}
{data.summary}
{data.stepCount} step{data.stepCount !== 1 ? "s" : ""}
); } export default memo(FlowNode); ``` - [ ] **Step 2: Create the StepNode component** ```typescript // understand-anything-plugin/packages/dashboard/src/components/StepNode.tsx import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { Node, NodeProps } from "@xyflow/react"; import { useDashboardStore } from "../store"; export interface StepNodeData { label: string; summary: string; filePath?: string; stepId: string; order: number; } export type StepFlowNode = Node; function StepNode({ data }: NodeProps) { const selectNode = useDashboardStore((s) => s.selectNode); const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); const isSelected = selectedNodeId === data.stepId; return (
selectNode(data.stepId)} >
{data.order} {data.label}
{data.summary}
{data.filePath && (
{data.filePath}
)}
); } export default memo(StepNode); ``` - [ ] **Step 3: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 4: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/components/FlowNode.tsx \ understand-anything-plugin/packages/dashboard/src/components/StepNode.tsx git commit -m "feat(dashboard): add FlowNode and StepNode components for domain view" ``` --- ## Task 9: Dashboard — Domain Graph View **Files:** - Create: `understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx` - Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` - [ ] **Step 1: Create the DomainGraphView component** ```typescript // understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx import { useCallback, useMemo } from "react"; import { ReactFlow, ReactFlowProvider, Background, BackgroundVariant, Controls, MiniMap, } from "@xyflow/react"; import type { Edge, Node } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import DomainClusterNode from "./DomainClusterNode"; import type { DomainClusterFlowNode } from "./DomainClusterNode"; import FlowNode from "./FlowNode"; import type { FlowFlowNode } from "./FlowNode"; import StepNode from "./StepNode"; import type { StepFlowNode } from "./StepNode"; import { useDashboardStore } from "../store"; import { useTheme } from "../themes/index.ts"; import { applyDagreLayout } from "../utils/layout"; import type { KnowledgeGraph, GraphNode } from "@understand-anything/core/types"; const nodeTypes = { "domain-cluster": DomainClusterNode, "flow-node": FlowNode, "step-node": StepNode, }; // Dimensions for domain-specific nodes const DOMAIN_NODE_DIMENSIONS = new Map(); function getDomainMeta(node: GraphNode): Record | undefined { return (node as any).domainMeta; } function buildDomainOverview(graph: KnowledgeGraph): { nodes: Node[]; edges: Edge[] } { const domainNodes = graph.nodes.filter((n) => n.type === "domain"); const flowNodes = graph.nodes.filter((n) => n.type === "flow"); // Count flows per domain const flowCountMap = new Map(); for (const edge of graph.edges) { if (edge.type === "contains_flow") { flowCountMap.set(edge.source, (flowCountMap.get(edge.source) ?? 0) + 1); } } const rfNodes: Node[] = domainNodes.map((node) => { const meta = getDomainMeta(node); const data = { label: node.name, summary: node.summary, entities: meta?.entities as string[] | undefined, flowCount: flowCountMap.get(node.id) ?? 0, businessRules: meta?.businessRules as string[] | undefined, domainId: node.id, }; DOMAIN_NODE_DIMENSIONS.set(node.id, { width: 320, height: 180 }); return { id: node.id, type: "domain-cluster" as const, position: { x: 0, y: 0 }, data, }; }); const rfEdges: Edge[] = graph.edges .filter((e) => e.type === "cross_domain") .map((e) => ({ id: `${e.source}-${e.target}`, source: e.source, target: e.target, label: e.description ?? "", style: { stroke: "var(--color-accent)", strokeDasharray: "6 3", strokeWidth: 2 }, labelStyle: { fill: "var(--color-text-muted)", fontSize: 10 }, animated: true, })); return applyDagreLayout(rfNodes, rfEdges, "LR", DOMAIN_NODE_DIMENSIONS); } function buildDomainDetail( graph: KnowledgeGraph, domainId: string, ): { nodes: Node[]; edges: Edge[] } { // Find flows for this domain const flowIds = new Set( graph.edges .filter((e) => e.type === "contains_flow" && e.source === domainId) .map((e) => e.target), ); const flowNodes = graph.nodes.filter((n) => flowIds.has(n.id)); const stepEdges = graph.edges.filter( (e) => e.type === "flow_step" && flowIds.has(e.source), ); const stepIds = new Set(stepEdges.map((e) => e.target)); const stepNodes = graph.nodes.filter((n) => stepIds.has(n.id)); // Build step order map const stepOrderMap = new Map(); for (const edge of stepEdges) { stepOrderMap.set(edge.target, edge.weight); } // Count steps per flow const stepCountMap = new Map(); for (const edge of stepEdges) { stepCountMap.set(edge.source, (stepCountMap.get(edge.source) ?? 0) + 1); } const dims = new Map(); const rfNodes: Node[] = [ ...flowNodes.map((node) => { const meta = getDomainMeta(node); dims.set(node.id, { width: 260, height: 120 }); return { id: node.id, type: "flow-node" as const, position: { x: 0, y: 0 }, data: { label: node.name, summary: node.summary, entryPoint: meta?.entryPoint as string | undefined, entryType: meta?.entryType as string | undefined, stepCount: stepCountMap.get(node.id) ?? 0, flowId: node.id, }, }; }), ...stepNodes.map((node) => { dims.set(node.id, { width: 200, height: 90 }); return { id: node.id, type: "step-node" as const, position: { x: 0, y: 0 }, data: { label: node.name, summary: node.summary, filePath: node.filePath, stepId: node.id, order: Math.round((stepOrderMap.get(node.id) ?? 0) * 10), }, }; }), ]; const rfEdges: Edge[] = stepEdges.map((e) => ({ id: `${e.source}-${e.target}`, source: e.source, target: e.target, style: { stroke: "var(--color-border-medium)", strokeWidth: 1.5 }, animated: false, })); return applyDagreLayout(rfNodes, rfEdges, "LR", dims); } function DomainGraphViewInner() { const domainGraph = useDashboardStore((s) => s.domainGraph); const activeDomainId = useDashboardStore((s) => s.activeDomainId); const navigateToDomain = useDashboardStore((s) => s.navigateToDomain); const navigateToOverview = useDashboardStore((s) => s.navigateToOverview); const theme = useTheme(); const { nodes, edges } = useMemo(() => { if (!domainGraph) return { nodes: [], edges: [] }; if (activeDomainId) { return buildDomainDetail(domainGraph, activeDomainId); } return buildDomainOverview(domainGraph); }, [domainGraph, activeDomainId]); const onNodeDoubleClick = useCallback( (_: React.MouseEvent, node: Node) => { if (node.type === "domain-cluster" && node.data && "domainId" in node.data) { navigateToDomain(node.data.domainId as string); } }, [navigateToDomain], ); if (!domainGraph) { return (
No domain graph available. Run /understand-domain to generate one.
); } return (
{activeDomainId && (
)} theme.colors?.accent ?? "#d4a574"} maskColor="rgba(0,0,0,0.7)" style={{ bottom: 16, right: 16, width: 160, height: 100 }} />
); } export default function DomainGraphView() { return ( ); } ``` - [ ] **Step 2: Wire DomainGraphView into App.tsx** In `understand-anything-plugin/packages/dashboard/src/App.tsx`: Add import at top: ```typescript import DomainGraphView from "./components/DomainGraphView"; ``` Replace the graph area section (around lines 394-400) to conditionally render: ```typescript {/* Graph area */}
{viewMode === "domain" && domainGraph ? ( ) : ( )}
Press ? for keyboard shortcuts
``` - [ ] **Step 3: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 4: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx \ understand-anything-plugin/packages/dashboard/src/App.tsx git commit -m "feat(dashboard): add DomainGraphView with domain overview and detail views" ``` --- ## Task 10: Dashboard — Domain-Aware NodeInfo Sidebar **Files:** - Modify: `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx` - [ ] **Step 1: Add domain-aware sections to NodeInfo** Read the existing NodeInfo.tsx first, then add domain-specific rendering. After the existing connection sections, add handling for domain/flow/step node types. The key changes: 1. Read `viewMode` and `domainGraph` from store 2. When `viewMode === "domain"`, look up the selected node in `domainGraph` instead of `graph` 3. For `domain` nodes: show entities, business rules, cross-domain interactions, list of flows 4. For `flow` nodes: show entry point, step list in order 5. For `step` nodes: show description, file path, "View in code" link Add a helper function above the component: ```typescript function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) { const navigateToDomain = useDashboardStore((s) => s.navigateToDomain); const selectNode = useDashboardStore((s) => s.selectNode); const meta = (node as any).domainMeta as Record | undefined; if (node.type === "domain") { const flows = graph.edges .filter((e) => e.type === "contains_flow" && e.source === node.id) .map((e) => graph.nodes.find((n) => n.id === e.target)) .filter(Boolean); return (
{meta?.entities && (meta.entities as string[]).length > 0 && (

Entities

{(meta.entities as string[]).map((e) => ( {e} ))}
)} {meta?.businessRules && (meta.businessRules as string[]).length > 0 && (

Business Rules

    {(meta.businessRules as string[]).map((r, i) => (
  • -{r}
  • ))}
)} {meta?.crossDomainInteractions && (meta.crossDomainInteractions as string[]).length > 0 && (

Cross-Domain

    {(meta.crossDomainInteractions as string[]).map((c, i) => (
  • {c}
  • ))}
)} {flows.length > 0 && (

Flows

{flows.map((f) => ( ))}
)}
); } if (node.type === "flow") { const steps = graph.edges .filter((e) => e.type === "flow_step" && e.source === node.id) .sort((a, b) => a.weight - b.weight) .map((e) => graph.nodes.find((n) => n.id === e.target)) .filter(Boolean); return (
{meta?.entryPoint && (

Entry Point

{meta.entryPoint as string}
)} {steps.length > 0 && (

Steps

    {steps.map((s, i) => (
  1. ))}
)}
); } if (node.type === "step") { return (
{node.filePath && (

Implementation

{node.filePath} {node.lineRange && :{node.lineRange[0]}-{node.lineRange[1]}}
)}
); } return null; } ``` Then in the main NodeInfo component, add domain-aware rendering. After getting the `node` from the graph, add logic to also check `domainGraph`: ```typescript const viewMode = useDashboardStore((s) => s.viewMode); const domainGraph = useDashboardStore((s) => s.domainGraph); const activeGraph = viewMode === "domain" && domainGraph ? domainGraph : graph; const node = activeGraph?.nodes.find((n) => n.id === selectedNodeId); ``` And after the summary section, add: ```typescript {/* Domain-specific details */} {activeGraph && node && (node.type === "domain" || node.type === "flow" || node.type === "step") && ( )} ``` - [ ] **Step 2: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 3: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx git commit -m "feat(dashboard): add domain-aware NodeInfo sidebar for domain/flow/step nodes" ``` --- ## Task 11: Dashboard — Update GraphView NODE_TYPE_TO_CATEGORY **Files:** - Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` - [ ] **Step 1: Add domain types to NODE_TYPE_TO_CATEGORY** In `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx`, update `NODE_TYPE_TO_CATEGORY` (lines 53-59): ```typescript const NODE_TYPE_TO_CATEGORY: Record = { file: "code", function: "code", class: "code", module: "code", concept: "code", config: "config", document: "docs", service: "infra", resource: "infra", pipeline: "infra", table: "data", endpoint: "data", schema: "data", // Domain types — categorized as "code" for filtering purposes domain: "code", flow: "code", step: "code", } as const; ``` - [ ] **Step 2: Verify dashboard builds** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 3: Commit** ```bash git add understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx git commit -m "feat(dashboard): add domain node types to NODE_TYPE_TO_CATEGORY mapping" ``` --- ## Task 12: Create Domain Analyzer Agent **Files:** - Create: `understand-anything-plugin/agents/domain-analyzer.md` - [ ] **Step 1: Create the agent definition** ```markdown --- name: domain-analyzer description: | Analyzes codebases to extract business domain knowledge — domains, business flows, and process steps. Produces a domain-graph.json that maps how business logic flows through the code. model: opus --- # Domain Analyzer Agent You are a business domain analysis expert. Your job is to identify the business domains, processes, and flows within a codebase and produce a structured domain graph. ## Your Task Analyze the provided context (either a preprocessed domain context file OR an existing knowledge graph) and produce a complete domain graph JSON. ## Three-Level Hierarchy 1. **Business Domain** — High-level business areas (e.g., "Order Management", "User Authentication", "Payment Processing") 2. **Business Flow** — Specific processes within a domain (e.g., "Create Order", "Process Refund") 3. **Business Step** — Individual actions within a flow (e.g., "Validate input", "Check inventory") ## Output Schema Produce a JSON object with this exact structure: ```json { "version": "1.0.0", "project": { "name": "", "languages": [""], "frameworks": [""], "description": "", "analyzedAt": "", "gitCommitHash": "" }, "nodes": [ { "id": "domain:", "type": "domain", "name": "", "summary": "<2-3 sentences about what this domain handles>", "tags": [""], "complexity": "simple|moderate|complex", "domainMeta": { "entities": [""], "businessRules": [""], "crossDomainInteractions": [""] } }, { "id": "flow:", "type": "flow", "name": "", "summary": "", "tags": [""], "complexity": "simple|moderate|complex", "domainMeta": { "entryPoint": "", "entryType": "http|cli|event|cron|manual" } }, { "id": "step::", "type": "step", "name": "", "summary": "", "tags": [""], "complexity": "simple|moderate|complex", "filePath": "", "lineRange": [, ] } ], "edges": [ { "source": "domain:", "target": "flow:", "type": "contains_flow", "direction": "forward", "weight": 1.0 }, { "source": "flow:", "target": "step::", "type": "flow_step", "direction": "forward", "weight": 0.1 }, { "source": "domain:", "target": "domain:", "type": "cross_domain", "direction": "forward", "description": "", "weight": 0.6 } ], "layers": [], "tour": [] } ``` ## Rules 1. **flow_step weight encodes order**: First step = 0.1, second = 0.2, etc. 2. **Every flow must connect to a domain** via `contains_flow` edge 3. **Every step must connect to a flow** via `flow_step` edge 4. **Cross-domain edges** describe how domains interact 5. **File paths** on step nodes should be relative to project root 6. **Be specific, not generic** — use the actual business terminology from the code 7. **Don't invent flows that aren't in the code** — only document what exists Respond ONLY with the JSON object, no additional text or markdown fences. ``` - [ ] **Step 2: Commit** ```bash git add understand-anything-plugin/agents/domain-analyzer.md git commit -m "feat(agents): add domain-analyzer agent for business domain extraction" ``` --- ## Task 13: Create /understand-domain Skill **Files:** - Create: `understand-anything-plugin/skills/understand-domain/SKILL.md` - [ ] **Step 1: Create the skill directory and SKILL.md** ```bash mkdir -p understand-anything-plugin/skills/understand-domain ``` ```markdown --- name: understand-domain description: Extract business domain knowledge from a codebase and generate an interactive domain flow graph. Works standalone (lightweight scan) or derives from an existing /understand knowledge graph. argument-hint: [--full] --- # /understand-domain Extracts business domain knowledge — domains, business flows, and process steps — from a codebase and produces an interactive horizontal flow graph in the dashboard. ## How It Works - If a knowledge graph already exists (`.understand-anything/knowledge-graph.json`), derives domain knowledge from it (cheap, no file scanning) - If no knowledge graph exists, performs a lightweight scan: file tree + entry point detection + sampled files - Use `--full` flag to force a fresh scan even if a knowledge graph exists ## Instructions ### Phase 1: Detect Existing Graph 1. Check if `.understand-anything/knowledge-graph.json` exists in the current project 2. If it exists AND `--full` was NOT passed → proceed to Phase 3 (derive from graph) 3. Otherwise → proceed to Phase 2 (lightweight scan) ### Phase 2: Lightweight Scan (Path 1) 1. Run the preprocessing script bundled with this skill: ``` python understand-anything-plugin/skills/understand-domain/extract-domain-context.py ``` This outputs `.understand-anything/intermediate/domain-context.json` containing: - File tree (respecting `.gitignore`) - Detected entry points (HTTP routes, CLI commands, event handlers, cron jobs, exported handlers) - File signatures (exports, imports per file) - Code snippets for each entry point (signature + first few lines) 2. Read the generated `domain-context.json` as context for Phase 4 3. Proceed to Phase 4 ### Phase 3: Derive from Existing Graph (Path 2) 1. Read `.understand-anything/knowledge-graph.json` 2. Format the graph data as structured context: - All nodes with their types, names, summaries, and tags - All edges with their types (especially `calls`, `imports`, `contains`) - All layers with their descriptions - Tour steps if available 3. This is the context for the domain analyzer — no file reading needed 4. Proceed to Phase 4 ### Phase 4: Domain Analysis 1. Read the domain-analyzer agent prompt from `agents/domain-analyzer.md` 2. Dispatch a subagent with the domain-analyzer prompt + the context from Phase 2 or 3 3. The agent writes its output to `.understand-anything/intermediate/domain-analysis.json` ### Phase 5: Validate and Save 1. Read the domain analysis output 2. Validate using the standard graph validation pipeline (the schema now supports domain/flow/step types) 3. If validation fails, log warnings but save what's valid (error tolerance) 4. Save to `.understand-anything/domain-graph.json` 5. Clean up `.understand-anything/intermediate/domain-analysis.json` ### Phase 6: Launch Dashboard 1. Auto-trigger `/understand-dashboard` to visualize the domain graph 2. The dashboard will detect `domain-graph.json` and show the domain view by default ``` - [ ] **Step 2: Commit** ```bash git add understand-anything-plugin/skills/understand-domain/SKILL.md git commit -m "feat(skills): add /understand-domain skill for business domain knowledge extraction" ``` --- ## Task 14: Dashboard — Serve domain-graph.json **Files:** - Modify: `understand-anything-plugin/skills/understand-dashboard/SKILL.md` - [ ] **Step 1: Read the existing understand-dashboard skill** Read `understand-anything-plugin/skills/understand-dashboard/SKILL.md` to understand how the dashboard server is configured, then add `domain-graph.json` to the list of served files. The dashboard server serves files from `.understand-anything/`. The domain graph file (`domain-graph.json`) needs to be served alongside `knowledge-graph.json` and `meta.json`. Update the skill to mention that `domain-graph.json` should also be served if it exists. The exact change depends on how the server is configured in the skill — typically it serves the entire `.understand-anything/` directory, so `domain-graph.json` should be automatically available. Verify this is the case. - [ ] **Step 2: Commit (if changes needed)** Only commit if the skill needed updating. --- ## Task 15: Full Build and Integration Verification **Files:** - None (verification only) - [ ] **Step 1: Build core package** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` Expected: Build succeeds - [ ] **Step 2: Run all core tests** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run` Expected: All tests PASS - [ ] **Step 3: Build dashboard** Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` Expected: Build succeeds - [ ] **Step 4: Run linter** Run: `cd understand-anything-plugin && pnpm lint` Expected: No errors (warnings acceptable) - [ ] **Step 5: Final commit (if any lint fixes needed)** Fix any lint issues and commit. --- ## Summary of All Files ### New Files - `understand-anything-plugin/packages/core/src/__tests__/domain-types.test.ts` - `understand-anything-plugin/packages/core/src/__tests__/domain-persistence.test.ts` - `understand-anything-plugin/packages/core/src/__tests__/domain-normalize.test.ts` - `understand-anything-plugin/packages/dashboard/src/components/DomainClusterNode.tsx` - `understand-anything-plugin/packages/dashboard/src/components/FlowNode.tsx` - `understand-anything-plugin/packages/dashboard/src/components/StepNode.tsx` - `understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx` - `understand-anything-plugin/agents/domain-analyzer.md` - `understand-anything-plugin/skills/understand-domain/SKILL.md` - `understand-anything-plugin/skills/understand-domain/extract-domain-context.py` ### Modified Files - `understand-anything-plugin/packages/core/src/types.ts` — 3 new node types, 3 new edge types, DomainMeta interface - `understand-anything-plugin/packages/core/src/schema.ts` — Zod schemas + aliases for domain types - `understand-anything-plugin/packages/core/src/persistence/index.ts` — saveDomainGraph/loadDomainGraph - `understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts` — domain ID prefixes - `understand-anything-plugin/packages/dashboard/src/store.ts` — viewMode, domainGraph, activeDomainId - `understand-anything-plugin/packages/dashboard/src/App.tsx` — domain graph loading, view toggle, conditional rendering - `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx` — domain-aware sidebar - `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` — NODE_TYPE_TO_CATEGORY update