fix: address code review issues across domain feature

- Remove direction-inverting `implemented_by` alias (same pattern as fd0df15)
- Replace ambiguous `process` alias with `business_process`
- Fix duplicate React Flow edge IDs in DomainGraphView
- Fix navigateToDomain clearing selectedNodeId and losing history
- Preserve domain viewMode when structural graph loads after domain graph
- Add domain/flow/step to fileLevelTypes in GraphView
- Add domain edge category to EDGE_CATEGORY_MAP
- Extend COMPLEXITY_STRING_MAP with trivial/basic/mid/average/advanced
- Normalize string complexity values in normalizeBatchOutput (not just numeric)
- Infer node type from ID prefix in edge fallback normalization
- Include flow discriminator in bare-path step ID normalization
- Clean up domain-context.json intermediate file in SKILL.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-02 21:20:33 +08:00
Unverified
parent e682ec1829
commit 9d1abd0a30
6 changed files with 88 additions and 26 deletions
@@ -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<string, string> = {
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<string, string> = {
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<string, string>();
// 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<string, string>();
const flowNodeNames = new Map<string, string>();
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<string, unknown> = { ...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;
}
@@ -49,10 +49,10 @@ export const NODE_TYPE_ALIASES: Record<string, string> = {
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<string, string> = {
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
@@ -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 },
@@ -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)
@@ -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<EdgeCategory, string[]> = {
structural: ["imports", "exports", "contains", "inherits", "implements"],
@@ -31,9 +31,10 @@ export const EDGE_CATEGORY_MAP: Record<EdgeCategory, string[]> = {
"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<NodeType>(ALL_NODE_TYPES),
@@ -209,6 +210,9 @@ export const useDashboardStore = create<DashboardStore>()((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<DashboardStore>()((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<DashboardStore>()((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,
});
},
@@ -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