diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d859288..b0874f0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.6.0", + "version": "2.6.1", "author": { "name": "Lum1104" }, diff --git a/.copilot-plugin/plugin.json b/.copilot-plugin/plugin.json index e7b425f..e65a374 100644 --- a/.copilot-plugin/plugin.json +++ b/.copilot-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.6.0", + "version": "2.6.1", "author": { "name": "Lum1104" }, diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index f222f35..c9b9bdb 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "understand-anything", "displayName": "Understand Anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.6.0", + "version": "2.6.1", "author": { "name": "Lum1104" }, diff --git a/understand-anything-plugin/.claude-plugin/plugin.json b/understand-anything-plugin/.claude-plugin/plugin.json index d859288..b0874f0 100644 --- a/understand-anything-plugin/.claude-plugin/plugin.json +++ b/understand-anything-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "2.6.0", + "version": "2.6.1", "author": { "name": "Lum1104" }, diff --git a/understand-anything-plugin/agents/file-analyzer.md b/understand-anything-plugin/agents/file-analyzer.md index 3889526..e44ff0e 100644 --- a/understand-anything-plugin/agents/file-analyzer.md +++ b/understand-anything-plugin/agents/file-analyzer.md @@ -27,11 +27,20 @@ Execute the pre-built structural extraction script bundled with the Understand-A Create the input file with the batch data. **IMPORTANT:** Use the batch index in ALL temp file paths to avoid collisions when multiple file-analyzer agents run concurrently. +Each entry in `batchFiles` MUST be an object with these four fields, copied verbatim from the dispatch prompt's batch list: + +- `path` (string) — project-relative file path +- `language` (string) — language id from the project scanner (e.g. `"python"`, `"typescript"`); never null +- `sizeLines` (integer) — line count +- `fileCategory` (string) — `code`, `config`, `docs`, `infra`, `data`, `script`, or `markup` + ```bash cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-.json << 'ENDJSON' { "projectRoot": "", - "batchFiles": [], + "batchFiles": [ + {"path": "", "language": "", "sizeLines": , "fileCategory": ""} + ], "batchImportData": } ENDJSON diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index 7269eab..31022e8 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@understand-anything/skill", - "version": "2.6.0", + "version": "2.6.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index 5f6ab21..39e543b 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -259,9 +259,9 @@ Fill in batch-specific parameters below and dispatch: > > ``` > -> Files to analyze in this batch: -> 1. `` ( lines, fileCategory: ``) -> 2. `` ( lines, fileCategory: ``) +> Files to analyze in this batch (every entry MUST be passed through to `batchFiles` with all four fields — `path`, `language`, `sizeLines`, `fileCategory`): +> 1. `` ( lines, language: ``, fileCategory: ``) +> 2. `` ( lines, language: ``, fileCategory: ``) > ... After ALL batches complete, run the merge-and-normalize script bundled with this skill (located next to this SKILL.md file — use the skill directory path, not the project root): diff --git a/understand-anything-plugin/skills/understand/extract-structure.mjs b/understand-anything-plugin/skills/understand/extract-structure.mjs index 5208e0b..1423cd4 100644 --- a/understand-anything-plugin/skills/understand/extract-structure.mjs +++ b/understand-anything-plugin/skills/understand/extract-structure.mjs @@ -18,7 +18,7 @@ import { createRequire } from 'node:module'; import { dirname, resolve, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { readFileSync, writeFileSync } from 'node:fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -39,19 +39,16 @@ try { const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core; -// --------------------------------------------------------------------------- -// Argument validation -// --------------------------------------------------------------------------- -const [,, inputPath, outputPath] = process.argv; -if (!inputPath || !outputPath) { - process.stderr.write('Usage: node extract-structure.mjs \n'); - process.exit(1); -} - // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main() { + const [,, inputPath, outputPath] = process.argv; + if (!inputPath || !outputPath) { + process.stderr.write('Usage: node extract-structure.mjs \n'); + process.exit(1); + } + // Read input const inputRaw = readFileSync(inputPath, 'utf-8'); const input = JSON.parse(inputRaw); @@ -133,9 +130,10 @@ async function main() { } // --------------------------------------------------------------------------- -// Result builder: maps StructuralAnalysis to the expected output schema +// Result builder: maps StructuralAnalysis to the expected output schema. +// Exported for unit tests; pure function, no I/O. // --------------------------------------------------------------------------- -function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData) { +export function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData) { const base = { path: file.path, language: file.language, @@ -244,9 +242,12 @@ function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batch // Metrics const metrics = {}; - // Import count from batchImportData (pre-resolved by project scanner) + // Import count from batchImportData (pre-resolved by project scanner). + // Empty arrays are truthy, so explicitly check length so we fall back to the + // parser's own import list when the scanner could not resolve any imports + // (e.g. Python absolute imports the scanner doesn't follow). const importPaths = batchImportData?.[file.path]; - if (importPaths) { + if (importPaths && importPaths.length > 0) { metrics.importCount = importPaths.length; } else if (analysis.imports) { metrics.importCount = analysis.imports.length; @@ -286,11 +287,17 @@ function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batch } // --------------------------------------------------------------------------- -// Run +// Run only when executed directly as a CLI; importing the module (e.g. from +// tests) must not trigger main(). // --------------------------------------------------------------------------- -try { - await main(); -} catch (err) { - process.stderr.write(`extract-structure.mjs failed: ${err.message}\n${err.stack}\n`); - process.exit(1); +const isCli = + process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; + +if (isCli) { + try { + await main(); + } catch (err) { + process.stderr.write(`extract-structure.mjs failed: ${err.message}\n${err.stack}\n`); + process.exit(1); + } } diff --git a/understand-anything-plugin/src/__tests__/extract-structure.test.mjs b/understand-anything-plugin/src/__tests__/extract-structure.test.mjs new file mode 100644 index 0000000..9a848f1 --- /dev/null +++ b/understand-anything-plugin/src/__tests__/extract-structure.test.mjs @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { buildResult } from "../../skills/understand/extract-structure.mjs"; + +const file = (overrides = {}) => ({ + path: "src/foo.py", + language: "python", + fileCategory: "code", + ...overrides, +}); + +const analysis = (overrides = {}) => ({ + functions: [], + classes: [], + imports: [], + exports: [], + ...overrides, +}); + +describe("extract-structure buildResult", () => { + describe("language pass-through", () => { + it("preserves the input language on the output", () => { + const result = buildResult(file({ language: "python" }), 10, 8, analysis(), null, {}); + expect(result.language).toBe("python"); + }); + + it("preserves null when caller did not set a language", () => { + // Documents the failure mode the SKILL.md/file-analyzer.md fix prevents: + // if the dispatch prompt loses `language`, it propagates to the output. + const result = buildResult(file({ language: null }), 10, 8, analysis(), null, {}); + expect(result.language).toBeNull(); + }); + }); + + describe("importCount fallback", () => { + const analysisWithImports = analysis({ + imports: [ + { source: "os", specifiers: [] }, + { source: "sys", specifiers: [] }, + { source: "pathlib", specifiers: [] }, + ], + }); + + it("uses pre-resolved imports when batchImportData has entries", () => { + const batchImportData = { "src/foo.py": ["src/bar.py", "src/baz.py"] }; + const result = buildResult(file(), 10, 8, analysisWithImports, null, batchImportData); + expect(result.metrics.importCount).toBe(2); + }); + + it("falls back to parser imports when batchImportData entry is an empty array", () => { + // Regression test: empty arrays are truthy in JS, so a naive `if (importPaths)` + // would clobber the parser's count with 0. This is the bug Python projects + // using absolute imports (which the project scanner doesn't resolve) hit. + const batchImportData = { "src/foo.py": [] }; + const result = buildResult(file(), 10, 8, analysisWithImports, null, batchImportData); + expect(result.metrics.importCount).toBe(3); + }); + + it("falls back to parser imports when batchImportData has no entry for the file", () => { + const result = buildResult(file(), 10, 8, analysisWithImports, null, {}); + expect(result.metrics.importCount).toBe(3); + }); + + it("falls back to parser imports when batchImportData is undefined", () => { + const result = buildResult(file(), 10, 8, analysisWithImports, null, undefined); + expect(result.metrics.importCount).toBe(3); + }); + + it("reports 0 imports when neither source has any", () => { + const result = buildResult(file(), 10, 8, analysis(), null, { "src/foo.py": [] }); + expect(result.metrics.importCount).toBe(0); + }); + }); +});