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