Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Pure result mapping for extract-structure.mjs.
// Kept separate from the CLI entrypoint so unit tests do not import a shebang script.
export function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData) {
const base = {
path: file.path,
language: file.language,
fileCategory: file.fileCategory,
totalLines,
nonEmptyLines,
};

if (!analysis) {
base.metrics = {};
return base;
}

if (analysis.functions && analysis.functions.length > 0) {
base.functions = analysis.functions.map(fn => ({
name: fn.name,
startLine: fn.lineRange[0],
endLine: fn.lineRange[1],
params: fn.params || [],
}));
}

if (analysis.classes && analysis.classes.length > 0) {
base.classes = analysis.classes.map(cls => ({
name: cls.name,
startLine: cls.lineRange[0],
endLine: cls.lineRange[1],
methods: cls.methods || [],
properties: cls.properties || [],
}));
}

if (analysis.exports && analysis.exports.length > 0) {
base.exports = analysis.exports.map(exp => ({
name: exp.name,
line: exp.lineNumber,
isDefault: exp.isDefault === true,
}));
}

if (analysis.sections && analysis.sections.length > 0) {
base.sections = analysis.sections.map(s => ({
heading: s.name,
level: s.level,
line: s.lineRange[0],
}));
}

if (analysis.definitions && analysis.definitions.length > 0) {
base.definitions = analysis.definitions.map(d => ({
name: d.name,
kind: d.kind,
fields: d.fields || [],
startLine: d.lineRange[0],
endLine: d.lineRange[1],
}));
}

if (analysis.services && analysis.services.length > 0) {
base.services = analysis.services.map(s => ({
name: s.name,
image: s.image,
ports: s.ports || [],
...(s.lineRange ? { startLine: s.lineRange[0], endLine: s.lineRange[1] } : {}),
}));
}

if (analysis.endpoints && analysis.endpoints.length > 0) {
base.endpoints = analysis.endpoints.map(e => ({
method: e.method,
path: e.path,
startLine: e.lineRange[0],
endLine: e.lineRange[1],
}));
}

if (analysis.steps && analysis.steps.length > 0) {
base.steps = analysis.steps.map(s => ({
name: s.name,
startLine: s.lineRange[0],
endLine: s.lineRange[1],
}));
}

if (analysis.resources && analysis.resources.length > 0) {
base.resources = analysis.resources.map(r => ({
name: r.name,
kind: r.kind,
startLine: r.lineRange[0],
endLine: r.lineRange[1],
}));
}

if (callGraph && callGraph.length > 0) {
base.callGraph = callGraph;
}

const metrics = {};

const importPaths = batchImportData?.[file.path];
if (importPaths && importPaths.length > 0) {
metrics.importCount = importPaths.length;
} else if (analysis.imports) {
const internal = analysis.imports.filter(imp => {
const src = imp?.source ?? '';
return src.startsWith('.');
});
metrics.importCount = internal.length;
}

if (analysis.exports) {
metrics.exportCount = analysis.exports.length;
}
if (analysis.functions) {
metrics.functionCount = analysis.functions.length;
}
if (analysis.classes) {
metrics.classCount = analysis.classes.length;
}
if (analysis.sections) {
metrics.sectionCount = analysis.sections.length;
}
if (analysis.definitions) {
metrics.definitionCount = analysis.definitions.length;
}
if (analysis.services) {
metrics.serviceCount = analysis.services.length;
}
if (analysis.endpoints) {
metrics.endpointCount = analysis.endpoints.length;
}
if (analysis.steps) {
metrics.stepCount = analysis.steps.length;
}
if (analysis.resources) {
metrics.resourceCount = analysis.resources.length;
}

base.metrics = metrics;

return base;
}
169 changes: 4 additions & 165 deletions understand-anything-plugin/skills/understand/extract-structure.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { createRequire } from 'node:module';
import { dirname, resolve, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { buildResult as buildExtractResult } from './extract-structure-result.mjs';

export { buildResult } from './extract-structure-result.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
// skills/understand/ -> plugin root is two dirs up
Expand Down Expand Up @@ -120,7 +123,7 @@ async function main() {
}

// Build result object
const result = buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData);
const result = buildExtractResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData);
results.push(result);
}

Expand All @@ -139,170 +142,6 @@ async function main() {
}
}

