Merge pull request #51 from Lum1104/feat/universal-file-type-support

feat: universal file type support for non-code files
This commit is contained in:
Yuxiang Lin
2026-03-28 23:31:37 +08:00
committed by GitHub
84 changed files with 4389 additions and 285 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": "1.3.1",
"version": "2.0.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": "1.3.1",
"version": "2.0.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": "1.3.1",
"version": "2.0.0",
"author": {
"name": "Lum1104"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@understand-anything/skill",
"version": "1.3.1",
"version": "2.0.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -41,6 +41,7 @@
"tree-sitter-javascript": "^0.25.0",
"tree-sitter-typescript": "^0.23.2",
"web-tree-sitter": "^0.26.6",
"yaml": "^2.8.3",
"zod": "^4.3.6"
}
}
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { LanguageRegistry } from "../languages/language-registry.js";
import { StrictLanguageConfigSchema } from "../languages/types.js";
import { typescriptConfig } from "../languages/configs/typescript.js";
import { pythonConfig } from "../languages/configs/python.js";
@@ -32,9 +33,9 @@ describe("LanguageRegistry", () => {
expect(registry.getForFile("file.unknown")).toBeNull();
});
it("returns null for files without extensions", () => {
it("returns null for files without extensions and no filename match", () => {
const registry = new LanguageRegistry();
expect(registry.getForFile("Makefile")).toBeNull();
expect(registry.getForFile("SOMEFILE")).toBeNull();
});
it("lists all registered languages", () => {
@@ -48,10 +49,10 @@ describe("LanguageRegistry", () => {
});
describe("createDefault", () => {
it("registers all 12 built-in language configs", () => {
it("registers all 38 built-in language configs", () => {
const registry = LanguageRegistry.createDefault();
const all = registry.getAllLanguages();
expect(all.length).toBe(12);
expect(all.length).toBe(38);
});
it("maps all expected extensions", () => {
@@ -88,4 +89,107 @@ describe("LanguageRegistry", () => {
}
});
});
describe("Non-code language configs", () => {
it("detects all non-code file types via extension", () => {
const registry = LanguageRegistry.createDefault();
const expectations: [string, string][] = [
["README.md", "markdown"],
["config.yaml", "yaml"],
["package.json", "json"],
["config.toml", "toml"],
[".env", "env"],
["pom.xml", "xml"],
["Dockerfile", "dockerfile"],
["schema.sql", "sql"],
["schema.graphql", "graphql"],
["types.proto", "protobuf"],
["main.tf", "terraform"],
["Makefile", "makefile"],
["deploy.sh", "shell"],
["index.html", "html"],
["styles.css", "css"],
["data.csv", "csv"],
["deploy.ps1", "powershell"],
];
for (const [file, expectedId] of expectations) {
const config = registry.getForFile(file);
expect(config?.id, `${file} should be detected as ${expectedId}`).toBe(expectedId);
}
});
it("detects filename-based configs (Dockerfile, Makefile, Jenkinsfile)", () => {
const registry = LanguageRegistry.createDefault();
expect(registry.getForFile("Dockerfile")?.id).toBe("dockerfile");
expect(registry.getForFile("Makefile")?.id).toBe("makefile");
expect(registry.getForFile("Jenkinsfile")?.id).toBe("jenkinsfile");
expect(registry.getForFile("src/Dockerfile")?.id).toBe("dockerfile");
expect(registry.getForFile("build/Makefile")?.id).toBe("makefile");
});
it("detects filename-based configs for docker-compose", () => {
const registry = LanguageRegistry.createDefault();
expect(registry.getForFile("docker-compose.yml")?.id).toBe("docker-compose");
expect(registry.getForFile("docker-compose.yaml")?.id).toBe("docker-compose");
expect(registry.getForFile("compose.yml")?.id).toBe("docker-compose");
});
it("detects .env file variants", () => {
const registry = LanguageRegistry.createDefault();
expect(registry.getForFile(".env")?.id).toBe("env");
expect(registry.getForFile(".env.local")?.id).toBe("env");
expect(registry.getForFile(".env.production")?.id).toBe("env");
});
});
describe("StrictLanguageConfigSchema refinement", () => {
it("rejects configs with empty extensions AND no filenames", () => {
const result = StrictLanguageConfigSchema.safeParse({
id: "empty-lang",
displayName: "Empty",
extensions: [],
concepts: ["nothing"],
filePatterns: { entryPoints: [], barrels: [], tests: [], config: [] },
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toContain("at least one extension or filename");
}
});
it("rejects configs with empty extensions AND empty filenames", () => {
const result = StrictLanguageConfigSchema.safeParse({
id: "empty-lang",
displayName: "Empty",
extensions: [],
filenames: [],
concepts: ["nothing"],
filePatterns: { entryPoints: [], barrels: [], tests: [], config: [] },
});
expect(result.success).toBe(false);
});
it("accepts configs with extensions but no filenames", () => {
const result = StrictLanguageConfigSchema.safeParse({
id: "ext-lang",
displayName: "ExtLang",
extensions: [".ext"],
concepts: ["something"],
filePatterns: { entryPoints: [], barrels: [], tests: [], config: [] },
});
expect(result.success).toBe(true);
});
it("accepts configs with filenames but empty extensions", () => {
const result = StrictLanguageConfigSchema.safeParse({
id: "filename-lang",
displayName: "FilenameLang",
extensions: [],
filenames: ["Specialfile"],
concepts: ["something"],
filePatterns: { entryPoints: [], barrels: [], tests: [], config: [] },
});
expect(result.success).toBe(true);
});
});
});
@@ -0,0 +1,503 @@
import { describe, it, expect } from "vitest";
import { MarkdownParser } from "../plugins/parsers/markdown-parser.js";
import { YAMLConfigParser } from "../plugins/parsers/yaml-parser.js";
import { JSONConfigParser } from "../plugins/parsers/json-parser.js";
import { TOMLParser } from "../plugins/parsers/toml-parser.js";
import { EnvParser } from "../plugins/parsers/env-parser.js";
import { DockerfileParser } from "../plugins/parsers/dockerfile-parser.js";
import { SQLParser } from "../plugins/parsers/sql-parser.js";
import { GraphQLParser } from "../plugins/parsers/graphql-parser.js";
import { ProtobufParser } from "../plugins/parsers/protobuf-parser.js";
import { TerraformParser } from "../plugins/parsers/terraform-parser.js";
import { MakefileParser } from "../plugins/parsers/makefile-parser.js";
import { ShellParser } from "../plugins/parsers/shell-parser.js";
import { registerAllParsers } from "../plugins/parsers/index.js";
import { PluginRegistry } from "../plugins/registry.js";
describe("MarkdownParser", () => {
const parser = new MarkdownParser();
it("extracts heading sections", () => {
const content = "# Title\n\nIntro\n\n## Section A\n\nContent A\n\n### Subsection\n\nContent B";
const result = parser.analyzeFile("README.md", content);
expect(result.sections).toHaveLength(3);
expect(result.sections![0]).toMatchObject({ name: "Title", level: 1 });
expect(result.sections![1]).toMatchObject({ name: "Section A", level: 2 });
expect(result.sections![2]).toMatchObject({ name: "Subsection", level: 3 });
});
it("extracts YAML front matter as imports", () => {
const content = "---\ntitle: Test\ntags: [a, b]\n---\n# Content";
const result = parser.analyzeFile("post.md", content);
expect(result.imports).toHaveLength(0);
});
it("extracts file references", () => {
const content = "See [guide](./docs/guide.md) and ![img](./assets/logo.png)";
const refs = parser.extractReferences!("README.md", content);
expect(refs).toHaveLength(2);
expect(refs[0]).toMatchObject({ target: "./docs/guide.md", referenceType: "file" });
expect(refs[1]).toMatchObject({ target: "./assets/logo.png", referenceType: "image" });
});
it("skips external URLs in references", () => {
const content = "[link](https://example.com) and [local](./file.md)";
const refs = parser.extractReferences!("README.md", content);
expect(refs).toHaveLength(1);
expect(refs[0].target).toBe("./file.md");
});
it("returns empty sections for empty content", () => {
const result = parser.analyzeFile("empty.md", "");
expect(result.sections).toHaveLength(0);
});
});
describe("YAMLConfigParser", () => {
const parser = new YAMLConfigParser();
it("extracts top-level key sections", () => {
const content = "name: my-app\nversion: 1.0\nservices:\n web:\n image: node\n db:\n image: postgres";
const result = parser.analyzeFile("config.yaml", content);
expect(result.sections).toBeDefined();
expect(result.sections!.length).toBeGreaterThanOrEqual(3);
expect(result.sections!.map(s => s.name)).toContain("name");
expect(result.sections!.map(s => s.name)).toContain("services");
});
it("handles invalid YAML gracefully", () => {
const content = "invalid: yaml: content: [[[";
const result = parser.analyzeFile("broken.yaml", content);
expect(result.sections).toBeDefined();
});
});
describe("JSONConfigParser", () => {
const parser = new JSONConfigParser();
it("extracts top-level key sections", () => {
const content = '{\n "name": "my-app",\n "version": "1.0",\n "dependencies": {}\n}';
const result = parser.analyzeFile("package.json", content);
expect(result.sections).toBeDefined();
expect(result.sections!.map(s => s.name)).toContain("name");
expect(result.sections!.map(s => s.name)).toContain("dependencies");
});
it("extracts $ref references", () => {
const content = '{\n "$ref": "./common.json#/defs/User"\n}';
const refs = parser.extractReferences!("schema.json", content);
expect(refs).toHaveLength(1);
expect(refs[0]).toMatchObject({ target: "./common.json#/defs/User", referenceType: "schema" });
});
it("skips internal $ref references", () => {
const content = '{\n "$ref": "#/definitions/User"\n}';
const refs = parser.extractReferences!("schema.json", content);
expect(refs).toHaveLength(0);
});
it("handles invalid JSON gracefully", () => {
const content = "not json at all";
const result = parser.analyzeFile("broken.json", content);
expect(result.sections).toHaveLength(0);
});
});
describe("TOMLParser", () => {
const parser = new TOMLParser();
it("extracts section headers", () => {
const content = "[package]\nname = \"my-app\"\n\n[dependencies]\nfoo = \"1.0\"\n\n[[bin]]\nname = \"cli\"";
const result = parser.analyzeFile("Cargo.toml", content);
expect(result.sections).toBeDefined();
expect(result.sections!.length).toBe(3);
expect(result.sections![0].name).toBe("package");
expect(result.sections![1].name).toBe("dependencies");
expect(result.sections![2].name).toBe("[[bin]]");
});
});
describe("EnvParser", () => {
const parser = new EnvParser();
it("extracts variable names", () => {
const content = "# Database config\nDB_HOST=localhost\nDB_PORT=5432\n\n# API\nAPI_KEY=secret123";
const result = parser.analyzeFile(".env", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!).toHaveLength(3);
expect(result.definitions!.map(d => d.name)).toEqual(["DB_HOST", "DB_PORT", "API_KEY"]);
});
it("skips comments and empty lines", () => {
const content = "# comment\n\nVAR=value";
const result = parser.analyzeFile(".env", content);
expect(result.definitions!).toHaveLength(1);
});
});
describe("DockerfileParser", () => {
const parser = new DockerfileParser();
it("extracts FROM stages", () => {
const content = "FROM node:22-slim AS builder\nRUN npm install\n\nFROM node:22-slim AS runner\nCOPY --from=builder /app /app\nEXPOSE 3000";
const result = parser.analyzeFile("Dockerfile", content);
expect(result.services).toBeDefined();
expect(result.services!).toHaveLength(2);
expect(result.services![0]).toMatchObject({ name: "builder", image: "node:22-slim" });
expect(result.services![1]).toMatchObject({ name: "runner", image: "node:22-slim" });
});
it("extracts EXPOSE ports", () => {
const content = "FROM node:22\nEXPOSE 3000 8080\nCMD [\"node\", \"server.js\"]";
const result = parser.analyzeFile("Dockerfile", content);
expect(result.services![0].ports).toContain(3000);
expect(result.services![0].ports).toContain(8080);
});
it("extracts steps", () => {
const content = "FROM node:22\nWORKDIR /app\nCOPY . .\nRUN npm install\nCMD [\"node\", \"start\"]";
const result = parser.analyzeFile("Dockerfile", content);
expect(result.steps).toBeDefined();
expect(result.steps!.length).toBe(5);
});
});
describe("SQLParser", () => {
const parser = new SQLParser();
it("extracts CREATE TABLE definitions with columns", () => {
const content = `CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
user_id INTEGER,
title TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);`;
const result = parser.analyzeFile("schema.sql", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!).toHaveLength(2);
expect(result.definitions![0]).toMatchObject({ name: "users", kind: "table" });
expect(result.definitions![0].fields).toContain("id");
expect(result.definitions![0].fields).toContain("name");
expect(result.definitions![0].fields).toContain("email");
expect(result.definitions![1]).toMatchObject({ name: "posts", kind: "table" });
});
it("extracts CREATE VIEW", () => {
const content = "CREATE VIEW active_users AS SELECT * FROM users WHERE active = true;";
const result = parser.analyzeFile("views.sql", content);
expect(result.definitions!.some(d => d.name === "active_users" && d.kind === "view")).toBe(true);
});
it("extracts CREATE INDEX", () => {
const content = "CREATE UNIQUE INDEX idx_users_email ON users(email);";
const result = parser.analyzeFile("indexes.sql", content);
expect(result.definitions!.some(d => d.name === "idx_users_email" && d.kind === "index")).toBe(true);
});
});
describe("GraphQLParser", () => {
const parser = new GraphQLParser();
it("extracts type definitions", () => {
const content = `type User {
id: ID!
name: String!
email: String!
}
type Post {
id: ID!
title: String!
author: User!
}`;
const result = parser.analyzeFile("schema.graphql", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!).toHaveLength(2);
expect(result.definitions![0]).toMatchObject({ name: "User", kind: "type" });
expect(result.definitions![0].fields).toContain("id");
expect(result.definitions![0].fields).toContain("name");
expect(result.definitions![1]).toMatchObject({ name: "Post", kind: "type" });
});
it("extracts Query/Mutation endpoints", () => {
const content = `type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}`;
const result = parser.analyzeFile("schema.graphql", content);
expect(result.endpoints).toBeDefined();
expect(result.endpoints!.length).toBeGreaterThanOrEqual(3);
expect(result.endpoints!.some(e => e.method === "Query" && e.path === "users")).toBe(true);
expect(result.endpoints!.some(e => e.method === "Mutation" && e.path === "createUser")).toBe(true);
});
it("extracts enum definitions", () => {
const content = "enum Role {\n ADMIN\n USER\n GUEST\n}";
const result = parser.analyzeFile("schema.graphql", content);
expect(result.definitions!.some(d => d.name === "Role" && d.kind === "enum")).toBe(true);
});
});
describe("ProtobufParser", () => {
const parser = new ProtobufParser();
it("extracts message definitions with fields", () => {
const content = `message User {
string name = 1;
int32 age = 2;
repeated string emails = 3;
}`;
const result = parser.analyzeFile("user.proto", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!).toHaveLength(1);
expect(result.definitions![0]).toMatchObject({ name: "User", kind: "message" });
expect(result.definitions![0].fields).toContain("name");
expect(result.definitions![0].fields).toContain("age");
expect(result.definitions![0].fields).toContain("emails");
});
it("extracts enum definitions", () => {
const content = "enum Status {\n UNKNOWN = 0;\n ACTIVE = 1;\n INACTIVE = 2;\n}";
const result = parser.analyzeFile("status.proto", content);
expect(result.definitions!.some(d => d.name === "Status" && d.kind === "enum")).toBe(true);
expect(result.definitions![0].fields).toContain("UNKNOWN");
expect(result.definitions![0].fields).toContain("ACTIVE");
});
it("extracts service RPC methods", () => {
const content = `service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc CreateUser (CreateUserRequest) returns (User);
}`;
const result = parser.analyzeFile("service.proto", content);
expect(result.endpoints).toBeDefined();
expect(result.endpoints!).toHaveLength(2);
expect(result.endpoints![0]).toMatchObject({ method: "rpc", path: "UserService.GetUser" });
expect(result.endpoints![1]).toMatchObject({ method: "rpc", path: "UserService.CreateUser" });
});
});
describe("TerraformParser", () => {
const parser = new TerraformParser();
it("extracts resource blocks", () => {
const content = `resource "aws_s3_bucket" "main" {
bucket = "my-bucket"
}
resource "aws_iam_role" "lambda" {
name = "lambda-role"
}`;
const result = parser.analyzeFile("main.tf", content);
expect(result.resources).toBeDefined();
expect(result.resources!).toHaveLength(2);
expect(result.resources![0]).toMatchObject({ name: "aws_s3_bucket.main", kind: "aws_s3_bucket" });
expect(result.resources![1]).toMatchObject({ name: "aws_iam_role.lambda", kind: "aws_iam_role" });
});
it("extracts data blocks", () => {
const content = 'data "aws_ami" "ubuntu" {\n most_recent = true\n}';
const result = parser.analyzeFile("data.tf", content);
expect(result.resources!.some(r => r.name === "data.aws_ami.ubuntu")).toBe(true);
});
it("extracts module blocks", () => {
const content = 'module "vpc" {\n source = "./modules/vpc"\n}';
const result = parser.analyzeFile("modules.tf", content);
expect(result.resources!.some(r => r.name === "module.vpc" && r.kind === "module")).toBe(true);
});
it("extracts variables and outputs", () => {
const content = 'variable "region" {\n default = "us-east-1"\n}\n\noutput "bucket_arn" {\n value = aws_s3_bucket.main.arn\n}';
const result = parser.analyzeFile("variables.tf", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!.some(d => d.name === "region" && d.kind === "variable")).toBe(true);
expect(result.definitions!.some(d => d.name === "bucket_arn" && d.kind === "output")).toBe(true);
});
});
describe("MakefileParser", () => {
const parser = new MakefileParser();
it("extracts make targets", () => {
const content = "build:\n\tgo build -o bin/app\n\ntest:\n\tgo test ./...\n\nclean:\n\trm -rf bin/";
const result = parser.analyzeFile("Makefile", content);
expect(result.steps).toBeDefined();
expect(result.steps!).toHaveLength(3);
expect(result.steps!.map(s => s.name)).toEqual(["build", "test", "clean"]);
});
it("does not confuse variable assignments with targets", () => {
const content = "CC := gcc\nCFLAGS := -Wall\n\nbuild:\n\t$(CC) $(CFLAGS) main.c";
const result = parser.analyzeFile("Makefile", content);
expect(result.steps!).toHaveLength(1);
expect(result.steps![0].name).toBe("build");
});
});
describe("ShellParser", () => {
const parser = new ShellParser();
it("extracts function definitions", () => {
const content = "#!/bin/bash\n\ngreet() {\n echo \"Hello $1\"\n}\n\nfunction cleanup {\n rm -rf tmp/\n}";
const result = parser.analyzeFile("script.sh", content);
expect(result.functions).toHaveLength(2);
expect(result.functions[0].name).toBe("greet");
expect(result.functions[1].name).toBe("cleanup");
});
it("extracts source references", () => {
const content = "#!/bin/bash\nsource ./lib/utils.sh\n. ./lib/config.sh";
const refs = parser.extractReferences!("script.sh", content);
expect(refs).toHaveLength(2);
expect(refs[0]).toMatchObject({ target: "./lib/utils.sh", referenceType: "file" });
expect(refs[1]).toMatchObject({ target: "./lib/config.sh", referenceType: "file" });
});
});
// --- Edge case tests ---
describe("SQLParser edge cases", () => {
const parser = new SQLParser();
it("handles CREATE TABLE IF NOT EXISTS", () => {
const content = "CREATE TABLE IF NOT EXISTS users (id INT);";
const result = parser.analyzeFile("schema.sql", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!).toHaveLength(1);
expect(result.definitions![0]).toMatchObject({ name: "users", kind: "table" });
expect(result.definitions![0].fields).toContain("id");
});
it("handles CREATE OR REPLACE VIEW", () => {
const content = "CREATE OR REPLACE VIEW active AS SELECT * FROM users;";
const result = parser.analyzeFile("views.sql", content);
expect(result.definitions).toBeDefined();
expect(result.definitions!.some(d => d.name === "active" && d.kind === "view")).toBe(true);
});
});
describe("GraphQLParser edge cases", () => {
const parser = new GraphQLParser();
it("extracts input type definitions", () => {
const content = "input CreateUserInput {\n name: String!\n email: String!\n}";
const result = parser.analyzeFile("schema.graphql", content);
expect(result.definitions).toBeDefined();
const inputDef = result.definitions!.find(d => d.name === "CreateUserInput");
expect(inputDef).toBeDefined();
expect(inputDef!.kind).toBe("input");
expect(inputDef!.fields).toContain("name");
});
});
describe("MakefileParser edge cases", () => {
const parser = new MakefileParser();
it("does not extract .PHONY as a target", () => {
const content = ".PHONY: build test\n\nbuild:\n\tgo build\n\ntest:\n\tgo test";
const result = parser.analyzeFile("Makefile", content);
expect(result.steps).toBeDefined();
const targetNames = result.steps!.map(s => s.name);
expect(targetNames).not.toContain(".PHONY");
expect(targetNames).toContain("build");
expect(targetNames).toContain("test");
});
});
describe("ShellParser edge cases", () => {
const parser = new ShellParser();
it("handles function with opening brace on next line", () => {
const content = "greet()\n{\n echo \"Hello\"\n}";
const result = parser.analyzeFile("script.sh", content);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("greet");
expect(result.functions[0].lineRange[1]).toBeGreaterThan(result.functions[0].lineRange[0]);
});
});
describe("TOMLParser edge cases", () => {
const parser = new TOMLParser();
it("returns empty sections for empty string", () => {
const result = parser.analyzeFile("empty.toml", "");
expect(result.sections).toBeDefined();
expect(result.sections).toHaveLength(0);
});
it("returns empty sections for garbage text", () => {
const result = parser.analyzeFile("garbage.toml", "this is not toml at all\nrandom garbage 123");
expect(result.sections).toBeDefined();
expect(result.sections).toHaveLength(0);
});
});
describe("DockerfileParser edge cases", () => {
const parser = new DockerfileParser();
it("assigns EXPOSE ports to the correct stage in multi-stage build", () => {
const content = "FROM node:22 AS builder\nRUN npm install\n\nFROM node:22-slim AS runner\nCOPY --from=builder /app /app\nEXPOSE 3000 8080\nCMD [\"node\", \"server.js\"]";
const result = parser.analyzeFile("Dockerfile", content);
expect(result.services).toBeDefined();
expect(result.services!).toHaveLength(2);
// Ports should be on the runner stage (second stage), not the builder
expect(result.services![0].ports).toHaveLength(0); // builder has no EXPOSE
expect(result.services![1].ports).toContain(3000);
expect(result.services![1].ports).toContain(8080);
});
it("includes lineRange for each stage", () => {
const content = "FROM node:22 AS builder\nRUN npm install\n\nFROM node:22-slim AS runner\nCOPY . .\nCMD [\"node\", \"start\"]";
const result = parser.analyzeFile("Dockerfile", content);
expect(result.services).toBeDefined();
expect(result.services!).toHaveLength(2);
expect(result.services![0].lineRange).toBeDefined();
expect(result.services![0].lineRange![0]).toBe(1);
expect(result.services![1].lineRange).toBeDefined();
expect(result.services![1].lineRange![0]).toBe(4);
});
});
describe("EnvParser edge cases", () => {
const parser = new EnvParser();
it("does not handle export VAR=value syntax", () => {
const content = "export DB_HOST=localhost\nAPI_KEY=secret";
const result = parser.analyzeFile(".env", content);
// The `export` prefix is not handled — only plain KEY=value is parsed
const names = result.definitions!.map(d => d.name);
expect(names).toContain("API_KEY");
expect(names).not.toContain("DB_HOST");
});
});
describe("registerAllParsers", () => {
it("registers all 12 parsers with a PluginRegistry", () => {
const registry = new PluginRegistry();
registerAllParsers(registry);
expect(registry.getPlugins()).toHaveLength(12);
expect(registry.getSupportedLanguages()).toContain("markdown");
expect(registry.getSupportedLanguages()).toContain("yaml");
expect(registry.getSupportedLanguages()).toContain("json");
expect(registry.getSupportedLanguages()).toContain("toml");
expect(registry.getSupportedLanguages()).toContain("env");
expect(registry.getSupportedLanguages()).toContain("dockerfile");
expect(registry.getSupportedLanguages()).toContain("sql");
expect(registry.getSupportedLanguages()).toContain("graphql");
expect(registry.getSupportedLanguages()).toContain("protobuf");
expect(registry.getSupportedLanguages()).toContain("terraform");
expect(registry.getSupportedLanguages()).toContain("makefile");
expect(registry.getSupportedLanguages()).toContain("shell");
});
});
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { PluginRegistry } from "../plugins/registry.js";
import { registerAllParsers } from "../plugins/parsers/index.js";
import type { AnalyzerPlugin, StructuralAnalysis, ImportResolution } from "../types.js";
const emptyAnalysis: StructuralAnalysis = {
@@ -178,4 +179,50 @@ describe("PluginRegistry", () => {
const result = registry.resolveImports("main.py", "import os");
expect(result).toBeNull();
});
it("handles plugins with optional resolveImports (non-code plugins)", () => {
const markdownPlugin: AnalyzerPlugin = {
name: "markdown",
languages: ["markdown"],
analyzeFile: () => ({ functions: [], classes: [], imports: [], exports: [] }),
// No resolveImports — optional for non-code plugins
};
const registry = new PluginRegistry();
registry.register(markdownPlugin);
const result = registry.resolveImports("README.md", "# Hello");
expect(result).toBeNull();
});
});
describe("registerAllParsers smoke test", () => {
it("all registered parsers return valid StructuralAnalysis for minimal content", () => {
const registry = new PluginRegistry();
registerAllParsers(registry);
// Map of file extension -> minimal content for each parser
const testCases: [string, string][] = [
["README.md", "# Hello"],
["config.yaml", "key: value"],
["config.json", '{"key": "value"}'],
["config.toml", 'key = "value"'],
[".env", "KEY=value"],
["Dockerfile", "FROM node:22"],
["schema.sql", "CREATE TABLE t (id INT);"],
["schema.graphql", "type Query { hello: String }"],
["types.proto", 'syntax = "proto3";'],
["main.tf", 'resource "null" "r" {}'],
["Makefile", "build:\n\techo build"],
["script.sh", "#!/bin/bash\necho hello"],
];
for (const [filePath, content] of testCases) {
const result = registry.analyzeFile(filePath, content);
expect(result, `analyzeFile should return a result for ${filePath}`).not.toBeNull();
// Verify basic structural analysis shape
expect(result).toHaveProperty("functions");
expect(result).toHaveProperty("classes");
expect(result).toHaveProperty("imports");
expect(result).toHaveProperty("exports");
}
});
});
@@ -662,3 +662,61 @@ describe("permissive validation", () => {
expect(result.errors).toContain('edges[0]: target "non-existent-node" does not exist in nodes — removed');
});
});
describe("Extended node/edge types", () => {
it("validates nodes with new types: config, document, service, table, endpoint, pipeline, schema, resource", () => {
const newTypes = ["config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource"];
for (const type of newTypes) {
const graph = structuredClone(validGraph);
(graph.nodes[0] as any).type = type;
const result = validateGraph(graph);
expect(result.success).toBe(true);
expect(result.data!.nodes[0].type).toBe(type);
}
});
it("validates edges with new types: deploys, serves, migrates, documents, provisions, routes, defines_schema, triggers", () => {
const newTypes = ["deploys", "serves", "migrates", "documents", "provisions", "routes", "defines_schema", "triggers"];
for (const type of newTypes) {
const graph = structuredClone(validGraph);
(graph.edges[0] as any).type = type;
const result = validateGraph(graph);
expect(result.success).toBe(true);
expect(result.data!.edges[0].type).toBe(type);
}
});
it("auto-fixes new node type aliases: container->service, doc->document, workflow->pipeline, etc.", () => {
const aliases: Record<string, string> = {
container: "service",
doc: "document",
workflow: "pipeline",
route: "endpoint",
setting: "config",
infra: "resource",
migration: "table",
};
for (const [alias, canonical] of Object.entries(aliases)) {
const graph = structuredClone(validGraph);
(graph.nodes[0] as any).type = alias;
const result = validateGraph(graph);
expect(result.success).toBe(true);
expect(result.data!.nodes[0].type).toBe(canonical);
}
});
it("auto-fixes new edge type aliases: describes->documents, creates->provisions, exposes->serves", () => {
const aliases: Record<string, string> = {
describes: "documents",
creates: "provisions",
exposes: "serves",
};
for (const [alias, canonical] of Object.entries(aliases)) {
const graph = structuredClone(validGraph);
(graph.edges[0] as any).type = alias;
const result = validateGraph(graph);
expect(result.success).toBe(true);
expect(result.data!.edges[0].type).toBe(canonical);
}
});
});
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { GraphBuilder } from "./graph-builder.js";
import type { StructuralAnalysis } from "../types.js";
@@ -212,4 +212,193 @@ 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 and nodeType-prefixed ID", () => {
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("document: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");
});
it("mapKindToNodeType falls back to concept for unknown kinds and warns", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const builder = new GraphBuilder("test", "abc123");
builder.addNonCodeFileWithAnalysis("schema.sql", {
nodeType: "file",
summary: "Schema",
tags: [],
complexity: "simple",
definitions: [
{ name: "doStuff", kind: "procedure", lineRange: [1, 10] as [number, number], fields: [] },
],
});
const graph = builder.build();
const childNode = graph.nodes.find(n => n.name === "doStuff");
expect(childNode).toBeDefined();
expect(childNode!.type).toBe("concept");
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Unknown definition kind "procedure"'),
);
warnSpy.mockRestore();
});
it("skips duplicate node IDs in addNonCodeFileWithAnalysis and warns", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const builder = new GraphBuilder("test", "abc123");
builder.addNonCodeFileWithAnalysis("schema.sql", {
nodeType: "file",
summary: "Schema",
tags: [],
complexity: "simple",
definitions: [
{ name: "users", kind: "table", lineRange: [1, 10] as [number, number], fields: ["id"] },
{ name: "users", kind: "table", lineRange: [12, 20] as [number, number], fields: ["id", "name"] },
],
});
const graph = builder.build();
// Only the file node + one table node (duplicate skipped)
const tableNodes = graph.nodes.filter(n => n.name === "users");
expect(tableNodes).toHaveLength(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Duplicate node ID "table:schema.sql:users"'),
);
warnSpy.mockRestore();
});
it("uses nodeType in fileId for contains edges", () => {
const builder = new GraphBuilder("test", "abc123");
builder.addNonCodeFileWithAnalysis("docker-compose.yml", {
nodeType: "config",
summary: "Docker compose config",
tags: [],
complexity: "simple",
services: [
{ name: "web", ports: [3000] },
],
});
const graph = builder.build();
const containsEdge = graph.edges.find(e => e.type === "contains");
expect(containsEdge).toBeDefined();
expect(containsEdge!.source).toBe("config:docker-compose.yml");
expect(containsEdge!.target).toBe("service:docker-compose.yml:web");
});
});
});
@@ -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,160 @@ 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: `${meta.nodeType ?? "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 = `${meta.nodeType ?? "file"}:${filePath}`;
const existingIds = new Set(this.nodes.map(n => n.id));
// Create child nodes for definitions (tables, schemas, etc.)
for (const def of meta.definitions ?? []) {
const childId = `${def.kind}:${filePath}:${def.name}`;
if (existingIds.has(childId)) {
console.warn(`[GraphBuilder] Duplicate node ID "${childId}" — skipping`);
continue;
}
existingIds.add(childId);
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}`;
if (existingIds.has(childId)) {
console.warn(`[GraphBuilder] Duplicate node ID "${childId}" — skipping`);
continue;
}
existingIds.add(childId);
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}`;
if (existingIds.has(childId)) {
console.warn(`[GraphBuilder] Duplicate node ID "${childId}" — skipping`);
continue;
}
existingIds.add(childId);
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}`;
if (existingIds.has(childId)) {
console.warn(`[GraphBuilder] Duplicate node ID "${childId}" — skipping`);
continue;
}
existingIds.add(childId);
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}`;
if (existingIds.has(childId)) {
console.warn(`[GraphBuilder] Duplicate node ID "${childId}" — skipping`);
continue;
}
existingIds.add(childId);
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",
};
const mapped = mapping[kind];
if (!mapped) {
console.warn(`[GraphBuilder] Unknown definition kind "${kind}" — falling back to "concept" node type`);
}
return mapped ?? "concept";
}
build(): KnowledgeGraph {
return {
version: "1.0.0",
@@ -90,3 +90,19 @@ export {
classifyUpdate,
type UpdateDecision,
} from "./change-classifier.js";
// Non-code parsers
export {
MarkdownParser,
YAMLConfigParser,
JSONConfigParser,
TOMLParser,
EnvParser,
DockerfileParser,
SQLParser,
GraphQLParser,
ProtobufParser,
TerraformParser,
MakefileParser,
ShellParser,
registerAllParsers,
} from "./plugins/parsers/index.js";
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const batchConfig = {
id: "batch",
displayName: "Batch Script",
extensions: [".bat", ".cmd"],
concepts: ["commands", "variables", "labels", "goto", "call", "echo", "set", "for loops", "if conditions"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const cssConfig = {
id: "css",
displayName: "CSS",
extensions: [".css", ".scss", ".less"],
concepts: ["selectors", "properties", "media queries", "flexbox", "grid", "variables", "animations", "specificity"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const csvConfig = {
id: "csv",
displayName: "CSV",
extensions: [".csv", ".tsv"],
concepts: ["headers", "rows", "delimiters", "quoting", "escaping"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,15 @@
import type { LanguageConfig } from "../types.js";
export const dockerComposeConfig = {
id: "docker-compose",
displayName: "Docker Compose",
extensions: [],
filenames: ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"],
concepts: ["services", "networks", "volumes", "ports", "environment", "depends_on", "build context", "healthchecks"],
filePatterns: {
entryPoints: ["docker-compose.yml", "compose.yml"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,15 @@
import type { LanguageConfig } from "../types.js";
export const dockerfileConfig = {
id: "dockerfile",
displayName: "Dockerfile",
extensions: [],
filenames: ["Dockerfile", "Dockerfile.dev", "Dockerfile.prod", "Dockerfile.test"],
concepts: ["multi-stage builds", "layers", "base images", "COPY/ADD", "EXPOSE", "ENTRYPOINT", "CMD", "ARG", "ENV"],
filePatterns: {
entryPoints: ["Dockerfile"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,15 @@
import type { LanguageConfig } from "../types.js";
export const envConfig = {
id: "env",
displayName: "Environment Variables",
extensions: [".env"],
filenames: [".env", ".env.local", ".env.development", ".env.production", ".env.test", ".env.example"],
concepts: ["key-value pairs", "variable interpolation", "secrets", "environment-specific config"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [".env", ".env.*"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const githubActionsConfig = {
id: "github-actions",
displayName: "GitHub Actions",
extensions: [],
concepts: ["workflows", "jobs", "steps", "actions", "triggers", "secrets", "matrix strategy", "artifacts"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [".github/workflows/*.yml"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const graphqlConfig = {
id: "graphql",
displayName: "GraphQL",
extensions: [".graphql", ".gql"],
concepts: ["types", "queries", "mutations", "subscriptions", "resolvers", "directives", "fragments", "schema"],
filePatterns: {
entryPoints: ["schema.graphql"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const htmlConfig = {
id: "html",
displayName: "HTML",
extensions: [".html", ".htm"],
concepts: ["elements", "attributes", "semantic tags", "forms", "meta tags", "scripts", "stylesheets", "accessibility"],
filePatterns: {
entryPoints: ["index.html"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -11,8 +11,36 @@ import { swiftConfig } from "./swift.js";
import { kotlinConfig } from "./kotlin.js";
import { cppConfig } from "./cpp.js";
import { csharpConfig } from "./csharp.js";
// Non-code language configs
import { markdownConfig } from "./markdown.js";
import { yamlConfig } from "./yaml.js";
import { jsonConfigConfig } from "./json-config.js";
import { tomlConfig } from "./toml.js";
import { envConfig } from "./env.js";
import { xmlConfig } from "./xml.js";
import { dockerfileConfig } from "./dockerfile.js";
import { sqlConfig } from "./sql.js";
import { graphqlConfig } from "./graphql.js";
import { protobufConfig } from "./protobuf.js";
import { terraformConfig } from "./terraform.js";
import { githubActionsConfig } from "./github-actions.js";
import { makefileConfig } from "./makefile.js";
import { shellConfig } from "./shell.js";
import { htmlConfig } from "./html.js";
import { cssConfig } from "./css.js";
import { openapiConfig } from "./openapi.js";
import { kubernetesConfig } from "./kubernetes.js";
import { dockerComposeConfig } from "./docker-compose.js";
import { jsonSchemaConfig } from "./json-schema.js";
import { csvConfig } from "./csv.js";
import { restructuredtextConfig } from "./restructuredtext.js";
import { powershellConfig } from "./powershell.js";
import { batchConfig } from "./batch.js";
import { jenkinsfileConfig } from "./jenkinsfile.js";
import { plaintextConfig } from "./plaintext.js";
export const builtinLanguageConfigs: LanguageConfig[] = [
// Code languages
typescriptConfig,
javascriptConfig,
pythonConfig,
@@ -25,9 +53,37 @@ export const builtinLanguageConfigs: LanguageConfig[] = [
kotlinConfig,
cppConfig,
csharpConfig,
// Non-code languages
markdownConfig,
yamlConfig,
jsonConfigConfig,
tomlConfig,
envConfig,
xmlConfig,
dockerfileConfig,
sqlConfig,
graphqlConfig,
protobufConfig,
terraformConfig,
githubActionsConfig,
makefileConfig,
shellConfig,
htmlConfig,
cssConfig,
openapiConfig,
kubernetesConfig,
dockerComposeConfig,
jsonSchemaConfig,
csvConfig,
restructuredtextConfig,
powershellConfig,
batchConfig,
jenkinsfileConfig,
plaintextConfig,
];
export {
// Code languages
typescriptConfig,
javascriptConfig,
pythonConfig,
@@ -40,4 +96,31 @@ export {
kotlinConfig,
cppConfig,
csharpConfig,
// Non-code languages
markdownConfig,
yamlConfig,
jsonConfigConfig,
tomlConfig,
envConfig,
xmlConfig,
dockerfileConfig,
sqlConfig,
graphqlConfig,
protobufConfig,
terraformConfig,
githubActionsConfig,
makefileConfig,
shellConfig,
htmlConfig,
cssConfig,
openapiConfig,
kubernetesConfig,
dockerComposeConfig,
jsonSchemaConfig,
csvConfig,
restructuredtextConfig,
powershellConfig,
batchConfig,
jenkinsfileConfig,
plaintextConfig,
};
@@ -0,0 +1,15 @@
import type { LanguageConfig } from "../types.js";
export const jenkinsfileConfig = {
id: "jenkinsfile",
displayName: "Jenkinsfile",
extensions: [],
filenames: ["Jenkinsfile"],
concepts: ["pipeline", "stages", "steps", "agents", "environment", "post actions", "parallel execution", "shared libraries"],
filePatterns: {
entryPoints: ["Jenkinsfile"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const jsonConfigConfig = {
id: "json",
displayName: "JSON",
extensions: [".json", ".jsonc"],
concepts: ["objects", "arrays", "nesting", "schema references", "comments (JSONC)"],
filePatterns: {
entryPoints: ["package.json"],
barrels: [],
tests: [],
config: ["tsconfig.json", "package.json", ".eslintrc.json"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,18 @@
import type { LanguageConfig } from "../types.js";
// TODO: JSON Schema files have no unique extension — *.schema.json files will match
// `jsonConfigConfig` by the `.json` extension. Detection requires content-based
// heuristics (e.g., checking for `"$schema"` or `"type"` keys at the root level).
// A future content-based detection pass could re-classify them as JSON Schema.
export const jsonSchemaConfig = {
id: "json-schema",
displayName: "JSON Schema",
extensions: [],
concepts: ["types", "properties", "required fields", "$ref", "$defs", "allOf/anyOf/oneOf", "patterns", "validation"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,19 @@
import type { LanguageConfig } from "../types.js";
// TODO: Kubernetes manifests are YAML files with no unique extension or filename.
// Detection requires content-based or path-pattern heuristics (e.g., checking for
// `apiVersion`/`kind` fields in YAML, or matching paths like `k8s/`, `kubernetes/`,
// `deploy/`). Currently these files will match `yamlConfig` by extension (.yaml/.yml).
// A future content-based detection pass could re-classify them as Kubernetes.
export const kubernetesConfig = {
id: "kubernetes",
displayName: "Kubernetes",
extensions: [],
concepts: ["deployments", "services", "pods", "configmaps", "secrets", "ingress", "volumes", "namespaces"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: ["k8s/*.yaml", "kubernetes/*.yaml"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,15 @@
import type { LanguageConfig } from "../types.js";
export const makefileConfig = {
id: "makefile",
displayName: "Makefile",
extensions: [".mk"],
filenames: ["Makefile", "GNUmakefile", "makefile"],
concepts: ["targets", "dependencies", "recipes", "variables", "pattern rules", "phony targets", "includes"],
filePatterns: {
entryPoints: ["Makefile"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const markdownConfig = {
id: "markdown",
displayName: "Markdown",
extensions: [".md", ".mdx"],
concepts: ["headings", "links", "code blocks", "front matter", "lists", "tables", "images"],
filePatterns: {
entryPoints: ["README.md"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,15 @@
import type { LanguageConfig } from "../types.js";
export const openapiConfig = {
id: "openapi",
displayName: "OpenAPI",
extensions: [],
filenames: ["openapi.yaml", "openapi.json", "swagger.yaml", "swagger.json"],
concepts: ["paths", "operations", "schemas", "parameters", "responses", "security schemes", "tags", "servers"],
filePatterns: {
entryPoints: ["openapi.yaml", "swagger.yaml"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const plaintextConfig = {
id: "plaintext",
displayName: "Plain Text",
extensions: [".txt", ".text"],
concepts: ["paragraphs", "lists", "sections"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const powershellConfig = {
id: "powershell",
displayName: "PowerShell",
extensions: [".ps1", ".psm1", ".psd1"],
concepts: ["cmdlets", "pipelines", "modules", "functions", "parameters", "variables", "error handling"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const protobufConfig = {
id: "protobuf",
displayName: "Protocol Buffers",
extensions: [".proto"],
concepts: ["messages", "services", "enums", "oneof", "repeated fields", "maps", "packages", "imports"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const restructuredtextConfig = {
id: "restructuredtext",
displayName: "reStructuredText",
extensions: [".rst"],
concepts: ["headings", "directives", "roles", "cross-references", "toctree", "code blocks", "admonitions"],
filePatterns: {
entryPoints: ["index.rst"],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const shellConfig = {
id: "shell",
displayName: "Shell Script",
extensions: [".sh", ".bash", ".zsh"],
concepts: ["variables", "functions", "conditionals", "loops", "pipes", "redirection", "subshells", "exit codes"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [".bashrc", ".zshrc", ".profile"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const sqlConfig = {
id: "sql",
displayName: "SQL",
extensions: [".sql"],
concepts: ["tables", "columns", "indexes", "foreign keys", "views", "stored procedures", "triggers", "migrations"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: [],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const terraformConfig = {
id: "terraform",
displayName: "Terraform",
extensions: [".tf", ".tfvars"],
concepts: ["resources", "data sources", "variables", "outputs", "modules", "providers", "state", "workspaces"],
filePatterns: {
entryPoints: ["main.tf"],
barrels: [],
tests: [],
config: ["terraform.tfvars", "variables.tf"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const tomlConfig = {
id: "toml",
displayName: "TOML",
extensions: [".toml"],
concepts: ["tables", "inline tables", "arrays of tables", "key-value pairs", "dotted keys"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: ["Cargo.toml", "pyproject.toml", "netlify.toml"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const xmlConfig = {
id: "xml",
displayName: "XML",
extensions: [".xml", ".xsl", ".xsd", ".svg", ".plist"],
concepts: ["elements", "attributes", "namespaces", "DTD", "XPath", "XSLT", "schemas"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: ["pom.xml", "web.xml", "AndroidManifest.xml"],
},
} satisfies LanguageConfig;
@@ -0,0 +1,14 @@
import type { LanguageConfig } from "../types.js";
export const yamlConfig = {
id: "yaml",
displayName: "YAML",
extensions: [".yaml", ".yml"],
concepts: ["mappings", "sequences", "anchors", "aliases", "multi-document", "tags"],
filePatterns: {
entryPoints: [],
barrels: [],
tests: [],
config: ["*.yaml", "*.yml"],
},
} satisfies LanguageConfig;
@@ -9,6 +9,7 @@ import { builtinLanguageConfigs } from "./configs/index.js";
export class LanguageRegistry {
private byId = new Map<string, LanguageConfig>();
private byExtension = new Map<string, LanguageConfig>();
private byFilename = new Map<string, LanguageConfig>();
register(config: LanguageConfig): void {
const parsed = LanguageConfigSchema.parse(config);
@@ -18,6 +19,11 @@ export class LanguageRegistry {
const key = ext.startsWith(".") ? ext : `.${ext}`;
this.byExtension.set(key, parsed);
}
if (parsed.filenames) {
for (const filename of parsed.filenames) {
this.byFilename.set(filename.toLowerCase(), parsed);
}
}
}
getById(id: string): LanguageConfig | null {
@@ -30,6 +36,11 @@ export class LanguageRegistry {
}
getForFile(filePath: string): LanguageConfig | null {
// Try filename-based lookup first (more specific: docker-compose.yml, Makefile, etc.)
const basename = filePath.split("/").pop() ?? filePath;
const filenameMatch = this.byFilename.get(basename.toLowerCase());
if (filenameMatch) return filenameMatch;
// Fall back to extension-based lookup
const lastDot = filePath.lastIndexOf(".");
if (lastDot === -1) return null;
const ext = filePath.slice(lastDot).toLowerCase();
@@ -22,11 +22,12 @@ export const FilePatternConfigSchema = z.object({
export type FilePatternConfig = z.infer<typeof FilePatternConfigSchema>;
// Complete language configuration
// Complete language configuration (base schema — used by LanguageRegistry.register())
export const LanguageConfigSchema = z.object({
id: z.string().min(1),
displayName: z.string().min(1),
extensions: z.array(z.string()).min(1),
extensions: z.array(z.string()),
filenames: z.array(z.string()).optional(),
treeSitter: TreeSitterConfigSchema.optional(),
concepts: z.array(z.string()),
filePatterns: FilePatternConfigSchema,
@@ -34,6 +35,18 @@ export const LanguageConfigSchema = z.object({
export type LanguageConfig = z.infer<typeof LanguageConfigSchema>;
/**
* Strict schema with refinement: ensures at least one extension or filename
* is provided so the config can actually be detected by the registry.
* Use this for validating new/user-supplied configs (some builtin configs like
* kubernetes/github-actions intentionally lack both and rely on future
* content-based detection).
*/
export const StrictLanguageConfigSchema = LanguageConfigSchema.refine(
(c) => c.extensions.length > 0 || (c.filenames !== undefined && c.filenames.length > 0),
{ message: "LanguageConfig must have at least one extension or filename for detection" }
);
// Framework configuration
export const FrameworkConfigSchema = z.object({
id: z.string().min(1),
@@ -0,0 +1,86 @@
import type { AnalyzerPlugin, StructuralAnalysis, ServiceInfo, StepInfo } from "../../types.js";
/**
* Parses Dockerfiles to extract multi-stage build stages, EXPOSE ports, and instruction steps.
* Associates EXPOSE ports with the correct stage based on FROM directive ordering.
* Does not parse ARG/ENV variable substitution or heredoc syntax.
*/
export class DockerfileParser implements AnalyzerPlugin {
name = "dockerfile-parser";
languages = ["dockerfile"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const services = this.extractStages(content);
const steps = this.extractSteps(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
services,
steps,
};
}
private extractStages(content: string): ServiceInfo[] {
const stages: ServiceInfo[] = [];
const lines = content.split("\n");
// First pass: find FROM line indices
const fromLines: number[] = [];
for (let i = 0; i < lines.length; i++) {
if (/^FROM\s+/i.test(lines[i])) {
fromLines.push(i);
}
}
// Second pass: for each stage, collect EXPOSE ports within its range and build ServiceInfo
for (let s = 0; s < fromLines.length; s++) {
const stageStartLine = fromLines[s];
const stageEndLine = s + 1 < fromLines.length ? fromLines[s + 1] - 1 : lines.length - 1;
const fromMatch = lines[stageStartLine].match(/^FROM\s+(\S+)(?:\s+[Aa][Ss]\s+(\S+))?/i);
if (!fromMatch) continue;
const image = fromMatch[1];
const name = fromMatch[2] ?? image.split(":")[0].split("/").pop() ?? image;
// Collect EXPOSE ports that appear within this stage's range
const ports: number[] = [];
for (let i = stageStartLine; i <= stageEndLine; i++) {
const exposeMatch = lines[i].match(/^EXPOSE\s+(.+)/i);
if (exposeMatch) {
const portValues = exposeMatch[1].split(/\s+/);
for (const p of portValues) {
const num = parseInt(p, 10);
if (!isNaN(num)) ports.push(num);
}
}
}
stages.push({
name,
image,
ports,
lineRange: [stageStartLine + 1, stageEndLine + 1],
});
}
return stages;
}
private extractSteps(content: string): StepInfo[] {
const steps: StepInfo[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/^(FROM|RUN|COPY|ADD|WORKDIR|CMD|ENTRYPOINT|ENV|ARG|EXPOSE|VOLUME|USER|HEALTHCHECK)\s/i);
if (match) {
steps.push({
name: `${match[1].toUpperCase()} ${lines[i].slice(match[1].length + 1).trim().slice(0, 60)}`,
lineRange: [i + 1, i + 1],
});
}
}
return steps;
}
}
@@ -0,0 +1,41 @@
import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo } from "../../types.js";
/**
* Parses .env files to extract environment variable definitions.
* Handles KEY=value syntax, skipping comments and empty lines.
* Does not handle `export VAR=value` syntax or multi-line values.
*/
export class EnvParser implements AnalyzerPlugin {
name = "env-parser";
languages = ["env"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const definitions = this.extractVariables(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
definitions,
};
}
private extractVariables(content: string): DefinitionInfo[] {
const definitions: DefinitionInfo[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith("#") || line === "") continue;
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
if (match) {
definitions.push({
name: match[1],
kind: "variable",
lineRange: [i + 1, i + 1],
fields: [],
});
}
}
return definitions;
}
}
@@ -0,0 +1,123 @@
import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo, EndpointInfo } from "../../types.js";
/**
* Parses GraphQL schema files to extract type, input, enum, interface, union, and scalar definitions.
* Extracts Query, Mutation, and Subscription endpoints as separate endpoint entries.
* Does not handle schema directives, fragments, or inline union members.
*/
export class GraphQLParser implements AnalyzerPlugin {
name = "graphql-parser";
languages = ["graphql"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const definitions = this.extractDefinitions(content);
const endpoints = this.extractEndpoints(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
definitions,
endpoints,
};
}
private extractDefinitions(content: string): DefinitionInfo[] {
const definitions: DefinitionInfo[] = [];
const lines = content.split("\n");
// Match type, input, enum, interface, union, scalar definitions
const typeRegex = /^(type|input|enum|interface|union|scalar)\s+(\w+)/gm;
let match;
while ((match = typeRegex.exec(content)) !== null) {
const kind = match[1];
const name = match[2];
if (name === "Query" || name === "Mutation" || name === "Subscription") continue;
const startLine = content.slice(0, match.index).split("\n").length;
// Extract fields (for type/input/interface/enum)
const fields = this.extractFields(content, match.index);
// Find closing brace
const afterMatch = content.slice(match.index);
const closeBrace = afterMatch.indexOf("}");
const endLine = closeBrace !== -1
? content.slice(0, match.index + closeBrace + 1).split("\n").length
: startLine;
definitions.push({
name,
kind,
lineRange: [startLine, endLine],
fields,
});
}
return definitions;
}
private extractEndpoints(content: string): EndpointInfo[] {
const endpoints: EndpointInfo[] = [];
// Find Query, Mutation, Subscription blocks and extract their fields
const blockRegex = /^(type)\s+(Query|Mutation|Subscription)\s*\{/gm;
let match;
while ((match = blockRegex.exec(content)) !== null) {
const method = match[2]; // Query, Mutation, Subscription
const startIdx = match.index + match[0].length;
// Find closing brace
let depth = 1;
let i = startIdx;
while (i < content.length && depth > 0) {
if (content[i] === "{") depth++;
if (content[i] === "}") depth--;
i++;
}
const blockContent = content.slice(startIdx, i - 1);
const blockLines = blockContent.split("\n");
const blockStartLine = content.slice(0, startIdx).split("\n").length;
for (let j = 0; j < blockLines.length; j++) {
const fieldMatch = blockLines[j].trim().match(/^(\w+)/);
if (fieldMatch && fieldMatch[1]) {
const lineNum = blockStartLine + j;
endpoints.push({
method,
path: fieldMatch[1],
lineRange: [lineNum, lineNum],
});
}
}
}
return endpoints;
}
private extractFields(content: string, startIdx: number): string[] {
const fields: string[] = [];
const afterType = content.slice(startIdx);
const openBrace = afterType.indexOf("{");
if (openBrace === -1) return fields;
let depth = 1;
let i = openBrace + 1;
while (i < afterType.length && depth > 0) {
if (afterType[i] === "{") depth++;
if (afterType[i] === "}") depth--;
i++;
}
const body = afterType.slice(openBrace + 1, i - 1);
const lines = body.split("\n");
for (const line of lines) {
const fieldMatch = line.trim().match(/^(\w+)/);
if (fieldMatch) {
fields.push(fieldMatch[1]);
}
}
return fields;
}
}
@@ -0,0 +1,44 @@
export { MarkdownParser } from "./markdown-parser.js";
export { YAMLConfigParser } from "./yaml-parser.js";
export { JSONConfigParser } from "./json-parser.js";
export { TOMLParser } from "./toml-parser.js";
export { EnvParser } from "./env-parser.js";
export { DockerfileParser } from "./dockerfile-parser.js";
export { SQLParser } from "./sql-parser.js";
export { GraphQLParser } from "./graphql-parser.js";
export { ProtobufParser } from "./protobuf-parser.js";
export { TerraformParser } from "./terraform-parser.js";
export { MakefileParser } from "./makefile-parser.js";
export { ShellParser } from "./shell-parser.js";
import type { PluginRegistry } from "../registry.js";
import { MarkdownParser } from "./markdown-parser.js";
import { YAMLConfigParser } from "./yaml-parser.js";
import { JSONConfigParser } from "./json-parser.js";
import { TOMLParser } from "./toml-parser.js";
import { EnvParser } from "./env-parser.js";
import { DockerfileParser } from "./dockerfile-parser.js";
import { SQLParser } from "./sql-parser.js";
import { GraphQLParser } from "./graphql-parser.js";
import { ProtobufParser } from "./protobuf-parser.js";
import { TerraformParser } from "./terraform-parser.js";
import { MakefileParser } from "./makefile-parser.js";
import { ShellParser } from "./shell-parser.js";
/**
* Register all built-in non-code parsers with a PluginRegistry.
*/
export function registerAllParsers(registry: PluginRegistry): void {
registry.register(new MarkdownParser());
registry.register(new YAMLConfigParser());
registry.register(new JSONConfigParser());
registry.register(new TOMLParser());
registry.register(new EnvParser());
registry.register(new DockerfileParser());
registry.register(new SQLParser());
registry.register(new GraphQLParser());
registry.register(new ProtobufParser());
registry.register(new TerraformParser());
registry.register(new MakefileParser());
registry.register(new ShellParser());
}
@@ -0,0 +1,70 @@
import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo, ReferenceResolution } from "../../types.js";
/**
* Parses JSON configuration files to extract top-level key sections and $ref references.
* Handles package.json, tsconfig.json, JSON Schema, and OpenAPI spec files.
* Does not descend into nested object structures beyond top-level keys.
*/
export class JSONConfigParser implements AnalyzerPlugin {
name = "json-config-parser";
languages = ["json"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const sections = this.extractSections(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
sections,
};
}
extractReferences(filePath: string, content: string): ReferenceResolution[] {
const refs: ReferenceResolution[] = [];
// Match $ref values (JSON Schema / OpenAPI)
const refRegex = /"\$ref"\s*:\s*"([^"]+)"/g;
let match;
while ((match = refRegex.exec(content)) !== null) {
const target = match[1];
if (target.startsWith("#")) continue; // Skip internal refs
const line = content.slice(0, match.index).split("\n").length;
refs.push({
source: filePath,
target,
referenceType: "schema",
line,
});
}
return refs;
}
private extractSections(content: string): SectionInfo[] {
const sections: SectionInfo[] = [];
try {
const doc = JSON.parse(content);
if (doc && typeof doc === "object" && !Array.isArray(doc)) {
const lines = content.split("\n");
for (const key of Object.keys(doc)) {
const escapedKey = JSON.stringify(key);
const lineIdx = lines.findIndex((l) => l.includes(escapedKey));
if (lineIdx !== -1) {
sections.push({
name: key,
level: 1,
lineRange: [lineIdx + 1, lineIdx + 1],
});
}
}
// Fix lineRange end
for (let i = 0; i < sections.length; i++) {
const next = sections[i + 1];
sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length;
}
}
} catch (err) {
console.warn(`[json-parser] Failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`);
}
return sections;
}
}
@@ -0,0 +1,51 @@
import type { AnalyzerPlugin, StructuralAnalysis, StepInfo } from "../../types.js";
/**
* Parses Makefiles to extract build targets and their line ranges.
* Filters out special Make targets (e.g., .PHONY, .DEFAULT, .SUFFIXES) and variable assignments.
* Does not parse target dependencies or recipe commands.
*/
export class MakefileParser implements AnalyzerPlugin {
name = "makefile-parser";
languages = ["makefile"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const steps = this.extractTargets(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
steps,
};
}
private extractTargets(content: string): StepInfo[] {
const targets: StepInfo[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
// Match target: dependencies (not variable assignments or comments)
const match = lines[i].match(/^([a-zA-Z_.][a-zA-Z0-9_.-]*)(?:\s+.*)?:/);
if (match && !lines[i].includes(":=") && !lines[i].includes("?=")) {
const name = match[1];
// Skip special Make targets (.PHONY, .DEFAULT, .SUFFIXES, etc.)
if (name.startsWith(".")) continue;
// Find end of target (next non-indented non-empty line or EOF)
let endLine = i + 1;
while (endLine < lines.length) {
const nextLine = lines[endLine];
if (nextLine === "" || nextLine.startsWith("\t") || nextLine.startsWith(" ")) {
endLine++;
} else {
break;
}
}
targets.push({
name,
lineRange: [i + 1, endLine],
});
}
}
return targets;
}
}
@@ -0,0 +1,61 @@
import type { AnalyzerPlugin, StructuralAnalysis, ReferenceResolution, SectionInfo } from "../../types.js";
/**
* Parses Markdown files to extract heading sections and local file/image references.
* Supports ATX-style headings (# through ######) with line range computation.
* Does not extract code blocks, front matter fields, or external URL references.
*/
export class MarkdownParser implements AnalyzerPlugin {
name = "markdown-parser";
languages = ["markdown"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const sections = this.extractSections(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
sections,
};
}
extractReferences(filePath: string, content: string): ReferenceResolution[] {
const refs: ReferenceResolution[] = [];
const linkRegex = /!?\[([^\]]*)\]\(([^)]+)\)/g;
let match;
while ((match = linkRegex.exec(content)) !== null) {
const target = match[2];
if (target.startsWith("http")) continue; // Skip external URLs
const line = content.slice(0, match.index).split("\n").length;
refs.push({
source: filePath,
target,
referenceType: match[0].startsWith("!") ? "image" : "file",
line,
});
}
return refs;
}
private extractSections(content: string): SectionInfo[] {
const sections: SectionInfo[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
if (match) {
sections.push({
name: match[2].trim(),
level: match[1].length,
lineRange: [i + 1, i + 1],
});
}
}
// Fix lineRange end for each section (extends to next heading or EOF)
for (let i = 0; i < sections.length; i++) {
const next = sections[i + 1];
sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length;
}
return sections;
}
}
@@ -0,0 +1,141 @@
import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo, EndpointInfo } from "../../types.js";
/**
* Parses Protocol Buffer (.proto) files to extract message, enum, and service definitions.
* Extracts message fields, enum values, and service RPC method endpoints.
* Does not handle nested message types, oneof fields, or proto2 extensions.
*/
export class ProtobufParser implements AnalyzerPlugin {
name = "protobuf-parser";
languages = ["protobuf"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const definitions = this.extractDefinitions(content);
const endpoints = this.extractServiceMethods(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
definitions,
endpoints,
};
}
private extractDefinitions(content: string): DefinitionInfo[] {
const definitions: DefinitionInfo[] = [];
// Match message definitions
const messageRegex = /^message\s+(\w+)\s*\{/gm;
let match;
while ((match = messageRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const fields = this.extractMessageFields(content, match.index);
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
definitions.push({
name: match[1],
kind: "message",
lineRange: [startLine, endLine],
fields,
});
}
// Match enum definitions
const enumRegex = /^enum\s+(\w+)\s*\{/gm;
while ((match = enumRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const fields = this.extractEnumValues(content, match.index);
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
definitions.push({
name: match[1],
kind: "enum",
lineRange: [startLine, endLine],
fields,
});
}
return definitions;
}
private extractServiceMethods(content: string): EndpointInfo[] {
const endpoints: EndpointInfo[] = [];
const serviceRegex = /^service\s+(\w+)\s*\{/gm;
let match;
while ((match = serviceRegex.exec(content)) !== null) {
const serviceName = match[1];
const startIdx = match.index + match[0].length;
const afterService = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterService);
const body = afterService.slice(match[0].length, closeBrace);
const rpcRegex = /rpc\s+(\w+)\s*\(/g;
let rpcMatch;
while ((rpcMatch = rpcRegex.exec(body)) !== null) {
const lineNum = content.slice(0, startIdx + rpcMatch.index).split("\n").length;
endpoints.push({
method: "rpc",
path: `${serviceName}.${rpcMatch[1]}`,
lineRange: [lineNum, lineNum],
});
}
}
return endpoints;
}
private extractMessageFields(content: string, startIdx: number): string[] {
const fields: string[] = [];
const afterMsg = content.slice(startIdx);
const openBrace = afterMsg.indexOf("{");
if (openBrace === -1) return fields;
const closeBrace = this.findClosingBrace(afterMsg);
const body = afterMsg.slice(openBrace + 1, closeBrace);
const fieldRegex = /^\s*(?:repeated\s+|optional\s+|required\s+|map<[^>]+>\s+)?\w+\s+(\w+)\s*=/gm;
let match;
while ((match = fieldRegex.exec(body)) !== null) {
fields.push(match[1]);
}
return fields;
}
private extractEnumValues(content: string, startIdx: number): string[] {
const values: string[] = [];
const afterEnum = content.slice(startIdx);
const openBrace = afterEnum.indexOf("{");
if (openBrace === -1) return values;
const closeBrace = this.findClosingBrace(afterEnum);
const body = afterEnum.slice(openBrace + 1, closeBrace);
const valueRegex = /^\s*(\w+)\s*=/gm;
let match;
while ((match = valueRegex.exec(body)) !== null) {
values.push(match[1]);
}
return values;
}
private findClosingBrace(content: string): number {
let depth = 0;
for (let i = 0; i < content.length; i++) {
if (content[i] === "{") depth++;
if (content[i] === "}") {
depth--;
if (depth === 0) return i;
}
}
if (depth !== 0) {
console.warn(`[protobuf-parser] Unbalanced braces detected (depth=${depth}), results may be incomplete`);
}
return content.length;
}
}
@@ -0,0 +1,76 @@
import type { AnalyzerPlugin, StructuralAnalysis, ReferenceResolution } from "../../types.js";
/**
* Parses shell scripts (.sh, .bash) to extract function definitions and source references.
* Handles both `name() {` and `function name {` styles, including brace on next line.
* Does not extract variable declarations, aliases, or trap handlers.
*/
export class ShellParser implements AnalyzerPlugin {
name = "shell-parser";
languages = ["shell"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const functions = this.extractFunctions(content);
return {
functions,
classes: [],
imports: [],
exports: [],
};
}
extractReferences(filePath: string, content: string): ReferenceResolution[] {
const refs: ReferenceResolution[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
// Match source/. commands
const sourceMatch = lines[i].match(/^\s*(?:source|\.)[ \t]+["']?([^"'\s]+)["']?/);
if (sourceMatch) {
refs.push({
source: filePath,
target: sourceMatch[1],
referenceType: "file",
line: i + 1,
});
}
}
return refs;
}
private extractFunctions(content: string): Array<{ name: string; lineRange: [number, number]; params: string[] }> {
const functions: Array<{ name: string; lineRange: [number, number]; params: string[] }> = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
// Match function name() { or function name {
const match = lines[i].match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?/) ||
lines[i].match(/^function\s+(\w+)\s*\{?/);
if (match) {
const name = match[1];
// Find closing brace (handle brace on same line or next line)
let endLine = i;
if (lines[i].includes("{") || (i + 1 < lines.length && lines[i + 1]?.trim() === "{")) {
const startBraceLine = lines[i].includes("{") ? i : i + 1;
let depth = 0;
for (let j = startBraceLine; j < lines.length; j++) {
for (const ch of lines[j]) {
if (ch === "{") depth++;
if (ch === "}") depth--;
}
if (depth === 0) {
endLine = j;
break;
}
}
}
functions.push({
name,
lineRange: [i + 1, endLine + 1],
params: [],
});
}
}
return functions;
}
}
@@ -0,0 +1,103 @@
import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo } from "../../types.js";
/**
* Parses SQL files to extract table, view, and index definitions.
* Handles CREATE TABLE, CREATE VIEW, CREATE INDEX with IF NOT EXISTS and OR REPLACE variants.
* Does not handle stored procedures, triggers, or schema-qualified names (e.g., public.users).
*/
export class SQLParser implements AnalyzerPlugin {
name = "sql-parser";
languages = ["sql"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const definitions = this.extractDefinitions(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
definitions,
};
}
private extractDefinitions(content: string): DefinitionInfo[] {
const definitions: DefinitionInfo[] = [];
const lines = content.split("\n");
// Match CREATE TABLE statements
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|")?(\w+)(?:`|")?/gi;
let match;
while ((match = tableRegex.exec(content)) !== null) {
const tableName = match[1];
const startLine = content.slice(0, match.index).split("\n").length;
// Extract columns (simplified: look for column names in parenthesized block)
const fields = this.extractColumns(content, match.index);
// Find the end of the CREATE TABLE statement
const afterMatch = content.slice(match.index);
const endParen = afterMatch.indexOf(");");
const endLine = endParen !== -1
? content.slice(0, match.index + endParen + 2).split("\n").length
: startLine + 5;
definitions.push({
name: tableName,
kind: "table",
lineRange: [startLine, endLine],
fields,
});
}
// Match CREATE VIEW
const viewRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:`|")?(\w+)(?:`|")?/gi;
while ((match = viewRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
definitions.push({
name: match[1],
kind: "view",
lineRange: [startLine, startLine],
fields: [],
});
}
// Match CREATE INDEX
const indexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|")?(\w+)(?:`|")?/gi;
while ((match = indexRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
definitions.push({
name: match[1],
kind: "index",
lineRange: [startLine, startLine],
fields: [],
});
}
return definitions;
}
private extractColumns(content: string, startIdx: number): string[] {
const fields: string[] = [];
const afterCreate = content.slice(startIdx);
const openParen = afterCreate.indexOf("(");
if (openParen === -1) return fields;
const closeParen = afterCreate.indexOf(");", openParen);
if (closeParen === -1) return fields;
const body = afterCreate.slice(openParen + 1, closeParen);
const lines = body.split(",");
for (const line of lines) {
const trimmed = line.trim();
// Skip constraints
if (/^(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT|INDEX|KEY)/i.test(trimmed)) continue;
const colMatch = trimmed.match(/^(?:`|")?(\w+)(?:`|")?\s+/);
if (colMatch) {
fields.push(colMatch[1]);
}
}
return fields;
}
}
@@ -0,0 +1,130 @@
import type { AnalyzerPlugin, StructuralAnalysis, ResourceInfo, DefinitionInfo } from "../../types.js";
/**
* Parses Terraform (.tf) files to extract resource, data, module, variable, and output blocks.
* Handles HCL block syntax with brace-matching for line range computation.
* Does not handle provider blocks, locals, or terraform configuration blocks.
*/
export class TerraformParser implements AnalyzerPlugin {
name = "terraform-parser";
languages = ["terraform"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const resources = this.extractResources(content);
const definitions = this.extractVariablesAndOutputs(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
resources,
definitions,
};
}
private extractResources(content: string): ResourceInfo[] {
const resources: ResourceInfo[] = [];
// Match resource blocks: resource "type" "name" {
const resourceRegex = /^resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/gm;
let match;
while ((match = resourceRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
resources.push({
name: `${match[1]}.${match[2]}`,
kind: match[1],
lineRange: [startLine, endLine],
});
}
// Match data blocks: data "type" "name" {
const dataRegex = /^data\s+"([^"]+)"\s+"([^"]+)"\s*\{/gm;
while ((match = dataRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
resources.push({
name: `data.${match[1]}.${match[2]}`,
kind: `data.${match[1]}`,
lineRange: [startLine, endLine],
});
}
// Match module blocks: module "name" {
const moduleRegex = /^module\s+"([^"]+)"\s*\{/gm;
while ((match = moduleRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
resources.push({
name: `module.${match[1]}`,
kind: "module",
lineRange: [startLine, endLine],
});
}
return resources;
}
private extractVariablesAndOutputs(content: string): DefinitionInfo[] {
const definitions: DefinitionInfo[] = [];
// Match variable blocks
const varRegex = /^variable\s+"([^"]+)"\s*\{/gm;
let match;
while ((match = varRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
definitions.push({
name: match[1],
kind: "variable",
lineRange: [startLine, endLine],
fields: [],
});
}
// Match output blocks
const outputRegex = /^output\s+"([^"]+)"\s*\{/gm;
while ((match = outputRegex.exec(content)) !== null) {
const startLine = content.slice(0, match.index).split("\n").length;
const afterMatch = content.slice(match.index);
const closeBrace = this.findClosingBrace(afterMatch);
const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length;
definitions.push({
name: match[1],
kind: "output",
lineRange: [startLine, endLine],
fields: [],
});
}
return definitions;
}
private findClosingBrace(content: string): number {
let depth = 0;
for (let i = 0; i < content.length; i++) {
if (content[i] === "{") depth++;
if (content[i] === "}") {
depth--;
if (depth === 0) return i;
}
}
if (depth !== 0) {
console.warn(`[terraform-parser] Unbalanced braces detected (depth=${depth}), results may be incomplete`);
}
return content.length;
}
}
@@ -0,0 +1,46 @@
import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo } from "../../types.js";
/**
* Parses TOML files to extract section headers ([section] and [[array-of-tables]]).
* Computes section nesting level from dotted key paths (e.g., [tool.poetry] = level 2).
* Does not parse individual key-value pairs within sections.
*/
export class TOMLParser implements AnalyzerPlugin {
name = "toml-parser";
languages = ["toml"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const sections = this.extractSections(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
sections,
};
}
private extractSections(content: string): SectionInfo[] {
const sections: SectionInfo[] = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
// Match [section] and [[array-of-tables]] headers
const match = lines[i].match(/^\s*\[(\[?)([^\]]+)\]?\]/);
if (match) {
const isArray = match[1] === "[";
const name = match[2].trim();
sections.push({
name: isArray ? `[[${name}]]` : name,
level: name.split(".").length,
lineRange: [i + 1, i + 1],
});
}
}
// Fix lineRange end
for (let i = 0; i < sections.length; i++) {
const next = sections[i + 1];
sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length;
}
return sections;
}
}
@@ -0,0 +1,72 @@
import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo } from "../../types.js";
import { parse as parseYAML } from "yaml";
/**
* Parses YAML configuration files to extract top-level key sections.
* Uses the `yaml` library for parsing with a regex fallback for malformed input.
* Only extracts top-level keys; does not descend into nested structures.
*/
export class YAMLConfigParser implements AnalyzerPlugin {
name = "yaml-config-parser";
languages = ["yaml"];
analyzeFile(_filePath: string, content: string): StructuralAnalysis {
const sections = this.extractSections(content);
return {
functions: [],
classes: [],
imports: [],
exports: [],
sections,
};
}
private extractSections(content: string): SectionInfo[] {
const sections: SectionInfo[] = [];
try {
const doc = parseYAML(content);
if (doc && typeof doc === "object" && !Array.isArray(doc)) {
const lines = content.split("\n");
for (const key of Object.keys(doc)) {
// Find the line where this top-level key appears
const lineIdx = lines.findIndex((l) => l.match(new RegExp(`^${this.escapeRegex(key)}\\s*:`)));
if (lineIdx !== -1) {
sections.push({
name: key,
level: 1,
lineRange: [lineIdx + 1, lineIdx + 1],
});
}
}
// Fix lineRange end
for (let i = 0; i < sections.length; i++) {
const next = sections[i + 1];
sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length;
}
}
} catch (err) {
console.warn(`[yaml-parser] YAML parse failed, falling back to regex extraction: ${err instanceof Error ? err.message : String(err)}`);
// If YAML parsing fails, fall back to regex
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/^(\w[\w-]*)\s*:/);
if (match) {
sections.push({
name: match[1],
level: 1,
lineRange: [i + 1, i + 1],
});
}
}
for (let i = 0; i < sections.length; i++) {
const next = sections[i + 1];
sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length;
}
}
return sections;
}
private escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
}
@@ -61,7 +61,7 @@ export class PluginRegistry {
resolveImports(filePath: string, content: string): ImportResolution[] | null {
const plugin = this.getPluginForFile(filePath);
if (!plugin) return null;
if (!plugin || !plugin.resolveImports) return null;
return plugin.resolveImports(filePath, content);
}
@@ -1,12 +1,14 @@
import { z } from "zod";
// Edge types (18 values across 5 categories)
// Edge types (26 values across 6 categories)
export const EdgeTypeSchema = z.enum([
"imports", "exports", "contains", "inherits", "implements", // Structural
"calls", "subscribes", "publishes", "middleware", // Behavioral
"reads_from", "writes_to", "transforms", "validates", // Data flow
"depends_on", "tested_by", "configures", // Dependencies
"related", "similar_to", // Semantic
"deploys", "serves", "provisions", "triggers", // Infrastructure
"migrates", "documents", "routes", "defines_schema", // Schema/Data
]);
// Aliases that LLMs commonly generate instead of canonical node types
@@ -19,6 +21,35 @@ export const NODE_TYPE_ALIASES: Record<string, string> = {
mod: "module",
pkg: "module",
package: "module",
// Non-code aliases
container: "service",
deployment: "service",
pod: "service",
doc: "document",
readme: "document",
docs: "document",
workflow: "pipeline",
job: "pipeline",
ci: "pipeline",
action: "pipeline",
route: "endpoint",
api: "endpoint",
query: "endpoint",
mutation: "endpoint",
setting: "config",
env: "config",
configuration: "config",
infra: "resource",
infrastructure: "resource",
terraform: "resource",
migration: "table",
database: "table",
db: "table",
view: "table",
proto: "schema",
protobuf: "schema",
definition: "schema",
typedef: "schema",
};
// Aliases that LLMs commonly generate instead of canonical edge types
@@ -36,6 +67,18 @@ export const EDGE_TYPE_ALIASES: Record<string, string> = {
contain: "contains",
publish: "publishes",
subscribe: "subscribes",
// Non-code aliases
describes: "documents",
documented_by: "documents",
creates: "provisions",
exposes: "serves",
listens: "serves",
deploys_to: "deploys",
migrates_to: "migrates",
routes_to: "routes",
triggers_on: "triggers",
fires: "triggers",
defines: "defines_schema",
};
// Aliases for complexity values LLMs commonly generate
@@ -266,7 +309,11 @@ export function autoFixGraph(data: Record<string, unknown>): {
export const GraphNodeSchema = z.object({
id: z.string(),
type: z.enum(["file", "function", "class", "module", "concept"]),
type: z.enum([
"file", "function", "class", "module", "concept",
"config", "document", "service", "table", "endpoint",
"pipeline", "schema", "resource",
]),
name: z.string(),
filePath: z.string().optional(),
lineRange: z.tuple([z.number(), z.number()]).optional(),
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import type { KnowledgeGraph, GraphNode, GraphEdge } from "./types.js";
import type { KnowledgeGraph, GraphNode, GraphEdge, EdgeType, NodeType, StructuralAnalysis, AnalyzerPlugin, ReferenceResolution } from "./types.js";
describe("KnowledgeGraph types", () => {
it("should create a valid empty KnowledgeGraph", () => {
@@ -115,3 +115,89 @@ describe("KnowledgeGraph types", () => {
expect(maxWeightEdge.weight).toBe(1);
});
});
describe("Extended types", () => {
it("accepts all 13 node types via NodeType alias", () => {
const nodeTypes: NodeType[] = [
"file", "function", "class", "module", "concept",
"config", "document", "service", "table", "endpoint",
"pipeline", "schema", "resource",
];
expect(nodeTypes).toHaveLength(13);
// NodeType and GraphNode["type"] should be interchangeable
const check: GraphNode["type"] = nodeTypes[0];
expect(check).toBe("file");
});
it("accepts all 26 edge types", () => {
const edgeTypes: EdgeType[] = [
"imports", "exports", "contains", "inherits", "implements",
"calls", "subscribes", "publishes", "middleware",
"reads_from", "writes_to", "transforms", "validates",
"depends_on", "tested_by", "configures",
"related", "similar_to",
"deploys", "serves", "migrates", "documents",
"provisions", "routes", "defines_schema", "triggers",
];
expect(edgeTypes).toHaveLength(26);
});
it("StructuralAnalysis has optional non-code fields", () => {
const analysis: StructuralAnalysis = {
functions: [], classes: [], imports: [], exports: [],
sections: [{ name: "Introduction", level: 1, lineRange: [1, 10] }],
definitions: [{ name: "users", kind: "table", lineRange: [1, 20], fields: ["id", "name"] }],
services: [{ name: "web", image: "node:22", ports: [3000], lineRange: [1, 5] }],
endpoints: [{ method: "GET", path: "/api/users", lineRange: [5, 15] }],
steps: [{ name: "build", lineRange: [1, 5] }],
resources: [{ name: "aws_s3_bucket.main", kind: "aws_s3_bucket", lineRange: [1, 10] }],
};
expect(analysis.sections).toHaveLength(1);
expect(analysis.definitions).toHaveLength(1);
expect(analysis.services).toHaveLength(1);
expect(analysis.services![0].lineRange).toEqual([1, 5]);
expect(analysis.endpoints).toHaveLength(1);
expect(analysis.steps).toHaveLength(1);
expect(analysis.resources).toHaveLength(1);
});
it("ServiceInfo.lineRange is optional for backward compat", () => {
const svcWithout: import("./types.js").ServiceInfo = { name: "web", ports: [3000] };
const svcWith: import("./types.js").ServiceInfo = { name: "db", ports: [5432], lineRange: [10, 20] };
expect(svcWithout.lineRange).toBeUndefined();
expect(svcWith.lineRange).toEqual([10, 20]);
});
it("StructuralAnalysis is backward compatible (non-code fields are optional)", () => {
const analysis: StructuralAnalysis = {
functions: [], classes: [], imports: [], exports: [],
};
expect(analysis.sections).toBeUndefined();
expect(analysis.definitions).toBeUndefined();
expect(analysis.services).toBeUndefined();
});
it("AnalyzerPlugin allows optional resolveImports", () => {
const plugin: AnalyzerPlugin = {
name: "test-plugin",
languages: ["markdown"],
analyzeFile: () => ({ functions: [], classes: [], imports: [], exports: [] }),
// resolveImports is optional — not provided
};
expect(plugin.resolveImports).toBeUndefined();
expect(plugin.analyzeFile).toBeDefined();
});
it("AnalyzerPlugin supports extractReferences", () => {
const refs: ReferenceResolution[] = [
{ source: "README.md", target: "./docs/guide.md", referenceType: "file", line: 5 },
];
const plugin: AnalyzerPlugin = {
name: "test-plugin",
languages: ["markdown"],
analyzeFile: () => ({ functions: [], classes: [], imports: [], exports: [] }),
extractReferences: () => refs,
};
expect(plugin.extractReferences!("README.md", "")).toEqual(refs);
});
});
@@ -1,15 +1,23 @@
// Edge types (18 total in 5 categories: Structural, Behavioral, Data flow, Dependencies, Semantic)
// Node types (13 total: 5 code + 8 non-code)
export type NodeType =
| "file" | "function" | "class" | "module" | "concept"
| "config" | "document" | "service" | "table" | "endpoint"
| "pipeline" | "schema" | "resource";
// Edge types (26 total in 6 categories: Structural, Behavioral, Data flow, Dependencies, Semantic, Infrastructure/Schema)
export type EdgeType =
| "imports" | "exports" | "contains" | "inherits" | "implements" // Structural
| "calls" | "subscribes" | "publishes" | "middleware" // Behavioral
| "reads_from" | "writes_to" | "transforms" | "validates" // Data flow
| "depends_on" | "tested_by" | "configures" // Dependencies
| "related" | "similar_to"; // Semantic
| "related" | "similar_to" // Semantic
| "deploys" | "serves" | "provisions" | "triggers" // Infrastructure
| "migrates" | "documents" | "routes" | "defines_schema"; // Schema/Data
// GraphNode with 5 types: file, function, class, module, concept
// GraphNode with 13 types: 5 code + 8 non-code
export interface GraphNode {
id: string;
type: "file" | "function" | "class" | "module" | "concept";
type: NodeType;
name: string;
filePath?: string;
lineRange?: [number, number];
@@ -86,12 +94,65 @@ export interface ProjectConfig {
autoUpdate: boolean;
}
// Non-code structural sub-interfaces
export interface SectionInfo {
name: string;
level: number;
lineRange: [number, number];
}
export interface DefinitionInfo {
name: string;
/** Parser-reported definition kind. Known values: "table", "view", "index", "message", "enum", "type", "input", "interface", "union", "scalar", "variable", "output", "resource", "data", "section", "target", "stage" */
kind: string;
lineRange: [number, number];
fields: string[];
}
export interface ServiceInfo {
name: string;
image?: string;
ports: number[];
lineRange?: [number, number];
}
export interface EndpointInfo {
method?: string;
path: string;
lineRange: [number, number];
}
export interface StepInfo {
name: string;
lineRange: [number, number];
}
export interface ResourceInfo {
name: string;
kind: string;
lineRange: [number, number];
}
export interface ReferenceResolution {
source: string;
target: string;
referenceType: string; // "file", "image", "schema", "service"
line?: number;
}
// Plugin interfaces
export interface StructuralAnalysis {
functions: Array<{ name: string; lineRange: [number, number]; params: string[]; returnType?: string }>;
classes: Array<{ name: string; lineRange: [number, number]; methods: string[]; properties: string[] }>;
imports: Array<{ source: string; specifiers: string[]; lineNumber: number }>;
exports: Array<{ name: string; lineNumber: number }>;
// Non-code structural data (all optional for backward compat)
sections?: SectionInfo[];
definitions?: DefinitionInfo[];
services?: ServiceInfo[];
endpoints?: EndpointInfo[];
steps?: StepInfo[];
resources?: ResourceInfo[];
}
export interface ImportResolution {
@@ -110,6 +171,7 @@ export interface AnalyzerPlugin {
name: string;
languages: string[];
analyzeFile(filePath: string, content: string): StructuralAnalysis;
resolveImports(filePath: string, content: string): ImportResolution[];
resolveImports?(filePath: string, content: string): ImportResolution[];
extractCallGraph?(filePath: string, content: string): CallGraphEntry[];
extractReferences?(filePath: string, content: string): ReferenceResolution[];
}
@@ -72,6 +72,8 @@ function Dashboard({ accessToken }: { accessToken: string }) {
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
const [loadError, setLoadError] = useState<string | null>(null);
const [graphIssues, setGraphIssues] = useState<GraphIssue[]>([]);
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
@@ -250,6 +252,35 @@ function Dashboard({ accessToken }: { accessToken: string }) {
</div>
<div className="flex items-center gap-4">
<DiffToggle />
<div className="flex items-center gap-1">
{([
{ key: "code", label: "Code", color: "var(--color-node-file)" },
{ key: "config", label: "Config", color: "var(--color-node-config)" },
{ 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)" },
] as const).map((cat) => (
<button
key={cat.key}
onClick={() => toggleNodeTypeFilter(cat.key)}
className={`text-[10px] font-semibold uppercase tracking-wider px-2 py-1 rounded border transition-colors flex items-center gap-1.5 ${
nodeTypeFilters[cat.key] !== false
? "border-border-medium bg-elevated text-text-secondary hover:text-text-primary"
: "border-transparent bg-transparent text-text-muted/40 line-through hover:text-text-muted"
}`}
title={`${nodeTypeFilters[cat.key] !== false ? "Hide" : "Show"} ${cat.label} nodes`}
>
<span
className="w-2 h-2 rounded-full shrink-0"
style={{
backgroundColor: cat.color,
opacity: nodeTypeFilters[cat.key] !== false ? 1 : 0.3,
}}
/>
{cat.label}
</button>
))}
</div>
<LayerLegend />
<ThemePicker />
<button
@@ -306,7 +337,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
{/* Code viewer overlay */}
{codeViewerOpen && (
<div className="absolute bottom-0 left-0 right-0 h-[40vh] bg-surface border-t border-border-subtle animate-slide-up z-20">
<div className="absolute bottom-0 left-0 right-0 h-[25vh] bg-surface border-t border-border-subtle animate-slide-up z-20">
<div className="h-full flex flex-col">
<div className="flex items-center justify-end px-3 py-1 shrink-0">
<button
@@ -1,21 +1,39 @@
import { memo } from "react";
import { Handle, Position } from "@xyflow/react";
import type { NodeProps, Node } from "@xyflow/react";
import type { NodeType } from "@understand-anything/core/types";
const typeColors: Record<string, string> = {
// Color maps keyed by NodeType — must be kept in sync with core NodeType union.
const typeColors: Record<NodeType, string> = {
file: "var(--color-node-file)",
function: "var(--color-node-function)",
class: "var(--color-node-class)",
module: "var(--color-node-module)",
concept: "var(--color-node-concept)",
config: "var(--color-node-config)",
document: "var(--color-node-document)",
service: "var(--color-node-service)",
table: "var(--color-node-table)",
endpoint: "var(--color-node-endpoint)",
pipeline: "var(--color-node-pipeline)",
schema: "var(--color-node-schema)",
resource: "var(--color-node-resource)",
};
const typeTextColors: Record<string, string> = {
const typeTextColors: Record<NodeType, string> = {
file: "text-node-file",
function: "text-node-function",
class: "text-node-class",
module: "text-node-module",
concept: "text-node-concept",
config: "text-node-config",
document: "text-node-document",
service: "text-node-service",
table: "text-node-table",
endpoint: "text-node-endpoint",
pipeline: "text-node-pipeline",
schema: "text-node-schema",
resource: "text-node-resource",
};
const complexityColors: Record<string, string> = {
@@ -47,10 +65,15 @@ function CustomNodeComponent({
id,
data,
}: NodeProps<CustomFlowNode>) {
const barColor = typeColors[data.nodeType] ?? typeColors.file;
const textColor = typeTextColors[data.nodeType] ?? typeTextColors.file;
const knownType = data.nodeType as NodeType;
const barColor = typeColors[knownType] ?? typeColors.file;
const textColor = typeTextColors[knownType] ?? typeTextColors.file;
const complexityColor = complexityColors[data.complexity] ?? complexityColors.simple;
if (import.meta.env.DEV && !(knownType in typeColors)) {
console.warn(`[CustomNode] Unknown node type "${data.nodeType}" — using "file" colors`);
}
let extraClass = "";
if (data.isSelected) {
extraClass = "ring-2 ring-accent node-glow";
@@ -21,7 +21,7 @@ import PortalNode from "./PortalNode";
import type { PortalFlowNode } from "./PortalNode";
import Breadcrumb from "./Breadcrumb";
import { useDashboardStore } from "../store";
import type { KnowledgeGraph } from "@understand-anything/core/types";
import type { KnowledgeGraph, NodeType } from "@understand-anything/core/types";
import { useTheme } from "../themes/index.ts";
import {
applyDagreLayout,
@@ -44,6 +44,20 @@ const nodeTypes = {
portal: PortalNode,
};
import type { NodeCategory } from "../store";
/**
* Maps each NodeType to a filter category. Must be kept in sync with core NodeType.
* Unknown types default to "code" with a development warning.
*/
const NODE_TYPE_TO_CATEGORY: Record<NodeType, NodeCategory> = {
file: "code", function: "code", class: "code", module: "code", concept: "code",
config: "config",
document: "docs",
service: "infra", resource: "infra", pipeline: "infra",
table: "data", endpoint: "data", schema: "data",
} as const;
// ── Helper components that must live inside <ReactFlow> ────────────────
/** Pans/zooms to tour-highlighted nodes. */
@@ -200,6 +214,7 @@ function useLayerDetailTopology() {
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
const focusNodeId = useDashboardStore((s) => s.focusNodeId);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const drillIntoLayer = useDashboardStore((s) => s.drillIntoLayer);
const handleNodeSelect = useCallback(
@@ -218,12 +233,32 @@ function useLayerDetailTopology() {
const layerNodeIds = new Set(activeLayer.nodeIds);
// Non-technical persona only sees concept/module/file nodes
// All top-level (file-level) node types that should appear in the graph.
// This includes the 8 new non-code types plus the original "file" type.
const fileLevelTypes = new Set([
"file", "config", "document", "service", "table",
"endpoint", "pipeline", "schema", "resource",
]);
// Non-technical persona: show module, concept, and file-level types (hide function/class)
// Junior/experienced persona: show everything including function/class
let filteredGraphNodes = persona === "non-technical"
? graph.nodes.filter(
(n) => layerNodeIds.has(n.id) && (n.type === "concept" || n.type === "module" || n.type === "file"),
(n) => layerNodeIds.has(n.id) && (n.type === "concept" || n.type === "module" || fileLevelTypes.has(n.type)),
)
: graph.nodes.filter((n) => layerNodeIds.has(n.id) && n.type === "file");
: graph.nodes.filter((n) => layerNodeIds.has(n.id) && (fileLevelTypes.has(n.type) || n.type === "module" || n.type === "concept" || n.type === "function" || n.type === "class"));
// Apply node type category filters
filteredGraphNodes = filteredGraphNodes.filter((n) => {
const category = NODE_TYPE_TO_CATEGORY[n.type as NodeType];
if (!category) {
if (import.meta.env.DEV) {
console.warn(`[GraphView] Unknown node type "${n.type}" — defaulting to "code" category`);
}
}
const effectiveCategory = category ?? "code";
return nodeTypeFilters[effectiveCategory] !== false;
});
let filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
@@ -360,7 +395,7 @@ function useLayerDetailTopology() {
const laid = applyDagreLayout(allFlowNodes, allFlowEdges, "TB", dims);
return { nodes: laid.nodes, edges: laid.edges, portalNodes, portalEdges, filteredEdges: filteredGraphEdges };
}, [graph, activeLayerId, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds, focusNodeId, drillIntoLayer]);
}, [graph, activeLayerId, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds, focusNodeId, nodeTypeFilters, drillIntoLayer]);
}
/**
@@ -1,12 +1,22 @@
import { useState } from "react";
import { useDashboardStore } from "../store";
import type { NodeType, EdgeType } from "@understand-anything/core/types";
const typeBadgeColors: Record<string, string> = {
// Badge color classes keyed by NodeType — must be kept in sync with core NodeType union.
const typeBadgeColors: Record<NodeType, string> = {
file: "text-node-file border border-node-file/30 bg-node-file/10",
function: "text-node-function border border-node-function/30 bg-node-function/10",
class: "text-node-class border border-node-class/30 bg-node-class/10",
module: "text-node-module border border-node-module/30 bg-node-module/10",
concept: "text-node-concept border border-node-concept/30 bg-node-concept/10",
config: "text-node-config border border-node-config/30 bg-node-config/10",
document: "text-node-document border border-node-document/30 bg-node-document/10",
service: "text-node-service border border-node-service/30 bg-node-service/10",
table: "text-node-table border border-node-table/30 bg-node-table/10",
endpoint: "text-node-endpoint border border-node-endpoint/30 bg-node-endpoint/10",
pipeline: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10",
schema: "text-node-schema border border-node-schema/30 bg-node-schema/10",
resource: "text-node-resource border border-node-resource/30 bg-node-resource/10",
};
const complexityBadgeColors: Record<string, string> = {
@@ -16,51 +26,50 @@ const complexityBadgeColors: Record<string, string> = {
};
/**
* Human-readable directional labels for edge types.
* Returns different text depending on whether the selected node is
* the source or target of the edge.
* Human-readable directional labels for all 26 edge types.
* Must be kept in sync with core EdgeType.
*/
const EDGE_LABELS: Record<EdgeType, { forward: string; backward: string }> = {
imports: { forward: "imports", backward: "imported by" },
exports: { forward: "exports to", backward: "exported by" },
contains: { forward: "contains", backward: "contained in" },
inherits: { forward: "inherits from", backward: "inherited by" },
implements: { forward: "implements", backward: "implemented by" },
calls: { forward: "calls", backward: "called by" },
subscribes: { forward: "subscribes to", backward: "subscribed by" },
publishes: { forward: "publishes to", backward: "consumed by" },
middleware: { forward: "middleware for", backward: "uses middleware" },
reads_from: { forward: "reads from", backward: "read by" },
writes_to: { forward: "writes to", backward: "written by" },
transforms: { forward: "transforms", backward: "transformed by" },
validates: { forward: "validates", backward: "validated by" },
depends_on: { forward: "depends on", backward: "depended on by" },
tested_by: { forward: "tested by", backward: "tests" },
configures: { forward: "configures", backward: "configured by" },
related: { forward: "related to", backward: "related to" },
similar_to: { forward: "similar to", backward: "similar to" },
deploys: { forward: "deploys", backward: "deployed by" },
serves: { forward: "serves", backward: "served by" },
migrates: { forward: "migrates", backward: "migrated by" },
documents: { forward: "documents", backward: "documented by" },
provisions: { forward: "provisions", backward: "provisioned by" },
routes: { forward: "routes to", backward: "routed from" },
defines_schema: { forward: "defines schema for", backward: "schema defined by" },
triggers: { forward: "triggers", backward: "triggered by" },
};
/**
* Returns a human-readable directional label for an edge type.
* Falls back to formatted type name for unknown edge types.
*/
function getDirectionalLabel(edgeType: string, isSource: boolean): string {
switch (edgeType) {
case "imports":
return isSource ? "imports" : "imported by";
case "exports":
return isSource ? "exports to" : "exported by";
case "contains":
return isSource ? "contains" : "contained in";
case "inherits":
return isSource ? "inherits from" : "inherited by";
case "implements":
return isSource ? "implements" : "implemented by";
case "calls":
return isSource ? "calls" : "called by";
case "subscribes":
return isSource ? "subscribes to" : "subscribed by";
case "publishes":
return isSource ? "publishes to" : "consumed by";
case "middleware":
return isSource ? "middleware for" : "uses middleware";
case "reads_from":
return isSource ? "reads from" : "read by";
case "writes_to":
return isSource ? "writes to" : "written by";
case "transforms":
return isSource ? "transforms" : "transformed by";
case "validates":
return isSource ? "validates" : "validated by";
case "depends_on":
return isSource ? "depends on" : "depended on by";
case "tested_by":
return isSource ? "tested by" : "tests";
case "configures":
return isSource ? "configures" : "configured by";
case "related":
return "related to";
case "similar_to":
return "similar to";
default:
return isSource ? edgeType : `${edgeType} (reverse)`;
const labels = (EDGE_LABELS as Record<string, { forward: string; backward: string }>)[edgeType];
if (!labels) {
// Fallback for unknown edge types
const formatted = edgeType.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
return isSource ? formatted : `${formatted} (reverse)`;
}
return isSource ? labels.forward : labels.backward;
}
export default function NodeInfo() {
@@ -108,10 +117,15 @@ export default function NodeInfo() {
.map((e) => graph?.nodes.find((n) => n.id === e.target))
.filter(Boolean);
const typeBadge = typeBadgeColors[node.type] ?? typeBadgeColors.file;
const knownType = node.type as NodeType;
const typeBadge = typeBadgeColors[knownType] ?? typeBadgeColors.file;
const complexityBadge =
complexityBadgeColors[node.complexity] ?? complexityBadgeColors.simple;
if (import.meta.env.DEV && !(knownType in typeBadgeColors)) {
console.warn(`[NodeInfo] Unknown node type "${node.type}" — using "file" badge colors`);
}
return (
<div className="h-full w-full overflow-auto p-5 animate-fade-slide-in">
{/* Navigation history trail */}
@@ -245,7 +259,7 @@ export default function NodeInfo() {
<div className="space-y-1">
{childNodes.map((child) => {
if (!child) return null;
const childTypeBadge = typeBadgeColors[child.type] ?? typeBadgeColors.file;
const childTypeBadge = typeBadgeColors[child.type as NodeType] ?? typeBadgeColors.file;
const childComplexity = complexityBadgeColors[child.complexity] ?? complexityBadgeColors.simple;
return (
<div
@@ -21,6 +21,16 @@ export default function ProjectOverview() {
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
}
// Category breakdowns
const categoryBreakdown = [
{ label: "Code", color: "var(--color-node-file)", count: (typeCounts["file"] ?? 0) + (typeCounts["function"] ?? 0) + (typeCounts["class"] ?? 0) },
{ label: "Config", color: "var(--color-node-config)", count: typeCounts["config"] ?? 0 },
{ label: "Docs", color: "var(--color-node-document)", count: typeCounts["document"] ?? 0 },
{ label: "Infra", color: "var(--color-node-service)", count: (typeCounts["service"] ?? 0) + (typeCounts["resource"] ?? 0) + (typeCounts["pipeline"] ?? 0) },
{ label: "Data", color: "var(--color-node-table)", count: (typeCounts["table"] ?? 0) + (typeCounts["endpoint"] ?? 0) + (typeCounts["schema"] ?? 0) },
];
const hasNonCodeNodes = categoryBreakdown.some((c) => c.label !== "Code" && c.count > 0);
return (
<div className="h-full w-full overflow-auto p-5 animate-fade-slide-in">
{/* Project name */}
@@ -47,6 +57,25 @@ export default function ProjectOverview() {
</div>
</div>
{/* File Types breakdown */}
{hasNonCodeNodes && (
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">File Types</h3>
<div className="space-y-1.5">
{categoryBreakdown.filter((c) => c.count > 0).map((cat) => (
<div key={cat.label} className="flex items-center gap-2">
<span
className="w-2.5 h-2.5 rounded-full shrink-0"
style={{ backgroundColor: cat.color }}
/>
<span className="text-xs text-text-secondary flex-1">{cat.label}</span>
<span className="text-xs font-mono text-text-muted">{cat.count}</span>
</div>
))}
</div>
</div>
)}
{/* Languages */}
{project.languages.length > 0 && (
<div className="mb-5">
@@ -27,6 +27,14 @@
--color-node-class: #8b6fb0;
--color-node-module: #c9a06c;
--color-node-concept: #b07a8a;
--color-node-config: #5eead4;
--color-node-document: #7dd3fc;
--color-node-service: #a78bfa;
--color-node-table: #6ee7b7;
--color-node-endpoint: #fdba74;
--color-node-pipeline: #fda4af;
--color-node-schema: #fcd34d;
--color-node-resource: #a5b4fc;
/* Diff */
--color-diff-changed: #e05252;
@@ -9,6 +9,8 @@ import type {
export type Persona = "non-technical" | "junior" | "experienced";
export type NavigationLevel = "overview" | "layer-detail";
/** Categories used for node type filter toggles. Single source of truth for NodeCategory. */
export type NodeCategory = "code" | "config" | "docs" | "infra" | "data";
/** Find which layer a node belongs to. Returns layerId or null. */
function findNodeLayer(graph: KnowledgeGraph, nodeId: string): string | null {
@@ -53,6 +55,10 @@ interface DashboardStore {
// Sidebar navigation history (stack of visited node IDs)
nodeHistory: string[];
// Node type category filters
nodeTypeFilters: Record<NodeCategory, boolean>;
toggleNodeTypeFilter: (category: NodeCategory) => void;
setGraph: (graph: KnowledgeGraph) => void;
selectNode: (nodeId: string | null) => void;
navigateToNode: (nodeId: string) => void;
@@ -125,6 +131,16 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
focusNodeId: null,
nodeHistory: [],
nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true },
toggleNodeTypeFilter: (category) =>
set((state) => ({
nodeTypeFilters: {
...state.nodeTypeFilters,
[category]: !state.nodeTypeFilters[category],
},
})),
setGraph: (graph) => {
const searchEngine = new SearchEngine(graph.nodes);
const query = get().searchQuery;
@@ -42,6 +42,14 @@ export const PRESETS: ThemePreset[] = [
"node-class": "#8b6fb0",
"node-module": "#c9a06c",
"node-concept": "#b07a8a",
"node-config": "#5eead4",
"node-document": "#7dd3fc",
"node-service": "#a78bfa",
"node-table": "#6ee7b7",
"node-endpoint": "#fdba74",
"node-pipeline": "#fda4af",
"node-schema": "#fcd34d",
"node-resource": "#a5b4fc",
},
},
{
@@ -63,6 +71,14 @@ export const PRESETS: ThemePreset[] = [
"node-class": "#8b6fb0",
"node-module": "#c9a06c",
"node-concept": "#b07a8a",
"node-config": "#5eead4",
"node-document": "#7dd3fc",
"node-service": "#a78bfa",
"node-table": "#6ee7b7",
"node-endpoint": "#fdba74",
"node-pipeline": "#fda4af",
"node-schema": "#fcd34d",
"node-resource": "#a5b4fc",
},
},
{
@@ -84,6 +100,14 @@ export const PRESETS: ThemePreset[] = [
"node-class": "#8b6fb0",
"node-module": "#c9a06c",
"node-concept": "#b07a8a",
"node-config": "#5eead4",
"node-document": "#7dd3fc",
"node-service": "#a78bfa",
"node-table": "#6ee7b7",
"node-endpoint": "#fdba74",
"node-pipeline": "#fda4af",
"node-schema": "#fcd34d",
"node-resource": "#a5b4fc",
},
},
{
@@ -105,6 +129,14 @@ export const PRESETS: ThemePreset[] = [
"node-class": "#8b6fb0",
"node-module": "#c9a06c",
"node-concept": "#b07a8a",
"node-config": "#5eead4",
"node-document": "#7dd3fc",
"node-service": "#a78bfa",
"node-table": "#6ee7b7",
"node-endpoint": "#fdba74",
"node-pipeline": "#fda4af",
"node-schema": "#fcd34d",
"node-resource": "#a5b4fc",
},
},
{
@@ -126,6 +158,14 @@ export const PRESETS: ThemePreset[] = [
"node-class": "#755d99",
"node-module": "#a88a56",
"node-concept": "#966674",
"node-config": "#14b8a6",
"node-document": "#38bdf8",
"node-service": "#8b5cf6",
"node-table": "#34d399",
"node-endpoint": "#fb923c",
"node-pipeline": "#fb7185",
"node-schema": "#facc15",
"node-resource": "#818cf8",
},
},
];
+41 -57
View File
@@ -20,19 +20,23 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.1.0
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
packages/core:
dependencies:
yaml:
specifier: ^2.8.3
version: 2.8.3
devDependencies:
'@types/node':
specifier: ^25.5.0
version: 25.5.0
'@vitest/coverage-v8':
specifier: 3.2.4
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
vitest:
specifier: ^3.1.0
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
packages/dashboard:
dependencies:
@@ -66,7 +70,7 @@ importers:
devDependencies:
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))
version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
'@types/react':
specifier: ^19.0.0
version: 19.2.14
@@ -75,7 +79,7 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^4.3.0
version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))
version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
tailwindcss:
specifier: ^4.0.0
version: 4.2.2
@@ -84,7 +88,7 @@ importers:
version: 5.9.3
vite:
specifier: ^6.0.0
version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
packages:
@@ -406,79 +410,66 @@ packages:
resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.0':
resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.0':
resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.0':
resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.0':
resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.0':
resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.0':
resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.0':
resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.0':
resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.0':
resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.0':
resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==}
@@ -548,28 +539,24 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
@@ -1092,28 +1079,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1579,6 +1562,11 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yaml@2.8.3:
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
engines: {node: '>= 14.6'}
hasBin: true
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
@@ -1991,12 +1979,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))':
'@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies:
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
'@types/babel__core@7.20.5':
dependencies:
@@ -2089,7 +2077,7 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))':
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
@@ -2097,11 +2085,11 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))':
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -2116,7 +2104,7 @@ snapshots:
std-env: 3.10.0
test-exclude: 7.0.2
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@@ -2128,21 +2116,13 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0))':
'@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)
'@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -3076,13 +3056,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0):
vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -3097,13 +3077,13 @@ snapshots:
- tsx
- yaml
vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0):
vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -3118,7 +3098,7 @@ snapshots:
- tsx
- yaml
vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0):
vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
@@ -3131,8 +3111,9 @@ snapshots:
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.32.0
yaml: 2.8.3
vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0):
vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
@@ -3145,12 +3126,13 @@ snapshots:
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.32.0
yaml: 2.8.3
vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0):
vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0))
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -3168,8 +3150,8 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)
vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.13
@@ -3188,11 +3170,11 @@ snapshots:
- tsx
- yaml
vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0):
vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -3210,8 +3192,8 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.13
@@ -3253,6 +3235,8 @@ snapshots:
yallist@3.1.1: {}
yaml@2.8.3: {}
zustand@4.5.7(@types/react@19.2.14)(react@19.2.4):
dependencies:
use-sync-external-store: 1.6.0(react@19.2.4)
@@ -90,18 +90,19 @@ Dispatch a subagent using the prompt template at `./project-scanner-prompt.md`.
Pass these parameters in the dispatch prompt:
> Scan this project directory to discover all source files, detect languages and frameworks.
> Scan this project directory to discover all project files (including non-code files like configs, docs, infrastructure), detect languages and frameworks.
> Project root: `$PROJECT_ROOT`
> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/scan-result.json`
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/scan-result.json` to get:
- Project name, description
- Languages, frameworks
- File list with line counts
- File list with line counts and `fileCategory` per file (`code`, `config`, `docs`, `infra`, `data`, `script`, `markup`)
- Complexity estimate
- Import map (`importMap`): pre-resolved project-internal imports per file
- Import map (`importMap`): pre-resolved project-internal imports per file (non-code files have empty arrays)
Store `importMap` in memory as `$IMPORT_MAP` for use in Phase 2 batch construction.
Store the file list as `$FILE_LIST` with `fileCategory` metadata for use in Phase 2 batch construction.
**Gate check:** If >200 files, inform the user and suggest scoping with a subdirectory argument. Proceed only if user confirms or add guidance that this may take a while.
@@ -113,6 +114,16 @@ Store `importMap` in memory as `$IMPORT_MAP` for use in Phase 2 batch constructi
Batch the file list from Phase 1 into groups of **20-30 files each** (aim for ~25 files per batch for balanced sizes).
**Batching strategy for non-code files:**
- Group related non-code files together in the same batch when possible:
- Dockerfile + docker-compose.yml + .dockerignore → same batch
- SQL migration files → same batch (ordered by filename)
- CI/CD config files (.github/workflows/*) → same batch
- Documentation files (docs/*.md) → same batch
- This allows the file-analyzer to create cross-file edges (e.g., docker-compose `depends_on` Dockerfile)
- Non-code files can be mixed with code files in the same batch if batch sizes are small
- Each file's `fileCategory` from Phase 1 must be included in the batch file list
For each batch, dispatch a subagent using the prompt template at `./file-analyzer-prompt.md`. Run up to **5 subagents concurrently** using parallel dispatch. Pass the template as the subagent's prompt, appending the following additional context:
> **Additional context from main session:**
@@ -129,7 +140,7 @@ for each file in this batch:
Fill in batch-specific parameters below and dispatch:
> Analyze these source files and produce GraphNode and GraphEdge objects.
> Analyze these files and produce GraphNode and GraphEdge objects.
> Project root: `$PROJECT_ROOT`
> Project: `<projectName>`
> Languages: `<languages>`
@@ -142,8 +153,8 @@ Fill in batch-specific parameters below and dispatch:
> ```
>
> Files to analyze in this batch:
> 1. `<path>` (<sizeLines> lines)
> 2. `<path>` (<sizeLines> lines)
> 1. `<path>` (<sizeLines> lines, fileCategory: `<fileCategory>`)
> 2. `<path>` (<sizeLines> lines, fileCategory: `<fileCategory>`)
> ...
After ALL batches complete, read each `batch-<N>.json` file and merge:
@@ -175,7 +186,7 @@ Merge all file-analyzer results into a single set of nodes and edges. Then perfo
**Build the combined prompt template:**
1. Read the base template at `./architecture-analyzer-prompt.md`.
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file.
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`, `markdown`, `dockerfile`, `yaml`, `sql`, `terraform`, `graphql`, `protobuf`, `shell`, `html`, `css`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`, `./languages/dockerfile.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. **Include non-code language snippets** — they provide edge patterns and summary styles for non-code files.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
Pass the combined content as the subagent's prompt, appending the following additional context:
@@ -189,7 +200,7 @@ Pass the combined content as the subagent's prompt, appending the following addi
> $DIR_TREE
> ```
>
> Use the directory tree, language context, and framework addendums (appended above) to inform layer assignments. Directory structure is strong evidence for layer boundaries.
> Use the directory tree, language context, and framework addendums (appended above) to inform layer assignments. Directory structure is strong evidence for layer boundaries. Non-code files (config, docs, infrastructure, data) should be assigned to appropriate layers — see the prompt template for guidance.
Pass these parameters in the dispatch prompt:
@@ -198,22 +209,27 @@ Pass these parameters in the dispatch prompt:
> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/layers.json`
> Project: `<projectName>` — `<projectDescription>`
>
> File nodes:
> File nodes (all node types — includes code files, config, document, service, pipeline, table, schema, resource, endpoint):
> ```json
> [list of {id, name, filePath, summary, tags} for all file-type nodes — omit complexity, languageNotes]
> [list of {id, type, name, filePath, summary, tags} for ALL file-level nodes — omit complexity, languageNotes]
> ```
>
> Import edges:
> ```json
> [list of edges with type "imports"]
> ```
>
> All edges (for cross-category analysis — includes configures, documents, deploys, triggers, etc.):
> ```json
> [list of ALL edges — include all edge types]
> ```
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/layers.json` and normalize it into a final `layers` array. Apply these steps **in order**:
1. **Unwrap envelope:** If the file contains `{ "layers": [...] }` instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.)
2. **Rename legacy fields:** If any layer object has a `nodes` field instead of `nodeIds`, rename `nodes` → `nodeIds`. If `nodes` entries are objects with an `id` field rather than plain strings, extract just the `id` values into `nodeIds`.
3. **Synthesize missing IDs:** If any layer is missing an `id`, generate one as `layer:<kebab-case-name>`.
4. **Convert file paths:** If `nodeIds` entries are raw file paths (not prefixed with `file:`), convert them to `file:<relative-path>`.
4. **Convert file paths:** If `nodeIds` entries are raw file paths without a known prefix (`file:`, `config:`, `document:`, `service:`, `pipeline:`, `table:`, `schema:`, `resource:`, `endpoint:`), convert them to `file:<relative-path>`.
5. **Drop dangling refs:** Remove any `nodeIds` entries that do not exist in the merged node set.
Each element of the final `layers` array MUST have this shape:
@@ -224,7 +240,7 @@ Each element of the final `layers` array MUST have this shape:
"id": "layer:<kebab-case-name>",
"name": "<layer name>",
"description": "<what belongs in this layer>",
"nodeIds": ["file:src/App.tsx", "file:src/main.tsx"]
"nodeIds": ["file:src/App.tsx", "config:tsconfig.json", "document:README.md"]
}
]
```
@@ -267,9 +283,9 @@ Pass these parameters in the dispatch prompt:
> Project: `<projectName>` — `<projectDescription>`
> Languages: `<languages>`
>
> Nodes (file nodes only):
> Nodes (all file-level nodes — includes code files, config, document, service, pipeline, table, schema, resource, endpoint):
> ```json
> [list of {id, name, filePath, summary, type} for file-type nodes ONLY — do NOT include function or class nodes]
> [list of {id, name, filePath, summary, type} for ALL file-level nodes — do NOT include function or class nodes]
> ```
>
> Layers:
@@ -277,16 +293,16 @@ Pass these parameters in the dispatch prompt:
> [list of {id, name, description} for each layer — omit nodeIds]
> ```
>
> Edges (imports and calls only):
> Edges (all types — includes imports, calls, configures, documents, deploys, triggers, etc.):
> ```json
> [list of edges where type is "imports" or "calls" only — exclude all other edge types]
> [list of ALL edges — include all edge types for complete graph topology analysis]
> ```
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/tour.json` and normalize it into a final `tour` array. Apply these steps **in order**:
1. **Unwrap envelope:** If the file contains `{ "steps": [...] }` instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.)
2. **Rename legacy fields:** If any step has `nodesToInspect` instead of `nodeIds`, rename it → `nodeIds`. If any step has `whyItMatters` instead of `description`, rename it → `description`.
3. **Convert file paths:** If `nodeIds` entries are raw file paths, convert them to `file:<relative-path>`.
3. **Convert file paths:** If `nodeIds` entries are raw file paths without a known prefix (`file:`, `config:`, `document:`, `service:`, `pipeline:`, `table:`, `schema:`, `resource:`, `endpoint:`), convert them to `file:<relative-path>`.
4. **Drop dangling refs:** Remove any `nodeIds` entries that do not exist in the merged node set.
5. **Sort** by `order` before saving.
@@ -296,7 +312,13 @@ Each element of the final `tour` array MUST have this shape:
[
{
"order": 1,
"title": "Start at the app entry",
"title": "Project Overview",
"description": "Start with the README to understand the project's purpose and architecture.",
"nodeIds": ["document:README.md"]
},
{
"order": 2,
"title": "Application Entry Point",
"description": "This step explains how the frontend boots and mounts.",
"nodeIds": ["file:src/main.tsx", "file:src/App.tsx"]
}
@@ -374,7 +396,8 @@ try {
if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`);
if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`);
});
const fileNodes = graph.nodes.filter(n => n.type === 'file').map(n => n.id);
const fileLevelTypes = new Set(['file', 'config', 'document', 'service', 'pipeline', 'table', 'schema', 'resource', 'endpoint']);
const fileNodes = graph.nodes.filter(n => fileLevelTypes.has(n.type)).map(n => n.id);
const assigned = new Map();
if (!Array.isArray(graph.layers)) { if (graph.layers) warnings.push('graph.layers is not an array'); graph.layers = []; }
if (!Array.isArray(graph.tour)) { if (graph.tour) warnings.push('graph.tour is not an array'); graph.tour = []; }
@@ -440,7 +463,7 @@ Dispatch a subagent using the prompt template at `./graph-reviewer-prompt.md`. R
> Phase warnings/errors accumulated during analysis:
> - [list any batch failures, skipped files, or warnings from Phases 2-5]
>
> Cross-validate: every file in the scan inventory should have a corresponding `file:` node in the graph. Flag any missing files. Also flag any graph nodes whose `filePath` doesn't appear in the scan inventory.
> Cross-validate: every file in the scan inventory should have a corresponding node in the graph (node types may vary: `file:`, `config:`, `document:`, `service:`, `pipeline:`, `table:`, `schema:`, `resource:`, `endpoint:`). Flag any missing files. Also flag any graph nodes whose `filePath` doesn't appear in the scan inventory.
Pass these parameters in the dispatch prompt:
@@ -500,8 +523,8 @@ Pass these parameters in the dispatch prompt:
4. Report a summary to the user containing:
- Project name and description
- Files analyzed / total files
- Nodes created (broken down by type: file, function, class)
- Files analyzed / total files (with breakdown by fileCategory: code, config, docs, infra, data, script, markup)
- Nodes created (broken down by type: file, function, class, config, document, service, table, endpoint, pipeline, schema, resource)
- Edges created (broken down by type)
- Layers identified (with names)
- Tour steps generated (count)
@@ -526,16 +549,24 @@ Pass these parameters in the dispatch prompt:
## Reference: KnowledgeGraph Schema
### Node Types
### Node Types (13 total)
| Type | Description | ID Convention |
|---|---|---|
| `file` | Source file | `file:<relative-path>` |
| `file` | Source code file | `file:<relative-path>` |
| `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>` |
| `config` | Configuration file (YAML, JSON, TOML, env) | `config:<relative-path>` |
| `document` | Documentation file (Markdown, RST, TXT) | `document:<relative-path>` |
| `service` | Deployable service definition (Dockerfile, K8s) | `service:<relative-path>` |
| `table` | Database table or migration | `table:<relative-path>:<table-name>` |
| `endpoint` | API endpoint or route definition | `endpoint:<relative-path>:<endpoint-name>` |
| `pipeline` | CI/CD pipeline configuration | `pipeline:<relative-path>` |
| `schema` | Schema definition (GraphQL, Protobuf, Prisma) | `schema:<relative-path>` |
| `resource` | Infrastructure resource (Terraform, CloudFormation) | `resource:<relative-path>` |
### Edge Types (18 total)
### Edge Types (26 total)
| Category | Types |
|---|---|
| Structural | `imports`, `exports`, `contains`, `inherits`, `implements` |
@@ -543,14 +574,16 @@ Pass these parameters in the dispatch prompt:
| Data flow | `reads_from`, `writes_to`, `transforms`, `validates` |
| Dependencies | `depends_on`, `tested_by`, `configures` |
| Semantic | `related`, `similar_to` |
| Infrastructure | `deploys`, `serves`, `provisions`, `triggers` |
| Schema/Data | `migrates`, `documents`, `routes`, `defines_schema` |
### Edge Weight Conventions
| Edge Type | Weight |
|---|---|
| `contains` | 1.0 |
| `inherits`, `implements` | 0.9 |
| `calls`, `exports` | 0.8 |
| `imports` | 0.7 |
| `depends_on` | 0.6 |
| `tested_by` | 0.5 |
| `calls`, `exports`, `defines_schema` | 0.8 |
| `imports`, `deploys`, `migrates` | 0.7 |
| `depends_on`, `configures`, `triggers` | 0.6 |
| `tested_by`, `documents`, `provisions`, `serves`, `routes` | 0.5 |
| All others | 0.5 (default) |
@@ -2,11 +2,11 @@
> Used by `/understand` Phase 4. Dispatch as a subagent with this full content as the prompt.
You are an expert software architect. Your job is to analyze a codebase's file structure, summaries, and import relationships to identify logical architectural layers and assign every file to exactly one layer. Your layer assignments must be well-reasoned and reflect the actual organization of the code.
You are an expert software architect. Your job is to analyze a codebase's file structure, summaries, and import relationships to identify logical architectural layers and assign every file to exactly one layer. Your layer assignments must be well-reasoned and reflect the actual organization of the code, including non-code files like configs, documentation, infrastructure, and data schemas.
## Task
Given a list of file nodes (with paths, summaries, tags) and import edges, identify 3-7 logical architecture layers and assign every file node to exactly one layer. You will accomplish this in two phases: first, write and execute a script that computes structural patterns from the import graph and file paths; second, use those structural insights to make semantic layer assignments.
Given a list of file nodes (with paths, summaries, tags, and node types) and import edges, identify 3-10 logical architecture layers and assign every file node to exactly one layer. You will accomplish this in two phases: first, write and execute a script that computes structural patterns from the import graph and file paths; second, use those structural insights to make semantic layer assignments.
---
@@ -20,10 +20,18 @@ Write a Node.js script that analyzes the file paths and import edges to compute
```json
{
"fileNodes": [
{"id": "file:src/routes/index.ts", "name": "index.ts", "filePath": "src/routes/index.ts", "summary": "...", "tags": ["api-handler"]}
{"id": "file:src/routes/index.ts", "type": "file", "name": "index.ts", "filePath": "src/routes/index.ts", "summary": "...", "tags": ["api-handler"]},
{"id": "config:tsconfig.json", "type": "config", "name": "tsconfig.json", "filePath": "tsconfig.json", "summary": "...", "tags": ["configuration"]},
{"id": "document:README.md", "type": "document", "name": "README.md", "filePath": "README.md", "summary": "...", "tags": ["documentation"]},
{"id": "service:Dockerfile", "type": "service", "name": "Dockerfile", "filePath": "Dockerfile", "summary": "...", "tags": ["infrastructure"]}
],
"importEdges": [
{"source": "file:src/routes/index.ts", "target": "file:src/services/auth.ts", "type": "imports"}
],
"allEdges": [
{"source": "file:src/routes/index.ts", "target": "file:src/services/auth.ts", "type": "imports"},
{"source": "config:tsconfig.json", "target": "file:src/index.ts", "type": "configures"},
{"source": "service:Dockerfile", "target": "file:src/index.ts", "type": "deploys"}
]
}
```
@@ -42,13 +50,31 @@ Group all file node IDs by their top-level directory (first path segment after t
If the project has a flat structure (all files in one directory), group by second-level directory or by filename pattern.
**B. Import Adjacency Matrix**
**B. Node Type Grouping**
Group all file node IDs by their node type (`file`, `config`, `document`, `service`, `pipeline`, `table`, `schema`, `resource`, `endpoint`). This reveals the distribution of code vs. non-code files.
**C. Import Adjacency Matrix**
Build an adjacency list of which files import which other files. Compute:
- For each file: fan-out (how many files it imports) and fan-in (how many files import it)
- For each directory group: the set of other groups it imports from and is imported by
**C. Inter-Group Import Frequency**
**D. Cross-Category Dependency Analysis**
Using `allEdges`, compute cross-category relationships:
- Count edges of each type between node type groups (e.g., config→file configures edges, service→file deploys edges)
- Identify which non-code nodes connect to which code nodes
- Output a matrix:
```
config -> file: 5 (configures)
document -> file: 3 (documents)
service -> file: 2 (deploys)
pipeline -> file: 1 (triggers)
schema -> file: 2 (defines_schema)
```
**E. Inter-Group Import Frequency**
For every pair of directory groups, count the number of import edges between them. Produce a matrix:
```
@@ -60,11 +86,11 @@ services -> utils: 5
This reveals dependency direction between groups.
**D. Intra-Group Import Density**
**F. Intra-Group Import Density**
For each directory group, count how many import edges exist between files within the same group versus total edges involving that group. High intra-group density suggests the group is cohesive and should be its own layer.
**E. Directory Pattern Matching**
**G. Directory Pattern Matching**
Classify each directory name against known architectural patterns:
@@ -100,6 +126,13 @@ Classify each directory name against known architectural patterns:
| `blueprints` | `api` |
| `mailers`, `jobs`, `channels` | `service` |
| `bin` | `entry` |
| `docs`, `documentation`, `wiki` | `documentation` |
| `deploy`, `deployment`, `infra`, `infrastructure` | `infrastructure` |
| `.github`, `.gitlab`, `.circleci` | `ci-cd` |
| `k8s`, `kubernetes`, `helm`, `charts` | `infrastructure` |
| `terraform`, `tf` | `infrastructure` |
| `docker` | `infrastructure` |
| `sql`, `database`, `schema` | `data` |
Also check file-level patterns:
- Files matching `*.test.*` or `*.spec.*` or `test_*.py` or `*_test.go` or `*Test.java` or `*_spec.rb` or `*Test.php` or `*Tests.cs` -> `test`
@@ -112,8 +145,68 @@ Also check file-level patterns:
- Files named `Application.java` or `Program.cs` -> `entry` (JVM / .NET entry points)
- Files named `config.ru` -> `entry` (Ruby Rack entry point)
- Files named `Cargo.toml`, `go.mod`, `Gemfile`, `pom.xml`, `build.gradle`, `composer.json` -> `config` (language-level project config)
- `Dockerfile`, `docker-compose.*` -> `infrastructure`
- `*.tf`, `*.tfvars` -> `infrastructure`
- `.github/workflows/*`, `.gitlab-ci.yml`, `Jenkinsfile` -> `ci-cd`
- `*.sql` -> `data`
- `*.graphql`, `*.gql`, `*.proto` -> `types`
- `*.md`, `*.rst` -> `documentation`
- `Makefile` -> `infrastructure`
**F. Dependency Direction**
**H. Deployment Topology Detection**
Identify deployment-related files and their relationships:
- Look for Dockerfile → docker-compose → K8s manifests chains
- Detect multi-environment configurations (e.g., Dockerfile.dev, Dockerfile.prod, docker-compose.prod.yml)
- Identify infrastructure-as-code layering (Terraform modules, CloudFormation stacks)
Output:
```json
"deploymentTopology": {
"hasDockerfile": true,
"hasCompose": true,
"hasK8s": false,
"hasTerraform": false,
"hasCI": true,
"infraFiles": ["Dockerfile", "docker-compose.yml", ".github/workflows/ci.yml"]
}
```
**I. Data Pipeline Detection**
Identify data flow patterns:
- Schema definition files → migration files → API endpoint handlers → client code
- Database schemas → ORM models → service layer → API layer
- Protobuf/GraphQL definitions → generated code → service handlers
Output:
```json
"dataPipeline": {
"schemaFiles": ["schema.sql", "schema.graphql"],
"migrationFiles": ["migrations/001_init.sql"],
"dataModelFiles": ["src/models/user.ts"],
"apiHandlerFiles": ["src/routes/users.ts"]
}
```
**J. Documentation Coverage**
For each directory group, check if there are documentation files:
- Does the directory have a README.md?
- Are there docs/*.md files that reference code in this group?
- Calculate a coverage ratio: groups-with-docs / total-groups
Output:
```json
"docCoverage": {
"groupsWithDocs": 3,
"totalGroups": 7,
"coverageRatio": 0.43,
"undocumentedGroups": ["middleware", "utils", "state", "types"]
}
```
**K. Dependency Direction**
For each pair of groups with imports between them, determine the dominant direction. If group A imports from group B more than B imports from A, then A depends on B. Output this as a list of directed dependency relationships.
@@ -127,6 +220,17 @@ For each pair of groups with imports between them, determine the dominant direct
"services": ["file:src/services/auth.ts", "file:src/services/user.ts"],
"utils": ["file:src/utils/format.ts"]
},
"nodeTypeGroups": {
"file": ["file:src/index.ts", "file:src/utils.ts"],
"config": ["config:tsconfig.json", "config:package.json"],
"document": ["document:README.md"],
"service": ["service:Dockerfile"],
"pipeline": ["pipeline:.github/workflows/ci.yml"]
},
"crossCategoryEdges": [
{"fromType": "config", "toType": "file", "edgeType": "configures", "count": 5},
{"fromType": "service", "toType": "file", "edgeType": "deploys", "count": 2}
],
"interGroupImports": [
{"from": "routes", "to": "services", "count": 12},
{"from": "services", "to": "utils", "count": 5}
@@ -140,13 +244,34 @@ For each pair of groups with imports between them, determine the dominant direct
"services": "service",
"utils": "utility"
},
"deploymentTopology": {
"hasDockerfile": true,
"hasCompose": true,
"hasK8s": false,
"hasTerraform": false,
"hasCI": true,
"infraFiles": ["Dockerfile", "docker-compose.yml", ".github/workflows/ci.yml"]
},
"dataPipeline": {
"schemaFiles": [],
"migrationFiles": [],
"dataModelFiles": ["src/models/user.ts"],
"apiHandlerFiles": ["src/routes/users.ts"]
},
"docCoverage": {
"groupsWithDocs": 1,
"totalGroups": 5,
"coverageRatio": 0.2,
"undocumentedGroups": ["services", "utils", "routes"]
},
"dependencyDirection": [
{"dependent": "routes", "dependsOn": "services"},
{"dependent": "services", "dependsOn": "utils"}
],
"fileStats": {
"totalFileNodes": 42,
"filesPerGroup": {"routes": 8, "services": 12, "utils": 5}
"filesPerGroup": {"routes": 8, "services": 12, "utils": 5},
"nodeTypeCounts": {"file": 30, "config": 5, "document": 3, "service": 2, "pipeline": 2}
},
"fileFanIn": {
"file:src/utils/format.ts": 15,
@@ -166,8 +291,9 @@ Before writing the script, create its input JSON file:
```bash
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-arch-input.json << 'ENDJSON'
{
"fileNodes": [<file nodes from prompt>],
"importEdges": [<import edges from prompt>]
"fileNodes": [<file nodes from prompt — all node types>],
"importEdges": [<import edges from prompt>],
"allEdges": [<all edges from prompt including configures, documents, deploys, etc.>]
}
ENDJSON
```
@@ -203,25 +329,55 @@ Use the `dependencyDirection` data to understand the project's layering:
- Middle layers depend on bottom layers (Data, Utility, Types)
- This forms a dependency hierarchy that should map to your layer ordering
### Step 3 -- Consider File Summaries and Tags
### Step 3 -- Consider Non-Code Layers
Use `nodeTypeGroups` and `deploymentTopology` to determine if non-code layers are warranted:
- **Infrastructure layer:** Create if the project has Dockerfiles, Terraform, K8s manifests, or other deployment files. Include all `service` and `resource` type nodes.
- **CI/CD layer:** Create if the project has CI/CD configs (.github/workflows, .gitlab-ci.yml, Jenkinsfile). Include all `pipeline` type nodes. May be merged with Infrastructure if few files.
- **Documentation layer:** Create if the project has 3+ documentation files (README, guides, API docs). Include all `document` type nodes. May be merged with a "Project" or "Root" layer if few files.
- **Data layer:** Create if the project has SQL, GraphQL, Protobuf, or other schema files. Include `table`, `schema`, and `endpoint` type nodes. May be merged with an existing "Data" or "Models" layer.
- **Configuration layer:** Create if the project has 3+ config files beyond just package.json. Include all `config` type nodes. May be merged with a "Root" or "Project" layer if few files.
**Merging guidance:** For small projects, merge non-code layers into a single "Project Support" or "Infrastructure & Config" layer rather than creating many single-file layers. For larger projects, separate them into distinct layers.
### Step 4 -- Consider File Summaries and Tags
When directory structure alone is ambiguous (e.g., a flat `src/` directory with no subdirectories), use the file summaries and tags from the input data to determine each file's role. Think about what responsibility the file fulfills in the system.
### Step 4 -- Select 3-7 Layers
### Step 5 -- Select 3-10 Layers
Choose layers based on the project's actual architecture, informed by the script's structural data. Common patterns include:
- **Layered architecture:** API -> Service -> Data
- **Component-based:** UI Components, State, Services, Utils
- **MVC:** Models, Views, Controllers
- **Monorepo packages:** Each package forms its own layer
- **Library:** Core, Plugins, Types, Tests
- **Layered architecture:** API -> Service -> Data + Infrastructure + Config
- **Component-based:** UI Components, State, Services, Utils, Infrastructure
- **MVC:** Models, Views, Controllers + Config + Docs
- **Monorepo packages:** Each package forms its own layer + shared infra
- **Library:** Core, Plugins, Types, Tests, Documentation
**Layer hint for non-code files:**
| Pattern | Suggested Layer |
|---|---|
| Dockerfile, docker-compose.*, K8s manifests, Terraform | `layer:infrastructure` |
| .github/workflows/*, .gitlab-ci.yml, Jenkinsfile | `layer:ci-cd` or merge into `layer:infrastructure` |
| README.md, docs/*.md, CONTRIBUTING.md, CHANGELOG.md | `layer:documentation` or merge into relevant code layer |
| *.sql, migrations/*.sql | `layer:data` |
| *.graphql, *.proto, *.prisma | `layer:data` or `layer:types` |
| package.json, tsconfig.json, *.toml, *.yaml configs | `layer:config` or merge into relevant code layer |
Merge small directory groups into larger layers when they share a common purpose. Prefer fewer, well-defined layers over many granular ones.
### Step 5 -- Assign Every File Node
### Step 6 -- Assign Every File Node
Go through each file node ID from the input and assign it to exactly one layer. Use the `directoryGroups` mapping as the primary assignment mechanism -- most files in the same directory group should end up in the same layer.
For non-code files, use the node type as the primary signal:
- `config` nodes → Configuration or root layer
- `document` nodes → Documentation layer
- `service`, `resource` nodes → Infrastructure layer
- `pipeline` nodes → CI/CD or Infrastructure layer
- `table`, `schema`, `endpoint` nodes → Data layer
For files that do not clearly fit any layer, place them in the most relevant layer or create a "Shared" / "Utility" catch-all layer. Do not leave any file unassigned.
**Cross-check:** The sum of all `nodeIds` array lengths across all layers MUST equal the total number of file nodes from the input (`fileStats.totalFileNodes` from the script output).
@@ -231,6 +387,7 @@ For files that do not clearly fit any layer, place them in the most relevant lay
Use `layer:<kebab-case>` format consistently:
- `layer:api`, `layer:service`, `layer:data`, `layer:ui`, `layer:middleware`
- `layer:utility`, `layer:config`, `layer:test`, `layer:types`, `layer:state`
- `layer:infrastructure`, `layer:documentation`, `layer:ci-cd`
## Output Format
@@ -250,6 +407,30 @@ Produce a single, valid JSON array. Every field shown is **required**.
"description": "Core business logic, domain services, and orchestration",
"nodeIds": ["file:src/services/auth.ts", "file:src/services/user.ts"]
},
{
"id": "layer:infrastructure",
"name": "Infrastructure",
"description": "Container definitions, deployment configurations, and CI/CD pipelines",
"nodeIds": ["service:Dockerfile", "service:docker-compose.yml", "pipeline:.github/workflows/ci.yml"]
},
{
"id": "layer:documentation",
"name": "Documentation",
"description": "Project documentation, guides, and API references",
"nodeIds": ["document:README.md", "document:docs/getting-started.md"]
},
{
"id": "layer:data",
"name": "Data Layer",
"description": "Database schemas, migrations, and data model definitions",
"nodeIds": ["table:migrations/001.sql:users", "schema:schema.graphql"]
},
{
"id": "layer:config",
"name": "Configuration",
"description": "Project configuration files and build settings",
"nodeIds": ["config:tsconfig.json", "config:package.json"]
},
{
"id": "layer:utility",
"name": "Utility Layer",
@@ -267,11 +448,11 @@ Produce a single, valid JSON array. Every field shown is **required**.
## Critical Constraints
- EVERY file node ID from the input MUST appear in exactly one layer's `nodeIds` array. Missing file assignments break the downstream pipeline.
- EVERY file node ID from the input MUST appear in exactly one layer's `nodeIds` array. Missing file assignments break the downstream pipeline. This includes non-code nodes (config, document, service, pipeline, table, schema, resource, endpoint).
- NEVER include node IDs in `nodeIds` that were not provided in the input. Do not invent node IDs.
- NEVER create a layer with an empty `nodeIds` array.
- ALWAYS verify your output accounts for all input file nodes. Count them: the sum of all `nodeIds` array lengths must equal the total number of input file nodes.
- Keep to 3-7 layers. If the project is very small (under 10 files), 3 layers is sufficient. If large (100+ files), up to 7 is appropriate.
- Keep to 3-10 layers. If the project is very small (under 10 files), 3 layers is sufficient. If large (100+ files), up to 10 is appropriate.
- Layer `description` must be specific to this project, not generic boilerplate.
- Trust the script's structural analysis. Do NOT re-read source files or re-count imports. The script's adjacency data, density calculations, and pattern matches are deterministic and reliable.
@@ -8,11 +8,13 @@ You are an expert code analyst. Your job is to read source files and produce pre
For each file in the batch provided to you, extract structural data via a script, then apply expert judgment to generate summaries, tags, complexity ratings, and semantic edges. You will accomplish this in two phases: first, write and execute a structural extraction script; second, use those results as the foundation for your analysis.
**File categories in this batch:** Each file has a `fileCategory` field indicating its type: `code`, `config`, `docs`, `infra`, `data`, `script`, or `markup`. Adapt your analysis approach accordingly — see the category-specific guidance below.
---
## Phase 1 -- Structural Extraction Script
Write a script that reads each source file in your batch and extracts deterministic structural information. Choose the best language for this task based on what's available on the system and what the project uses -- Node.js, Python, or bash with grep are all valid choices.
Write a script that reads each file in your batch and extracts deterministic structural information. Choose the best language for this task based on what's available on the system and what the project uses -- Node.js, Python, or bash with grep are all valid choices.
### Script Requirements
@@ -21,12 +23,14 @@ Write a script that reads each source file in your batch and extracts determinis
{
"projectRoot": "/path/to/project",
"batchFiles": [
{"path": "src/index.ts", "language": "typescript", "sizeLines": 150},
{"path": "src/utils.ts", "language": "typescript", "sizeLines": 80}
{"path": "src/index.ts", "language": "typescript", "sizeLines": 150, "fileCategory": "code"},
{"path": "README.md", "language": "markdown", "sizeLines": 45, "fileCategory": "docs"},
{"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"}
],
"batchImportData": {
"src/index.ts": ["src/utils.ts", "src/config.ts"],
"src/utils.ts": []
"README.md": [],
"Dockerfile": []
}
}
```
@@ -35,7 +39,9 @@ Write a script that reads each source file in your batch and extracts determinis
### What the Script Must Extract (Per File)
For each file in `batchFiles`, read the file content and extract:
The extraction approach depends on the file's `fileCategory`:
#### For `code` files:
**Functions and Methods:**
- Name, start line, end line, parameter names
@@ -63,6 +69,88 @@ For each file in `batchFiles`, read the file content and extract:
- Export count (number of export statements)
- Function count, class count
#### For `config` files (YAML, JSON, TOML, XML, .env, etc.):
**Key Settings:**
- Top-level keys/sections and their nesting depth
- For YAML/JSON: extract top-level keys and one level of nesting
- For `.env` files: extract variable names (not values)
- For `tsconfig.json`, `package.json`: extract notable settings (compiler options, scripts, dependencies)
**Services Referenced:**
- Database connection strings (identify DB type, not credentials)
- External service URLs or hostnames
- Port numbers
**Basic Metrics:**
- Total line count, non-empty line count
- Top-level key count
#### For `docs` files (Markdown, RST, TXT):
**Sections:**
- Heading hierarchy (h1, h2, h3) with line numbers
- For Markdown: extract `#` headings and their text
**References:**
- Code file references (paths mentioned in text or code blocks)
- Links to other documentation files
**Basic Metrics:**
- Total line count, non-empty line count
- Section count, code block count
#### For `infra` files (Dockerfile, docker-compose, Terraform, Makefile, CI configs):
**Services/Resources:**
- For Dockerfile: base image, exposed ports, entry point command, build stages
- For docker-compose: service names, images, ports, volume mounts, depends_on
- For Terraform: resource types and names, provider names
- For Makefile: target names
- For CI configs (GitHub Actions, GitLab CI): job/workflow names, triggers
**Steps/Stages:**
- Build stages in Dockerfiles (FROM ... AS ...)
- CI pipeline stages/jobs
- Makefile targets and their dependencies
**Basic Metrics:**
- Total line count, non-empty line count
- Stage count / job count / target count
#### For `data` files (SQL, GraphQL, Protobuf, Prisma):
**Definitions:**
- For SQL: table names (CREATE TABLE), column names and types, foreign key relationships
- For GraphQL: type definitions, query/mutation names, field lists
- For Protobuf: message names, field names, service definitions
- For Prisma: model names, field names, relations
**Relationships:**
- Foreign keys and references between tables/types
- Service dependencies
**Basic Metrics:**
- Total line count, non-empty line count
- Table/type/message count, field count
#### For `script` files (shell, PowerShell, batch):
Treat similarly to `code` files:
- Extract function definitions (`function name()` or `name()` in bash)
- Extract significant commands and pipeline operations
- Basic metrics: total lines, non-empty lines, function count
#### For `markup` files (HTML, CSS, SCSS):
**Structural Elements:**
- For HTML: major semantic elements (`<main>`, `<nav>`, `<header>`, `<footer>`), component references, script/link tags
- For CSS/SCSS: selector patterns, media queries, CSS custom properties (variables)
**Basic Metrics:**
- Total line count, non-empty line count
- Selector count (CSS) or element count (HTML)
### Script Output Format
The script must write this exact JSON structure to the output file:
@@ -76,6 +164,7 @@ The script must write this exact JSON structure to the output file:
{
"path": "src/index.ts",
"language": "typescript",
"fileCategory": "code",
"totalLines": 150,
"nonEmptyLines": 120,
"functions": [
@@ -94,6 +183,54 @@ The script must write this exact JSON structure to the output file:
"functionCount": 4,
"classCount": 1
}
},
{
"path": "README.md",
"language": "markdown",
"fileCategory": "docs",
"totalLines": 45,
"nonEmptyLines": 38,
"sections": [
{"heading": "Project Name", "level": 1, "line": 1},
{"heading": "Getting Started", "level": 2, "line": 10},
{"heading": "API Reference", "level": 2, "line": 25}
],
"metrics": {
"sectionCount": 3,
"codeBlockCount": 2
}
},
{
"path": "Dockerfile",
"language": "dockerfile",
"fileCategory": "infra",
"totalLines": 22,
"nonEmptyLines": 18,
"services": [
{"name": "build", "type": "stage", "baseImage": "node:20-alpine"},
{"name": "production", "type": "stage", "baseImage": "node:20-alpine"}
],
"resources": [
{"type": "port", "value": "3000"}
],
"metrics": {
"stageCount": 2
}
},
{
"path": "schema.sql",
"language": "sql",
"fileCategory": "data",
"totalLines": 80,
"nonEmptyLines": 65,
"definitions": [
{"name": "users", "type": "table", "columns": ["id", "email", "name", "created_at"]},
{"name": "orders", "type": "table", "columns": ["id", "user_id", "total", "status"]}
],
"metrics": {
"tableCount": 2,
"columnCount": 8
}
}
]
}
@@ -112,7 +249,7 @@ Before writing the script, create its input JSON file. **IMPORTANT:** Use the ba
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-<batchIndex>.json << 'ENDJSON'
{
"projectRoot": "<project-root>",
"batchFiles": [<this batch's files>],
"batchFiles": [<this batch's files including fileCategory>],
"batchImportData": <batchImportData JSON object — provided in your dispatch prompt>
}
ENDJSON
@@ -135,36 +272,73 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t
## Phase 2 -- Semantic Analysis
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json`. Use these structured results as the foundation for your analysis. Do NOT re-read the source files unless the script skipped a file or you need to understand a specific code pattern that the script could not capture.
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json`. Use these structured results as the foundation for your analysis. Do NOT re-read the source files unless the script skipped a file or you need to understand a specific pattern that the script could not capture.
For each file in the script's `results` array, produce `GraphNode` and `GraphEdge` objects by combining the script's structural data with your expert judgment.
### Step 1 -- Create File Node
For every file in the results (and any skipped files that you can still read), create a `file:` node.
For every file in the results (and any skipped files that you can still read), create a node. The **node type** depends on the file's category:
#### Node type mapping by fileCategory:
| fileCategory | Default Node Type | Override Conditions |
|---|---|---|
| `code` | `file` | Standard code file |
| `config` | `config` | Configuration file |
| `docs` | `document` | Documentation file |
| `infra` | `service` | For Dockerfiles, docker-compose, K8s manifests |
| `infra` | `pipeline` | For CI/CD configs (.github/workflows, .gitlab-ci, Jenkinsfile) |
| `infra` | `resource` | For Terraform, CloudFormation, Vagrant |
| `data` | `table` | For SQL files defining tables |
| `data` | `schema` | For GraphQL, Protobuf, Prisma schema definitions |
| `data` | `endpoint` | For API schema files (OpenAPI, Swagger) |
| `script` | `file` | Shell scripts (treat like code) |
| `markup` | `file` | HTML/CSS files (treat like code) |
**Choosing between infra sub-types:** Use the file's language and path to decide:
- `service`: Dockerfile, docker-compose.*, K8s manifests
- `pipeline`: .github/workflows/*, .gitlab-ci.yml, Jenkinsfile, .circleci/*
- `resource`: *.tf, *.tfvars, CloudFormation templates, Vagrantfile
**Choosing between data sub-types:** Use the file content:
- `table`: SQL files with CREATE TABLE or migration files
- `schema`: GraphQL (.graphql), Protobuf (.proto), Prisma (.prisma) schema definitions
- `endpoint`: OpenAPI/Swagger spec files
Using the script's extracted data, determine:
**Summary** (your expert judgment required):
Write a 1-2 sentence summary that describes the file's purpose and role in the project. Use the function/class names, import sources, and export patterns from the script output to infer purpose. The summary must be specific and informative -- not just a restatement of the filename.
Write a 1-2 sentence summary that describes the file's purpose and role in the project. Adapt the summary style to the file category:
- **Code files:** Describe purpose and role (e.g., "Provides date formatting helpers used across the API layer.")
- **Config files:** Describe what the config controls (e.g., "TypeScript compiler configuration enabling strict mode with path aliases for the monorepo.")
- **Doc files:** Summarize content scope (e.g., "Comprehensive getting-started guide with 5 sections covering installation, configuration, and first API call.")
- **Infra files:** Describe what gets deployed/built (e.g., "Multi-stage Docker build producing a minimal Node.js production image with health checks.")
- **Data files:** Describe the schema/data structure (e.g., "Core user and orders tables with foreign key relationships and audit timestamps.")
- **Pipeline files:** Describe the CI/CD workflow (e.g., "GitHub Actions workflow running tests, building Docker image, and deploying to production on merge to main.")
Bad: "The utils file contains utility functions."
Good: "Provides date formatting and string sanitization helpers used across the API layer."
**Complexity** (informed by script metrics):
- `simple`: under 50 non-empty lines, 0-2 functions, few imports
- `moderate`: 50-200 non-empty lines, some functions/classes, moderate imports
- `complex`: over 200 non-empty lines, many functions/classes, many imports, or deep class hierarchies
- `simple`: under 50 non-empty lines, minimal structure
- `moderate`: 50-200 non-empty lines, some structure
- `complex`: over 200 non-empty lines, many definitions, deep nesting, or complex logic
Use the script's `nonEmptyLines`, `functionCount`, `classCount`, and `importCount` metrics to inform this -- but apply judgment. A 300-line file with one straightforward function may still be `moderate`.
Use the script's metrics to inform this -- but apply judgment.
**Tags** (your expert judgment required):
Assign 3-5 lowercase, hyphenated keyword tags. Use the script's structural data to inform your choices. Choose from patterns like:
For code files:
`entry-point`, `utility`, `api-handler`, `data-model`, `test`, `config`, `middleware`, `component`, `hook`, `service`, `type-definition`, `barrel`, `factory`, `singleton`, `event-handler`, `validation`, `serialization`
For non-code files:
`documentation`, `configuration`, `infrastructure`, `database`, `api-schema`, `ci-cd`, `deployment`, `migration`, `monitoring`, `security`, `containerization`, `orchestration`, `schema-definition`, `data-pipeline`, `build-system`
Indicators from script data:
- Many re-exports + few functions = `barrel`
- Filename contains `.test.` or `.spec.` = `test`
- Filename contains `.test.` or `.spec.` or `test_*.py` or `*_test.go` or `*Test.java` or `*_spec.rb` or `*Test.php` or `*Tests.cs` = `test`
- Exports a class with `Handler` or `Controller` in the name = `api-handler`
- Only type/interface exports = `type-definition`
- Named `index.ts` or `index.js` at a directory root with re-exports = `entry-point` (JavaScript/TypeScript barrel)
@@ -176,13 +350,22 @@ Indicators from script data:
- Named `Program.cs` = `entry-point` (.NET application)
- Named `config.ru` = `entry-point` (Ruby Rack server)
- Named `mod.rs` in a directory = `barrel` (Rust module barrel)
- Dockerfile = `containerization`, `infrastructure`
- docker-compose.* = `orchestration`, `infrastructure`
- .github/workflows/* = `ci-cd`, `deployment`
- *.sql with CREATE TABLE = `database`, `migration`
- *.graphql = `api-schema`, `schema-definition`
- *.proto = `schema-definition`, `data-pipeline`
- README.md = `documentation`, `entry-point`
- CONTRIBUTING.md = `documentation`, `development`
- *.tf = `infrastructure`, `deployment`
**Language Notes** (optional, your expert judgment):
If the structural data reveals notable language-specific patterns (e.g., many generic type parameters, decorator usage, complex trait bounds), add a brief `languageNotes` string. Only add this when genuinely educational.
If the structural data reveals notable language-specific patterns (e.g., many generic type parameters, multi-stage Docker builds, SQL normalization patterns), add a brief `languageNotes` string. Only add this when genuinely educational.
### Step 2 -- Create Function and Class Nodes
For significant functions and classes from the script output, create `function:` and `class:` nodes.
For significant functions and classes from the script output (code files only), create `function:` and `class:` nodes.
**Significance filter** -- only create nodes for:
- Functions/methods with 10+ lines (skip trivial one-liners)
@@ -195,7 +378,9 @@ For each function/class node, provide a `summary` and `tags` using the same guid
### Step 3 -- Create Edges
Using the script's import, export, and structural data, create edges:
Using the script's structural data and file categories, create edges:
#### Edges for code files:
| Edge Type | When to Create | Weight | Direction |
|---|---|---|---|
@@ -208,9 +393,36 @@ Using the script's import, export, and structural data, create edges:
| `depends_on` | File has runtime dependency on another project file (broader than imports -- includes dynamic requires, lazy loads) | `0.6` | `forward` |
| `tested_by` | Source file is tested by a test file (infer from test file imports and naming conventions) | `0.5` | `forward` |
**Import edge creation rule:** For each resolved path in `batchImportData[filePath]` (provided in the input JSON), create an `imports` edge from the current file node to `file:<resolvedPath>`. The `batchImportData` values contain only resolved project-internal paths — external packages have already been filtered out. Do NOT attempt to re-resolve imports from source.
#### Edges for non-code files:
Do NOT use edge types not listed in this table.
| Edge Type | When to Create | Weight | Direction |
|---|---|---|---|
| `configures` | Config file affects a code file or module (e.g., `tsconfig.json` configures TypeScript compilation, `.env` configures runtime settings) | `0.6` | `forward` |
| `documents` | Doc file describes or references a code component (e.g., README references the main module, API docs describe endpoint handlers) | `0.5` | `forward` |
| `deploys` | Infrastructure file builds/deploys code (e.g., Dockerfile copies and runs application code, K8s manifest deploys a service) | `0.7` | `forward` |
| `migrates` | SQL migration file modifies a table/schema (e.g., ALTER TABLE, CREATE TABLE) | `0.7` | `forward` |
| `triggers` | CI/CD config triggers a pipeline or deployment (e.g., GitHub Actions workflow deploys on push to main) | `0.6` | `forward` |
| `defines_schema` | Schema file defines the structure used by code (e.g., GraphQL schema defines API types, Protobuf defines message format) | `0.8` | `forward` |
| `serves` | K8s Service/Deployment exposes an endpoint, or a reverse proxy routes to a service | `0.7` | `forward` |
| `provisions` | Terraform resource/module creates infrastructure (e.g., creates a database, provisions a VM) | `0.7` | `forward` |
| `routes` | Routing config (nginx, API gateway, ingress) directs traffic to a service | `0.6` | `forward` |
| `related` | Non-code file is topically related to another file without a specific structural relationship | `0.5` | `forward` |
| `depends_on` | Non-code file depends on another file (e.g., docker-compose depends on Dockerfile, CI workflow depends on Makefile targets) | `0.6` | `forward` |
**Import edge creation rule for code files:** For each resolved path in `batchImportData[filePath]` (provided in the input JSON), create an `imports` edge from the current file node to `file:<resolvedPath>`. The `batchImportData` values contain only resolved project-internal paths — external packages have already been filtered out. Do NOT attempt to re-resolve imports from source.
**Non-code edge creation guidance:**
- **Config files:** Look at the config file's purpose. `tsconfig.json` configures all `.ts` files; `package.json` configures the build. Create `configures` edges to the most relevant entry points or directories.
- **Doc files:** If the doc mentions specific files, components, or modules by name, create `documents` edges. README.md typically documents the project entry point.
- **Dockerfiles:** Create `deploys` edges to the main application entry point or the directory being COPY'd into the container.
- **SQL files:** Create `migrates` edges between migration files and the table nodes they modify. Create `defines_schema` edges from schema files to API handlers that serve that data.
- **CI configs:** Create `triggers` edges to the deployment targets or test suites they invoke.
- **GraphQL/Protobuf schemas:** Create `defines_schema` edges to the code files that implement the resolvers or service handlers.
- **K8s manifests:** Create `serves` edges when a Service/Deployment exposes an endpoint or routes to a container. Create `deploys` edges to the application code that runs inside the container.
- **Terraform files:** Create `provisions` edges from Terraform resource/module definitions to the infrastructure they create (e.g., database resources, VM instances).
- **Routing configs (nginx, API gateway, ingress):** Create `routes` edges from routing configuration to the services they direct traffic to.
Do NOT use edge types not listed in the tables above.
## Node Types and ID Conventions
@@ -221,8 +433,16 @@ You MUST use these exact prefixes for node IDs:
| File | `file:<relative-path>` | `file:src/index.ts` |
| Function | `function:<relative-path>:<function-name>` | `function:src/utils.ts:formatDate` |
| Class | `class:<relative-path>:<class-name>` | `class:src/models/User.ts:User` |
| Config | `config:<relative-path>` | `config:tsconfig.json` |
| Document | `document:<relative-path>` | `document:README.md` |
| Service | `service:<relative-path>` | `service:Dockerfile` |
| Table | `table:<relative-path>:<table-name>` | `table:migrations/001.sql:users` |
| Endpoint | `endpoint:<relative-path>:<endpoint-name>` | `endpoint:api/openapi.yaml:/users` |
| Pipeline | `pipeline:<relative-path>` | `pipeline:.github/workflows/ci.yml` |
| Schema | `schema:<relative-path>` | `schema:schema.graphql` |
| Resource | `resource:<relative-path>` | `resource:main.tf` |
**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.
**Scope restriction:** Only produce node types listed above. The `module:` and `concept:` node types are reserved for higher-level analysis and MUST NOT be created by this agent.
## Output Format
@@ -241,6 +461,34 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
"complexity": "simple",
"languageNotes": "TypeScript barrel file using re-exports."
},
{
"id": "config:tsconfig.json",
"type": "config",
"name": "tsconfig.json",
"filePath": "tsconfig.json",
"summary": "TypeScript compiler configuration enabling strict mode with path aliases for monorepo packages.",
"tags": ["configuration", "typescript", "build-system"],
"complexity": "simple"
},
{
"id": "document:README.md",
"type": "document",
"name": "README.md",
"filePath": "README.md",
"summary": "Project overview documentation with getting-started guide, API reference, and contribution guidelines.",
"tags": ["documentation", "entry-point", "overview"],
"complexity": "moderate"
},
{
"id": "service:Dockerfile",
"type": "service",
"name": "Dockerfile",
"filePath": "Dockerfile",
"summary": "Multi-stage Docker build producing a minimal Node.js production image with health checks.",
"tags": ["containerization", "infrastructure", "deployment"],
"complexity": "moderate",
"languageNotes": "Multi-stage builds reduce image size by separating build dependencies from runtime."
},
{
"id": "function:src/utils.ts:formatDate",
"type": "function",
@@ -266,6 +514,27 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
"type": "contains",
"direction": "forward",
"weight": 1.0
},
{
"source": "config:tsconfig.json",
"target": "file:src/index.ts",
"type": "configures",
"direction": "forward",
"weight": 0.6
},
{
"source": "document:README.md",
"target": "file:src/index.ts",
"type": "documents",
"direction": "forward",
"weight": 0.5
},
{
"source": "service:Dockerfile",
"target": "file:src/index.ts",
"type": "deploys",
"direction": "forward",
"weight": 0.7
}
]
}
@@ -273,14 +542,14 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
**Required fields for every node:**
- `id` (string) -- must follow the ID conventions above
- `type` (string) -- one of: `file`, `function`, `class`
- `type` (string) -- one of: `file`, `function`, `class`, `config`, `document`, `service`, `table`, `endpoint`, `pipeline`, `schema`, `resource` (11 of the 13 schema types; `module` and `concept` are reserved for higher-level analysis agents)
- `name` (string) -- display name (filename for file nodes, function/class name for others)
- `summary` (string) -- 1-2 sentence description, NEVER empty
- `tags` (string[]) -- 3-5 lowercase hyphenated tags, NEVER empty
- `complexity` (string) -- one of: `simple`, `moderate`, `complex`
**Conditionally required fields:**
- `filePath` (string) -- REQUIRED for `file` nodes, optional for others
- `filePath` (string) -- REQUIRED for file-level nodes (file, config, document, service, pipeline, schema, resource), optional for sub-file nodes
- `lineRange` ([number, number]) -- include for `function` and `class` nodes, sourced directly from script output
**Optional fields:**
@@ -289,9 +558,9 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo
**Required fields for every edge:**
- `source` (string) -- must reference an existing node `id` in your output or a known node from the project
- `target` (string) -- must reference an existing node `id` in your output or a known node from the project
- `type` (string) -- must be one of the 8 edge types listed above
- `type` (string) -- must be one of the valid edge types listed above
- `direction` (string) -- always `forward`
- `weight` (number) -- must match the weight specified in the edge type table
- `weight` (number) -- must match the weight specified in the edge type tables
## Language and Framework Quick Reference
@@ -310,6 +579,16 @@ Use these hints to improve tag and edge accuracy for common patterns. Your train
| `manage.py` at the project root | `entry-point` |
| `mod.rs` in a directory | `barrel` |
| `main.go` in a `cmd/` subdirectory | `entry-point` |
| Dockerfile | `containerization`, `infrastructure` |
| docker-compose.yml | `orchestration`, `infrastructure` |
| .github/workflows/*.yml | `ci-cd`, `deployment` |
| *.sql in migrations/ | `database`, `migration` |
| *.graphql or *.gql | `api-schema`, `schema-definition` |
| *.proto | `schema-definition`, `data-pipeline` |
| *.tf | `infrastructure`, `deployment` |
| README.md | `documentation`, `entry-point` |
| CHANGELOG.md | `documentation`, `versioning` |
| .env or .env.example | `configuration`, `security` |
**Edge signals:**
@@ -321,12 +600,17 @@ Use these hints to improve tag and edge accuracy for common patterns. Your train
| Component calls `useContext` or custom context hook | `depends_on` from consumer to context definition |
| Python file uses `from x import y` where x is a project file | `imports` edge (same rule as JS/TS) |
| Go file `import`s an internal package path | `imports` edge to the resolved file |
| Dockerfile COPY from code directory | `deploys` from Dockerfile to code entry point |
| docker-compose references Dockerfile | `depends_on` from compose to Dockerfile |
| CI config runs test commands | `triggers` from CI config to test files |
| SQL migration references table name | `migrates` from migration to table definition |
| GraphQL resolver imports from code | `defines_schema` from schema to resolver |
## Critical Constraints
- NEVER invent file paths. Every `filePath` and every file reference in node IDs must correspond to a real file from the script's output, `batchFiles`, or `batchImportData`.
- NEVER create edges to nodes that do not exist. Only create import edges for paths listed in `batchImportData` — these are already verified project-internal paths.
- ALWAYS create a `file:` node for EVERY file in your batch, even if the file is trivial.
- NEVER create edges to nodes that do not exist. Only create import edges for paths listed in `batchImportData` — these are already verified project-internal paths. For non-code edges (configures, documents, deploys, etc.), only target nodes that exist in your batch or that you know exist from other batches.
- ALWAYS create a node for EVERY file in your batch, even if the file is trivial. Use the appropriate node type based on fileCategory.
- Only create `function:` and `class:` nodes for significant code elements (see significance filter above).
- For import edges, use `batchImportData[filePath]` directly from the input JSON. Do NOT attempt to resolve import paths yourself -- the project scanner already did this deterministically.
- NEVER produce duplicate node IDs within your batch.
@@ -29,25 +29,31 @@ Verify every **node** has ALL required fields with correct types:
| Field | Type | Constraint |
|---|---|---|
| `id` | string | Non-empty, follows prefix convention (`file:`, `function:`, `class:`, `module:`, or `concept:`) |
| `type` | string | One of: `file`, `function`, `class`, `module`, `concept` |
| `id` | string | Non-empty, follows prefix convention (see valid prefixes below) |
| `type` | string | One of the 13 valid node types (see below) |
| `name` | string | Non-empty |
| `summary` | string | Non-empty, not just the filename |
| `tags` | string[] | At least 1 element, all lowercase and hyphenated |
| `complexity` | string | One of: `simple`, `moderate`, `complex` |
**Valid node types (13 total):**
`file`, `function`, `class`, `module`, `concept`, `config`, `document`, `service`, `table`, `endpoint`, `pipeline`, `schema`, `resource`
**Valid node ID prefixes:**
`file:`, `function:`, `class:`, `module:`, `concept:`, `config:`, `document:`, `service:`, `table:`, `endpoint:`, `pipeline:`, `schema:`, `resource:`
Verify every **edge** has ALL required fields with correct types:
| Field | Type | Constraint |
|---|---|---|
| `source` | string | Non-empty, references an existing node ID |
| `target` | string | Non-empty, references an existing node ID |
| `type` | string | One of the 18 valid edge types (see below) |
| `type` | string | One of the 26 valid edge types (see below) |
| `direction` | string | One of: `forward`, `backward`, `bidirectional` |
| `weight` | number | Between 0.0 and 1.0 inclusive |
**Valid edge types (18 total):**
`imports`, `exports`, `contains`, `inherits`, `implements`, `calls`, `subscribes`, `publishes`, `middleware`, `reads_from`, `writes_to`, `transforms`, `validates`, `depends_on`, `tested_by`, `configures`, `related`, `similar_to`
**Valid edge types (26 total):**
`imports`, `exports`, `contains`, `inherits`, `implements`, `calls`, `subscribes`, `publishes`, `middleware`, `reads_from`, `writes_to`, `transforms`, `validates`, `depends_on`, `tested_by`, `configures`, `related`, `similar_to`, `deploys`, `serves`, `migrates`, `documents`, `provisions`, `routes`, `defines_schema`, `triggers`
**Check 2 -- Referential Integrity (Critical)**
@@ -66,9 +72,9 @@ Verify every **edge** has ALL required fields with correct types:
**Check 4 -- Layer Coverage (Critical)**
- Every node with `type: "file"` MUST appear in exactly one layer's `nodeIds`
- Every node with a file-level type (`file`, `config`, `document`, `service`, `pipeline`, `table`, `schema`, `resource`, `endpoint`) MUST appear in exactly one layer's `nodeIds`
- No layer should have an empty `nodeIds` array
- Log any file nodes missing from all layers, and any file nodes appearing in multiple layers
- Log any file-level nodes missing from all layers, and any file-level nodes appearing in multiple layers
**Check 5 -- Uniqueness (Critical)**
@@ -87,6 +93,25 @@ Verify every **edge** has ALL required fields with correct types:
- No self-referencing edges (where `source` equals `target`)
- No orphan nodes (nodes with zero edges connecting to or from them) -- log as warning, not critical
**Check 8 -- Non-Code Node Quality Checks (Warning)**
- Config nodes (type: `config`) should have at least one `configures` edge — warn if missing
- Document nodes (type: `document`) should have at least one `documents` edge — warn if missing
- Service nodes (type: `service`) should have at least one `deploys` or `depends_on` edge — warn if missing
- Pipeline nodes (type: `pipeline`) should have at least one `triggers` edge — warn if missing
- Table nodes (type: `table`) should have at least one `migrates` or `defines_schema` edge — warn if missing
- Schema nodes (type: `schema`) should have at least one `defines_schema` edge — warn if missing
- Resource nodes (type: `resource`) should have at least one `provisions` or `depends_on` edge — warn if missing
- Endpoint nodes (type: `endpoint`) should have at least one `routes` or `defines_schema` edge — warn if missing
**Check 9 -- Node Type / ID Prefix Consistency (Warning)**
- Verify that each node's `type` field matches its ID prefix. For example:
- A node with `type: "config"` should have an ID starting with `config:`
- A node with `type: "document"` should have an ID starting with `document:`
- A node with `type: "file"` should have an ID starting with `file:`
- Log any mismatches as warnings
### Script Output Format
The script must write this exact JSON structure to the output file:
@@ -95,14 +120,17 @@ The script must write this exact JSON structure to the output file:
{
"scriptCompleted": true,
"issues": ["Edge at index 14 references non-existent target node 'file:src/missing.ts'"],
"warnings": ["3 function nodes have no edges connecting to them"],
"warnings": [
"3 function nodes have no edges connecting to them",
"Config node 'config:tsconfig.json' has no 'configures' edges"
],
"stats": {
"totalNodes": 42,
"totalEdges": 87,
"totalLayers": 5,
"tourSteps": 8,
"nodeTypes": {"file": 20, "function": 15, "class": 7},
"edgeTypes": {"imports": 30, "contains": 40, "calls": 17}
"nodeTypes": {"file": 20, "function": 15, "class": 7, "config": 3, "document": 2, "service": 1},
"edgeTypes": {"imports": 30, "contains": 40, "calls": 17, "configures": 5, "documents": 3, "deploys": 2}
}
}
```
@@ -120,7 +148,7 @@ The script must write this exact JSON structure to the output file:
- Zero nodes, edges, layers, or tour steps
- Invalid edge types or node types
- Edge weights outside 0.0-1.0 range
- File nodes missing from all layers
- File-level nodes missing from all layers
- Duplicate node IDs
**Warnings** (go into `warnings`):
@@ -128,6 +156,8 @@ The script must write this exact JSON structure to the output file:
- Short or generic summaries
- Tour step count outside 5-15 range
- Self-referencing edges
- Non-code nodes missing expected edge types (configures, documents, deploys, etc.)
- Node type / ID prefix mismatches
### Executing the Script
@@ -160,15 +190,17 @@ Produce the final validation report JSON:
"issues": [],
"warnings": [
"3 function nodes have no edges connecting to them",
"Node 'file:src/config.ts' has a generic summary"
"Node 'file:src/config.ts' has a generic summary",
"Config node 'config:tsconfig.json' has no 'configures' edges",
"Document node 'document:CHANGELOG.md' has no 'documents' edges"
],
"stats": {
"totalNodes": 42,
"totalEdges": 87,
"totalLayers": 5,
"tourSteps": 8,
"nodeTypes": {"file": 20, "function": 15, "class": 7},
"edgeTypes": {"imports": 30, "contains": 40, "calls": 17}
"nodeTypes": {"file": 20, "function": 15, "class": 7, "config": 3, "document": 2, "service": 1},
"edgeTypes": {"imports": 30, "contains": 40, "calls": 17, "configures": 5, "documents": 3, "deploys": 2}
}
}
```
@@ -0,0 +1,37 @@
# CSS Language Prompt Snippet
## Key Concepts
- **Selectors**: Element, class (`.name`), ID (`#name`), attribute (`[attr]`), and pseudo-class (`:hover`) targeting
- **Specificity**: Inline > ID > Class > Element cascade priority determining which rules win
- **Box Model**: `margin`, `border`, `padding`, `content` dimensions controlling element sizing
- **Flexbox**: `display: flex` with `justify-content`, `align-items` for one-dimensional layouts
- **Grid**: `display: grid` with `grid-template-columns/rows` for two-dimensional layouts
- **Custom Properties (Variables)**: `--name: value` with `var(--name)` for reusable design tokens
- **Media Queries**: `@media (max-width: ...)` for responsive design breakpoints
- **SCSS/Sass Features**: Nesting, `$variables`, `@mixin`, `@include`, `@extend`, `@use`, `@forward`
- **CSS Modules**: Scoped class names (`.module.css`) preventing global style collisions
- **Cascade Layers**: `@layer` for explicit control over cascade ordering
## Notable File Patterns
- `*.css` — Standard CSS stylesheets
- `*.scss` / `*.sass` — Sass/SCSS preprocessor files
- `*.less` — Less preprocessor files
- `*.module.css` / `*.module.scss` — CSS Modules (scoped styles)
- `globals.css` / `reset.css` / `normalize.css` — Global base styles
- `tailwind.config.js` — Tailwind CSS configuration (though a JS file)
- `variables.scss` / `_variables.scss` — Design token definitions
## Edge Patterns
- CSS files are `related` to the HTML or component files that import them for styling
- SCSS partial files (`_*.scss`) are `depends_on` by the main stylesheet that `@use`s them
- CSS variable definition files are `related` to all stylesheets that reference those variables
- CSS Modules are `related` to the component files that import them
## Summary Style
> "Global stylesheet defining CSS custom properties for the design system color palette and typography."
> "Responsive layout styles with flexbox and grid for the dashboard page across 3 breakpoints."
> "SCSS partial defining shared mixins for spacing, shadows, and media query breakpoints."
@@ -0,0 +1,34 @@
# Dockerfile Language Prompt Snippet
## Key Concepts
- **Multi-Stage Builds**: Multiple `FROM` statements to separate build and runtime stages, reducing image size
- **Layer Caching**: Each instruction creates a layer; order instructions from least to most frequently changing for cache efficiency
- **Base Images**: `FROM image:tag` selects the starting image; prefer slim/alpine variants for smaller images
- **COPY vs ADD**: `COPY` for local files (preferred), `ADD` for URLs and tar extraction
- **Build Arguments**: `ARG` for build-time variables, `ENV` for runtime environment variables
- **Health Checks**: `HEALTHCHECK` instruction for container orchestrator readiness probes
- **Entry Point vs CMD**: `ENTRYPOINT` sets the executable, `CMD` provides default arguments
- **User Permissions**: `USER` instruction to run as non-root for security
- **Ignore Patterns**: `.dockerignore` excludes files from the build context (like `.gitignore`)
## Notable File Patterns
- `Dockerfile` — Primary container image definition (at project root)
- `Dockerfile.dev` / `Dockerfile.prod` — Environment-specific Dockerfiles
- `docker-compose.yml` — Multi-container application orchestration
- `docker-compose.override.yml` — Local development overrides
- `.dockerignore` — Build context exclusion patterns
## Edge Patterns
- Dockerfile `deploys` the application entry point it packages (COPY/CMD target)
- docker-compose `depends_on` Dockerfile(s) it references for building
- Dockerfile `depends_on` package manifests (package.json, requirements.txt) it copies for dependency installation
- docker-compose services create `related` edges between co-deployed components
## Summary Style
> "Multi-stage Docker build producing a minimal Node.js production image with N build stages."
> "Docker Compose configuration orchestrating N services with shared networking and persistent volumes."
> "Development Dockerfile with hot-reload support and mounted source volumes."
@@ -0,0 +1,35 @@
# GraphQL Language Prompt Snippet
## Key Concepts
- **Type System**: Strongly typed schema defining the API contract with scalar, object, enum, and union types
- **Queries**: Read operations fetching data with field-level selection (no over-fetching)
- **Mutations**: Write operations for creating, updating, and deleting data
- **Subscriptions**: Real-time data push over WebSocket connections
- **Resolvers**: Functions mapping schema fields to data sources (database, API, cache)
- **Fragments**: Reusable field selections reducing query duplication across operations
- **Directives**: `@deprecated`, `@include`, `@skip` for conditional field inclusion and schema metadata
- **Input Types**: `input` keyword for complex mutation arguments
- **Interfaces and Unions**: Polymorphic types for shared fields across multiple object types
- **Schema Stitching / Federation**: Composing multiple GraphQL services into a unified graph
## Notable File Patterns
- `schema.graphql` / `*.graphql` — Schema definition files
- `*.gql` — Alternative extension for GraphQL files
- `schema/*.graphql` — Split schema files by domain (users.graphql, orders.graphql)
- `*.resolvers.ts` / `*.resolvers.js` — Resolver implementations (TypeScript/JavaScript convention)
- `codegen.yml` — GraphQL Code Generator configuration
## Edge Patterns
- GraphQL schema files `defines_schema` for the resolver code that implements query/mutation handlers
- Type definitions create `related` edges between types connected by field references
- Schema files `defines_schema` for client-side query/mutation files that consume the API
- Codegen config `configures` the schema-to-code generation pipeline
## Summary Style
> "GraphQL schema defining N types, M queries, and K mutations for the user management API."
> "API schema with type definitions for products, orders, and payment processing with pagination."
> "Subscription schema enabling real-time notifications for order status updates."
@@ -0,0 +1,34 @@
# HTML Language Prompt Snippet
## Key Concepts
- **Semantic Elements**: `<main>`, `<nav>`, `<header>`, `<footer>`, `<article>`, `<section>` for meaningful structure
- **Document Structure**: `<!DOCTYPE html>`, `<html>`, `<head>`, `<body>` forming the page skeleton
- **Forms**: `<form>`, `<input>`, `<select>`, `<textarea>` for user data collection with validation attributes
- **Accessibility**: `aria-*` attributes, `role`, `alt` text, and semantic markup for screen readers
- **Meta Tags**: `<meta>` for viewport, charset, description, Open Graph, and SEO metadata
- **Script and Style Loading**: `<script>`, `<link>`, `<style>` for JavaScript and CSS inclusion
- **Data Attributes**: `data-*` custom attributes for storing element-specific data
- **Template Syntax**: Framework-specific templating (`{{ }}` for Jinja/Django, `<%= %>` for ERB)
- **Web Components**: `<template>`, `<slot>`, Custom Elements for encapsulated reusable components
## Notable File Patterns
- `index.html` — Application entry point or SPA shell
- `*.html` / `*.htm` — Static HTML pages
- `templates/**/*.html` — Server-side template files (Django, Jinja2, Go templates)
- `public/index.html` — SPA root document (React, Vue)
- `*.ejs` / `*.hbs` / `*.pug` — Templating engine files
## Edge Patterns
- HTML files `depends_on` JavaScript and CSS files they include via `<script>` and `<link>` tags
- Template HTML files `depends_on` the server-side code that renders them
- HTML entry points are `deploys` targets for build systems and web servers
- HTML files `related` to the components or routes they render
## Summary Style
> "Single-page application shell with viewport meta, CSS reset, and React root mount point."
> "Server-rendered template with navigation, content area, and footer using Django template inheritance."
> "Static landing page with responsive layout, form, and third-party script integrations."
@@ -0,0 +1,34 @@
# JSON Language Prompt Snippet
## Key Concepts
- **Strict Syntax**: No trailing commas, no comments (unlike JSONC or JSON5), double-quoted strings only
- **Data Types**: Objects, arrays, strings, numbers, booleans, and null — no undefined or date types
- **Nested Structure**: Arbitrary nesting depth for hierarchical configuration or data
- **Schema Validation**: JSON Schema (`$schema` keyword) for validating structure and types
- **JSONC**: JSON with Comments variant used by VS Code, tsconfig.json, and other tooling
- **JSON5**: Extended JSON allowing comments, trailing commas, unquoted keys, and more
- **JSON Lines** (`.jsonl`): One JSON object per line for streaming data processing
## Notable File Patterns
- `package.json` — Node.js project manifest with dependencies, scripts, and metadata
- `tsconfig.json` — TypeScript compiler configuration (actually JSONC)
- `.eslintrc.json` — ESLint linting rules and configuration
- `*.schema.json` — JSON Schema definitions for validation
- `composer.json` — PHP Composer project manifest
- `appsettings.json` — .NET application configuration
- `manifest.json` — Browser extension or PWA manifest
## Edge Patterns
- `package.json` `configures` the build toolchain and defines project dependencies
- `tsconfig.json` `configures` TypeScript compilation for all `.ts` files
- JSON Schema files `defines_schema` for API request/response validation
- Config JSON files `configures` the runtime behavior of the application
## Summary Style
> "Node.js project manifest defining N dependencies, build scripts, and project metadata."
> "TypeScript compiler configuration enabling strict mode with path aliases for monorepo packages."
> "JSON Schema defining the request/response structure for the user API endpoint."
@@ -0,0 +1,34 @@
# Markdown Language Prompt Snippet
## Key Concepts
- **Heading Hierarchy**: `#` through `######` for document structure, with h1 as the title
- **Front Matter**: YAML metadata between `---` delimiters at the top of the file
- **Fenced Code Blocks**: Triple backticks with optional language identifier for syntax highlighting
- **Reference-Style Links**: `[text][ref]` with `[ref]: url` definitions, useful for repeated URLs
- **Tables**: Pipe-delimited columns with alignment markers (`:---`, `:---:`, `---:`)
- **Admonitions**: Blockquote-based callouts (`> **Note:**`, `> **Warning:**`) for emphasis
- **Task Lists**: `- [ ]` and `- [x]` for checklists in issue trackers and READMEs
- **HTML Embedding**: Raw HTML allowed inline for features Markdown does not support natively
## Notable File Patterns
- `README.md` — Project overview and entry point for new contributors (high-value)
- `CONTRIBUTING.md` — Contribution guidelines, code style, PR process
- `CHANGELOG.md` — Version history following Keep a Changelog or similar format
- `docs/**/*.md` — Documentation directory with guides, API references, tutorials
- `*.md` in source directories — Co-located documentation for modules or packages
- `ADR-*.md` or `adr/*.md` — Architecture Decision Records
## Edge Patterns
- Markdown files `documents` the code components they describe or reference
- Links to other `.md` files create `related` edges between documentation nodes
- Code block references mentioning file paths may imply `documents` edges to those files
- README files in subdirectories typically `documents` the module at that path
## Summary Style
> "Project overview documentation with N sections covering installation, usage, and API reference."
> "Architecture Decision Record documenting the choice of [technology] for [purpose]."
> "Contributing guide with code style rules, testing requirements, and pull request process."
@@ -0,0 +1,34 @@
# Protobuf Language Prompt Snippet
## Key Concepts
- **Message Types**: `message` blocks defining structured data with typed, numbered fields
- **Field Numbers**: Permanent identifiers (1-536870911) — never reuse deleted numbers for backward compatibility
- **Scalar Types**: `int32`, `int64`, `string`, `bytes`, `bool`, `float`, `double`, and more
- **Enums**: Named integer constants for categorical values
- **Services**: `service` blocks defining RPC (Remote Procedure Call) method signatures
- **Oneof**: Mutually exclusive field groups — only one field in the group can be set
- **Repeated Fields**: `repeated` keyword for list/array fields
- **Maps**: `map<key_type, value_type>` for dictionary/hash fields
- **Packages and Imports**: Namespace organization and cross-file references
- **Proto2 vs Proto3**: Proto3 (current) removes required/optional distinction and defaults all fields
## Notable File Patterns
- `*.proto` — Protocol Buffer definition files
- `proto/**/*.proto` — Organized proto definitions by service or domain
- `buf.yaml` / `buf.gen.yaml` — Buf tool configuration for linting and code generation
- `*_pb2.py` / `*.pb.go` / `*_pb.ts` — Generated code (should be excluded from analysis)
## Edge Patterns
- Protobuf files `defines_schema` for the gRPC service handlers that implement the declared RPCs
- Message type references create `related` edges between proto files sharing types
- Proto `import` statements create `depends_on` edges between proto files
- Generated code files are `depends_on` the proto source that produces them
## Summary Style
> "Protocol Buffer definitions for N message types and M RPC services in the user authentication domain."
> "Shared proto types defining common request/response envelopes and error codes."
> "gRPC service definition with N methods for real-time data streaming and batch processing."
@@ -0,0 +1,35 @@
# Shell Language Prompt Snippet
## Key Concepts
- **Shebang Line**: `#!/bin/bash` or `#!/usr/bin/env bash` specifying the interpreter
- **Variables**: `VAR=value` assignment, `$VAR` or `${VAR}` expansion, no spaces around `=`
- **Functions**: `function name()` or `name()` for reusable command groups
- **Conditionals**: `if [[ condition ]]; then ... fi` with `[[ ]]` for extended tests
- **Loops**: `for item in list`, `while condition`, `until condition` iteration patterns
- **Pipes and Redirection**: `|` for chaining commands, `>` / `>>` / `2>&1` for output redirection
- **Exit Codes**: `$?` captures last command status; `set -e` exits on any failure
- **Strict Mode**: `set -euo pipefail` for robust error handling (exit on error, undefined vars, pipe failures)
- **Command Substitution**: `$(command)` captures command output as a string
- **Here Documents**: `<<EOF ... EOF` for multi-line string input to commands
## Notable File Patterns
- `*.sh` / `*.bash` — Shell script files
- `scripts/*.sh` — Project automation scripts (build, deploy, setup)
- `entrypoint.sh` — Docker container entry point script
- `install.sh` / `setup.sh` — Environment setup scripts
- `.bashrc` / `.bash_profile` / `.zshrc` — Shell configuration files
## Edge Patterns
- Shell scripts `triggers` other scripts or build processes they invoke
- Entry point scripts `deploys` the application they start
- Setup scripts `configures` the development environment
- Build scripts `depends_on` the source files they compile or package
## Summary Style
> "Build automation script compiling TypeScript, running tests, and packaging the release artifact."
> "Docker entry point script handling signal forwarding and graceful shutdown."
> "Environment setup script installing dependencies and configuring development tools."
@@ -0,0 +1,36 @@
# SQL Language Prompt Snippet
## Key Concepts
- **DDL (Data Definition)**: `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE` for schema management
- **DML (Data Manipulation)**: `SELECT`, `INSERT`, `UPDATE`, `DELETE` for data operations
- **Normalization**: Organizing tables to reduce redundancy through 1NF, 2NF, 3NF relationships
- **Foreign Keys**: `REFERENCES` constraints enforcing referential integrity between tables
- **Indexes**: `CREATE INDEX` for query performance optimization on frequently queried columns
- **Migrations**: Numbered, sequential schema changes applied in order for version control
- **Transactions**: `BEGIN`/`COMMIT`/`ROLLBACK` for atomic multi-statement operations
- **Views**: Named queries (`CREATE VIEW`) providing virtual tables for complex joins
- **Stored Procedures**: Server-side functions for encapsulating business logic in the database
- **Constraints**: `NOT NULL`, `UNIQUE`, `CHECK`, `DEFAULT` for data integrity rules
## Notable File Patterns
- `migrations/*.sql` — Numbered migration files (e.g., `001_create_users.sql`, `002_add_orders.sql`)
- `schema.sql` — Full database schema definition (often generated from migrations)
- `seeds/*.sql` — Seed data for development and testing environments
- `*.up.sql` / `*.down.sql` — Reversible migration pairs (up applies, down reverts)
- `init.sql` — Database initialization script for Docker or fresh setup
- `procedures/*.sql` — Stored procedure definitions
## Edge Patterns
- SQL migration files `migrates` the tables they create or alter
- Schema definition files `defines_schema` for the ORM models or data layer code that reads them
- Table definitions create implicit `related` edges between tables connected by foreign keys
- Seed files `depends_on` the migration files that create the tables they populate
## Summary Style
> "Database migration creating the users table with email, name, and authentication columns."
> "Schema definition with N tables covering user management, orders, and payment processing."
> "Seed data populating N tables with development fixtures for testing."
@@ -0,0 +1,38 @@
# Terraform Language Prompt Snippet
## Key Concepts
- **Declarative Infrastructure**: Define desired state; Terraform computes and applies the diff
- **Providers**: Plugins connecting to cloud APIs (AWS, GCP, Azure, Kubernetes, etc.)
- **Resources**: `resource "type" "name"` blocks declaring infrastructure components
- **Data Sources**: `data "type" "name"` blocks reading existing infrastructure state
- **Variables**: `variable` blocks for parameterizing configurations with defaults and validation
- **Outputs**: `output` blocks exposing values for cross-module references or human consumption
- **Modules**: Reusable, composable infrastructure packages with their own variables and outputs
- **State Management**: `.tfstate` files tracking real-world resource mapping (never commit to git)
- **Workspaces**: Isolated state environments for managing dev/staging/prod from one codebase
- **Plan and Apply**: `terraform plan` previews changes, `terraform apply` executes them
## Notable File Patterns
- `main.tf` — Primary resource definitions
- `variables.tf` — Input variable declarations with types and defaults
- `outputs.tf` — Output value definitions
- `providers.tf` — Provider configuration and version constraints
- `backend.tf` — Remote state backend configuration (S3, GCS, etc.)
- `modules/**/*.tf` — Reusable infrastructure modules
- `*.tfvars` — Variable value files for different environments
- `terraform.lock.hcl` — Provider version lock file
## Edge Patterns
- Terraform files `provisions` the infrastructure resources they define
- Module references create `depends_on` edges between terraform files
- Terraform `deploys` application code by referencing container images or deployment targets
- Variable files `configures` the terraform modules they parameterize
## Summary Style
> "Terraform configuration provisioning N AWS resources including VPC, ECS cluster, and RDS instance."
> "Infrastructure module defining a reusable Kubernetes namespace with RBAC and network policies."
> "Variable definitions for N environment-specific settings (region, instance type, scaling)."
@@ -0,0 +1,35 @@
# YAML Language Prompt Snippet
## Key Concepts
- **Indentation-Based Nesting**: Whitespace-sensitive structure (spaces only, no tabs) defining hierarchy
- **Anchors and Aliases**: `&anchor` defines a reusable block, `*anchor` references it to avoid duplication
- **Merge Keys**: `<<: *anchor` merges anchor contents into the current mapping
- **Multi-Line Strings**: Literal block (`|`) preserves newlines, folded block (`>`) joins lines
- **Document Separators**: `---` starts a new document, `...` ends one (multi-document streams)
- **Tags and Types**: `!!str`, `!!int`, `!!bool` for explicit typing; custom tags for application-specific types
- **Flow Style**: Inline JSON-like syntax `{key: value}` and `[item1, item2]` for compact notation
- **Environment Variable Substitution**: `${VAR}` patterns used in docker-compose and CI configs
## Notable File Patterns
- `docker-compose.yml` / `docker-compose.yaml` — Multi-container Docker application definition
- `.github/workflows/*.yml` — GitHub Actions CI/CD workflow definitions
- `.gitlab-ci.yml` — GitLab CI/CD pipeline configuration
- `kubernetes/*.yaml` / `k8s/*.yaml` — Kubernetes resource manifests
- `*.config.yaml` — Application configuration files
- `mkdocs.yml` — MkDocs documentation site configuration
- `serverless.yml` — Serverless Framework configuration
## Edge Patterns
- YAML config files `configures` the code modules they control (e.g., database settings affect data layer)
- CI/CD YAML files `triggers` build and deployment pipelines
- docker-compose YAML `deploys` services and `depends_on` Dockerfiles
- Kubernetes YAML `deploys` and `provisions` application services
## Summary Style
> "Docker Compose configuration defining N services with networking, volumes, and health checks."
> "GitHub Actions workflow running tests on push and deploying to production on merge to main."
> "Kubernetes deployment manifest with N replicas, resource limits, and liveness probes."
@@ -2,7 +2,7 @@
> Used by `/understand` Phase 1. Dispatch as a subagent with this full content as the prompt.
You are a meticulous project inventory specialist. Your job is to scan a codebase directory and produce a precise, structured inventory of all source files, detected languages, frameworks, and estimated complexity. Accuracy is paramount -- every file path you report must actually exist on disk.
You are a meticulous project inventory specialist. Your job is to scan a codebase directory and produce a precise, structured inventory of all project files, detected languages, frameworks, and estimated complexity. Accuracy is paramount -- every file path you report must actually exist on disk.
## Task
@@ -12,7 +12,7 @@ Scan the project directory provided in the prompt and produce a JSON inventory.
## Phase 1 -- Discovery Script
Write a script that discovers all source files, detects languages and frameworks, counts lines, and produces structured JSON. Choose the best language for this task (bash, Node.js, or Python -- whichever is available on the system). The script must handle errors gracefully and never crash on unexpected input.
Write a script that discovers all project files (including non-code files like configs, docs, and infrastructure), detects languages and frameworks, counts lines, and produces structured JSON. Choose the best language for this task (bash, Node.js, or Python -- whichever is available on the system). The script must handle errors gracefully and never crash on unexpected input.
### Script Requirements
@@ -38,10 +38,19 @@ Remove ALL files matching these patterns:
- **Binary/asset files:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.mp3`, `.mp4`, `.pdf`, `.zip`, `.tar`, `.gz`
- **Generated files:** `*.min.js`, `*.min.css`, `*.map`, `*.d.ts`, `*.generated.*`
- **IDE/editor config:** paths containing `.idea/`, `.vscode/`
- **Config/doc files:** `*.md`, `*.txt`, `*.yml`, `*.yaml`, `*.toml`, `*.json`, `*.xml`, `*.lock`, `*.cfg`, `*.ini`, `Makefile`, `Dockerfile`
- **Misc non-source:** `LICENSE`, `.gitignore`, `.editorconfig`, `.prettierrc`, `.eslintrc*`, `*.log`
The goal is to keep ONLY source code files (`.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.go`, `.rs`, `.java`, `.rb`, `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp`, `.c`, `.cs`, `.swift`, `.kt`, `.php`, `.vue`, `.svelte`, `.sh`, `.bash`).
**IMPORTANT:** Do NOT exclude non-code project files. The following MUST be kept:
- Documentation: `*.md`, `*.rst`, `*.txt` (except `LICENSE`)
- Configuration: `*.yaml`, `*.yml`, `*.json`, `*.toml`, `*.xml`, `*.cfg`, `*.ini`, `*.env`, `*.env.example`
- Infrastructure: `Dockerfile`, `docker-compose.*`, `*.tf`, `Makefile`, `Jenkinsfile`, `Procfile`, `Vagrantfile`
- CI/CD: `.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/*`, `Jenkinsfile`
- Data/Schema: `*.sql`, `*.graphql`, `*.gql`, `*.proto`, `*.prisma`, `*.schema.json`
- Web markup: `*.html`, `*.css`, `*.scss`, `*.sass`, `*.less`
- Shell scripts: `*.sh`, `*.bash`, `*.ps1`, `*.bat`
- Kubernetes: `*.k8s.yaml`, `*.k8s.yml`, paths containing `k8s/`, paths containing `kubernetes/`
**Note on package manifests:** Config files read for framework detection (`package.json`, `tsconfig.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.) should also appear in the file list with `fileCategory: "config"`.
**Step 3 -- Language Detection**
@@ -64,17 +73,48 @@ Map file extensions to language identifiers:
| `.php` | `php` |
| `.vue` | `vue` |
| `.svelte` | `svelte` |
| `.sh`, `.bash` | `bash` |
| `.sh`, `.bash` | `shell` |
| `.md`, `.rst` | `markdown` |
| `.yaml`, `.yml` | `yaml` |
| `.json` | `json` |
| `.toml` | `toml` |
| `.sql` | `sql` |
| `.graphql`, `.gql` | `graphql` |
| `.proto` | `protobuf` |
| `.tf`, `.tfvars` | `terraform` |
| `.html`, `.htm` | `html` |
| `.css`, `.scss`, `.sass`, `.less` | `css` |
| `.xml` | `xml` |
| `.cfg`, `.ini`, `.env` | `config` |
| `Dockerfile` (no extension) | `dockerfile` |
| `Makefile` (no extension) | `makefile` |
| `Jenkinsfile` (no extension) | `jenkinsfile` |
Collect unique languages, sorted alphabetically.
**Step 4 -- Line Counting**
**Step 4 -- File Category Detection**
For each source file, count lines using `wc -l`. For efficiency:
Assign a `fileCategory` to each discovered file based on its extension and path:
| Pattern | Category |
|---|---|
| `.md`, `.rst`, `.txt` (except `LICENSE`) | `docs` |
| `.yaml`, `.yml`, `.json`, `.toml`, `.xml`, `.cfg`, `.ini`, `.env`, `tsconfig.json`, `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod` | `config` |
| `Dockerfile`, `docker-compose.*`, `.tf`, `.tfvars`, `Makefile`, `Jenkinsfile`, `Procfile`, `Vagrantfile`, `.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/*`, `*.k8s.yaml`, `*.k8s.yml`, paths in `k8s/` or `kubernetes/` | `infra` |
| `.sql`, `.graphql`, `.gql`, `.proto`, `.prisma`, `*.schema.json`, `.csv` | `data` |
| `.sh`, `.bash`, `.ps1`, `.bat` | `script` |
| `.html`, `.htm`, `.css`, `.scss`, `.sass`, `.less` | `markup` |
| All other extensions (`.ts`, `.tsx`, `.js`, `.py`, `.go`, `.rs`, etc.) | `code` |
**Priority rule:** When a file matches multiple categories, use the first match from the table above (most specific wins). For example, `docker-compose.yml` is `infra`, not `config`.
**Step 5 -- Line Counting**
For each file, count lines using `wc -l`. For efficiency:
- If fewer than 500 files, count all of them
- If 500+ files, count all of them but batch the `wc -l` calls (pass multiple files per invocation to avoid spawning thousands of processes)
**Step 5 -- Framework Detection**
**Step 6 -- Framework Detection**
Read config files (if they exist) and extract framework information:
- `package.json` -- parse JSON, extract `name`, `description`, `dependencies`, `devDependencies`. Match dependency names against known frameworks: `react`, `vue`, `svelte`, `@angular/core`, `express`, `fastify`, `koa`, `next`, `nuxt`, `vite`, `vitest`, `jest`, `mocha`, `tailwindcss`, `prisma`, `typeorm`, `sequelize`, `mongoose`, `redux`, `zustand`, `mobx`
@@ -89,15 +129,23 @@ Read config files (if they exist) and extract framework information:
- `Cargo.toml` dependencies -- if present, read `[dependencies]` and match crate names against known Rust frameworks: `actix-web`, `axum`, `rocket`, `diesel`, `tokio`, `serde`, `warp`
- `pom.xml` / `build.gradle` / `build.gradle.kts` -- if present, confirms Java/Kotlin project; match dependency names against known JVM frameworks: `spring-boot`, `spring-web`, `spring-data`, `quarkus`, `micronaut`, `hibernate`, `jakarta`, `junit`, `ktor`
**Step 6 -- Complexity Estimation**
Also detect infrastructure tooling from discovered files:
- Presence of `Dockerfile` → add `Docker` to frameworks
- Presence of `docker-compose.yml` or `docker-compose.yaml` → add `Docker Compose` to frameworks
- Presence of `*.tf` files → add `Terraform` to frameworks
- Presence of `.github/workflows/*.yml` → add `GitHub Actions` to frameworks
- Presence of `.gitlab-ci.yml` → add `GitLab CI` to frameworks
- Presence of `Jenkinsfile` → add `Jenkins` to frameworks
Classify by source file count:
- `small`: 1-20 files
- `moderate`: 21-100 files
- `large`: 101-500 files
**Step 7 -- Complexity Estimation**
Classify by total file count (including non-code files):
- `small`: 1-30 files
- `moderate`: 31-150 files
- `large`: 151-500 files
- `very-large`: >500 files
**Step 7 -- Project Name**
**Step 8 -- Project Name**
Extract from (in priority order):
1. `package.json` `name` field
@@ -106,11 +154,13 @@ Extract from (in priority order):
4. `pyproject.toml` -- check `[project].name` first, then `[tool.poetry].name`
5. Directory name of project root
**Step 8 -- Import Resolution**
**Step 9 -- Import Resolution**
For each file in the discovered source list, extract and resolve relative import statements. The goal is to produce a map from each file's path to the list of project-internal files it imports. External package imports are ignored.
For each **code-category** file in the discovered list (`fileCategory === "code"`), extract and resolve relative import statements. The goal is to produce a map from each file's path to the list of project-internal files it imports. External package imports are ignored.
For each file, read its content and extract import paths using language-appropriate patterns:
**Non-code files** (config, docs, infra, data, script, markup) should have an empty array `[]` in the import map — they do not participate in code-level import resolution.
For each code file, read its content and extract import paths using language-appropriate patterns:
| Language | Import patterns to match |
|---|---|
@@ -134,6 +184,8 @@ Output format in the script result:
"importMap": {
"src/index.ts": ["src/utils.ts", "src/config.ts"],
"src/utils.ts": [],
"README.md": [],
"Dockerfile": [],
"src/components/App.tsx": ["src/hooks/useAuth.ts", "src/store/index.ts"]
}
```
@@ -150,16 +202,22 @@ The script must write this exact JSON structure to the output file:
"name": "project-name",
"rawDescription": "Description from package.json or empty string",
"readmeHead": "First 10 lines of README.md or empty string",
"languages": ["javascript", "typescript"],
"frameworks": ["React", "Vite", "Vitest"],
"languages": ["javascript", "markdown", "typescript", "yaml"],
"frameworks": ["React", "Vite", "Vitest", "Docker"],
"files": [
{"path": "src/index.ts", "language": "typescript", "sizeLines": 150}
{"path": "src/index.ts", "language": "typescript", "sizeLines": 150, "fileCategory": "code"},
{"path": "README.md", "language": "markdown", "sizeLines": 45, "fileCategory": "docs"},
{"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"},
{"path": "package.json", "language": "json", "sizeLines": 35, "fileCategory": "config"}
],
"totalFiles": 42,
"estimatedComplexity": "moderate",
"importMap": {
"src/index.ts": ["src/utils.ts", "src/config.ts"],
"src/utils.ts": []
"src/utils.ts": [],
"README.md": [],
"Dockerfile": [],
"package.json": []
}
}
```
@@ -170,10 +228,11 @@ The script must write this exact JSON structure to the output file:
- `readmeHead` (string) -- first 10 lines of `README.md` or empty string if no README exists
- `languages` (string[]) -- deduplicated, sorted alphabetically
- `frameworks` (string[]) -- only confirmed frameworks; empty array if none detected
- `files` (object[]) -- every source file, sorted by `path` alphabetically
- `files` (object[]) -- every discovered file, sorted by `path` alphabetically
- `files[].fileCategory` (string) -- one of: `code`, `config`, `docs`, `infra`, `data`, `script`, `markup`
- `totalFiles` (integer) -- must equal `files.length`
- `estimatedComplexity` (string) -- one of `small`, `moderate`, `large`, `very-large`
- `importMap` (object) map from every source file path to its list of resolved project-internal import paths; empty array if no resolved imports; external packages excluded
- `importMap` (object) -- map from every file path to its list of resolved project-internal import paths; empty array for non-code files and files with no resolved imports; external packages excluded
### Executing the Script
@@ -208,10 +267,12 @@ Then assemble the final output JSON:
{
"name": "project-name",
"description": "Brief description from README or package.json",
"languages": ["typescript", "javascript"],
"frameworks": ["React", "Vite", "Vitest"],
"languages": ["markdown", "typescript", "yaml"],
"frameworks": ["React", "Vite", "Vitest", "Docker"],
"files": [
{"path": "src/index.ts", "language": "typescript", "sizeLines": 150}
{"path": "src/index.ts", "language": "typescript", "sizeLines": 150, "fileCategory": "code"},
{"path": "README.md", "language": "markdown", "sizeLines": 45, "fileCategory": "docs"},
{"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"}
],
"totalFiles": 42,
"estimatedComplexity": "moderate",
@@ -226,7 +287,7 @@ Then assemble the final output JSON:
- `description` (string): your synthesized 1-2 sentence description
- `languages` (string[]): directly from script output
- `frameworks` (string[]): directly from script output
- `files` (object[]): directly from script output
- `files` (object[]): directly from script output, including `fileCategory` per file
- `totalFiles` (integer): directly from script output
- `estimatedComplexity` (string): directly from script output
- `importMap` (object): directly from script output
@@ -237,7 +298,8 @@ Then assemble the final output JSON:
- NEVER include files that do not exist on disk.
- ALWAYS validate that `totalFiles` matches the actual length of the `files` array.
- ALWAYS sort `files` by `path` for deterministic output.
- Only include source code files in `files` -- no configs, docs, images, or assets.
- Include ALL discovered project files in `files` -- code, configs, docs, infrastructure, and data files. Only exclude binaries, lock files, generated files, and dependency directories.
- Every file MUST have a `fileCategory` field with one of: `code`, `config`, `docs`, `infra`, `data`, `script`, `markup`.
- Trust the script's output for all structural data. Your only contribution is the `description` field.
## Writing Results
@@ -246,6 +308,6 @@ After producing the final JSON:
1. Create the output directory: `mkdir -p <project-root>/.understand-anything/intermediate`
2. Write the JSON to: `<project-root>/.understand-anything/intermediate/scan-result.json`
3. Respond with ONLY a brief text summary: project name, total file count, detected languages, estimated complexity.
3. Respond with ONLY a brief text summary: project name, total file count (with breakdown by category), detected languages, estimated complexity.
Do NOT include the full JSON in your text response.
@@ -6,7 +6,7 @@ You are an expert technical educator who designs learning paths through codebase
## Task
Given a codebase's nodes, edges, and layers, design a guided tour that teaches the project's architecture and key concepts. The tour must reference only real node IDs from the provided graph data. You will accomplish this in two phases: first, write and execute a script that computes structural properties of the graph to identify key files and dependency paths; second, use those insights to design the pedagogical flow.
Given a codebase's nodes, edges, and layers, design a guided tour that teaches the project's architecture and key concepts. The tour must reference only real node IDs from the provided graph data. The tour should include both code and non-code files (documentation, infrastructure, data schemas) to give a complete picture of the project. You will accomplish this in two phases: first, write and execute a script that computes structural properties of the graph to identify key files and dependency paths; second, use those insights to design the pedagogical flow.
---
@@ -20,13 +20,19 @@ Write a Node.js script that analyzes the graph's topology to surface structural
```json
{
"nodes": [
{"id": "file:src/index.ts", "type": "file", "name": "index.ts", "filePath": "src/index.ts", "summary": "..."}
{"id": "file:src/index.ts", "type": "file", "name": "index.ts", "filePath": "src/index.ts", "summary": "..."},
{"id": "document:README.md", "type": "document", "name": "README.md", "filePath": "README.md", "summary": "..."},
{"id": "service:Dockerfile", "type": "service", "name": "Dockerfile", "filePath": "Dockerfile", "summary": "..."},
{"id": "config:package.json", "type": "config", "name": "package.json", "filePath": "package.json", "summary": "..."}
],
"edges": [
{"source": "file:src/index.ts", "target": "file:src/utils.ts", "type": "imports"}
{"source": "file:src/index.ts", "target": "file:src/utils.ts", "type": "imports"},
{"source": "service:Dockerfile", "target": "file:src/index.ts", "type": "deploys"},
{"source": "document:README.md", "target": "file:src/index.ts", "type": "documents"}
],
"layers": [
{"id": "layer:core", "name": "Core", "description": "Core application logic"}
{"id": "layer:core", "name": "Core", "description": "Core application logic"},
{"id": "layer:infrastructure", "name": "Infrastructure", "description": "Deployment and CI/CD"}
]
}
```
@@ -45,12 +51,18 @@ For every node, count how many other nodes it has edges pointing TO (fan-out). H
**C. Entry Point Candidates**
Identify likely entry points using these signals (score each file node, sum the scores):
Identify likely entry points using these signals (score each node, sum the scores):
For code files:
- Filename matches `index.ts`, `index.js`, `main.ts`, `main.js`, `app.ts`, `app.js`, `server.ts`, `server.js`, `mod.rs`, `main.go`, `main.py`, `main.rs`, `manage.py`, `app.py`, `wsgi.py`, `asgi.py`, `run.py`, `__main__.py`, `Application.java`, `Main.java`, `Program.cs`, `config.ru`, `index.php`, `App.swift`, `Application.kt`, `main.cpp`, `main.c` -> +3 points
- File is at the project root or one level deep (e.g., `src/index.ts`) -> +1 point
- High fan-out (top 10%) -> +1 point
- Low fan-in (bottom 25%) -> +1 point (entry points are imported by few files)
For documentation files:
- `README.md` at project root -> +5 points (highest priority as tour start)
- Other `*.md` at project root -> +2 points
Output the top 5 candidates sorted by score descending.
**D. Dependency Chains (BFS from Entry Points)**
@@ -62,7 +74,17 @@ Output:
- The depth of each node (distance from entry point)
- Group nodes by depth level: depth 0 (entry), depth 1 (direct dependencies), depth 2, etc.
**E. Tightly Coupled Clusters**
**E. Non-Code File Inventory**
Separate non-code files by category for tour inclusion:
- Documentation files (type: `document`)
- Infrastructure files (type: `service`, `pipeline`, `resource`)
- Data/Schema files (type: `table`, `schema`, `endpoint`)
- Configuration files (type: `config`)
For each, include the node ID, name, type, and summary.
**F. Tightly Coupled Clusters**
Identify groups of 2-5 nodes that have many edges between them (high mutual connectivity). These often represent a feature or subsystem that should be explained together in one tour step.
@@ -70,15 +92,15 @@ Algorithm: For each pair of nodes with a bidirectional relationship (A imports B
Output the top 5-10 clusters, each as a list of node IDs.
**F. Layer List**
**G. Layer List**
Record the layers provided in the input. Since layers contain only `{id, name, description}` (no node membership), simply output the layer count and the list of layers with their id, name, and description.
**G. Node Summary Index**
**H. Node Summary Index**
Create a lookup of each node ID to its `summary`, `type`, and `name` for easy reference. This lets the LLM phase quickly access semantic information without re-reading the full input.
Note: input nodes are file-type only. The nodeSummaryIndex will contain only file nodes.
Note: input nodes may include all node types (file, config, document, service, pipeline, table, schema, resource, endpoint). The nodeSummaryIndex should include all of them.
### Script Output Format
@@ -86,6 +108,7 @@ Note: input nodes are file-type only. The nodeSummaryIndex will contain only fil
{
"scriptCompleted": true,
"entryPointCandidates": [
{"id": "document:README.md", "score": 5, "name": "README.md", "summary": "Project overview..."},
{"id": "file:src/index.ts", "score": 7, "name": "index.ts", "summary": "..."}
],
"fanInRanking": [
@@ -108,6 +131,21 @@ Note: input nodes are file-type only. The nodeSummaryIndex will contain only fil
"2": ["file:src/models/user.ts"]
}
},
"nonCodeFiles": {
"documentation": [
{"id": "document:README.md", "name": "README.md", "summary": "Project overview..."}
],
"infrastructure": [
{"id": "service:Dockerfile", "name": "Dockerfile", "summary": "Multi-stage build..."},
{"id": "pipeline:.github/workflows/ci.yml", "name": "ci.yml", "summary": "CI pipeline..."}
],
"data": [
{"id": "table:schema.sql:users", "name": "users", "summary": "User table..."}
],
"config": [
{"id": "config:package.json", "name": "package.json", "summary": "Project manifest..."}
]
},
"clusters": [
{"nodes": ["file:src/services/auth.ts", "file:src/models/user.ts"], "edgeCount": 4}
],
@@ -115,13 +153,13 @@ Note: input nodes are file-type only. The nodeSummaryIndex will contain only fil
"count": 3,
"list": [
{"id": "layer:core", "name": "Core", "description": "Core application logic"},
{"id": "layer:services", "name": "Services", "description": "Business logic services"},
{"id": "layer:ui", "name": "UI", "description": "User interface components"}
{"id": "layer:infrastructure", "name": "Infrastructure", "description": "Deployment and CI/CD"}
]
},
"nodeSummaryIndex": {
"file:src/index.ts": {"name": "index.ts", "type": "file", "summary": "Main entry point..."},
"file:src/utils.ts": {"name": "utils.ts", "type": "file", "summary": "Shared helpers..."}
"document:README.md": {"name": "README.md", "type": "document", "summary": "Project overview..."},
"service:Dockerfile": {"name": "Dockerfile", "type": "service", "summary": "Multi-stage Docker build..."}
},
"totalNodes": 42,
"totalEdges": 87
@@ -135,8 +173,8 @@ Before writing the script, create its input JSON file:
```bash
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-tour-input.json << 'ENDJSON'
{
"nodes": [<nodes from prompt>],
"edges": [<edges from prompt>],
"nodes": [<nodes from prompt — all types including non-code>],
"edges": [<edges from prompt — all types>],
"layers": [<layers from prompt>]
}
ENDJSON
@@ -160,7 +198,13 @@ After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-tour
### Step 1 -- Choose the Starting Point
Use `entryPointCandidates[0]` as Step 1 of the tour. This is the file with the highest entry-point score. If the top candidate is a trivial barrel file (re-exports only), consider using the second candidate or grouping both together.
Consider two options for Step 1:
**Option A: README.md first** — If `document:README.md` appears in `entryPointCandidates` or `nonCodeFiles.documentation`, start with it. A README gives newcomers the project's purpose and context before diving into code.
**Option B: Code entry point first** — If there is no README or it is trivial, use the top code entry point from `entryPointCandidates[0]`.
For most projects with a README, **Option A is preferred** — the tour starts with "What is this project?" (README) then moves to "How does it start?" (code entry point in Step 2).
### Step 2 -- Map the BFS Traversal to Tour Steps
@@ -168,23 +212,48 @@ The `bfsTraversal.byDepth` structure gives you the natural reading order of the
| BFS Depth | Tour Mapping | Purpose |
|---|---|---|
| Depth 0 | Step 1 | Entry point / project overview |
| Depth 1 | Steps 2-3 | Direct dependencies: core types, config, main modules |
| Depth 2 | Steps 4-6 | Feature modules, services, primary functionality |
| Depth 3+ | Steps 7-9 | Supporting infrastructure, utilities |
| (clusters) | Steps 10+ | Advanced topics, cross-cutting concerns |
| Depth 0 | Step 1-2 | Project overview (README) + code entry point |
| Depth 1 | Steps 3-4 | Direct dependencies: core types, config, main modules |
| Depth 2 | Steps 5-7 | Feature modules, services, primary functionality |
| Depth 3+ | Steps 8-10 | Supporting infrastructure, utilities |
| (non-code) | Steps 11+ | Infrastructure, data, deployment |
You do not need to include every node from the BFS. Select the most important and illustrative nodes at each depth level, using `fanInRanking` to prioritize.
### Step 3 -- Use Clusters for Grouped Steps
### Step 3 -- Integrate Non-Code Tour Stops
Use `nonCodeFiles` to add non-code stops at appropriate points in the tour:
**Documentation stops:**
- README.md → Step 1 (project overview, if available)
- API docs → After the API layer code
- Architecture docs → After explaining the code structure
**Infrastructure stops:**
- Dockerfile → "How the app gets containerized" — place after the code's entry point and main modules are explained
- docker-compose.yml → "How services are orchestrated" — place after Dockerfile
- K8s manifests → "How the app gets deployed to production"
**Data stops:**
- SQL schema/migrations → "The database schema" — place near the data model code
- GraphQL schema → "The API contract" — place near the API handlers
- Protobuf definitions → "The message protocol" — place near the service handlers
**CI/CD stops:**
- GitHub Actions / GitLab CI → "How code gets tested and deployed" — place near the end as a capstone
**Configuration stops:**
- Key config files → Weave into relevant code steps rather than grouping all configs together
### Step 4 -- Use Clusters for Grouped Steps
When a `cluster` from the script output appears at the same BFS depth, group those nodes into a single tour step. Clusters represent tightly coupled code that should be explained together.
### Step 4 -- Use Layers for Narrative Arc
### Step 5 -- Use Layers for Narrative Arc
The `layers` list gives you the project's architectural groupings. Use layer names and descriptions to understand which areas are foundational vs. top-level, and structure the tour to explain foundational layers before the layers that depend on them.
### Step 5 -- Write Step Descriptions
### Step 6 -- Write Step Descriptions
For each step, use the `nodeSummaryIndex` to access node summaries and names without re-reading files. Each description must:
@@ -194,18 +263,36 @@ For each step, use the `nodeSummaryIndex` to access node summaries and names wit
- Be written for someone who has never seen this codebase before
- Be 2-4 sentences long
Bad description: "This is the auth service file."
Good description: "The authentication service handles user login, token generation, and session management. It builds on the User model from Step 2 and uses the JWT utility from Step 3. Notice the strategy pattern here -- different auth providers (OAuth, email/password) implement a common AuthProvider interface."
**For non-code stops, adapt the description style:**
### Step 6 -- Add Language Lessons (Optional)
Bad description: "This is the Dockerfile."
Good description: "The Dockerfile defines how the application gets packaged into a container image. It uses a multi-stage build: the first stage installs dependencies and compiles TypeScript, while the second stage copies only the compiled output into a minimal Alpine image. This keeps the production image under 100MB while including everything needed to run the server from Step 2."
If a step involves notable language-specific patterns, include a brief `languageLesson` string. Only add these when genuinely educational:
Bad description: "These are the SQL migrations."
Good description: "The database schema defines the core data model underpinning the entire application. The users table (Step 3's User model) maps directly to the columns defined here, while the orders table introduces the foreign key relationship that drives the business logic in Step 5's OrderService."
### Step 7 -- Add Language Lessons (Optional)
If a step involves notable language-specific or format-specific patterns, include a brief `languageLesson` string. Only add these when genuinely educational:
**For code files:**
- **TypeScript:** generics, discriminated unions, utility types, decorators, template literal types
- **React:** hooks, context, render patterns, suspense, compound components
- **Python:** decorators, generators, context managers, metaclasses, protocols
- **Go:** goroutines, channels, interfaces, embedding, error wrapping
- **Rust:** ownership, lifetimes, traits, pattern matching, async/await
**For non-code files:**
- **Dockerfile:** multi-stage builds reduce image size by separating build and runtime dependencies. Layer ordering matters for Docker cache efficiency — put rarely-changing layers (OS packages) before frequently-changing ones (app code).
- **docker-compose:** service dependency ordering with `depends_on`, health checks, named volumes for persistent data, network isolation between services.
- **SQL:** database normalization reduces redundancy through foreign keys. Migrations should be idempotent and reversible. Index placement affects query performance.
- **GraphQL:** type system enforces API contracts at the schema level. Resolvers map schema fields to data sources. Fragments reduce query duplication.
- **Protobuf:** field numbers are permanent (never reuse deleted numbers). Backward compatibility requires only adding optional fields. Services define RPC contracts.
- **YAML (CI/CD):** GitHub Actions use `on` triggers, `jobs` for parallelism, and `steps` for sequential execution. Matrix builds test across multiple OS/language versions. Caching speeds up dependency installation.
- **Terraform:** resources declare desired infrastructure state. State files track what exists. Modules encapsulate reusable infrastructure patterns. Plan before apply to preview changes.
- **Makefile:** targets define build steps with dependency tracking. Phony targets for non-file actions. Variables and pattern rules reduce repetition.
- **Kubernetes:** Deployments manage pod replicas with rolling updates. Services expose pods via stable DNS names. ConfigMaps/Secrets separate config from images.
## Output Format
Produce a single, valid JSON array.
@@ -214,16 +301,36 @@ Produce a single, valid JSON array.
[
{
"order": 1,
"title": "Entry Point",
"description": "Start with src/index.ts, the main entry point that bootstraps the application. This file imports and initializes core modules, sets up configuration, and starts the server. It gives you a bird's-eye view of the project's structure.",
"title": "Project Overview",
"description": "Start with README.md to understand the project's purpose, architecture, and how to get started. This document outlines the main components and their relationships, providing a roadmap for the tour ahead.",
"nodeIds": ["document:README.md"]
},
{
"order": 2,
"title": "Application Entry Point",
"description": "The main entry point bootstraps the application, importing core modules, setting up configuration, and starting the server. This file gives you a bird's-eye view of the project's runtime structure.",
"nodeIds": ["file:src/index.ts"],
"languageLesson": "TypeScript barrel files use 'export * from' to re-export modules, creating a clean public API surface."
},
{
"order": 2,
"order": 3,
"title": "Core Types and Models",
"description": "The type system defines the domain model. These interfaces establish the vocabulary used throughout the codebase and form the contract between layers.",
"nodeIds": ["file:src/types.ts", "file:src/interfaces/user.ts"]
},
{
"order": 8,
"title": "Database Schema",
"description": "The SQL migrations define the database tables that back the User and Order models from Steps 3-4. Foreign keys enforce the relationships the code relies on.",
"nodeIds": ["table:migrations/001.sql:users", "table:migrations/002.sql:orders"],
"languageLesson": "SQL migrations should be idempotent and ordered. Each migration file applies incremental changes to the schema, allowing the database to evolve alongside the application code."
},
{
"order": 12,
"title": "Containerization & Deployment",
"description": "The Dockerfile packages the application into a production-ready container image. The multi-stage build compiles TypeScript in a builder stage and copies only the runtime artifacts, keeping the final image small.",
"nodeIds": ["service:Dockerfile", "service:docker-compose.yml"],
"languageLesson": "Multi-stage Docker builds use multiple FROM statements. The builder stage has dev dependencies for compilation, while the final stage only includes runtime dependencies, reducing image size by 50-80%."
}
]
```
@@ -235,7 +342,7 @@ Produce a single, valid JSON array.
- `nodeIds` (string[]) -- 1-5 node IDs from the provided graph, NEVER empty
**Optional fields:**
- `languageLesson` (string) -- brief explanation of a language pattern, only when genuinely useful
- `languageLesson` (string) -- brief explanation of a language or format pattern, only when genuinely useful
## Critical Constraints
@@ -245,7 +352,8 @@ Produce a single, valid JSON array.
- Tour MUST have between 5 and 15 steps inclusive.
- Steps MUST build on each other -- the tour tells a story, not a random list of files.
- Not every file needs to appear in the tour. Focus on the most important and illustrative files that teach the architecture. Use the fan-in ranking to identify which files are most worth covering.
- ALWAYS start with the project entry point or overview in Step 1.
- Non-code files are valid tour stops. Include at least 1-2 non-code stops if the project has meaningful documentation, infrastructure, or data schema files.
- ALWAYS start with the project overview (README or entry point) in Step 1.
- Trust the script's structural analysis. Do NOT re-read source files, re-count edges, or re-trace dependencies. The script's BFS traversal, fan-in rankings, and cluster analysis are deterministic and reliable.
## Writing Results