mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user