From b1f763adac9ded43bd9df97eef944ef16ce76d04 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:29:01 +0800 Subject: [PATCH 01/24] feat(core): extend GraphNode/EdgeType/StructuralAnalysis for non-code file types Add 8 new node types (config, document, service, table, endpoint, pipeline, schema, resource) and 8 new edge types (deploys, serves, migrates, documents, provisions, routes, defines_schema, triggers). Add StructuralAnalysis sub-interfaces: SectionInfo, DefinitionInfo, ServiceInfo, EndpointInfo, StepInfo, ResourceInfo, ReferenceResolution. Make resolveImports optional on AnalyzerPlugin and add extractReferences. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/core/src/types.test.ts | 77 ++++++++++++++++++- .../packages/core/src/types.ts | 66 ++++++++++++++-- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/types.test.ts b/understand-anything-plugin/packages/core/src/types.test.ts index e155b8f..c12c106 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, StructuralAnalysis, AnalyzerPlugin, ReferenceResolution } from "./types.js"; describe("KnowledgeGraph types", () => { it("should create a valid empty KnowledgeGraph", () => { @@ -115,3 +115,78 @@ describe("KnowledgeGraph types", () => { expect(maxWeightEdge.weight).toBe(1); }); }); + +describe("Extended types", () => { + it("accepts all 13 node types", () => { + const nodeTypes: GraphNode["type"][] = [ + "file", "function", "class", "module", "concept", + "config", "document", "service", "table", "endpoint", + "pipeline", "schema", "resource", + ]; + expect(nodeTypes).toHaveLength(13); + }); + + 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] }], + 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.endpoints).toHaveLength(1); + expect(analysis.steps).toHaveLength(1); + expect(analysis.resources).toHaveLength(1); + }); + + 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..d38a68f 100644 --- a/understand-anything-plugin/packages/core/src/types.ts +++ b/understand-anything-plugin/packages/core/src/types.ts @@ -1,15 +1,19 @@ -// Edge types (18 total in 5 categories: Structural, Behavioral, Data flow, Dependencies, Semantic) +// Edge types (26 total in 6 categories: Structural, Behavioral, Data flow, Dependencies, Semantic, Infrastructure) 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" | "migrates" | "documents" // Infrastructure + | "provisions" | "routes" | "defines_schema" | "triggers"; // Infrastructure -// 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: "file" | "function" | "class" | "module" | "concept" + | "config" | "document" | "service" | "table" | "endpoint" + | "pipeline" | "schema" | "resource"; name: string; filePath?: string; lineRange?: [number, number]; @@ -86,12 +90,63 @@ 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; + kind: string; // "table", "message", "type", "schema" + lineRange: [number, number]; + fields: string[]; +} + +export interface ServiceInfo { + name: string; + image?: string; + ports: 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 +165,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[]; } From 1f554146c030ef8194f9d9a2b1241c0e1fddecc5 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:30:17 +0800 Subject: [PATCH 02/24] feat(core): extend Zod schemas and aliases for 8 new node/edge types Update EdgeTypeSchema with 8 new edge types (deploys, serves, migrates, documents, provisions, routes, defines_schema, triggers). Update GraphNodeSchema with 8 new node types (config, document, service, table, endpoint, pipeline, schema, resource). Add NODE_TYPE_ALIASES for non-code types (container->service, doc->document, workflow->pipeline, etc.) and EDGE_TYPE_ALIASES (describes->documents, creates->provisions, exposes->serves, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/src/__tests__/schema.test.ts | 58 +++++++++++++++++++ .../packages/core/src/schema.ts | 51 +++++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) 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/schema.ts b/understand-anything-plugin/packages/core/src/schema.ts index 18ccbb6..f661c28 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", "migrates", "documents", // Infrastructure + "provisions", "routes", "defines_schema", "triggers", // Infrastructure ]); // 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(), From 9eb220c4924c56a6dbcc8eee57ab30d5a4f375ee Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:31:00 +0800 Subject: [PATCH 03/24] feat(core): make resolveImports optional on AnalyzerPlugin Update PluginRegistry.resolveImports() to check for plugin.resolveImports existence before calling it. Non-code plugins (e.g., markdown, dockerfile) don't need import resolution, so this method is now optional. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/src/__tests__/plugin-registry.test.ts | 13 +++++++++++++ .../packages/core/src/plugins/registry.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) 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..5e5976c 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 @@ -178,4 +178,17 @@ 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(); + }); }); 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); } From c76b0a1f2d43df97e48c56d7647aed41339a09da Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:34:16 +0800 Subject: [PATCH 04/24] feat(core): add 26 non-code language configs with filename-based detection Add LanguageConfig files for: markdown, yaml, json, toml, env, xml, dockerfile, sql, graphql, protobuf, terraform, github-actions, makefile, shell, html, css, openapi, kubernetes, docker-compose, json-schema, csv, restructuredtext, powershell, batch, jenkinsfile, plaintext. Add filenames? field to LanguageConfigSchema for filename-based detection (Dockerfile, Makefile, Jenkinsfile, docker-compose.yml, .env variants). Update LanguageRegistry.getForFile() to check filename matches first (more specific) before falling back to extension-based lookup. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/src/languages/configs/batch.ts | 14 ++++ .../core/src/languages/configs/css.ts | 14 ++++ .../core/src/languages/configs/csv.ts | 14 ++++ .../src/languages/configs/docker-compose.ts | 15 ++++ .../core/src/languages/configs/dockerfile.ts | 15 ++++ .../core/src/languages/configs/env.ts | 15 ++++ .../src/languages/configs/github-actions.ts | 14 ++++ .../core/src/languages/configs/graphql.ts | 14 ++++ .../core/src/languages/configs/html.ts | 14 ++++ .../core/src/languages/configs/index.ts | 83 +++++++++++++++++++ .../core/src/languages/configs/jenkinsfile.ts | 15 ++++ .../core/src/languages/configs/json-config.ts | 14 ++++ .../core/src/languages/configs/json-schema.ts | 14 ++++ .../core/src/languages/configs/kubernetes.ts | 14 ++++ .../core/src/languages/configs/makefile.ts | 15 ++++ .../core/src/languages/configs/markdown.ts | 14 ++++ .../core/src/languages/configs/openapi.ts | 14 ++++ .../core/src/languages/configs/plaintext.ts | 14 ++++ .../core/src/languages/configs/powershell.ts | 14 ++++ .../core/src/languages/configs/protobuf.ts | 14 ++++ .../src/languages/configs/restructuredtext.ts | 14 ++++ .../core/src/languages/configs/shell.ts | 14 ++++ .../core/src/languages/configs/sql.ts | 14 ++++ .../core/src/languages/configs/terraform.ts | 14 ++++ .../core/src/languages/configs/toml.ts | 14 ++++ .../core/src/languages/configs/xml.ts | 14 ++++ .../core/src/languages/configs/yaml.ts | 14 ++++ .../core/src/languages/language-registry.ts | 11 +++ .../packages/core/src/languages/types.ts | 3 +- 29 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/batch.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/css.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/csv.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/docker-compose.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/dockerfile.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/env.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/github-actions.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/graphql.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/html.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/jenkinsfile.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/json-config.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/json-schema.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/kubernetes.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/makefile.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/markdown.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/openapi.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/plaintext.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/powershell.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/protobuf.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/restructuredtext.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/shell.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/sql.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/terraform.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/toml.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/xml.ts create mode 100644 understand-anything-plugin/packages/core/src/languages/configs/yaml.ts 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..8a132c8 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/json-schema.ts @@ -0,0 +1,14 @@ +import type { LanguageConfig } from "../types.js"; + +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..9380319 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/kubernetes.ts @@ -0,0 +1,14 @@ +import type { LanguageConfig } from "../types.js"; + +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..ae250e5 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/openapi.ts @@ -0,0 +1,14 @@ +import type { LanguageConfig } from "../types.js"; + +export const openapiConfig = { + id: "openapi", + displayName: "OpenAPI", + extensions: [], + 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..c5499a9 100644 --- a/understand-anything-plugin/packages/core/src/languages/types.ts +++ b/understand-anything-plugin/packages/core/src/languages/types.ts @@ -26,7 +26,8 @@ export type FilePatternConfig = z.infer; 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, From 3fd688ac374f25ca04be2fabdfbd922f880cfe71 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:38:25 +0800 Subject: [PATCH 05/24] feat(core): add 12 custom parsers for non-code file types Add regex/parser-based analyzers for: Markdown, YAML, JSON, TOML, Env, Dockerfile, SQL, GraphQL, Protobuf, Terraform, Makefile, Shell. Each implements AnalyzerPlugin with analyzeFile() and optional extractReferences(). Uses `yaml` npm package for YAML parsing, built-in JSON.parse for JSON, regex for all others. Add registerAllParsers() helper to register all parsers at once. Add comprehensive test suite with 35 tests covering all parsers. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/core/package.json | 1 + .../core/src/__tests__/parsers.test.ts | 386 ++++++++++++++++++ .../src/plugins/parsers/dockerfile-parser.ts | 68 +++ .../core/src/plugins/parsers/env-parser.ts | 36 ++ .../src/plugins/parsers/graphql-parser.ts | 118 ++++++ .../core/src/plugins/parsers/index.ts | 44 ++ .../core/src/plugins/parsers/json-parser.ts | 65 +++ .../src/plugins/parsers/makefile-parser.ts | 43 ++ .../src/plugins/parsers/markdown-parser.ts | 56 +++ .../src/plugins/parsers/protobuf-parser.ts | 133 ++++++ .../core/src/plugins/parsers/shell-parser.ts | 70 ++++ .../core/src/plugins/parsers/sql-parser.ts | 98 +++++ .../src/plugins/parsers/terraform-parser.ts | 122 ++++++ .../core/src/plugins/parsers/toml-parser.ts | 41 ++ .../core/src/plugins/parsers/yaml-parser.ts | 66 +++ understand-anything-plugin/pnpm-lock.yaml | 98 ++--- 16 files changed, 1388 insertions(+), 57 deletions(-) create mode 100644 understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/index.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.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__/parsers.test.ts b/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts new file mode 100644 index 0000000..13dca7e --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts @@ -0,0 +1,386 @@ +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" }); + }); +}); + +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/plugins/parsers/dockerfile-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts new file mode 100644 index 0000000..a70726b --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts @@ -0,0 +1,68 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ServiceInfo, StepInfo } from "../../types.js"; + +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"); + const ports: number[] = []; + + // Collect all EXPOSE ports + for (const line of lines) { + const exposeMatch = line.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); + } + } + } + + // Extract FROM stages + for (const line of lines) { + const fromMatch = line.match(/^FROM\s+(\S+)(?:\s+[Aa][Ss]\s+(\S+))?/i); + if (fromMatch) { + const image = fromMatch[1]; + const name = fromMatch[2] ?? image.split(":")[0].split("/").pop() ?? image; + stages.push({ + name, + image, + ports: stages.length === 0 ? ports : [], // Assign ports to first stage only as default + }); + } + } + + 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..a2f22a3 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts @@ -0,0 +1,36 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo } from "../../types.js"; + +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..184b6b2 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts @@ -0,0 +1,118 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo, EndpointInfo } from "../../types.js"; + +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..baf74a5 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts @@ -0,0 +1,65 @@ +import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo, ReferenceResolution } from "../../types.js"; + +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 { + // JSON parse failed — skip + } + 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..4c1b9bf --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts @@ -0,0 +1,43 @@ +import type { AnalyzerPlugin, StructuralAnalysis, StepInfo } from "../../types.js"; + +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_][\w.-]*)(?:\s+.*)?:/); + if (match && !lines[i].includes(":=") && !lines[i].includes("?=")) { + // 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: match[1], + 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..f0192f9 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts @@ -0,0 +1,56 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ReferenceResolution, SectionInfo } from "../../types.js"; + +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..6c49886 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts @@ -0,0 +1,133 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo, EndpointInfo } from "../../types.js"; + +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; + } + } + 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..09f6c30 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts @@ -0,0 +1,70 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ReferenceResolution } from "../../types.js"; + +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 + let endLine = i; + if (lines[i].includes("{")) { + let depth = 0; + for (let j = i; 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..01240b1 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts @@ -0,0 +1,98 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo } from "../../types.js"; + +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..0fd44bf --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts @@ -0,0 +1,122 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ResourceInfo, DefinitionInfo } from "../../types.js"; + +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; + } + } + 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..114db91 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts @@ -0,0 +1,41 @@ +import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo } from "../../types.js"; + +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..7f9dfd6 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts @@ -0,0 +1,66 @@ +import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo } from "../../types.js"; +import { parse as parseYAML } from "yaml"; + +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 { + // 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/pnpm-lock.yaml b/understand-anything-plugin/pnpm-lock.yaml index c3844a3..d2f0f61 100644 --- a/understand-anything-plugin/pnpm-lock.yaml +++ b/understand-anything-plugin/pnpm-lock.yaml @@ -20,19 +20,23 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages/core: + dependencies: + yaml: + specifier: ^2.8.3 + version: 2.8.3 devDependencies: '@types/node': specifier: ^25.5.0 version: 25.5.0 '@vitest/coverage-v8': specifier: 3.2.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages/dashboard: dependencies: @@ -66,7 +70,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@types/react': specifier: ^19.0.0 version: 19.2.14 @@ -75,7 +79,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) tailwindcss: specifier: ^4.0.0 version: 4.2.2 @@ -84,7 +88,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages: @@ -406,79 +410,66 @@ packages: resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.0': resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.0': resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.0': resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.0': resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.0': resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.0': resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.0': resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.0': resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.0': resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.0': resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.0': resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.0': resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.0': resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} @@ -548,28 +539,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -1092,28 +1079,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1579,6 +1562,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -1991,12 +1979,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) '@types/babel__core@7.20.5': dependencies: @@ -2089,7 +2077,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -2097,11 +2085,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -2116,7 +2104,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -2128,21 +2116,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) - - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -3076,13 +3056,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3097,13 +3077,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3118,7 +3098,7 @@ snapshots: - tsx - yaml - vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -3131,8 +3111,9 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + yaml: 2.8.3 - vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -3145,12 +3126,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + yaml: 2.8.3 - vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3168,8 +3150,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -3188,11 +3170,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3210,8 +3192,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -3253,6 +3235,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.3: {} + zustand@4.5.7(@types/react@19.2.14)(react@19.2.4): dependencies: use-sync-external-store: 1.6.0(react@19.2.4) From 2bb107e2d9bdae85851209c5ca76299364527968 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:40:04 +0800 Subject: [PATCH 06/24] feat(core): add non-code file support to GraphBuilder Add addNonCodeFile() and addNonCodeFileWithAnalysis() methods that create graph nodes with the appropriate non-code types (document, config, service, table, endpoint, pipeline, schema, resource). addNonCodeFileWithAnalysis() creates child nodes for definitions, services, endpoints, steps, and resources with "contains" edges to the parent file. Add mapKindToNodeType() helper for mapping definition kinds to node types. Extend EXTENSION_LANGUAGE map with all non-code file extensions. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/src/analyzer/graph-builder.test.ts | 126 +++++++++++++ .../core/src/analyzer/graph-builder.ts | 168 +++++++++++++++++- 2 files changed, 292 insertions(+), 2 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts index 461e252..6dac807 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts @@ -212,4 +212,130 @@ describe("GraphBuilder", () => { const graph = builder.build(); expect(graph.project.languages).toEqual(["go", "javascript", "rust"]); }); + + describe("Non-code file support", () => { + it("adds non-code file nodes with correct types", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFile("README.md", { + nodeType: "document", + summary: "Project documentation", + tags: ["documentation"], + complexity: "simple", + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(1); + expect(graph.nodes[0].type).toBe("document"); + expect(graph.nodes[0].id).toBe("file:README.md"); + }); + + it("adds non-code child nodes (definitions)", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("schema.sql", { + nodeType: "file", + summary: "Database schema", + tags: ["database"], + complexity: "moderate", + definitions: [ + { name: "users", kind: "table", lineRange: [1, 20] as [number, number], fields: ["id", "name", "email"] }, + ], + }); + const graph = builder.build(); + // File node + table child node + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[1].type).toBe("table"); + expect(graph.nodes[1].name).toBe("users"); + // Contains edge + expect(graph.edges.some(e => e.type === "contains" && e.target.includes("users"))).toBe(true); + }); + + it("adds service child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("docker-compose.yml", { + nodeType: "config", + summary: "Docker compose config", + tags: ["infra"], + complexity: "moderate", + services: [ + { name: "web", image: "node:22", ports: [3000] }, + { name: "db", image: "postgres:15", ports: [5432] }, + ], + }); + const graph = builder.build(); + // File node + 2 service child nodes + expect(graph.nodes).toHaveLength(3); + expect(graph.nodes[1].type).toBe("service"); + expect(graph.nodes[1].name).toBe("web"); + expect(graph.nodes[2].type).toBe("service"); + expect(graph.nodes[2].name).toBe("db"); + }); + + it("adds endpoint child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("schema.graphql", { + nodeType: "schema", + summary: "GraphQL schema", + tags: ["api"], + complexity: "moderate", + endpoints: [ + { method: "Query", path: "users", lineRange: [5, 5] as [number, number] }, + ], + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[1].type).toBe("endpoint"); + }); + + it("adds resource child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("main.tf", { + nodeType: "resource", + summary: "Terraform config", + tags: ["infra"], + complexity: "moderate", + resources: [ + { name: "aws_s3_bucket.main", kind: "aws_s3_bucket", lineRange: [1, 10] as [number, number] }, + ], + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[1].type).toBe("resource"); + expect(graph.nodes[1].name).toBe("aws_s3_bucket.main"); + }); + + it("adds step child nodes", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addNonCodeFileWithAnalysis("Makefile", { + nodeType: "pipeline", + summary: "Build targets", + tags: ["build"], + complexity: "simple", + steps: [ + { name: "build", lineRange: [1, 3] as [number, number] }, + { name: "test", lineRange: [5, 7] as [number, number] }, + ], + }); + const graph = builder.build(); + expect(graph.nodes).toHaveLength(3); + expect(graph.nodes[1].type).toBe("pipeline"); + expect(graph.nodes[1].name).toBe("build"); + }); + + it("detects non-code languages from EXTENSION_LANGUAGE map", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addFile("config.yaml", { summary: "Config", tags: [], complexity: "simple" }); + const graph = builder.build(); + expect(graph.project.languages).toContain("yaml"); + }); + + it("detects new non-code extensions", () => { + const builder = new GraphBuilder("test", "abc123"); + builder.addFile("schema.graphql", { summary: "Schema", tags: [], complexity: "simple" }); + builder.addFile("main.tf", { summary: "Terraform", tags: [], complexity: "simple" }); + builder.addFile("types.proto", { summary: "Protobuf", tags: [], complexity: "simple" }); + const graph = builder.build(); + expect(graph.project.languages).toContain("graphql"); + expect(graph.project.languages).toContain("terraform"); + expect(graph.project.languages).toContain("protobuf"); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts index aaa170b..1e30c88 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts @@ -3,6 +3,12 @@ import type { GraphNode, GraphEdge, StructuralAnalysis, + DefinitionInfo, + ServiceInfo, + EndpointInfo, + StepInfo, + ResourceInfo, + SectionInfo, } from "../types.js"; interface FileMeta { @@ -16,7 +22,21 @@ interface FileAnalysisMeta extends FileMeta { fileSummary: string; } +interface NonCodeFileMeta extends FileMeta { + nodeType: GraphNode["type"]; +} + +interface NonCodeFileAnalysisMeta extends NonCodeFileMeta { + definitions?: DefinitionInfo[]; + services?: ServiceInfo[]; + endpoints?: EndpointInfo[]; + steps?: StepInfo[]; + resources?: ResourceInfo[]; + sections?: SectionInfo[]; +} + const EXTENSION_LANGUAGE: Record = { + // Code languages ".ts": "typescript", ".tsx": "typescript", ".js": "javascript", @@ -37,20 +57,41 @@ const EXTENSION_LANGUAGE: Record = { ".cs": "csharp", ".php": "php", ".lua": "lua", + // Non-code languages ".sh": "shell", ".bash": "shell", ".zsh": "shell", ".json": "json", + ".jsonc": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".xml": "xml", ".html": "html", + ".htm": "html", ".css": "css", - ".scss": "scss", - ".less": "less", + ".scss": "css", + ".less": "css", ".md": "markdown", + ".mdx": "markdown", ".sql": "sql", + ".graphql": "graphql", + ".gql": "graphql", + ".proto": "protobuf", + ".tf": "terraform", + ".tfvars": "terraform", + ".mk": "makefile", + ".env": "env", + ".csv": "csv", + ".tsv": "csv", + ".rst": "restructuredtext", + ".ps1": "powershell", + ".psm1": "powershell", + ".psd1": "powershell", + ".bat": "batch", + ".cmd": "batch", + ".txt": "plaintext", + ".svg": "xml", }; function detectLanguage(filePath: string): string { @@ -187,6 +228,129 @@ export class GraphBuilder { }); } + addNonCodeFile(filePath: string, meta: NonCodeFileMeta): void { + const lang = detectLanguage(filePath); + if (lang !== "unknown") this.languages.add(lang); + const name = filePath.split("/").pop() ?? filePath; + this.nodes.push({ + id: `file:${filePath}`, + type: meta.nodeType, + name, + filePath, + summary: meta.summary, + tags: meta.tags, + complexity: meta.complexity, + }); + } + + addNonCodeFileWithAnalysis(filePath: string, meta: NonCodeFileAnalysisMeta): void { + this.addNonCodeFile(filePath, meta); + const fileId = `file:${filePath}`; + + // Create child nodes for definitions (tables, schemas, etc.) + for (const def of meta.definitions ?? []) { + const childId = `${def.kind}:${filePath}:${def.name}`; + this.nodes.push({ + id: childId, + type: this.mapKindToNodeType(def.kind), + name: def.name, + filePath, + lineRange: def.lineRange, + summary: `${def.kind}: ${def.name} (${def.fields.length} fields)`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for services + for (const svc of meta.services ?? []) { + const childId = `service:${filePath}:${svc.name}`; + this.nodes.push({ + id: childId, + type: "service", + name: svc.name, + filePath, + summary: `Service ${svc.name}${svc.image ? ` (image: ${svc.image})` : ""}`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for endpoints + for (const ep of meta.endpoints ?? []) { + const childId = `endpoint:${filePath}:${ep.path}`; + this.nodes.push({ + id: childId, + type: "endpoint", + name: `${ep.method ?? ""} ${ep.path}`.trim(), + filePath, + lineRange: ep.lineRange, + summary: `Endpoint: ${ep.method ?? ""} ${ep.path}`.trim(), + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for steps (pipeline/makefile targets) + for (const step of meta.steps ?? []) { + const childId = `step:${filePath}:${step.name}`; + this.nodes.push({ + id: childId, + type: "pipeline", + name: step.name, + filePath, + lineRange: step.lineRange, + summary: `Step: ${step.name}`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + + // Create child nodes for resources (Terraform, etc.) + for (const res of meta.resources ?? []) { + const childId = `resource:${filePath}:${res.name}`; + this.nodes.push({ + id: childId, + type: "resource", + name: res.name, + filePath, + lineRange: res.lineRange, + summary: `Resource: ${res.name} (${res.kind})`, + tags: [], + complexity: meta.complexity, + }); + this.edges.push({ source: fileId, target: childId, type: "contains", direction: "forward", weight: 1 }); + } + } + + private mapKindToNodeType(kind: string): GraphNode["type"] { + const mapping: Record = { + table: "table", + view: "table", + index: "table", + message: "schema", + type: "schema", + enum: "schema", + resource: "resource", + module: "resource", + service: "service", + deployment: "service", + job: "pipeline", + stage: "pipeline", + target: "pipeline", + route: "endpoint", + query: "endpoint", + mutation: "endpoint", + variable: "config", + output: "config", + }; + return mapping[kind] ?? "concept"; + } + build(): KnowledgeGraph { return { version: "1.0.0", From db572231f8127931050c13c7ff172f99e68cf713 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:40:55 +0800 Subject: [PATCH 07/24] feat(core): export new types and parsers from core Export all 12 non-code parsers (MarkdownParser, YAMLConfigParser, JSONConfigParser, TOMLParser, EnvParser, DockerfileParser, SQLParser, GraphQLParser, ProtobufParser, TerraformParser, MakefileParser, ShellParser) and the registerAllParsers() helper from core index. New type exports (SectionInfo, DefinitionInfo, ServiceInfo, etc.) are already covered by the existing `export * from "./types.js"`. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/core/src/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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"; From 10173b19c589d0e0506ed8a9810230a1ac9920ef Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:45:08 +0800 Subject: [PATCH 08/24] feat(dashboard): add 8 new node type colors to all theme presets Add colors for config, document, service, table, endpoint, pipeline, schema, and resource node types to all 5 theme presets (dark-gold, dark-ocean, dark-forest, dark-rose, light-minimal) and register the CSS variables in the Tailwind v4 @theme block. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/dashboard/src/index.css | 8 ++++ .../packages/dashboard/src/themes/presets.ts | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css index 95b5440..bb8f99d 100644 --- a/understand-anything-plugin/packages/dashboard/src/index.css +++ b/understand-anything-plugin/packages/dashboard/src/index.css @@ -27,6 +27,14 @@ --color-node-class: #8b6fb0; --color-node-module: #c9a06c; --color-node-concept: #b07a8a; + --color-node-config: #5eead4; + --color-node-document: #7dd3fc; + --color-node-service: #a78bfa; + --color-node-table: #6ee7b7; + --color-node-endpoint: #fdba74; + --color-node-pipeline: #fda4af; + --color-node-schema: #fcd34d; + --color-node-resource: #a5b4fc; /* Diff */ --color-diff-changed: #e05252; diff --git a/understand-anything-plugin/packages/dashboard/src/themes/presets.ts b/understand-anything-plugin/packages/dashboard/src/themes/presets.ts index 35e0517..b1f9c05 100644 --- a/understand-anything-plugin/packages/dashboard/src/themes/presets.ts +++ b/understand-anything-plugin/packages/dashboard/src/themes/presets.ts @@ -42,6 +42,14 @@ export const PRESETS: ThemePreset[] = [ "node-class": "#8b6fb0", "node-module": "#c9a06c", "node-concept": "#b07a8a", + "node-config": "#5eead4", + "node-document": "#7dd3fc", + "node-service": "#a78bfa", + "node-table": "#6ee7b7", + "node-endpoint": "#fdba74", + "node-pipeline": "#fda4af", + "node-schema": "#fcd34d", + "node-resource": "#a5b4fc", }, }, { @@ -63,6 +71,14 @@ export const PRESETS: ThemePreset[] = [ "node-class": "#8b6fb0", "node-module": "#c9a06c", "node-concept": "#b07a8a", + "node-config": "#5eead4", + "node-document": "#7dd3fc", + "node-service": "#a78bfa", + "node-table": "#6ee7b7", + "node-endpoint": "#fdba74", + "node-pipeline": "#fda4af", + "node-schema": "#fcd34d", + "node-resource": "#a5b4fc", }, }, { @@ -84,6 +100,14 @@ export const PRESETS: ThemePreset[] = [ "node-class": "#8b6fb0", "node-module": "#c9a06c", "node-concept": "#b07a8a", + "node-config": "#5eead4", + "node-document": "#7dd3fc", + "node-service": "#a78bfa", + "node-table": "#6ee7b7", + "node-endpoint": "#fdba74", + "node-pipeline": "#fda4af", + "node-schema": "#fcd34d", + "node-resource": "#a5b4fc", }, }, { @@ -105,6 +129,14 @@ export const PRESETS: ThemePreset[] = [ "node-class": "#8b6fb0", "node-module": "#c9a06c", "node-concept": "#b07a8a", + "node-config": "#5eead4", + "node-document": "#7dd3fc", + "node-service": "#a78bfa", + "node-table": "#6ee7b7", + "node-endpoint": "#fdba74", + "node-pipeline": "#fda4af", + "node-schema": "#fcd34d", + "node-resource": "#a5b4fc", }, }, { @@ -126,6 +158,14 @@ export const PRESETS: ThemePreset[] = [ "node-class": "#755d99", "node-module": "#a88a56", "node-concept": "#966674", + "node-config": "#14b8a6", + "node-document": "#38bdf8", + "node-service": "#8b5cf6", + "node-table": "#34d399", + "node-endpoint": "#fb923c", + "node-pipeline": "#fb7185", + "node-schema": "#facc15", + "node-resource": "#818cf8", }, }, ]; From 231ef484a0268f1054f030b67ee57611a74cef7e Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:45:20 +0800 Subject: [PATCH 09/24] feat(agents): update project-scanner to discover all file types Remove the code-only file filter so the scanner discovers non-code files (.md, .yaml, .json, .sql, .tf, Dockerfile, etc.). Add fileCategory field to each discovered file (code/config/docs/infra/data/script/markup) with extension-based category detection logic. Expand language detection table to cover 26+ file types. Infrastructure tooling detection added to framework detection step. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../understand/project-scanner-prompt.md | 120 +++++++++++++----- 1 file changed, 91 insertions(+), 29 deletions(-) diff --git a/understand-anything-plugin/skills/understand/project-scanner-prompt.md b/understand-anything-plugin/skills/understand/project-scanner-prompt.md index 8febcc8..0a0f264 100644 --- a/understand-anything-plugin/skills/understand/project-scanner-prompt.md +++ b/understand-anything-plugin/skills/understand/project-scanner-prompt.md @@ -2,7 +2,7 @@ > Used by `/understand` Phase 1. Dispatch as a subagent with this full content as the prompt. -You are a meticulous project inventory specialist. Your job is to scan a codebase directory and produce a precise, structured inventory of all source files, detected languages, frameworks, and estimated complexity. Accuracy is paramount -- every file path you report must actually exist on disk. +You are a meticulous project inventory specialist. Your job is to scan a codebase directory and produce a precise, structured inventory of all project files, detected languages, frameworks, and estimated complexity. Accuracy is paramount -- every file path you report must actually exist on disk. ## Task @@ -12,7 +12,7 @@ Scan the project directory provided in the prompt and produce a JSON inventory. ## Phase 1 -- Discovery Script -Write a script that discovers all source files, detects languages and frameworks, counts lines, and produces structured JSON. Choose the best language for this task (bash, Node.js, or Python -- whichever is available on the system). The script must handle errors gracefully and never crash on unexpected input. +Write a script that discovers all project files (including non-code files like configs, docs, and infrastructure), detects languages and frameworks, counts lines, and produces structured JSON. Choose the best language for this task (bash, Node.js, or Python -- whichever is available on the system). The script must handle errors gracefully and never crash on unexpected input. ### Script Requirements @@ -38,10 +38,19 @@ Remove ALL files matching these patterns: - **Binary/asset files:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.mp3`, `.mp4`, `.pdf`, `.zip`, `.tar`, `.gz` - **Generated files:** `*.min.js`, `*.min.css`, `*.map`, `*.d.ts`, `*.generated.*` - **IDE/editor config:** paths containing `.idea/`, `.vscode/` -- **Config/doc files:** `*.md`, `*.txt`, `*.yml`, `*.yaml`, `*.toml`, `*.json`, `*.xml`, `*.lock`, `*.cfg`, `*.ini`, `Makefile`, `Dockerfile` - **Misc non-source:** `LICENSE`, `.gitignore`, `.editorconfig`, `.prettierrc`, `.eslintrc*`, `*.log` -The goal is to keep ONLY source code files (`.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.go`, `.rs`, `.java`, `.rb`, `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp`, `.c`, `.cs`, `.swift`, `.kt`, `.php`, `.vue`, `.svelte`, `.sh`, `.bash`). +**IMPORTANT:** Do NOT exclude non-code project files. The following MUST be kept: +- Documentation: `*.md`, `*.rst`, `*.txt` (except `LICENSE`) +- Configuration: `*.yaml`, `*.yml`, `*.json`, `*.toml`, `*.xml`, `*.cfg`, `*.ini`, `*.env`, `*.env.example` +- Infrastructure: `Dockerfile`, `docker-compose.*`, `*.tf`, `Makefile`, `Jenkinsfile`, `Procfile`, `Vagrantfile` +- CI/CD: `.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/*`, `Jenkinsfile` +- Data/Schema: `*.sql`, `*.graphql`, `*.gql`, `*.proto`, `*.prisma`, `*.schema.json` +- Web markup: `*.html`, `*.css`, `*.scss`, `*.sass`, `*.less` +- Shell scripts: `*.sh`, `*.bash`, `*.ps1`, `*.bat` +- Kubernetes: `*.k8s.yaml`, `*.k8s.yml`, paths containing `k8s/`, paths containing `kubernetes/` + +**Note on package manifests:** Config files read for framework detection (`package.json`, `tsconfig.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.) should also appear in the file list with `fileCategory: "config"`. **Step 3 -- Language Detection** @@ -64,17 +73,48 @@ Map file extensions to language identifiers: | `.php` | `php` | | `.vue` | `vue` | | `.svelte` | `svelte` | -| `.sh`, `.bash` | `bash` | +| `.sh`, `.bash` | `shell` | +| `.md`, `.rst` | `markdown` | +| `.yaml`, `.yml` | `yaml` | +| `.json` | `json` | +| `.toml` | `toml` | +| `.sql` | `sql` | +| `.graphql`, `.gql` | `graphql` | +| `.proto` | `protobuf` | +| `.tf`, `.tfvars` | `terraform` | +| `.html`, `.htm` | `html` | +| `.css`, `.scss`, `.sass`, `.less` | `css` | +| `.xml` | `xml` | +| `.cfg`, `.ini`, `.env` | `config` | +| `Dockerfile` (no extension) | `dockerfile` | +| `Makefile` (no extension) | `makefile` | +| `Jenkinsfile` (no extension) | `groovy` | Collect unique languages, sorted alphabetically. -**Step 4 -- Line Counting** +**Step 4 -- File Category Detection** -For each source file, count lines using `wc -l`. For efficiency: +Assign a `fileCategory` to each discovered file based on its extension and path: + +| Pattern | Category | +|---|---| +| `.md`, `.rst`, `.txt` (except `LICENSE`) | `docs` | +| `.yaml`, `.yml`, `.json`, `.toml`, `.xml`, `.cfg`, `.ini`, `.env`, `tsconfig.json`, `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod` | `config` | +| `Dockerfile`, `docker-compose.*`, `.tf`, `.tfvars`, `Makefile`, `Jenkinsfile`, `Procfile`, `Vagrantfile`, `.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/*`, `*.k8s.yaml`, `*.k8s.yml`, paths in `k8s/` or `kubernetes/` | `infra` | +| `.sql`, `.graphql`, `.gql`, `.proto`, `.prisma`, `*.schema.json`, `.csv` | `data` | +| `.sh`, `.bash`, `.ps1`, `.bat` | `script` | +| `.html`, `.htm`, `.css`, `.scss`, `.sass`, `.less` | `markup` | +| All other extensions (`.ts`, `.tsx`, `.js`, `.py`, `.go`, `.rs`, etc.) | `code` | + +**Priority rule:** When a file matches multiple categories, use the first match from the table above (most specific wins). For example, `docker-compose.yml` is `infra`, not `config`. + +**Step 5 -- Line Counting** + +For each file, count lines using `wc -l`. For efficiency: - If fewer than 500 files, count all of them - If 500+ files, count all of them but batch the `wc -l` calls (pass multiple files per invocation to avoid spawning thousands of processes) -**Step 5 -- Framework Detection** +**Step 6 -- Framework Detection** Read config files (if they exist) and extract framework information: - `package.json` -- parse JSON, extract `name`, `description`, `dependencies`, `devDependencies`. Match dependency names against known frameworks: `react`, `vue`, `svelte`, `@angular/core`, `express`, `fastify`, `koa`, `next`, `nuxt`, `vite`, `vitest`, `jest`, `mocha`, `tailwindcss`, `prisma`, `typeorm`, `sequelize`, `mongoose`, `redux`, `zustand`, `mobx` @@ -89,15 +129,23 @@ Read config files (if they exist) and extract framework information: - `Cargo.toml` dependencies -- if present, read `[dependencies]` and match crate names against known Rust frameworks: `actix-web`, `axum`, `rocket`, `diesel`, `tokio`, `serde`, `warp` - `pom.xml` / `build.gradle` / `build.gradle.kts` -- if present, confirms Java/Kotlin project; match dependency names against known JVM frameworks: `spring-boot`, `spring-web`, `spring-data`, `quarkus`, `micronaut`, `hibernate`, `jakarta`, `junit`, `ktor` -**Step 6 -- Complexity Estimation** +Also detect infrastructure tooling from discovered files: +- Presence of `Dockerfile` → add `Docker` to frameworks +- Presence of `docker-compose.yml` or `docker-compose.yaml` → add `Docker Compose` to frameworks +- Presence of `*.tf` files → add `Terraform` to frameworks +- Presence of `.github/workflows/*.yml` → add `GitHub Actions` to frameworks +- Presence of `.gitlab-ci.yml` → add `GitLab CI` to frameworks +- Presence of `Jenkinsfile` → add `Jenkins` to frameworks -Classify by source file count: -- `small`: 1-20 files -- `moderate`: 21-100 files -- `large`: 101-500 files +**Step 7 -- Complexity Estimation** + +Classify by total file count (including non-code files): +- `small`: 1-30 files +- `moderate`: 31-150 files +- `large`: 151-500 files - `very-large`: >500 files -**Step 7 -- Project Name** +**Step 8 -- Project Name** Extract from (in priority order): 1. `package.json` `name` field @@ -106,11 +154,13 @@ Extract from (in priority order): 4. `pyproject.toml` -- check `[project].name` first, then `[tool.poetry].name` 5. Directory name of project root -**Step 8 -- Import Resolution** +**Step 9 -- Import Resolution** -For each file in the discovered source list, extract and resolve relative import statements. The goal is to produce a map from each file's path to the list of project-internal files it imports. External package imports are ignored. +For each **code-category** file in the discovered list (`fileCategory === "code"`), extract and resolve relative import statements. The goal is to produce a map from each file's path to the list of project-internal files it imports. External package imports are ignored. -For each file, read its content and extract import paths using language-appropriate patterns: +**Non-code files** (config, docs, infra, data, script, markup) should have an empty array `[]` in the import map — they do not participate in code-level import resolution. + +For each code file, read its content and extract import paths using language-appropriate patterns: | Language | Import patterns to match | |---|---| @@ -134,6 +184,8 @@ Output format in the script result: "importMap": { "src/index.ts": ["src/utils.ts", "src/config.ts"], "src/utils.ts": [], + "README.md": [], + "Dockerfile": [], "src/components/App.tsx": ["src/hooks/useAuth.ts", "src/store/index.ts"] } ``` @@ -150,16 +202,22 @@ The script must write this exact JSON structure to the output file: "name": "project-name", "rawDescription": "Description from package.json or empty string", "readmeHead": "First 10 lines of README.md or empty string", - "languages": ["javascript", "typescript"], - "frameworks": ["React", "Vite", "Vitest"], + "languages": ["javascript", "markdown", "typescript", "yaml"], + "frameworks": ["React", "Vite", "Vitest", "Docker"], "files": [ - {"path": "src/index.ts", "language": "typescript", "sizeLines": 150} + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150, "fileCategory": "code"}, + {"path": "README.md", "language": "markdown", "sizeLines": 45, "fileCategory": "docs"}, + {"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"}, + {"path": "package.json", "language": "json", "sizeLines": 35, "fileCategory": "config"} ], "totalFiles": 42, "estimatedComplexity": "moderate", "importMap": { "src/index.ts": ["src/utils.ts", "src/config.ts"], - "src/utils.ts": [] + "src/utils.ts": [], + "README.md": [], + "Dockerfile": [], + "package.json": [] } } ``` @@ -170,10 +228,11 @@ The script must write this exact JSON structure to the output file: - `readmeHead` (string) -- first 10 lines of `README.md` or empty string if no README exists - `languages` (string[]) -- deduplicated, sorted alphabetically - `frameworks` (string[]) -- only confirmed frameworks; empty array if none detected -- `files` (object[]) -- every source file, sorted by `path` alphabetically +- `files` (object[]) -- every discovered file, sorted by `path` alphabetically +- `files[].fileCategory` (string) -- one of: `code`, `config`, `docs`, `infra`, `data`, `script`, `markup` - `totalFiles` (integer) -- must equal `files.length` - `estimatedComplexity` (string) -- one of `small`, `moderate`, `large`, `very-large` -- `importMap` (object) — map from every source file path to its list of resolved project-internal import paths; empty array if no resolved imports; external packages excluded +- `importMap` (object) -- map from every file path to its list of resolved project-internal import paths; empty array for non-code files and files with no resolved imports; external packages excluded ### Executing the Script @@ -208,10 +267,12 @@ Then assemble the final output JSON: { "name": "project-name", "description": "Brief description from README or package.json", - "languages": ["typescript", "javascript"], - "frameworks": ["React", "Vite", "Vitest"], + "languages": ["markdown", "typescript", "yaml"], + "frameworks": ["React", "Vite", "Vitest", "Docker"], "files": [ - {"path": "src/index.ts", "language": "typescript", "sizeLines": 150} + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150, "fileCategory": "code"}, + {"path": "README.md", "language": "markdown", "sizeLines": 45, "fileCategory": "docs"}, + {"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"} ], "totalFiles": 42, "estimatedComplexity": "moderate", @@ -226,7 +287,7 @@ Then assemble the final output JSON: - `description` (string): your synthesized 1-2 sentence description - `languages` (string[]): directly from script output - `frameworks` (string[]): directly from script output -- `files` (object[]): directly from script output +- `files` (object[]): directly from script output, including `fileCategory` per file - `totalFiles` (integer): directly from script output - `estimatedComplexity` (string): directly from script output - `importMap` (object): directly from script output @@ -237,7 +298,8 @@ Then assemble the final output JSON: - NEVER include files that do not exist on disk. - ALWAYS validate that `totalFiles` matches the actual length of the `files` array. - ALWAYS sort `files` by `path` for deterministic output. -- Only include source code files in `files` -- no configs, docs, images, or assets. +- Include ALL discovered project files in `files` -- code, configs, docs, infrastructure, and data files. Only exclude binaries, lock files, generated files, and dependency directories. +- Every file MUST have a `fileCategory` field with one of: `code`, `config`, `docs`, `infra`, `data`, `script`, `markup`. - Trust the script's output for all structural data. Your only contribution is the `description` field. ## Writing Results @@ -246,6 +308,6 @@ After producing the final JSON: 1. Create the output directory: `mkdir -p /.understand-anything/intermediate` 2. Write the JSON to: `/.understand-anything/intermediate/scan-result.json` -3. Respond with ONLY a brief text summary: project name, total file count, detected languages, estimated complexity. +3. Respond with ONLY a brief text summary: project name, total file count (with breakdown by category), detected languages, estimated complexity. Do NOT include the full JSON in your text response. From 07ac08248e623467dfafaf35c1b8e7990770260a Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:45:37 +0800 Subject: [PATCH 10/24] feat(dashboard): add new node type colors to CustomNode Add typeColors and typeTextColors entries for config, document, service, table, endpoint, pipeline, schema, and resource node types. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dashboard/src/components/CustomNode.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index 1ec549e..75bb172 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -8,6 +8,14 @@ const typeColors: Record = { class: "var(--color-node-class)", module: "var(--color-node-module)", concept: "var(--color-node-concept)", + config: "var(--color-node-config)", + document: "var(--color-node-document)", + service: "var(--color-node-service)", + table: "var(--color-node-table)", + endpoint: "var(--color-node-endpoint)", + pipeline: "var(--color-node-pipeline)", + schema: "var(--color-node-schema)", + resource: "var(--color-node-resource)", }; const typeTextColors: Record = { @@ -16,6 +24,14 @@ const typeTextColors: Record = { class: "text-node-class", module: "text-node-module", concept: "text-node-concept", + config: "text-node-config", + document: "text-node-document", + service: "text-node-service", + table: "text-node-table", + endpoint: "text-node-endpoint", + pipeline: "text-node-pipeline", + schema: "text-node-schema", + resource: "text-node-resource", }; const complexityColors: Record = { From 6e3213e84983deada60eca777dbfff8d43e94b43 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:46:11 +0800 Subject: [PATCH 11/24] feat(dashboard): add new node/edge type support to NodeInfo sidebar Add badge colors for 8 new node types (config, document, service, table, endpoint, pipeline, schema, resource) and directional labels for 8 new edge types (deploys, serves, migrates, documents, provisions, routes, defines_schema, triggers). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dashboard/src/components/NodeInfo.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx index b626006..976e24a 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx @@ -7,6 +7,14 @@ const typeBadgeColors: Record = { class: "text-node-class border border-node-class/30 bg-node-class/10", module: "text-node-module border border-node-module/30 bg-node-module/10", concept: "text-node-concept border border-node-concept/30 bg-node-concept/10", + config: "text-node-config border border-node-config/30 bg-node-config/10", + document: "text-node-document border border-node-document/30 bg-node-document/10", + service: "text-node-service border border-node-service/30 bg-node-service/10", + table: "text-node-table border border-node-table/30 bg-node-table/10", + endpoint: "text-node-endpoint border border-node-endpoint/30 bg-node-endpoint/10", + pipeline: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10", + schema: "text-node-schema border border-node-schema/30 bg-node-schema/10", + resource: "text-node-resource border border-node-resource/30 bg-node-resource/10", }; const complexityBadgeColors: Record = { @@ -58,6 +66,22 @@ function getDirectionalLabel(edgeType: string, isSource: boolean): string { return "related to"; case "similar_to": return "similar to"; + case "deploys": + return isSource ? "deploys" : "deployed by"; + case "serves": + return isSource ? "serves" : "served by"; + case "migrates": + return isSource ? "migrates" : "migrated by"; + case "documents": + return isSource ? "documents" : "documented by"; + case "provisions": + return isSource ? "provisions" : "provisioned by"; + case "routes": + return isSource ? "routes to" : "routed from"; + case "defines_schema": + return isSource ? "defines schema for" : "schema defined by"; + case "triggers": + return isSource ? "triggers" : "triggered by"; default: return isSource ? edgeType : `${edgeType} (reverse)`; } From 78c8ce260228be0714c137a273d6d402adca7f5a Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:46:49 +0800 Subject: [PATCH 12/24] feat(dashboard): add file type breakdown to ProjectOverview Show a categorized count of node types (Code, Config, Docs, Infra, Data) with colored dots matching node type colors. Only displayed when non-code nodes are present in the graph. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/ProjectOverview.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx b/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx index 3969511..81d4018 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx @@ -21,6 +21,16 @@ export default function ProjectOverview() { typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1; } + // Category breakdowns + const categoryBreakdown = [ + { label: "Code", color: "var(--color-node-file)", count: (typeCounts["file"] ?? 0) + (typeCounts["function"] ?? 0) + (typeCounts["class"] ?? 0) }, + { label: "Config", color: "var(--color-node-config)", count: typeCounts["config"] ?? 0 }, + { label: "Docs", color: "var(--color-node-document)", count: typeCounts["document"] ?? 0 }, + { label: "Infra", color: "var(--color-node-service)", count: (typeCounts["service"] ?? 0) + (typeCounts["resource"] ?? 0) + (typeCounts["pipeline"] ?? 0) }, + { label: "Data", color: "var(--color-node-table)", count: (typeCounts["table"] ?? 0) + (typeCounts["endpoint"] ?? 0) + (typeCounts["schema"] ?? 0) }, + ]; + const hasNonCodeNodes = categoryBreakdown.some((c) => c.label !== "Code" && c.count > 0); + return (
{/* Project name */} @@ -47,6 +57,25 @@ export default function ProjectOverview() {
+ {/* File Types breakdown */} + {hasNonCodeNodes && ( +
+

File Types

+
+ {categoryBreakdown.filter((c) => c.count > 0).map((cat) => ( +
+ + {cat.label} + {cat.count} +
+ ))} +
+
+ )} + {/* Languages */} {project.languages.length > 0 && (
From 642626653c49059df69917d8b84ee0681bcec3a2 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:48:08 +0800 Subject: [PATCH 13/24] feat(dashboard): add node type category filter controls Add nodeTypeFilters state and toggleNodeTypeFilter action to the Zustand store, apply category-based filtering in GraphView's useLayerDetailTopology, and render filter toggle buttons in the App header for Code, Config, Docs, Infra, and Data categories with colored indicator dots. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/dashboard/src/App.tsx | 31 +++++++++++++++++++ .../dashboard/src/components/GraphView.tsx | 18 ++++++++++- .../packages/dashboard/src/store.ts | 14 +++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 0a39b73..26b4423 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) => ( + + ))} +