diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts index 461e252..6dac807 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts @@ -212,4 +212,130 @@ describe("GraphBuilder", () => { const graph = builder.build(); expect(graph.project.languages).toEqual(["go", "javascript", "rust"]); }); + + describe("Non-code file support", () => { + it("adds non-code file nodes with correct types", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFile("README.md", { + nodeType: "document", + summary: "Project documentation", + tags: ["documentation"], + complexity: "simple", + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(1); + expect(graph.nodes[0].type).toBe("document"); + expect(graph.nodes[0].id).toBe("file:README.md"); + }); + + it("adds non-code child nodes (definitions)", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("schema.sql", { + nodeType: "file", + summary: "Database schema", + tags: ["database"], + complexity: "moderate", + definitions: [ + { name: "users", kind: "table", lineRange: [1, 20] as [number, number], fields: ["id", "name", "email"] }, + ], + }); + const graph = builder.build(); + // File node + table child node + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[1].type).toBe("table"); + expect(graph.nodes[1].name).toBe("users"); + // Contains edge + expect(graph.edges.some(e => e.type === "contains" && e.target.includes("users"))).toBe(true); + }); + + it("adds service child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("docker-compose.yml", { + nodeType: "config", + summary: "Docker compose config", + tags: ["infra"], + complexity: "moderate", + services: [ + { name: "web", image: "node:22", ports: [3000] }, + { name: "db", image: "postgres:15", ports: [5432] }, + ], + }); + const graph = builder.build(); + // File node + 2 service child nodes + expect(graph.nodes).toHaveLength(3); + expect(graph.nodes[1].type).toBe("service"); + expect(graph.nodes[1].name).toBe("web"); + expect(graph.nodes[2].type).toBe("service"); + expect(graph.nodes[2].name).toBe("db"); + }); + + it("adds endpoint child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("schema.graphql", { + nodeType: "schema", + summary: "GraphQL schema", + tags: ["api"], + complexity: "moderate", + endpoints: [ + { method: "Query", path: "users", lineRange: [5, 5] as [number, number] }, + ], + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[1].type).toBe("endpoint"); + }); + + it("adds resource child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("main.tf", { + nodeType: "resource", + summary: "Terraform config", + tags: ["infra"], + complexity: "moderate", + resources: [ + { name: "aws_s3_bucket.main", kind: "aws_s3_bucket", lineRange: [1, 10] as [number, number] }, + ], + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[1].type).toBe("resource"); + expect(graph.nodes[1].name).toBe("aws_s3_bucket.main"); + }); + + it("adds step child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("Makefile", { + nodeType: "pipeline", + summary: "Build targets", + tags: ["build"], + complexity: "simple", + steps: [ + { name: "build", lineRange: [1, 3] as [number, number] }, + { name: "test", lineRange: [5, 7] as [number, number] }, + ], + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(3); + expect(graph.nodes[1].type).toBe("pipeline"); + expect(graph.nodes[1].name).toBe("build"); + }); + + it("detects non-code languages from EXTENSION_LANGUAGE map", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addFile("config.yaml", { summary: "Config", tags: [], complexity: "simple" }); + const graph = builder.build(); + expect(graph.project.languages).toContain("yaml"); + }); + + it("detects new non-code extensions", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addFile("schema.graphql", { summary: "Schema", tags: [], complexity: "simple" }); + builder.addFile("main.tf", { summary: "Terraform", tags: [], complexity: "simple" }); + builder.addFile("types.proto", { summary: "Protobuf", tags: [], complexity: "simple" }); + const graph = builder.build(); + expect(graph.project.languages).toContain("graphql"); + expect(graph.project.languages).toContain("terraform"); + expect(graph.project.languages).toContain("protobuf"); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts index aaa170b..1e30c88 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts @@ -3,6 +3,12 @@ import type { GraphNode, GraphEdge, StructuralAnalysis, + DefinitionInfo, + ServiceInfo, + EndpointInfo, + StepInfo, + ResourceInfo, + SectionInfo, } from "../types.js"; interface FileMeta { @@ -16,7 +22,21 @@ interface FileAnalysisMeta extends FileMeta { fileSummary: string; } +interface NonCodeFileMeta extends FileMeta { + nodeType: GraphNode["type"]; +} + +interface NonCodeFileAnalysisMeta extends NonCodeFileMeta { + definitions?: DefinitionInfo[]; + services?: ServiceInfo[]; + endpoints?: EndpointInfo[]; + steps?: StepInfo[]; + resources?: ResourceInfo[]; + sections?: SectionInfo[]; +} + const EXTENSION_LANGUAGE: Record = { + // Code languages ".ts": "typescript", ".tsx": "typescript", ".js": "javascript", @@ -37,20 +57,41 @@ const EXTENSION_LANGUAGE: Record = { ".cs": "csharp", ".php": "php", ".lua": "lua", + // Non-code languages ".sh": "shell", ".bash": "shell", ".zsh": "shell", ".json": "json", + ".jsonc": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".xml": "xml", ".html": "html", + ".htm": "html", ".css": "css", - ".scss": "scss", - ".less": "less", + ".scss": "css", + ".less": "css", ".md": "markdown", + ".mdx": "markdown", ".sql": "sql", + ".graphql": "graphql", + ".gql": "graphql", + ".proto": "protobuf", + ".tf": "terraform", + ".tfvars": "terraform", + ".mk": "makefile", + ".env": "env", + ".csv": "csv", + ".tsv": "csv", + ".rst": "restructuredtext", + ".ps1": "powershell", + ".psm1": "powershell", + ".psd1": "powershell", + ".bat": "batch", + ".cmd": "batch", + ".txt": "plaintext", + ".svg": "xml", }; function detectLanguage(filePath: string): string { @@ -187,6 +228,129 @@ export class GraphBuilder { }); } + addNonCodeFile(filePath: string, meta: NonCodeFileMeta): void { + const lang = detectLanguage(filePath); + if (lang !== "unknown") this.languages.add(lang); + const name = filePath.split("/").pop() ?? filePath; + this.nodes.push({ + id: `file:${filePath}`, + type: meta.nodeType, + name, + filePath, + summary: meta.summary, + tags: meta.tags, + complexity: meta.complexity, + }); + } + + addNonCodeFileWithAnalysis(filePath: string, meta: NonCodeFileAnalysisMeta): void { + this.addNonCodeFile(filePath, meta); + const fileId = `file:${filePath}`; + + // Create child nodes for definitions (tables, schemas, etc.) + for (const def of meta.definitions ?? []) { + const childId = `${def.kind}:${filePath}:${def.name}`; + this.nodes.push({ + id: childId, + type: this.mapKindToNodeType(def.kind), + name: def.name, + filePath, + lineRange: def.lineRange, + summary: `${def.kind}: ${def.name} (${def.fields.length} fields)`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for services + for (const svc of meta.services ?? []) { + const childId = `service:${filePath}:${svc.name}`; + this.nodes.push({ + id: childId, + type: "service", + name: svc.name, + filePath, + summary: `Service ${svc.name}${svc.image ? ` (image: ${svc.image})` : ""}`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for endpoints + for (const ep of meta.endpoints ?? []) { + const childId = `endpoint:${filePath}:${ep.path}`; + this.nodes.push({ + id: childId, + type: "endpoint", + name: `${ep.method ?? ""} ${ep.path}`.trim(), + filePath, + lineRange: ep.lineRange, + summary: `Endpoint: ${ep.method ?? ""} ${ep.path}`.trim(), + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for steps (pipeline/makefile targets) + for (const step of meta.steps ?? []) { + const childId = `step:${filePath}:${step.name}`; + this.nodes.push({ + id: childId, + type: "pipeline", + name: step.name, + filePath, + lineRange: step.lineRange, + summary: `Step: ${step.name}`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for resources (Terraform, etc.) + for (const res of meta.resources ?? []) { + const childId = `resource:${filePath}:${res.name}`; + this.nodes.push({ + id: childId, + type: "resource", + name: res.name, + filePath, + lineRange: res.lineRange, + summary: `Resource: ${res.name} (${res.kind})`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + } + + private mapKindToNodeType(kind: string): GraphNode["type"] { + const mapping: Record = { + table: "table", + view: "table", + index: "table", + message: "schema", + type: "schema", + enum: "schema", + resource: "resource", + module: "resource", + service: "service", + deployment: "service", + job: "pipeline", + stage: "pipeline", + target: "pipeline", + route: "endpoint", + query: "endpoint", + mutation: "endpoint", + variable: "config", + output: "config", + }; + return mapping[kind] ?? "concept"; + } + build(): KnowledgeGraph { return { version: "1.0.0",