// ---------------------------------------------------------------------------
// Result builder: maps StructuralAnalysis to the expected output schema.
// Exported for unit tests; pure function, no I/O.
// ---------------------------------------------------------------------------
export function buildResult(file, totalLines, nonEmptyLines, analysis, callGraph, batchImportData) {
const base = {
path: file.path,
language: file.language,
fileCategory: file.fileCategory,
totalLines,
nonEmptyLines,
};

if (!analysis) {
// No parser matched — return basic metrics only
base.metrics = {};
return base;
}

// Functions (code files)
if (analysis.functions && analysis.functions.length > 0) {
base.functions = analysis.functions.map(fn => ({
name: fn.name,
startLine: fn.lineRange[0],
endLine: fn.lineRange[1],
params: fn.params || [],
}));
}

// Classes (code files)
if (analysis.classes && analysis.classes.length > 0) {
base.classes = analysis.classes.map(cls => ({
name: cls.name,
startLine: cls.lineRange[0],
endLine: cls.lineRange[1],
methods: cls.methods || [],
properties: cls.properties || [],
}));
}

// Exports (code files)
if (analysis.exports && analysis.exports.length > 0) {
base.exports = analysis.exports.map(exp => ({
name: exp.name,
line: exp.lineNumber,
isDefault: exp.isDefault === true,
}));
}

// Non-code structural data: pass through directly
if (analysis.sections && analysis.sections.length > 0) {
base.sections = analysis.sections.map(s => ({
heading: s.name,
level: s.level,
line: s.lineRange[0],
}));
}

if (analysis.definitions && analysis.definitions.length > 0) {
base.definitions = analysis.definitions.map(d => ({
name: d.name,
kind: d.kind,
fields: d.fields || [],
startLine: d.lineRange[0],
endLine: d.lineRange[1],
}));
}

if (analysis.services && analysis.services.length > 0) {
base.services = analysis.services.map(s => ({
name: s.name,
image: s.image,
ports: s.ports || [],
...(s.lineRange ? { startLine: s.lineRange[0], endLine: s.lineRange[1] } : {}),
}));
}

if (analysis.endpoints && analysis.endpoints.length > 0) {
base.endpoints = analysis.endpoints.map(e => ({
method: e.method,
path: e.path,
startLine: e.lineRange[0],
endLine: e.lineRange[1],
}));
}

if (analysis.steps && analysis.steps.length > 0) {
base.steps = analysis.steps.map(s => ({
name: s.name,
startLine: s.lineRange[0],
endLine: s.lineRange[1],
}));
}

if (analysis.resources && analysis.resources.length > 0) {
base.resources = analysis.resources.map(r => ({
name: r.name,
kind: r.kind,
startLine: r.lineRange[0],
endLine: r.lineRange[1],
}));
}

// Call graph
if (callGraph && callGraph.length > 0) {
base.callGraph = callGraph;
}

// Metrics
const metrics = {};

// 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).
//
// The fallback counts only relative-style imports (those starting with `.`)
// so the metric stays *internal-import* in semantics rather than mixing in
// every external package import seen by the parser. Resolved external imports
// can never produce edges anyway, so counting them would be misleading.
const importPaths = batchImportData?.[file.path];
if (importPaths && importPaths.length > 0) {
metrics.importCount = importPaths.length;
} else if (analysis.imports) {
const internal = analysis.imports.filter(imp => {
const src = imp?.source ?? '';
return src.startsWith('.');
});
metrics.importCount = internal.length;
}

if (analysis.exports) {
metrics.exportCount = analysis.exports.length;
}
if (analysis.functions) {
metrics.functionCount = analysis.functions.length;
}
if (analysis.classes) {
metrics.classCount = analysis.classes.length;
}
if (analysis.sections) {
metrics.sectionCount = analysis.sections.length;
}
if (analysis.definitions) {
metrics.definitionCount = analysis.definitions.length;
}
if (analysis.services) {
metrics.serviceCount = analysis.services.length;
}
if (analysis.endpoints) {
metrics.endpointCount = analysis.endpoints.length;
}
if (analysis.steps) {
metrics.stepCount = analysis.steps.length;
}
if (analysis.resources) {
metrics.resourceCount = analysis.resources.length;
}

base.metrics = metrics;

return base;
}

// ---------------------------------------------------------------------------
// Run only when executed directly as a CLI; importing the module (e.g. from
// tests) must not trigger main().
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { buildResult } from "../../skills/understand/extract-structure.mjs";
import { buildResult } from "../../skills/understand/extract-structure-result.mjs";

const file = (overrides = {}) => ({
path: "src/foo.py",
Expand Down
Loading
Loading