feat(core): add non-code file support to GraphBuilder

Add addNonCodeFile() and addNonCodeFileWithAnalysis() methods that create
graph nodes with the appropriate non-code types (document, config, service,
table, endpoint, pipeline, schema, resource).

addNonCodeFileWithAnalysis() creates child nodes for definitions, services,
endpoints, steps, and resources with "contains" edges to the parent file.

Add mapKindToNodeType() helper for mapping definition kinds to node types.
Extend EXTENSION_LANGUAGE map with all non-code file extensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-03-28 18:40:04 +08:00
Unverified
parent 3fd688ac37
commit 2bb107e2d9
2 changed files with 292 additions and 2 deletions
@@ -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");
});
});
});
@@ -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<string, string> = {
// Code languages
".ts": "typescript",
".tsx": "typescript",
".js": "javascript",
@@ -37,20 +57,41 @@ const EXTENSION_LANGUAGE: Record<string, string> = {
".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<string, GraphNode["type"]> = {
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",