Files
Fulfilled-Knowledge/Understand-Anything-main/docs/superpowers/plans/2026-04-01-business-domain-knowledge-impl.md
2026-05-27 15:40:32 +08:00

58 KiB

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:

// 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):

// 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):

// 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):

// 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:

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):

  // 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):

  // 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():

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
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

// 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):

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
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

// 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):

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):

  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
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):

export type ViewMode = "structural" | "domain";

Update NodeType (line 12) to include domain types:

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):

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):

export const EDGE_CATEGORY_MAP: Record<EdgeCategory, string[]> = {
  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):

  // 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<DashboardStore>() call (after line 183):

  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
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):

  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):

  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:

          {graph && domainGraph && (
            <>
              <div className="w-px h-5 bg-border-subtle" />
              <div className="flex items-center bg-elevated rounded-lg p-0.5">
                <button
                  onClick={() => setViewMode("domain")}
                  className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
                    viewMode === "domain"
                      ? "bg-accent/20 text-accent"
                      : "text-text-muted hover:text-text-secondary"
                  }`}
                >
                  Domain
                </button>
                <button
                  onClick={() => setViewMode("structural")}
                  className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
                    viewMode === "structural"
                      ? "bg-accent/20 text-accent"
                      : "text-text-muted hover:text-text-secondary"
                  }`}
                >
                  Structural
                </button>
              </div>
            </>
          )}
  • Step 3: Verify dashboard builds

Run: cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build

Expected: Build succeeds

  • Step 4: Commit
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

// 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<DomainClusterData, "domain-cluster">;

function DomainClusterNode({ data, id }: NodeProps<DomainClusterFlowNode>) {
  const navigateToDomain = useDashboardStore((s) => s.navigateToDomain);
  const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
  const selectNode = useDashboardStore((s) => s.selectNode);
  const isSelected = selectedNodeId === data.domainId;

  return (
    <div
      className={`rounded-xl border-2 px-5 py-4 min-w-[280px] max-w-[360px] cursor-pointer transition-all ${
        isSelected
          ? "border-accent bg-accent/10 shadow-lg shadow-accent/10"
          : "border-accent/40 bg-surface hover:border-accent/70"
      }`}
      onClick={() => selectNode(data.domainId)}
      onDoubleClick={() => navigateToDomain(data.domainId)}
    >
      <Handle type="target" position={Position.Left} className="!bg-accent/60 !w-2 !h-2" />
      <Handle type="source" position={Position.Right} className="!bg-accent/60 !w-2 !h-2" />

      <div className="font-serif text-sm text-accent font-semibold mb-1 truncate">
        {data.label}
      </div>
      <div className="text-[11px] text-text-secondary line-clamp-2 mb-2">
        {data.summary}
      </div>

      {data.entities && data.entities.length > 0 && (
        <div className="mb-2">
          <div className="text-[9px] uppercase tracking-wider text-text-muted mb-1">Entities</div>
          <div className="flex flex-wrap gap-1">
            {data.entities.slice(0, 5).map((e) => (
              <span key={e} className="text-[10px] px-1.5 py-0.5 rounded bg-elevated text-text-secondary">
                {e}
              </span>
            ))}
            {data.entities.length > 5 && (
              <span className="text-[10px] text-text-muted">+{data.entities.length - 5}</span>
            )}
          </div>
        </div>
      )}

      <div className="text-[10px] text-text-muted">
        {data.flowCount} flow{data.flowCount !== 1 ? "s" : ""}
      </div>
    </div>
  );
}

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
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

// 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<FlowNodeData, "flow-node">;

function FlowNode({ data }: NodeProps<FlowFlowNode>) {
  const selectNode = useDashboardStore((s) => s.selectNode);
  const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
  const isSelected = selectedNodeId === data.flowId;

  return (
    <div
      className={`rounded-lg border px-4 py-3 min-w-[240px] max-w-[320px] cursor-pointer transition-all ${
        isSelected
          ? "border-accent bg-accent/10"
          : "border-border-medium bg-surface hover:border-accent/50"
      }`}
      onClick={() => selectNode(data.flowId)}
    >
      <Handle type="target" position={Position.Left} className="!bg-accent/60 !w-2 !h-2" />
      <Handle type="source" position={Position.Right} className="!bg-accent/60 !w-2 !h-2" />

      {data.entryPoint && (
        <div className="text-[9px] font-mono text-accent/70 mb-1 truncate">
          {data.entryPoint}
        </div>
      )}
      <div className="text-xs font-semibold text-text-primary mb-1 truncate">
        {data.label}
      </div>
      <div className="text-[10px] text-text-secondary line-clamp-2">
        {data.summary}
      </div>
      <div className="text-[9px] text-text-muted mt-1">
        {data.stepCount} step{data.stepCount !== 1 ? "s" : ""}
      </div>
    </div>
  );
}

