diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index b6c07b70..4d5b7d22 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/cli.js b/src/cli.js index e048ac44..fc570886 100644 --- a/src/cli.js +++ b/src/cli.js @@ -502,6 +502,7 @@ program .option('-d, --db ', 'Path to graph.db') .option('--depth ', 'Max directory depth') .option('--sort ', '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') @@ -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) { diff --git a/src/mcp.js b/src/mcp.js index 2daeeb84..8233cdff 100644 --- a/src/mcp.js +++ b/src/mcp.js @@ -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: { @@ -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, + }, }, }, }, @@ -571,6 +576,7 @@ export async function startMCPServer(customDbPath, options = {}) { directory: args.directory, depth: args.depth, sort: args.sort, + full: args.full, }); break; } diff --git a/src/structure.js b/src/structure.js index e094e72a..30f341bf 100644 --- a/src/structure.js +++ b/src/structure.js @@ -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 @@ -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 }; } @@ -539,6 +568,10 @@ export function formatStructure(data) { ); } } + if (data.warning) { + lines.push(''); + lines.push(`⚠ ${data.warning}`); + } return lines.join('\n'); } diff --git a/tests/integration/structure.test.js b/tests/integration/structure.test.js index 81a38529..5dae68f6 100644 --- a/tests/integration/structure.test.js +++ b/tests/integration/structure.test.js @@ -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 });