fix: address all code review issues from both reviewers

- Fix race condition: setGraph no longer wipes domainGraph on parallel fetch
- Remove workflow/action aliases that conflicted with pipeline type
- Remove duplicate onNodeDoubleClick handler in DomainGraphView
- Add clearActiveDomain store action (replaces direct setState call)
- Remove auto-switch to domain viewMode in setDomainGraph
- Add DomainMetaSchema Zod validation for domainMeta fields
- Add Array.isArray guards for domainMeta collections in NodeInfo
- Remove as-any cast in getDomainMeta (use typed domainMeta directly)
- Add "domain" filter category for domain/flow/step nodes
- Keep flow discriminator in step ID normalization to prevent collisions
- Update SKILL.md Phase 2 to use tool-based scanning (no missing script)
- Update EDGE_LABELS comment to reflect 29 edge types
- Bump version to 2.1.0 in all 4 required files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-02 14:52:34 +08:00
Unverified
parent 9e8b99bc9e
commit 7d3c049422
15 changed files with 62 additions and 56 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
{
"name": "understand-anything",
"description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands",
"version": "2.0.0",
"version": "2.1.0",
"source": "./understand-anything-plugin"
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.0.0",
"version": "2.1.0",
"author": {
"name": "Lum1104"
},
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "understand-anything",
"displayName": "Understand Anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.0.0",
"version": "2.1.0",
"author": {
"name": "Lum1104"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@understand-anything/skill",
"version": "2.0.0",
"version": "2.1.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -24,7 +24,7 @@ describe("normalizeNodeId — domain types", () => {
name: "Validate",
filePath: "src/validators/order.ts",
});
expect(result).toBe("step:src/validators/order.ts:validate");
expect(result).toBe("step:create-order:src/validators/order.ts:validate");
});
it("normalizes step node IDs without filePath", () => {
@@ -110,8 +110,8 @@ describe("domain graph types", () => {
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";
(graph.nodes[1] as any).type = "business_flow";
(graph.nodes[2] as any).type = "business_step";
const result = validateGraph(graph);
expect(result.success).toBe(true);
expect(result.data!.nodes[0].type).toBe("domain");
@@ -686,11 +686,11 @@ describe("Extended node/edge types", () => {
}
});
it("auto-fixes new node type aliases: container->service, doc->document, workflow->flow, etc.", () => {
it("auto-fixes new node type aliases: container->service, doc->document, business_flow->flow, etc.", () => {
const aliases: Record<string, string> = {
container: "service",
doc: "document",
workflow: "flow",
business_flow: "flow",
route: "endpoint",
setting: "config",
infra: "resource",
@@ -72,16 +72,16 @@ export function normalizeNodeId(
const { prefix, path } = stripToValidPrefix(trimmed);
if (prefix) {
// For step nodes with filePath, reconstruct as step:filePath:stepSlug.
// This intentionally drops the flow slug (e.g. "create-order" in
// "step:create-order:validate") — the normalized form anchors to
// file paths instead of flow parentage, so the ID is stable across
// renames of the parent flow.
// For step nodes with filePath, reconstruct as step:flowSlug:filePath:stepSlug.
// Keeps the flow discriminator to avoid collisions when two flows
// have a same-named step in the same file.
if (node.type === "step" && node.filePath) {
// Use the last colon-separated segment of the path as the step slug
const lastColon = path.lastIndexOf(":");
const stepSlug = lastColon >= 0 ? path.slice(lastColon + 1) : path;
return `${prefix}:${node.filePath}:${stepSlug}`;
const segments = path.split(":");
const stepSlug = segments.length > 0 ? segments[segments.length - 1] : path;
const flowSlug = segments.length > 1 ? segments[segments.length - 2] : "";
return flowSlug
? `${prefix}:${flowSlug}:${node.filePath}:${stepSlug}`
: `${prefix}:${node.filePath}:${stepSlug}`;
}
return `${prefix}:${path}`;
}
@@ -99,6 +99,7 @@ export function normalizeNodeId(
// For step nodes with filePath, reconstruct as step: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}`;
}
return `${expectedPrefix}:${path}`;
@@ -52,9 +52,9 @@ export const NODE_TYPE_ALIASES: Record<string, string> = {
// Domain aliases
business_domain: "domain",
process: "flow",
workflow: "flow",
action: "step",
business_flow: "flow",
task: "step",
business_step: "step",
};
// Aliases that LLMs commonly generate instead of canonical edge types
@@ -317,6 +317,14 @@ export function autoFixGraph(data: Record<string, unknown>): {
return { data: result, issues };
}
const DomainMetaSchema = z.object({
entities: z.array(z.string()).optional(),
businessRules: z.array(z.string()).optional(),
crossDomainInteractions: z.array(z.string()).optional(),
entryPoint: z.string().optional(),
entryType: z.enum(["http", "cli", "event", "cron", "manual"]).optional(),
}).passthrough();
export const GraphNodeSchema = z.object({
id: z.string(),
type: z.enum([
@@ -332,6 +340,7 @@ export const GraphNodeSchema = z.object({
tags: z.array(z.string()),
complexity: z.enum(["simple", "moderate", "complex"]),
languageNotes: z.string().optional(),
domainMeta: DomainMetaSchema.optional(),
}).passthrough();
export const GraphEdgeSchema = z.object({
@@ -357,6 +357,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
{ key: "docs", label: "Docs", color: "var(--color-node-document)" },
{ key: "infra", label: "Infra", color: "var(--color-node-service)" },
{ key: "data", label: "Data", color: "var(--color-node-table)" },
{ key: "domain", label: "Domain", color: "var(--color-node-concept)" },
] as const).map((cat) => (
<button
key={cat.key}
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import {
ReactFlow,
ReactFlowProvider,
@@ -26,8 +26,8 @@ const nodeTypes = {
"step-node": StepNode,
};
function getDomainMeta(node: GraphNode): Record<string, unknown> | undefined {
return (node as any).domainMeta;
function getDomainMeta(node: GraphNode) {
return node.domainMeta;
}
function buildDomainOverview(graph: KnowledgeGraph): { nodes: Node[]; edges: Edge[] } {
@@ -156,7 +156,7 @@ function buildDomainDetail(
function DomainGraphViewInner() {
const domainGraph = useDashboardStore((s) => s.domainGraph);
const activeDomainId = useDashboardStore((s) => s.activeDomainId);
const navigateToDomain = useDashboardStore((s) => s.navigateToDomain);
const clearActiveDomain = useDashboardStore((s) => s.clearActiveDomain);
const { nodes, edges } = useMemo(() => {
if (!domainGraph) return { nodes: [], edges: [] };
@@ -166,14 +166,7 @@ function DomainGraphViewInner() {
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],
);
// Double-click is handled by individual node components (e.g. DomainClusterNode)
if (!domainGraph) {
return (
@@ -189,9 +182,7 @@ function DomainGraphViewInner() {
<div className="absolute top-3 left-3 z-10">
<button
type="button"
onClick={() => {
useDashboardStore.setState({ activeDomainId: null, selectedNodeId: null });
}}
onClick={() => clearActiveDomain()}
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
@@ -202,7 +193,6 @@ function DomainGraphViewInner() {
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodeDoubleClick={onNodeDoubleClick}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.1}
@@ -56,8 +56,7 @@ const NODE_TYPE_TO_CATEGORY: Record<NodeType, NodeCategory> = {
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",
domain: "domain", flow: "domain", step: "domain",
} as const;
// ── Helper components that must live inside <ReactFlow> ────────────────
@@ -29,7 +29,7 @@ const complexityBadgeColors: Record<string, string> = {
};
/**
* Human-readable directional labels for all 26 edge types.
* Human-readable directional labels for all 29 edge types.
* Must be kept in sync with core EdgeType.
*/
const EDGE_LABELS: Record<EdgeType, { forward: string; backward: string }> = {
@@ -91,7 +91,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
return (
<div className="space-y-3">
{meta?.entities && meta.entities.length > 0 ? (
{Array.isArray(meta?.entities) && meta.entities.length > 0 ? (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entities</h4>
<div className="flex flex-wrap gap-1">
@@ -101,7 +101,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
</div>
</div>
) : null}
{meta?.businessRules && meta.businessRules.length > 0 ? (
{Array.isArray(meta?.businessRules) && meta.businessRules.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">
@@ -111,7 +111,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
</ul>
</div>
) : null}
{meta?.crossDomainInteractions && meta.crossDomainInteractions.length > 0 ? (
{Array.isArray(meta?.crossDomainInteractions) && meta.crossDomainInteractions.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">
@@ -43,7 +43,7 @@ const DEFAULT_FILTERS: FilterState = {
};
/** Categories used for node type filter toggles. Single source of truth for NodeCategory. */
export type NodeCategory = "code" | "config" | "docs" | "infra" | "data";
export type NodeCategory = "code" | "config" | "docs" | "infra" | "data" | "domain";
/** Find which layer a node belongs to. Returns layerId or null. */
function findNodeLayer(graph: KnowledgeGraph, nodeId: string): string | null {
@@ -139,6 +139,7 @@ interface DashboardStore {
setDomainGraph: (graph: KnowledgeGraph) => void;
setViewMode: (mode: ViewMode) => void;
navigateToDomain: (domainId: string) => void;
clearActiveDomain: () => void;
}
function getSortedTour(graph: KnowledgeGraph): TourStep[] {
@@ -194,7 +195,7 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
pathFinderOpen: false,
reactFlowInstance: null,
nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true },
nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true, domain: true },
toggleNodeTypeFilter: (category) =>
set((state) => ({
@@ -218,7 +219,6 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
focusNodeId: null,
nodeHistory: [],
viewMode: "structural" as const,
domainGraph: null,
activeDomainId: null,
});
},
@@ -463,7 +463,7 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
activeDomainId: null,
setDomainGraph: (graph) => {
set({ domainGraph: graph, viewMode: "domain" });
set({ domainGraph: graph });
},
setViewMode: (mode) => {
@@ -483,4 +483,12 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
focusNodeId: null,
});
},
clearActiveDomain: () => {
set({
activeDomainId: null,
selectedNodeId: null,
focusNodeId: null,
});
},
}));
@@ -24,16 +24,14 @@ Extracts business domain knowledge — domains, business flows, and process step
### 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 <project-root>
```
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
Perform a lightweight scan of the project to gather domain context:
1. Use Glob and Grep tools to build a project context:
- Scan the file tree (respect `.gitignore` patterns)
- Detect entry points: HTTP routes, CLI commands, event handlers, cron jobs, exported handlers
- Read key files: package.json/Cargo.toml/go.mod for project metadata, README for business context
- Sample representative source files (routers, controllers, services, models) for domain terminology
2. Assemble the context into a structured summary for Phase 4
3. Proceed to Phase 4
### Phase 3: Derive from Existing Graph (Path 2)