export default memo(FlowNode);
  • Step 2: Create the StepNode component
// 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<StepNodeData, "step-node">;

function StepNode({ data }: NodeProps<StepFlowNode>) {
  const selectNode = useDashboardStore((s) => s.selectNode);
  const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
  const isSelected = selectedNodeId === data.stepId;

  return (
    <div
      className={`rounded-lg border px-3 py-2.5 min-w-[180px] max-w-[240px] cursor-pointer transition-all ${
        isSelected
          ? "border-accent bg-accent/10"
          : "border-border-subtle bg-elevated hover:border-accent/40"
      }`}
      onClick={() => selectNode(data.stepId)}
    >
      <Handle type="target" position={Position.Left} className="!bg-text-muted/40 !w-1.5 !h-1.5" />
      <Handle type="source" position={Position.Right} className="!bg-text-muted/40 !w-1.5 !h-1.5" />

      <div className="flex items-center gap-1.5 mb-1">
        <span className="text-[9px] font-mono text-accent/60 shrink-0">
          {data.order}
        </span>
        <span className="text-[11px] font-medium text-text-primary truncate">
          {data.label}
        </span>
      </div>
      <div className="text-[10px] text-text-secondary line-clamp-2">
        {data.summary}
      </div>
      {data.filePath && (
        <div className="text-[9px] font-mono text-text-muted mt-1 truncate">
          {data.filePath}
        </div>
      )}
    </div>
  );
}

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
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

// 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<string, { width: number; height: number }>();

function getDomainMeta(node: GraphNode): Record<string, unknown> | 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<string, number>();
  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<string, number>();
  for (const edge of stepEdges) {
    stepOrderMap.set(edge.target, edge.weight);
  }

  // Count steps per flow
  const stepCountMap = new Map<string, number>();
  for (const edge of stepEdges) {
    stepCountMap.set(edge.source, (stepCountMap.get(edge.source) ?? 0) + 1);
  }

  const dims = new Map<string, { width: number; height: number }>();

  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 (
      <div className="h-full flex items-center justify-center text-text-muted text-sm">
        No domain graph available. Run /understand-domain to generate one.
      </div>
    );
  }

  return (
    <div className="h-full w-full relative">
      {activeDomainId && (
        <div className="absolute top-3 left-3 z-10">
          <button
            onClick={() => {
              useDashboardStore.setState({ activeDomainId: null });
            }}
            className="px-3 py-1.5 text-xs rounded-lg bg-elevated border border-border-subtle text-text-secondary hover:text-text-primary transition-colors"
          >
            Back to domains
          </button>
        </div>
      )}
      <ReactFlow
        nodes={nodes}
        edges={edges}
        nodeTypes={nodeTypes}
        onNodeDoubleClick={onNodeDoubleClick}
        fitView
        fitViewOptions={{ padding: 0.2 }}
        minZoom={0.1}
        maxZoom={2}
        proOptions={{ hideAttribution: true }}
      >
        <Background
          variant={BackgroundVariant.Dots}
          gap={20}
          size={1}
          color="var(--color-border-subtle)"
        />
        <Controls
          showInteractive={false}
          style={{ bottom: 16, left: 16 }}
        />
        <MiniMap
          nodeColor={() => theme.colors?.accent ?? "#d4a574"}
          maskColor="rgba(0,0,0,0.7)"
          style={{ bottom: 16, right: 16, width: 160, height: 100 }}
        />
      </ReactFlow>
    </div>
  );
}

export default function DomainGraphView() {
  return (
    <ReactFlowProvider>
      <DomainGraphViewInner />
    </ReactFlowProvider>
  );
}
  • Step 2: Wire DomainGraphView into App.tsx

In understand-anything-plugin/packages/dashboard/src/App.tsx:

Add import at top:

import DomainGraphView from "./components/DomainGraphView";

Replace the graph area section (around lines 394-400) to conditionally render:

        {/* Graph area */}
        <div className="flex-1 min-w-0 min-h-0 relative">
          {viewMode === "domain" && domainGraph ? (
            <DomainGraphView />
          ) : (
            <GraphView />
          )}
          <div className="absolute top-3 right-3 text-sm text-text-muted/60 pointer-events-none select-none">
            Press <kbd className="kbd">?</kbd> for keyboard shortcuts
          </div>
        </div>
  • Step 3: Verify dashboard builds

Run: cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build

