diff --git a/understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts b/understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts index 898825d..5940428 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/normalize-graph.ts @@ -63,7 +63,7 @@ function stripToValidPrefix(id: string): { prefix: string | null; path: string } */ export function normalizeNodeId( id: string, - node: { type: string; filePath?: string; name?: string }, + node: { type: string; filePath?: string; name?: string; parentFlowSlug?: string }, ): string { const trimmed = id.trim(); if (!trimmed) return trimmed; @@ -96,11 +96,13 @@ export function normalizeNodeId( ) { return `${expectedPrefix}:${node.filePath}:${node.name}`; } - // For step nodes with filePath, reconstruct as step:filePath:slug + // For step nodes with filePath, reconstruct as step:[flowSlug:]filePath:slug if (node.type === "step" && node.filePath) { const slug = path.toLowerCase().replace(/\s+/g, "-"); - // No flow discriminator available from bare path — use filePath:slug - return `${expectedPrefix}:${node.filePath}:${slug}`; + // Include flow discriminator if available (from edge-based lookup) + return node.parentFlowSlug + ? `${expectedPrefix}:${node.parentFlowSlug}:${node.filePath}:${slug}` + : `${expectedPrefix}:${node.filePath}:${slug}`; } return `${expectedPrefix}:${path}`; } @@ -113,11 +115,16 @@ const VALID_COMPLEXITIES = new Set(["simple", "moderate", "complex"]); const COMPLEXITY_STRING_MAP: Record = { low: "simple", easy: "simple", + trivial: "simple", + basic: "simple", medium: "moderate", intermediate: "moderate", + mid: "moderate", + average: "moderate", high: "complex", hard: "complex", difficult: "complex", + advanced: "complex", }; /** @@ -166,6 +173,24 @@ export interface NormalizeBatchResult { stats: NormalizationStats; } +const PREFIX_TO_TYPE: Record = { + file: "file", func: "function", class: "class", module: "module", + concept: "concept", config: "config", document: "document", + service: "service", table: "table", endpoint: "endpoint", + pipeline: "pipeline", schema: "schema", resource: "resource", + domain: "domain", flow: "flow", step: "step", +}; + +/** Infer node type from an ID's prefix (e.g. "step:foo" → "step"). Falls back to "file". */ +function inferTypeFromId(id: string): string { + const colonIdx = id.indexOf(":"); + if (colonIdx > 0) { + const prefix = id.slice(0, colonIdx); + if (prefix in PREFIX_TO_TYPE) return PREFIX_TO_TYPE[prefix]; + } + return "file"; +} + /** * Normalizes a merged batch output: fixes node IDs and numeric complexity, * rewrites edge references, deduplicates nodes and edges, and drops dangling edges. @@ -188,6 +213,24 @@ export function normalizeBatchOutput(data: { const idMap = new Map(); + // Build step→flow slug map from flow_step edges so bare-path step IDs + // can include the flow discriminator to avoid collisions. + const stepToFlowSlug = new Map(); + const flowNodeNames = new Map(); + for (const raw of data.nodes) { + if (String(raw.type ?? "") === "flow" && raw.id && raw.name) { + flowNodeNames.set(String(raw.id), String(raw.name).toLowerCase().replace(/\s+/g, "-")); + } + } + for (const raw of data.edges) { + if (String(raw.type ?? "") === "flow_step" && raw.source && raw.target) { + const flowSlug = flowNodeNames.get(String(raw.source)); + if (flowSlug) { + stepToFlowSlug.set(String(raw.target), flowSlug); + } + } + } + // Pass 1: Normalize node IDs and numeric complexity const nodes = data.nodes.map((raw) => { const oldId = String(raw.id ?? ""); @@ -196,6 +239,7 @@ export function normalizeBatchOutput(data: { type: nodeType, filePath: typeof raw.filePath === "string" ? raw.filePath : undefined, name: typeof raw.name === "string" ? raw.name : undefined, + parentFlowSlug: nodeType === "step" ? stepToFlowSlug.get(oldId) : undefined, }); if (newId !== oldId) { @@ -205,11 +249,15 @@ export function normalizeBatchOutput(data: { const result: Record = { ...raw, id: newId }; - // Only fix numeric complexity here — string aliases are handled by upstream's - // COMPLEXITY_ALIASES in autoFixGraph - if (typeof raw.complexity === "number") { - result.complexity = normalizeComplexity(raw.complexity); - stats.complexityFixed++; + // Normalize both numeric and non-canonical string complexity values. + // Upstream's COMPLEXITY_ALIASES handles some strings, but not all variants + // (e.g. "trivial", "advanced"). Normalizing here catches them early. + if (raw.complexity !== undefined) { + const normalized = normalizeComplexity(raw.complexity); + if (normalized !== raw.complexity) { + result.complexity = normalized; + stats.complexityFixed++; + } } return result; @@ -233,13 +281,16 @@ export function normalizeBatchOutput(data: { let newTarget = idMap.get(oldTarget) ?? oldTarget; // Fallback: if endpoint not found in idMap, normalize it directly - // (handles cross-variant malformed IDs between nodes and edges) + // (handles cross-variant malformed IDs between nodes and edges). + // Try the edge's implied type first (from prefix), then fall back to "file". if (!validNodeIds.has(newSource)) { - const normalized = normalizeNodeId(newSource, { type: "file" }); + const inferredType = inferTypeFromId(newSource); + const normalized = normalizeNodeId(newSource, { type: inferredType }); if (validNodeIds.has(normalized)) newSource = normalized; } if (!validNodeIds.has(newTarget)) { - const normalized = normalizeNodeId(newTarget, { type: "file" }); + const inferredType = inferTypeFromId(newTarget); + const normalized = normalizeNodeId(newTarget, { type: inferredType }); if (validNodeIds.has(normalized)) newTarget = normalized; } diff --git a/understand-anything-plugin/packages/core/src/schema.ts b/understand-anything-plugin/packages/core/src/schema.ts index fd59ecd..88790a8 100644 --- a/understand-anything-plugin/packages/core/src/schema.ts +++ b/understand-anything-plugin/packages/core/src/schema.ts @@ -49,10 +49,10 @@ export const NODE_TYPE_ALIASES: Record = { protobuf: "schema", definition: "schema", typedef: "schema", - // Domain aliases + // Domain aliases — "process" intentionally excluded (ambiguous with OS/Node.js process) business_domain: "domain", - process: "flow", business_flow: "flow", + business_process: "flow", task: "step", business_step: "step", }; @@ -88,7 +88,9 @@ export const EDGE_TYPE_ALIASES: Record = { has_flow: "contains_flow", next_step: "flow_step", interacts_with: "cross_domain", - implemented_by: "implements", + // Note: "implemented_by" is intentionally NOT aliased to "implements" — + // it inverts edge direction (see commit fd0df15). The LLM should use + // "implements" with correct source/target instead. }; // Aliases for complexity values LLMs commonly generate diff --git a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx index ab6ce6f..6b747c0 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx @@ -63,8 +63,8 @@ function buildDomainOverview(graph: KnowledgeGraph): { nodes: Node[]; edges: Edg const rfEdges: Edge[] = graph.edges .filter((e) => e.type === "cross_domain") - .map((e) => ({ - id: `${e.source}-${e.target}`, + .map((e, i) => ({ + id: `cd-${i}-${e.source}-${e.target}`, source: e.source, target: e.target, label: e.description ?? "", @@ -142,8 +142,8 @@ function buildDomainDetail( }); const rfNodes: Node[] = [...flowRfNodes, ...stepRfNodes]; - const rfEdges: Edge[] = stepEdges.map((e) => ({ - id: `${e.source}-${e.target}`, + const rfEdges: Edge[] = stepEdges.map((e, i) => ({ + id: `fs-${i}-${e.source}-${e.target}`, source: e.source, target: e.target, style: { stroke: "var(--color-border-medium)", strokeWidth: 1.5 }, diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index bda407a..925ea87 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx @@ -239,6 +239,7 @@ function useLayerDetailTopology() { const fileLevelTypes = new Set([ "file", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", + "domain", "flow", "step", ]); // Non-technical persona: show module, concept, and file-level types (hide function/class) diff --git a/understand-anything-plugin/packages/dashboard/src/store.ts b/understand-anything-plugin/packages/dashboard/src/store.ts index e540551..19e6e37 100644 --- a/understand-anything-plugin/packages/dashboard/src/store.ts +++ b/understand-anything-plugin/packages/dashboard/src/store.ts @@ -11,7 +11,7 @@ export type Persona = "non-technical" | "junior" | "experienced"; export type NavigationLevel = "overview" | "layer-detail"; export type NodeType = "file" | "function" | "class" | "module" | "concept" | "config" | "document" | "service" | "table" | "endpoint" | "pipeline" | "schema" | "resource" | "domain" | "flow" | "step"; export type Complexity = "simple" | "moderate" | "complex"; -export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic"; +export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic" | "domain"; export type ViewMode = "structural" | "domain"; export interface FilterState { @@ -23,7 +23,7 @@ export interface FilterState { export const ALL_NODE_TYPES: NodeType[] = ["file", "function", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step"]; export const ALL_COMPLEXITIES: Complexity[] = ["simple", "moderate", "complex"]; -export const ALL_EDGE_CATEGORIES: EdgeCategory[] = ["structural", "behavioral", "data-flow", "dependencies", "semantic"]; +export const ALL_EDGE_CATEGORIES: EdgeCategory[] = ["structural", "behavioral", "data-flow", "dependencies", "semantic", "domain"]; export const EDGE_CATEGORY_MAP: Record = { structural: ["imports", "exports", "contains", "inherits", "implements"], @@ -31,9 +31,10 @@ export const EDGE_CATEGORY_MAP: Record = { "data-flow": ["reads_from", "writes_to", "transforms", "validates"], dependencies: ["depends_on", "tested_by", "configures"], semantic: ["related", "similar_to"], + domain: ["contains_flow", "flow_step", "cross_domain"], }; -export const DOMAIN_EDGE_TYPES = ["contains_flow", "flow_step", "cross_domain"] as const; +export const DOMAIN_EDGE_TYPES = EDGE_CATEGORY_MAP.domain; const DEFAULT_FILTERS: FilterState = { nodeTypes: new Set(ALL_NODE_TYPES), @@ -209,6 +210,9 @@ export const useDashboardStore = create()((set, get) => ({ const searchEngine = new SearchEngine(graph.nodes); const query = get().searchQuery; const searchResults = query.trim() ? searchEngine.search(query) : []; + const { viewMode, domainGraph, activeDomainId } = get(); + // Preserve domain view if a domain graph is already loaded + const keepDomainView = viewMode === "domain" && domainGraph !== null; set({ graph, searchEngine, @@ -218,8 +222,8 @@ export const useDashboardStore = create()((set, get) => ({ selectedNodeId: null, focusNodeId: null, nodeHistory: [], - viewMode: "structural" as const, - activeDomainId: null, + viewMode: keepDomainView ? "domain" as const : "structural" as const, + activeDomainId: keepDomainView ? activeDomainId : null, }); }, @@ -477,10 +481,14 @@ export const useDashboardStore = create()((set, get) => ({ }, navigateToDomain: (domainId) => { + const { selectedNodeId, nodeHistory } = get(); + const newHistory = selectedNodeId + ? [...nodeHistory, selectedNodeId].slice(-MAX_HISTORY) + : nodeHistory; set({ activeDomainId: domainId, - selectedNodeId: null, focusNodeId: null, + nodeHistory: newHistory, }); }, diff --git a/understand-anything-plugin/skills/understand-domain/SKILL.md b/understand-anything-plugin/skills/understand-domain/SKILL.md index ac590bd..5c455e8 100644 --- a/understand-anything-plugin/skills/understand-domain/SKILL.md +++ b/understand-anything-plugin/skills/understand-domain/SKILL.md @@ -62,7 +62,7 @@ The preprocessing script does NOT produce a domain graph — it produces **raw m 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` +5. Clean up `.understand-anything/intermediate/domain-analysis.json` and `.understand-anything/intermediate/domain-context.json` ### Phase 6: Launch Dashboard