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
16 changes: 12 additions & 4 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"

- run: npm install
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund

- name: Run build benchmark
run: node scripts/benchmark.js 2>/dev/null > benchmark-result.json
Expand Down Expand Up @@ -91,8 +93,10 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"

- run: npm install
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund

- name: Cache HuggingFace models
uses: actions/cache@v4
Expand Down Expand Up @@ -166,8 +170,10 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"

- run: npm install
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund

- name: Run query benchmark
run: node scripts/query-benchmark.js 2>/dev/null > query-benchmark-result.json
Expand Down Expand Up @@ -229,8 +235,10 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"

- run: npm install
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund

- name: Run incremental benchmark
run: node scripts/incremental-benchmark.js 2>/dev/null > incremental-benchmark-result.json
Expand Down
2 changes: 2 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ program
.option('-d, --db <path>', 'Path to graph.db')
.option('--depth <n>', 'Max directory depth')
.option('--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files')
.option('--full', 'Show all files without limit')
.option('-T, --no-tests', 'Exclude test/spec files')
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
.option('-j, --json', 'Output as JSON')
Expand All @@ -511,6 +512,7 @@ program
directory: dir,
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
sort: opts.sort,
full: opts.full,
noTests: resolveNoTests(opts),
});
if (opts.json) {
Expand Down
8 changes: 7 additions & 1 deletion src/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ const BASE_TOOLS = [
{
name: 'structure',
description:
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics',
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics. Per-file details are capped at 25 files by default; use full=true to show all.',
inputSchema: {
type: 'object',
properties: {
Expand All @@ -270,6 +270,11 @@ const BASE_TOOLS = [
enum: ['cohesion', 'fan-in', 'fan-out', 'density', 'files'],
description: 'Sort directories by metric',
},
full: {
type: 'boolean',
description: 'Return all files without limit',
default: false,
},
},
},
},
Expand Down Expand Up @@ -571,6 +576,7 @@ export async function startMCPServer(customDbPath, options = {}) {
directory: args.directory,
depth: args.depth,
sort: args.sort,
full: args.full,
});
break;
}
Expand Down
33 changes: 33 additions & 0 deletions src/structure.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ export function structureData(customDbPath, opts = {}) {
const maxDepth = opts.depth || null;
const sortBy = opts.sort || 'files';
const noTests = opts.noTests || false;
const full = opts.full || false;
const fileLimit = opts.fileLimit || 25;

// Get all directory nodes with their metrics
let dirs = db
Expand Down Expand Up @@ -403,6 +405,33 @@ export function structureData(customDbPath, opts = {}) {
});

db.close();

// Apply global file limit unless full mode
if (!full) {
const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
if (totalFiles > fileLimit) {
let shown = 0;
for (const d of result) {
const remaining = fileLimit - shown;
if (remaining <= 0) {
d.files = [];
} else if (d.files.length > remaining) {
d.files = d.files.slice(0, remaining);
shown = fileLimit;
} else {
shown += d.files.length;
}
}
const suppressed = totalFiles - fileLimit;
return {
directories: result,
count: result.length,
suppressed,
warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
};
}
}

return { directories: result, count: result.length };
}

Expand Down Expand Up @@ -539,6 +568,10 @@ export function formatStructure(data) {
);
}
}
if (data.warning) {
lines.push('');
lines.push(`⚠ ${data.warning}`);
}
return lines.join('\n');
}

Expand Down
27 changes: 27 additions & 0 deletions tests/integration/structure.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,33 @@ describe('structureData', () => {
});
});

describe('structureData file limit', () => {
test('default fileLimit truncates files and includes warning when exceeded', () => {
// Use a very low fileLimit to trigger truncation on the small fixture
const data = structureData(dbPath, { fileLimit: 2 });
const shownFiles = data.directories.reduce((sum, d) => sum + d.files.length, 0);
expect(shownFiles).toBeLessThanOrEqual(2);
expect(data.suppressed).toBeGreaterThan(0);
expect(data.warning).toMatch(/files omitted/);
expect(data.warning).toMatch(/--full/);
});

test('full: true returns all files without warning', () => {
const data = structureData(dbPath, { full: true });
const totalFiles = data.directories.reduce((sum, d) => sum + d.files.length, 0);
expect(totalFiles).toBeGreaterThan(0);
expect(data.suppressed).toBeUndefined();
expect(data.warning).toBeUndefined();
});

test('no truncation when total files are within limit', () => {
// fileLimit higher than total files should not add warning
const data = structureData(dbPath, { fileLimit: 100 });
expect(data.suppressed).toBeUndefined();
expect(data.warning).toBeUndefined();
});
});

describe('hotspotsData', () => {
test('returns file hotspots ranked by fan-in', () => {
const data = hotspotsData(dbPath, { metric: 'fan-in', level: 'file', limit: 5 });
Expand Down