Expected: Build succeeds

  • Step 4: Commit
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:

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<string, unknown> | 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 (
      <div className="space-y-3">
        {meta?.entities && (meta.entities as string[]).length > 0 && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entities</h4>
            <div className="flex flex-wrap gap-1">
              {(meta.entities as string[]).map((e) => (
                <span key={e} className="text-[11px] px-2 py-0.5 rounded bg-elevated text-text-secondary">{e}</span>
              ))}
            </div>
          </div>
        )}
        {meta?.businessRules && (meta.businessRules as string[]).length > 0 && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Business Rules</h4>
            <ul className="text-[11px] text-text-secondary space-y-1">
              {(meta.businessRules as string[]).map((r, i) => (
                <li key={i} className="flex gap-1.5"><span className="text-accent shrink-0">-</span>{r}</li>
              ))}
            </ul>
          </div>
        )}
        {meta?.crossDomainInteractions && (meta.crossDomainInteractions as string[]).length > 0 && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Cross-Domain</h4>
            <ul className="text-[11px] text-text-secondary space-y-1">
              {(meta.crossDomainInteractions as string[]).map((c, i) => (
                <li key={i}>{c}</li>
              ))}
            </ul>
          </div>
        )}
        {flows.length > 0 && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Flows</h4>
            <div className="space-y-1">
              {flows.map((f) => (
                <button
                  key={f!.id}
                  onClick={() => { navigateToDomain(node.id); selectNode(f!.id); }}
                  className="block w-full text-left px-2 py-1.5 rounded bg-elevated hover:bg-accent/10 text-[11px] text-text-secondary hover:text-accent transition-colors"
                >
                  {f!.name}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    );
  }

  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 (
      <div className="space-y-3">
        {meta?.entryPoint && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entry Point</h4>
            <div className="text-[11px] font-mono text-accent">{meta.entryPoint as string}</div>
          </div>
        )}
        {steps.length > 0 && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Steps</h4>
            <ol className="space-y-1">
              {steps.map((s, i) => (
                <li key={s!.id}>
                  <button
                    onClick={() => selectNode(s!.id)}
                    className="block w-full text-left px-2 py-1.5 rounded bg-elevated hover:bg-accent/10 text-[11px] transition-colors"
                  >
                    <span className="text-accent/60 mr-1.5">{i + 1}.</span>
                    <span className="text-text-secondary hover:text-accent">{s!.name}</span>
                  </button>
                </li>
              ))}
            </ol>
          </div>
        )}
      </div>
    );
  }

  if (node.type === "step") {
    return (
      <div className="space-y-3">
        {node.filePath && (
          <div>
            <h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Implementation</h4>
            <div className="text-[11px] font-mono text-text-secondary">
              {node.filePath}
              {node.lineRange && <span className="text-text-muted">:{node.lineRange[0]}-{node.lineRange[1]}</span>}
            </div>
          </div>
        )}
      </div>
    );
  }

  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:

  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:

        {/* Domain-specific details */}
        {activeGraph && node && (node.type === "domain" || node.type === "flow" || node.type === "step") && (
          <DomainNodeDetails node={node} graph={activeGraph} />
        )}
  • Step 2: Verify dashboard builds

Run: cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build

Expected: Build succeeds

  • Step 3: Commit
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):

const NODE_TYPE_TO_CATEGORY: Record<NodeType, NodeCategory> = {
  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
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

---
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": "<project name>",
    "languages": ["<detected languages>"],
    "frameworks": ["<detected frameworks>"],
    "description": "<project description focused on business purpose>",
    "analyzedAt": "<ISO timestamp>",
    "gitCommitHash": "<commit hash>"
  },
  "nodes": [
    {
      "id": "domain:<kebab-case-name>",
      "type": "domain",
      "name": "<Human Readable Domain Name>",
      "summary": "<2-3 sentences about what this domain handles>",
      "tags": ["<relevant-tags>"],
      "complexity": "simple|moderate|complex",
      "domainMeta": {
        "entities": ["<key domain objects>"],
        "businessRules": ["<important constraints/invariants>"],
        "crossDomainInteractions": ["<how this domain interacts with others>"]
      }
    },
    {
      "id": "flow:<kebab-case-name>",
      "type": "flow",
      "name": "<Flow Name>",
      "summary": "<what this flow accomplishes>",
      "tags": ["<relevant-tags>"],
      "complexity": "simple|moderate|complex",
      "domainMeta": {
        "entryPoint": "<trigger, e.g. POST /api/orders>",
        "entryType": "http|cli|event|cron|manual"
      }
    },
    {
      "id": "step:<flow-name>:<step-name>",
      "type": "step",
      "name": "<Step Name>",
      "summary": "<what this step does>",
      "tags": ["<relevant-tags>"],
      "complexity": "simple|moderate|complex",
      "filePath": "<relative path to implementing file>",
      "lineRange": [<start>, <end>]
    }
  ],
  "edges": [
    { "source": "domain:<name>", "target": "flow:<name>", "type": "contains_flow", "direction": "forward", "weight": 1.0 },
    { "source": "flow:<name>", "target": "step:<flow>:<step>", "type": "flow_step", "direction": "forward", "weight": 0.1 },
    { "source": "domain:<name>", "target": "domain:<other>", "type": "cross_domain", "direction": "forward", "description": "<interaction 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

mkdir -p understand-anything-plugin/skills/understand-domain
---
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
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