diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a72d7ec..c67965a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b857341..08d46de 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" }, diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 19f3f2b..c1a0f00 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -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" }, diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index f96d583..a330d4e 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -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", diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json index dc27f16..457aae8 100644 --- a/understand-anything-plugin/packages/core/package.json +++ b/understand-anything-plugin/packages/core/package.json @@ -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" } } diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts index c3a8b75..03617ca 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts @@ -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); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts b/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts new file mode 100644 index 0000000..b4e008a --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts @@ -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"); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/__tests__/plugin-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/plugin-registry.test.ts index 8000924..39fec82 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/plugin-registry.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/plugin-registry.test.ts @@ -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"); + } + }); }); diff --git a/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts b/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts index 0504557..fe5e205 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts @@ -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 = { + 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 = { + 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); + } + }); +}); diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts index 461e252..a9dc9c9 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts @@ -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"); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts index aaa170b..423b49f 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts @@ -3,6 +3,12 @@ import type { GraphNode, GraphEdge, StructuralAnalysis, + DefinitionInfo, + ServiceInfo, + EndpointInfo, + StepInfo, + ResourceInfo, + SectionInfo, } from "../types.js"; interface FileMeta { @@ -16,7 +22,21 @@ interface FileAnalysisMeta extends FileMeta { fileSummary: string; } +interface NonCodeFileMeta extends FileMeta { + nodeType: GraphNode["type"]; +} + +interface NonCodeFileAnalysisMeta extends NonCodeFileMeta { + definitions?: DefinitionInfo[]; + services?: ServiceInfo[]; + endpoints?: EndpointInfo[]; + steps?: StepInfo[]; + resources?: ResourceInfo[]; + sections?: SectionInfo[]; +} + const EXTENSION_LANGUAGE: Record = { + // Code languages ".ts": "typescript", ".tsx": "typescript", ".js": "javascript", @@ -37,20 +57,41 @@ const EXTENSION_LANGUAGE: Record = { ".cs": "csharp", ".php": "php", ".lua": "lua", + // Non-code languages ".sh": "shell", ".bash": "shell", ".zsh": "shell", ".json": "json", + ".jsonc": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".xml": "xml", ".html": "html", + ".htm": "html", ".css": "css", - ".scss": "scss", - ".less": "less", + ".scss": "css", + ".less": "css", ".md": "markdown", + ".mdx": "markdown", ".sql": "sql", + ".graphql": "graphql", + ".gql": "graphql", + ".proto": "protobuf", + ".tf": "terraform", + ".tfvars": "terraform", + ".mk": "makefile", + ".env": "env", + ".csv": "csv", + ".tsv": "csv", + ".rst": "restructuredtext", + ".ps1": "powershell", + ".psm1": "powershell", + ".psd1": "powershell", + ".bat": "batch", + ".cmd": "batch", + ".txt": "plaintext", + ".svg": "xml", }; function detectLanguage(filePath: string): string { @@ -187,6 +228,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 = { + 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", diff --git a/understand-anything-plugin/packages/core/src/index.ts b/understand-anything-plugin/packages/core/src/index.ts index 3c6783e..0213ad5 100644 --- a/understand-anything-plugin/packages/core/src/index.ts +++ b/understand-anything-plugin/packages/core/src/index.ts @@ -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"; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/batch.ts b/understand-anything-plugin/packages/core/src/languages/configs/batch.ts new file mode 100644 index 0000000..3fc1905 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/batch.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/css.ts b/understand-anything-plugin/packages/core/src/languages/configs/css.ts new file mode 100644 index 0000000..e8b4de4 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/css.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/csv.ts b/understand-anything-plugin/packages/core/src/languages/configs/csv.ts new file mode 100644 index 0000000..d7f6e06 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/csv.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/docker-compose.ts b/understand-anything-plugin/packages/core/src/languages/configs/docker-compose.ts new file mode 100644 index 0000000..d9fadd4 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/docker-compose.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/dockerfile.ts b/understand-anything-plugin/packages/core/src/languages/configs/dockerfile.ts new file mode 100644 index 0000000..918bcbc --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/dockerfile.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/env.ts b/understand-anything-plugin/packages/core/src/languages/configs/env.ts new file mode 100644 index 0000000..791b4b0 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/env.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/github-actions.ts b/understand-anything-plugin/packages/core/src/languages/configs/github-actions.ts new file mode 100644 index 0000000..a37a566 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/github-actions.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/graphql.ts b/understand-anything-plugin/packages/core/src/languages/configs/graphql.ts new file mode 100644 index 0000000..b91e862 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/graphql.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/html.ts b/understand-anything-plugin/packages/core/src/languages/configs/html.ts new file mode 100644 index 0000000..9708f85 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/html.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/index.ts b/understand-anything-plugin/packages/core/src/languages/configs/index.ts index f0a7676..5db6616 100644 --- a/understand-anything-plugin/packages/core/src/languages/configs/index.ts +++ b/understand-anything-plugin/packages/core/src/languages/configs/index.ts @@ -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, }; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/jenkinsfile.ts b/understand-anything-plugin/packages/core/src/languages/configs/jenkinsfile.ts new file mode 100644 index 0000000..383fb36 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/jenkinsfile.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/json-config.ts b/understand-anything-plugin/packages/core/src/languages/configs/json-config.ts new file mode 100644 index 0000000..549deec --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/json-config.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/json-schema.ts b/understand-anything-plugin/packages/core/src/languages/configs/json-schema.ts new file mode 100644 index 0000000..4e0d9d3 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/json-schema.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/kubernetes.ts b/understand-anything-plugin/packages/core/src/languages/configs/kubernetes.ts new file mode 100644 index 0000000..94e03af --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/kubernetes.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/makefile.ts b/understand-anything-plugin/packages/core/src/languages/configs/makefile.ts new file mode 100644 index 0000000..f8c679a --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/makefile.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/markdown.ts b/understand-anything-plugin/packages/core/src/languages/configs/markdown.ts new file mode 100644 index 0000000..f20de85 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/markdown.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/openapi.ts b/understand-anything-plugin/packages/core/src/languages/configs/openapi.ts new file mode 100644 index 0000000..ad026be --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/openapi.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/plaintext.ts b/understand-anything-plugin/packages/core/src/languages/configs/plaintext.ts new file mode 100644 index 0000000..e9e737d --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/plaintext.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/powershell.ts b/understand-anything-plugin/packages/core/src/languages/configs/powershell.ts new file mode 100644 index 0000000..409c30d --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/powershell.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/protobuf.ts b/understand-anything-plugin/packages/core/src/languages/configs/protobuf.ts new file mode 100644 index 0000000..4ae5fe9 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/protobuf.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/restructuredtext.ts b/understand-anything-plugin/packages/core/src/languages/configs/restructuredtext.ts new file mode 100644 index 0000000..42cc732 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/restructuredtext.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/shell.ts b/understand-anything-plugin/packages/core/src/languages/configs/shell.ts new file mode 100644 index 0000000..e3448fc --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/shell.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/sql.ts b/understand-anything-plugin/packages/core/src/languages/configs/sql.ts new file mode 100644 index 0000000..cad3ca4 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/sql.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/terraform.ts b/understand-anything-plugin/packages/core/src/languages/configs/terraform.ts new file mode 100644 index 0000000..23edb2f --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/terraform.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/toml.ts b/understand-anything-plugin/packages/core/src/languages/configs/toml.ts new file mode 100644 index 0000000..70e444c --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/toml.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/xml.ts b/understand-anything-plugin/packages/core/src/languages/configs/xml.ts new file mode 100644 index 0000000..31b512a --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/xml.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/yaml.ts b/understand-anything-plugin/packages/core/src/languages/configs/yaml.ts new file mode 100644 index 0000000..1fb5427 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/yaml.ts @@ -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; diff --git a/understand-anything-plugin/packages/core/src/languages/language-registry.ts b/understand-anything-plugin/packages/core/src/languages/language-registry.ts index 542cd1d..d8eb0c2 100644 --- a/understand-anything-plugin/packages/core/src/languages/language-registry.ts +++ b/understand-anything-plugin/packages/core/src/languages/language-registry.ts @@ -9,6 +9,7 @@ import { builtinLanguageConfigs } from "./configs/index.js"; export class LanguageRegistry { private byId = new Map(); private byExtension = new Map(); + private byFilename = new Map(); 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(); diff --git a/understand-anything-plugin/packages/core/src/languages/types.ts b/understand-anything-plugin/packages/core/src/languages/types.ts index 7d06c3a..61a35fc 100644 --- a/understand-anything-plugin/packages/core/src/languages/types.ts +++ b/understand-anything-plugin/packages/core/src/languages/types.ts @@ -22,11 +22,12 @@ export const FilePatternConfigSchema = z.object({ export type FilePatternConfig = z.infer; -// 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; +/** + * 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), diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts new file mode 100644 index 0000000..cba4b24 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts new file mode 100644 index 0000000..c39a6b0 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts new file mode 100644 index 0000000..6985514 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/index.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/index.ts new file mode 100644 index 0000000..5832091 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/index.ts @@ -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()); +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts new file mode 100644 index 0000000..36db1b7 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts new file mode 100644 index 0000000..5b20139 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts new file mode 100644 index 0000000..23008b8 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts new file mode 100644 index 0000000..1df1b55 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts new file mode 100644 index 0000000..8090c08 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts new file mode 100644 index 0000000..eb741a2 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts new file mode 100644 index 0000000..902b6bb --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts new file mode 100644 index 0000000..b54456b --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts @@ -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; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts new file mode 100644 index 0000000..bbbaa13 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts @@ -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, "\\$&"); + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/registry.ts b/understand-anything-plugin/packages/core/src/plugins/registry.ts index 71233fe..67261f5 100644 --- a/understand-anything-plugin/packages/core/src/plugins/registry.ts +++ b/understand-anything-plugin/packages/core/src/plugins/registry.ts @@ -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); } diff --git a/understand-anything-plugin/packages/core/src/schema.ts b/understand-anything-plugin/packages/core/src/schema.ts index 18ccbb6..5b9ad1a 100644 --- a/understand-anything-plugin/packages/core/src/schema.ts +++ b/understand-anything-plugin/packages/core/src/schema.ts @@ -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 = { 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 = { 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): { 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(), diff --git a/understand-anything-plugin/packages/core/src/types.test.ts b/understand-anything-plugin/packages/core/src/types.test.ts index e155b8f..2407d8d 100644 --- a/understand-anything-plugin/packages/core/src/types.test.ts +++ b/understand-anything-plugin/packages/core/src/types.test.ts @@ -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); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/types.ts b/understand-anything-plugin/packages/core/src/types.ts index 819f17e..ab92cd7 100644 --- a/understand-anything-plugin/packages/core/src/types.ts +++ b/understand-anything-plugin/packages/core/src/types.ts @@ -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[]; } diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 0a39b73..c32e098 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -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(null); const [graphIssues, setGraphIssues] = useState([]); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); @@ -250,6 +252,35 @@ function Dashboard({ accessToken }: { accessToken: string }) {
+
+ {([ + { 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) => ( + + ))} +