mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
fix(file-analyzer): preserve language and fall back when imports unresolved
Two bugs surfaced when analyzing Python projects that use absolute imports: - The dispatch prompt (SKILL.md) and file-analyzer agent omitted the per-file `language` field, so `extract-structure.mjs` received null and passed it through to the graph. - `extract-structure.mjs` used `if (importPaths)` to decide whether to trust pre-resolved imports. Empty arrays are truthy, so files where the project scanner could not resolve any imports (e.g. Python absolute imports) clobbered the parser's import count with 0, never falling back to tree-sitter's own analysis. Bumps plugin version to 2.6.1 across the five tracked manifests and adds unit tests for `buildResult` covering language pass-through and the importCount fallback paths. To make the script testable, `buildResult` is now exported and the CLI invocation is guarded so importing the module no longer triggers `main()`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
53248fea8b
commit
7834f9bc75
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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-<batchIndex>.json << 'ENDJSON'
|
||||
{
|
||||
"projectRoot": "<project-root>",
|
||||
"batchFiles": [<this batch's files including fileCategory>],
|
||||
"batchFiles": [
|
||||
{"path": "<path>", "language": "<language>", "sizeLines": <sizeLines>, "fileCategory": "<fileCategory>"}
|
||||
],
|
||||
"batchImportData": <batchImportData JSON object — provided in your dispatch prompt>
|
||||
}
|
||||
ENDJSON
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -259,9 +259,9 @@ Fill in batch-specific parameters below and dispatch:
|
||||
> <batchImportData JSON>
|
||||
> ```
|
||||
>
|
||||
> Files to analyze in this batch:
|
||||
> 1. `<path>` (<sizeLines> lines, fileCategory: `<fileCategory>`)
|
||||
> 2. `<path>` (<sizeLines> lines, fileCategory: `<fileCategory>`)
|
||||
> Files to analyze in this batch (every entry MUST be passed through to `batchFiles` with all four fields — `path`, `language`, `sizeLines`, `fileCategory`):
|
||||
> 1. `<path>` (<sizeLines> lines, language: `<language>`, fileCategory: `<fileCategory>`)
|
||||
> 2. `<path>` (<sizeLines> lines, language: `<language>`, fileCategory: `<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):
|
||||
|
||||
@@ -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 <input.json> <output.json>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
async function main() {
|
||||
const [,, inputPath, outputPath] = process.argv;
|
||||
if (!inputPath || !outputPath) {
|
||||
process.stderr.write('Usage: node extract-structure.mjs <input.json> <output.json>\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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user