mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
fix: normalize LLM-generated type aliases before schema validation
LLMs systematically abbreviate node types (e.g. "func" instead of "function") and edge types (e.g. "extends" instead of "inherits"), causing dashboard validation failures. This combines two fixes: Option A: Rename the ambiguous `func:` ID prefix to `function:` across all prompts, source code, tests, and example data so LLMs see consistent naming. Also fix `relates_to` ghost edge type in django.md. Option B: Add NODE_TYPE_ALIASES and EDGE_TYPE_ALIASES normalization maps in schema.ts that transparently correct common abbreviations before Zod validation, as a runtime safety net. Closes #36 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ The JSON has this top-level shape:
|
||||
| Type | ID Convention | Description |
|
||||
|---|---|---|
|
||||
| `file` | `file:<relative-path>` | Source file |
|
||||
| `function` | `func:<relative-path>:<name>` | Function or method |
|
||||
| `function` | `function:<relative-path>:<name>` | Function or method |
|
||||
| `class` | `class:<relative-path>:<name>` | Class, interface, or type |
|
||||
| `module` | `module:<name>` | Logical module or package |
|
||||
| `concept` | `concept:<name>` | Abstract concept or pattern |
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { GraphNode, GraphEdge } from "../types.js";
|
||||
import { typescriptConfig } from "../languages/configs/typescript.js";
|
||||
|
||||
const sampleNode: GraphNode = {
|
||||
id: "func:auth:verifyToken",
|
||||
id: "function:auth:verifyToken",
|
||||
type: "function",
|
||||
name: "verifyToken",
|
||||
filePath: "src/auth/verify.ts",
|
||||
@@ -20,7 +20,7 @@ const sampleNode: GraphNode = {
|
||||
|
||||
const sampleEdges: GraphEdge[] = [
|
||||
{
|
||||
source: "func:auth:verifyToken",
|
||||
source: "function:auth:verifyToken",
|
||||
target: "file:src/config.ts",
|
||||
type: "reads_from",
|
||||
direction: "forward",
|
||||
@@ -28,7 +28,7 @@ const sampleEdges: GraphEdge[] = [
|
||||
},
|
||||
{
|
||||
source: "file:src/middleware.ts",
|
||||
target: "func:auth:verifyToken",
|
||||
target: "function:auth:verifyToken",
|
||||
type: "calls",
|
||||
direction: "forward",
|
||||
weight: 0.8,
|
||||
@@ -128,7 +128,7 @@ describe("language-lesson", () => {
|
||||
|
||||
it("detects middleware pattern", () => {
|
||||
const middlewareNode: GraphNode = {
|
||||
id: "func:middleware:auth",
|
||||
id: "function:middleware:auth",
|
||||
type: "function",
|
||||
name: "authMiddleware",
|
||||
filePath: "src/middleware/auth.ts",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validateGraph } from "../schema.js";
|
||||
import { validateGraph, normalizeGraph } from "../schema.js";
|
||||
import type { KnowledgeGraph } from "../types.js";
|
||||
|
||||
const validGraph: KnowledgeGraph = {
|
||||
@@ -109,4 +109,135 @@ describe("schema validation", () => {
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
});
|
||||
|
||||
it('normalizes "func" node type to "function"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.nodes[0] as any).type = "func";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.nodes[0].type).toBe("function");
|
||||
});
|
||||
|
||||
it('normalizes "fn" node type to "function"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.nodes[0] as any).type = "fn";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.nodes[0].type).toBe("function");
|
||||
});
|
||||
|
||||
it('normalizes "method" node type to "function"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.nodes[0] as any).type = "method";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.nodes[0].type).toBe("function");
|
||||
});
|
||||
|
||||
it('normalizes "interface" node type to "class"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.nodes[0] as any).type = "interface";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.nodes[0].type).toBe("class");
|
||||
});
|
||||
|
||||
it('normalizes "struct" node type to "class"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.nodes[0] as any).type = "struct";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.nodes[0].type).toBe("class");
|
||||
});
|
||||
|
||||
it("normalizes multiple aliased node types in one graph", () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.nodes[0] as any).type = "func";
|
||||
graph.nodes.push({
|
||||
id: "node-2",
|
||||
type: "file" as any,
|
||||
name: "utils.ts",
|
||||
filePath: "src/utils.ts",
|
||||
lineRange: [1, 30],
|
||||
summary: "Utility helpers",
|
||||
tags: ["utils"],
|
||||
complexity: "simple",
|
||||
});
|
||||
(graph.nodes[1] as any).type = "pkg";
|
||||
graph.nodes.push({
|
||||
id: "node-3",
|
||||
type: "file" as any,
|
||||
name: "MyClass.ts",
|
||||
filePath: "src/MyClass.ts",
|
||||
lineRange: [1, 80],
|
||||
summary: "A class",
|
||||
tags: ["class"],
|
||||
complexity: "moderate",
|
||||
});
|
||||
(graph.nodes[2] as any).type = "struct";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.nodes[0].type).toBe("function");
|
||||
expect(result.data!.nodes[1].type).toBe("module");
|
||||
expect(result.data!.nodes[2].type).toBe("class");
|
||||
});
|
||||
|
||||
it('normalizes "extends" edge type to "inherits"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.edges[0] as any).type = "extends";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.edges[0].type).toBe("inherits");
|
||||
});
|
||||
|
||||
it('normalizes "invokes" edge type to "calls"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.edges[0] as any).type = "invokes";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.edges[0].type).toBe("calls");
|
||||
});
|
||||
|
||||
it('normalizes "relates_to" edge type to "related"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.edges[0] as any).type = "relates_to";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.edges[0].type).toBe("related");
|
||||
});
|
||||
|
||||
it('normalizes "uses" edge type to "depends_on"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.edges[0] as any).type = "uses";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.edges[0].type).toBe("depends_on");
|
||||
});
|
||||
|
||||
it('normalizes "tests" edge type to "tested_by"', () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.edges[0] as any).type = "tests";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data!.edges[0].type).toBe("tested_by");
|
||||
});
|
||||
|
||||
it("still rejects truly invalid edge types after normalization", () => {
|
||||
const graph = structuredClone(validGraph);
|
||||
(graph.edges[0] as any).type = "totally_bogus";
|
||||
|
||||
const result = validateGraph(graph);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,14 +74,14 @@ describe("GraphBuilder", () => {
|
||||
expect(fileNode!.type).toBe("file");
|
||||
expect(fileNode!.summary).toBe("Handles data processing");
|
||||
|
||||
const funcNode = graph.nodes.find((n) => n.id === "func:src/service.ts:processData");
|
||||
const funcNode = graph.nodes.find((n) => n.id === "function:src/service.ts:processData");
|
||||
expect(funcNode).toBeDefined();
|
||||
expect(funcNode!.type).toBe("function");
|
||||
expect(funcNode!.name).toBe("processData");
|
||||
expect(funcNode!.lineRange).toEqual([10, 25]);
|
||||
expect(funcNode!.summary).toBe("Processes raw input data");
|
||||
|
||||
const validateNode = graph.nodes.find((n) => n.id === "func:src/service.ts:validate");
|
||||
const validateNode = graph.nodes.find((n) => n.id === "function:src/service.ts:validate");
|
||||
expect(validateNode).toBeDefined();
|
||||
expect(validateNode!.summary).toBe("Validates data format");
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("GraphBuilder", () => {
|
||||
|
||||
expect(containsEdges[0]).toMatchObject({
|
||||
source: "file:src/widget.ts",
|
||||
target: "func:src/widget.ts:helper",
|
||||
target: "function:src/widget.ts:helper",
|
||||
type: "contains",
|
||||
direction: "forward",
|
||||
weight: 1,
|
||||
@@ -170,8 +170,8 @@ describe("GraphBuilder", () => {
|
||||
const callEdges = graph.edges.filter((e) => e.type === "calls");
|
||||
expect(callEdges).toHaveLength(1);
|
||||
expect(callEdges[0]).toMatchObject({
|
||||
source: "func:src/index.ts:main",
|
||||
target: "func:src/utils.ts:helper",
|
||||
source: "function:src/index.ts:main",
|
||||
target: "function:src/utils.ts:helper",
|
||||
type: "calls",
|
||||
direction: "forward",
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ export class GraphBuilder {
|
||||
|
||||
// Create function nodes with "contains" edges
|
||||
for (const fn of analysis.functions) {
|
||||
const funcId = `func:${filePath}:${fn.name}`;
|
||||
const funcId = `function:${filePath}:${fn.name}`;
|
||||
this.nodes.push({
|
||||
id: funcId,
|
||||
type: "function",
|
||||
@@ -179,8 +179,8 @@ export class GraphBuilder {
|
||||
calleeFunc: string,
|
||||
): void {
|
||||
this.edges.push({
|
||||
source: `func:${callerFile}:${callerFunc}`,
|
||||
target: `func:${calleeFile}:${calleeFunc}`,
|
||||
source: `function:${callerFile}:${callerFunc}`,
|
||||
target: `function:${calleeFile}:${calleeFunc}`,
|
||||
type: "calls",
|
||||
direction: "forward",
|
||||
weight: 0.8,
|
||||
|
||||
@@ -9,6 +9,36 @@ export const EdgeTypeSchema = z.enum([
|
||||
"related", "similar_to", // Semantic
|
||||
]);
|
||||
|
||||
// Aliases that LLMs commonly generate instead of canonical node types
|
||||
export const NODE_TYPE_ALIASES: Record<string, string> = {
|
||||
func: "function",
|
||||
fn: "function",
|
||||
method: "function",
|
||||
interface: "class",
|
||||
struct: "class",
|
||||
mod: "module",
|
||||
pkg: "module",
|
||||
package: "module",
|
||||
};
|
||||
|
||||
// Aliases that LLMs commonly generate instead of canonical edge types
|
||||
export const EDGE_TYPE_ALIASES: Record<string, string> = {
|
||||
extends: "inherits",
|
||||
invokes: "calls",
|
||||
invoke: "calls",
|
||||
uses: "depends_on",
|
||||
requires: "depends_on",
|
||||
relates_to: "related",
|
||||
related_to: "related",
|
||||
similar: "similar_to",
|
||||
tests: "tested_by",
|
||||
import: "imports",
|
||||
export: "exports",
|
||||
contain: "contains",
|
||||
publish: "publishes",
|
||||
subscribe: "subscribes",
|
||||
};
|
||||
|
||||
export const GraphNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.enum(["file", "function", "class", "module", "concept"]),
|
||||
@@ -69,8 +99,45 @@ export interface ValidationResult {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export function normalizeGraph(data: unknown): unknown {
|
||||
if (typeof data !== "object" || data === null) return data;
|
||||
|
||||
const d = data as Record<string, unknown>;
|
||||
const result = { ...d };
|
||||
|
||||
if (Array.isArray(d.nodes)) {
|
||||
result.nodes = (d.nodes as any[]).map((node) => {
|
||||
if (
|
||||
typeof node === "object" &&
|
||||
node !== null &&
|
||||
typeof node.type === "string" &&
|
||||
node.type in NODE_TYPE_ALIASES
|
||||
) {
|
||||
return { ...node, type: NODE_TYPE_ALIASES[node.type] };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(d.edges)) {
|
||||
result.edges = (d.edges as any[]).map((edge) => {
|
||||
if (
|
||||
typeof edge === "object" &&
|
||||
edge !== null &&
|
||||
typeof edge.type === "string" &&
|
||||
edge.type in EDGE_TYPE_ALIASES
|
||||
) {
|
||||
return { ...edge, type: EDGE_TYPE_ALIASES[edge.type] };
|
||||
}
|
||||
return edge;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function validateGraph(data: unknown): ValidationResult {
|
||||
const result = KnowledgeGraphSchema.safeParse(data);
|
||||
const result = KnowledgeGraphSchema.safeParse(normalizeGraph(data));
|
||||
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ The knowledge graph JSON has this structure:
|
||||
- `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash}
|
||||
- `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?}
|
||||
- Node types: file, function, class, module, concept
|
||||
- IDs: `file:path`, `func:path:name`, `class:path:name`
|
||||
- IDs: `file:path`, `function:path:name`, `class:path:name`
|
||||
- `edges[]` — each has {source, target, type, direction, weight}
|
||||
- Key types: imports, contains, calls, depends_on
|
||||
- `layers[]` — each has {id, name, description, nodeIds[]}
|
||||
|
||||
@@ -13,7 +13,7 @@ The knowledge graph JSON has this structure:
|
||||
- `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash}
|
||||
- `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?}
|
||||
- Node types: file, function, class, module, concept
|
||||
- IDs: `file:path`, `func:path:name`, `class:path:name`
|
||||
- IDs: `file:path`, `function:path:name`, `class:path:name`
|
||||
- `edges[]` — each has {source, target, type, direction, weight}
|
||||
- Key types: imports, contains, calls, depends_on
|
||||
- `layers[]` — each has {id, name, description, nodeIds[]}
|
||||
|
||||
@@ -14,7 +14,7 @@ The knowledge graph JSON has this structure:
|
||||
- `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash}
|
||||
- `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?}
|
||||
- Node types: file, function, class, module, concept
|
||||
- IDs: `file:path`, `func:path:name`, `class:path:name`
|
||||
- IDs: `file:path`, `function:path:name`, `class:path:name`
|
||||
- `edges[]` — each has {source, target, type, direction, weight}
|
||||
- Key types: imports, contains, calls, depends_on
|
||||
- `layers[]` — each has {id, name, description, nodeIds[]}
|
||||
|
||||
@@ -13,7 +13,7 @@ The knowledge graph JSON has this structure:
|
||||
- `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash}
|
||||
- `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?}
|
||||
- Node types: file, function, class, module, concept
|
||||
- IDs: `file:path`, `func:path:name`, `class:path:name`
|
||||
- IDs: `file:path`, `function:path:name`, `class:path:name`
|
||||
- `edges[]` — each has {source, target, type, direction, weight}
|
||||
- Key types: imports, contains, calls, depends_on
|
||||
- `layers[]` — each has {id, name, description, nodeIds[]}
|
||||
|
||||
@@ -415,7 +415,7 @@ Pass these parameters in the dispatch prompt:
|
||||
| Type | Description | ID Convention |
|
||||
|---|---|---|
|
||||
| `file` | Source file | `file:<relative-path>` |
|
||||
| `function` | Function or method | `func:<relative-path>:<name>` |
|
||||
| `function` | Function or method | `function:<relative-path>:<name>` |
|
||||
| `class` | Class, interface, or type | `class:<relative-path>:<name>` |
|
||||
| `module` | Logical module or package | `module:<name>` |
|
||||
| `concept` | Abstract concept or pattern | `concept:<name>` |
|
||||
|
||||
@@ -184,7 +184,7 @@ If the structural data reveals notable language-specific patterns (e.g., many ge
|
||||
|
||||
### Step 2 -- Create Function and Class Nodes
|
||||
|
||||
For significant functions and classes from the script output, create `func:` and `class:` nodes.
|
||||
For significant functions and classes from the script output, create `function:` and `class:` nodes.
|
||||
|
||||
**Significance filter** -- only create nodes for:
|
||||
- Functions/methods with 10+ lines (skip trivial one-liners)
|
||||
@@ -221,10 +221,10 @@ You MUST use these exact prefixes for node IDs:
|
||||
| Node Type | ID Format | Example |
|
||||
|---|---|---|
|
||||
| File | `file:<relative-path>` | `file:src/index.ts` |
|
||||
| Function | `func:<relative-path>:<function-name>` | `func:src/utils.ts:formatDate` |
|
||||
| Function | `function:<relative-path>:<function-name>` | `function:src/utils.ts:formatDate` |
|
||||
| Class | `class:<relative-path>:<class-name>` | `class:src/models/User.ts:User` |
|
||||
|
||||
**Scope restriction:** Only produce `file:`, `func:`, and `class:` nodes. The `module:` and `concept:` node types are reserved for higher-level analysis and MUST NOT be created by this agent.
|
||||
**Scope restriction:** Only produce `file:`, `function:`, and `class:` nodes. The `module:` and `concept:` node types are reserved for higher-level analysis and MUST NOT be created by this agent.
|
||||
|
||||
## Output Format
|
||||
|
||||
@@ -244,7 +244,7 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
|
||||
"languageNotes": "TypeScript barrel file using re-exports."
|
||||
},
|
||||
{
|
||||
"id": "func:src/utils.ts:formatDate",
|
||||
"id": "function:src/utils.ts:formatDate",
|
||||
"type": "function",
|
||||
"name": "formatDate",
|
||||
"filePath": "src/utils.ts",
|
||||
@@ -264,7 +264,7 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
|
||||
},
|
||||
{
|
||||
"source": "file:src/utils.ts",
|
||||
"target": "func:src/utils.ts:formatDate",
|
||||
"target": "function:src/utils.ts:formatDate",
|
||||
"type": "contains",
|
||||
"direction": "forward",
|
||||
"weight": 1.0
|
||||
@@ -300,7 +300,7 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
|
||||
- NEVER invent file paths. Every `filePath` and every file reference in node IDs must correspond to a real file from the script's output or the project file list provided to you.
|
||||
- NEVER create edges to nodes that do not exist. If an import target is external (`isExternal: true` in script output), do NOT create an edge for it.
|
||||
- ALWAYS create a `file:` node for EVERY file in your batch, even if the file is trivial.
|
||||
- Only create `func:` and `class:` nodes for significant code elements (see significance filter above).
|
||||
- Only create `function:` and `class:` nodes for significant code elements (see significance filter above).
|
||||
- For import edges, use the script's `resolvedPath` field directly. Do NOT attempt to resolve import paths yourself -- the script already did this deterministically.
|
||||
- NEVER produce duplicate node IDs within your batch.
|
||||
- NEVER create self-referencing edges (where source equals target).
|
||||
|
||||
@@ -38,7 +38,7 @@ When analyzing a Django project, apply these additional conventions on top of th
|
||||
|
||||
**Signal wiring** — When `signals.py` uses `post_save.connect(handler, sender=Model)` or `@receiver(post_save, sender=Model)`, create `subscribes` edges from the signal handler function to the model class. Create `publishes` edges from the model to the signal handler to show the trigger direction.
|
||||
|
||||
**ORM relationships** — When `models.py` defines `ForeignKey`, `OneToOneField`, or `ManyToManyField`, create `relates_to` edges (use `depends_on` edge type) between the model classes with a description indicating the relationship type and cardinality.
|
||||
**ORM relationships** — When `models.py` defines `ForeignKey`, `OneToOneField`, or `ManyToManyField`, create `depends_on` edges between the model classes with a description indicating the relationship type and cardinality.
|
||||
|
||||
**Serializer-to-model binding** — When a DRF serializer has `model = MyModel` in its `Meta` class, create a `depends_on` edge from the serializer to the model.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Verify every **node** has ALL required fields with correct types:
|
||||
|
||||
| Field | Type | Constraint |
|
||||
|---|---|---|
|
||||
| `id` | string | Non-empty, follows prefix convention (`file:`, `func:`, `class:`, `module:`, or `concept:`) |
|
||||
| `id` | string | Non-empty, follows prefix convention (`file:`, `function:`, `class:`, `module:`, or `concept:`) |
|
||||
| `type` | string | One of: `file`, `function`, `class`, `module`, `concept` |
|
||||
| `name` | string | Non-empty |
|
||||
| `summary` | string | Non-empty, not just the filename |
|
||||
|
||||
@@ -16,18 +16,18 @@ const sampleGraph: KnowledgeGraph = {
|
||||
{ id: "file:src/index.ts", type: "file", name: "index.ts", filePath: "src/index.ts", summary: "Entry point", tags: ["entry"], complexity: "simple" },
|
||||
{ id: "file:src/routes.ts", type: "file", name: "routes.ts", filePath: "src/routes.ts", summary: "Routes", tags: ["routes"], complexity: "moderate" },
|
||||
{ id: "file:src/service.ts", type: "file", name: "service.ts", filePath: "src/service.ts", summary: "Service", tags: ["service"], complexity: "complex" },
|
||||
{ id: "func:src/service.ts:process", type: "function", name: "process", filePath: "src/service.ts", lineRange: [10, 30], summary: "Process function", tags: ["core"], complexity: "complex" },
|
||||
{ id: "function:src/service.ts:process", type: "function", name: "process", filePath: "src/service.ts", lineRange: [10, 30], summary: "Process function", tags: ["core"], complexity: "complex" },
|
||||
{ id: "file:src/db.ts", type: "file", name: "db.ts", filePath: "src/db.ts", summary: "Database", tags: ["db"], complexity: "simple" },
|
||||
],
|
||||
edges: [
|
||||
{ source: "file:src/index.ts", target: "file:src/routes.ts", type: "imports", direction: "forward", weight: 0.9 },
|
||||
{ source: "file:src/routes.ts", target: "file:src/service.ts", type: "calls", direction: "forward", weight: 0.8 },
|
||||
{ source: "file:src/service.ts", target: "func:src/service.ts:process", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "file:src/service.ts", target: "function:src/service.ts:process", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "file:src/service.ts", target: "file:src/db.ts", type: "reads_from", direction: "forward", weight: 0.7 },
|
||||
],
|
||||
layers: [
|
||||
{ id: "layer:api", name: "API Layer", description: "HTTP routes", nodeIds: ["file:src/index.ts", "file:src/routes.ts"] },
|
||||
{ id: "layer:service", name: "Service Layer", description: "Business logic", nodeIds: ["file:src/service.ts", "func:src/service.ts:process"] },
|
||||
{ id: "layer:service", name: "Service Layer", description: "Business logic", nodeIds: ["file:src/service.ts", "function:src/service.ts:process"] },
|
||||
{ id: "layer:data", name: "Data Layer", description: "Database", nodeIds: ["file:src/db.ts"] },
|
||||
],
|
||||
tour: [],
|
||||
@@ -42,7 +42,7 @@ describe("diff-analyzer", () => {
|
||||
|
||||
it("identifies child nodes of changed files", () => {
|
||||
const ctx = buildDiffContext(sampleGraph, ["src/service.ts"]);
|
||||
expect(ctx.changedNodes.map((n) => n.id)).toContain("func:src/service.ts:process");
|
||||
expect(ctx.changedNodes.map((n) => n.id)).toContain("function:src/service.ts:process");
|
||||
});
|
||||
|
||||
it("identifies affected nodes via edges (1-hop)", () => {
|
||||
|
||||
@@ -14,17 +14,17 @@ const sampleGraph: KnowledgeGraph = {
|
||||
},
|
||||
nodes: [
|
||||
{ id: "file:src/auth.ts", type: "file", name: "auth.ts", filePath: "src/auth.ts", summary: "Auth module", tags: ["auth"], complexity: "complex" },
|
||||
{ id: "func:src/auth.ts:login", type: "function", name: "login", filePath: "src/auth.ts", lineRange: [10, 30], summary: "Login handler", tags: ["auth", "login"], complexity: "moderate" },
|
||||
{ id: "func:src/auth.ts:verify", type: "function", name: "verify", filePath: "src/auth.ts", lineRange: [32, 50], summary: "Token verification", tags: ["auth", "jwt"], complexity: "moderate" },
|
||||
{ id: "function:src/auth.ts:login", type: "function", name: "login", filePath: "src/auth.ts", lineRange: [10, 30], summary: "Login handler", tags: ["auth", "login"], complexity: "moderate" },
|
||||
{ id: "function:src/auth.ts:verify", type: "function", name: "verify", filePath: "src/auth.ts", lineRange: [32, 50], summary: "Token verification", tags: ["auth", "jwt"], complexity: "moderate" },
|
||||
{ id: "file:src/db.ts", type: "file", name: "db.ts", filePath: "src/db.ts", summary: "Database", tags: ["db"], complexity: "simple" },
|
||||
],
|
||||
edges: [
|
||||
{ source: "file:src/auth.ts", target: "func:src/auth.ts:login", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "file:src/auth.ts", target: "func:src/auth.ts:verify", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "func:src/auth.ts:login", target: "file:src/db.ts", type: "reads_from", direction: "forward", weight: 0.8 },
|
||||
{ source: "file:src/auth.ts", target: "function:src/auth.ts:login", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "file:src/auth.ts", target: "function:src/auth.ts:verify", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "function:src/auth.ts:login", target: "file:src/db.ts", type: "reads_from", direction: "forward", weight: 0.8 },
|
||||
],
|
||||
layers: [
|
||||
{ id: "layer:auth", name: "Auth Layer", description: "Authentication", nodeIds: ["file:src/auth.ts", "func:src/auth.ts:login", "func:src/auth.ts:verify"] },
|
||||
{ id: "layer:auth", name: "Auth Layer", description: "Authentication", nodeIds: ["file:src/auth.ts", "function:src/auth.ts:login", "function:src/auth.ts:verify"] },
|
||||
],
|
||||
tour: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user