fix(context): the busiest file is found even when 20 test or generated files outrank it (#2785)

Root cause: getDominantFile and getTopRouteFile took the top 20 files in
SQL and only then dropped test and generated files in code. Twenty test
files busier than the app's busiest file left nothing, so getDominantFile
returned null and the context builder's core-directory boost never applied.
Nineteen left the dominant file without its runner-up, and nextEdgeCount 0
passes the "3x the next file" test for a file that is not dominant at all.
getTopRouteFile returned null the same way, and measured its 30% share
against the routes of the 20 files in the window, not the project's.

Fix:
- DOMINANT_FILE_SQL ranks every file. The window saved no work: ORDER BY
  needs every group, so the aggregate runs to the end either way, and only
  one row per file comes back.
- computeDominantFile filters the ranking 20 rows at a time until it has
  the dominant file and its runner-up. The generated-file lookup stays a
  bounded probe per batch.
- getTopRouteFile counts every route file, with ties broken by path.

Validation:
- New dominant-file-window.test.ts, 6 tests, all failing on main: the
  issue's layout with 20 and with 19 test files, 25 header-declared
  generated files (past the first batch), and three route shapes.
- The dominant-file cache, plan and allocation suites and the route-file
  detector test pass.
- Seven of the eight real repos give the same dominant file before and
  after (express, flask, excalidraw, django, airflow, grpc-go, etcd). The
  closest to the window was airflow, with the runner-up at rank 16.
- envoyproxy/go-control-plane crosses the window: 254 generated and test
  files outrank its first hand-written file. Main returns null; the fix
  returns xdsmatcher/pkg/matcher/matcher.go (167 edges, runner-up 165).
- getTopRouteFile on main reported django's admin/sites.py at 8 of 16
  routes and airflow's ui router at 65 of 65. Counting every route file
  gives 8 of 32 and 65 of 332, both under 30%, so null as documented.
- Cost, median of 3 interleaved rounds of 5: getDominantFile on airflow
  (8,564 ranked files) 581 -> 587 ms, django 189 -> 181 ms,
  go-control-plane 277 -> 284 ms. getTopRouteFile on airflow 1.3 -> 2.5 ms.
- Explore A/B, main vs fix: 996 queries on express, flask, excalidraw,
  django, airflow and go-control-plane, all byte-identical.

Fixes #2578

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-22 06:45:02 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 800e582d55
commit c46523079f
3 changed files with 202 additions and 16 deletions
+154
View File
@@ -0,0 +1,154 @@
/**
* The dominant file and the top route file are chosen from every candidate,
* not from a 20-row window that test and generated files can fill (#2578).
*
* Both queries took the top 20 files in SQL and only then dropped test and
* generated files in code. Twenty test files with more in-file edges than the
* app's busiest file left nothing, so `getDominantFile` returned null; nineteen
* left the dominant file without its runner-up, and `nextEdgeCount: 0` passes
* the context builder's "3x the next file" test for a file that is not
* dominant at all. `getTopRouteFile` returned null the same way, and measured
* its 30% share against the routes of the 20 files in the window instead of
* the project's.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
let root: string | undefined;
let cg: CodeGraph | undefined;
afterEach(() => {
cg?.close();
cg = undefined;
if (root) fs.rmSync(root, { recursive: true, force: true });
root = undefined;
});
/** `n + 1` functions, each calling the next: n call edges inside the file. */
function chain(prefix: string, n: number, header = ''): string {
return header
+ Array.from({ length: n }, (_, i) => `export function ${prefix}${i}() { return ${prefix}${i + 1}(); }`).join('\n')
+ `\nexport function ${prefix}${n}() { return 0; }\n`;
}
async function project(files: Record<string, string>): Promise<CodeGraph> {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2578-'));
for (const [rel, text] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), text);
}
cg = await CodeGraph.init(root, { index: true });
return cg;
}
const queries = (g: CodeGraph) => (g as any).queries;
/** Edges between two symbols of `file`: what the dominant-file ranking counts. */
function inFileEdges(g: CodeGraph, file: string): number {
return ((g as any).db.db.prepare(`
SELECT COUNT(*) AS n FROM edges e
JOIN nodes s ON e.source = s.id JOIN nodes t ON e.target = t.id
WHERE s.file_path = ? AND t.file_path = ?
`).get(file, file) as { n: number }).n;
}
/** The issue's layout: the app's two busiest files, below `tests` test files that each outrank both. */
function appWithTests(tests: number): Record<string, string> {
const files: Record<string, string> = {
'src/core.ts': chain('core', 60),
'src/second.ts': chain('second', 50),
};
for (let k = 0; k < tests; k++) files[`tests/t${k}.test.ts`] = chain(`t${k}_`, 70);
return files;
}
describe('getDominantFile looks past the test and generated files that outrank the app (#2578)', () => {
it('finds the dominant file when 20 test files outrank it', async () => {
const g = await project(appWithTests(20));
const core = inFileEdges(g, 'src/core.ts');
const second = inFileEdges(g, 'src/second.ts');
// Fixture shape: every test file outranks the app's busiest file.
expect(inFileEdges(g, 'tests/t0.test.ts')).toBeGreaterThan(core);
expect(queries(g).getDominantFile()).toEqual({ filePath: 'src/core.ts', edgeCount: core, nextEdgeCount: second });
}, 60_000);
it('keeps the runner-up when 19 test files leave room for only one app file in the top 20', async () => {
const g = await project(appWithTests(19));
const dominant = queries(g).getDominantFile();
expect(dominant?.filePath).toBe('src/core.ts');
// 0 here would pass the context builder's `edgeCount >= 3 * nextEdgeCount`
// directory boost for a file with a close runner-up.
expect(dominant?.nextEdgeCount).toBe(inFileEdges(g, 'src/second.ts'));
expect(dominant!.edgeCount).toBeLessThan(3 * dominant!.nextEdgeCount);
}, 60_000);
it('looks past files whose header says they are generated, beyond the first batch', async () => {
const files: Record<string, string> = {
'src/core.ts': chain('core', 60),
'src/second.ts': chain('second', 50),
};
// Named like hand-written code; only the banner says otherwise, so the
// index's generated flag is what has to drop them.
for (let k = 0; k < 25; k++) files[`src/models/model${k}.ts`] = chain(`m${k}_`, 70, '// @generated by the model compiler. DO NOT EDIT.\n');
const g = await project(files);
expect(queries(g).getGeneratedPathsAmong(['src/models/model0.ts', 'src/core.ts'])).toEqual(new Set(['src/models/model0.ts']));
expect(queries(g).getDominantFile()).toEqual({
filePath: 'src/core.ts',
edgeCount: inFileEdges(g, 'src/core.ts'),
nextEdgeCount: inFileEdges(g, 'src/second.ts'),
});
}, 60_000);
});
/** An Express router file with one handler per route. */
function routes(paths: string[]): string {
return `const express = require('express');
const router = express.Router();
${paths.map((p, i) => `function handler${i}(req, res) { res.json({ at: '${p}' }); }`).join('\n')}
${paths.map((p, i) => `router.get('${p}', handler${i});`).join('\n')}
module.exports = router;
`;
}
const EXPRESS_PACKAGE = JSON.stringify({ name: 'shop', dependencies: { express: '^4.19.0' } });
describe('getTopRouteFile counts every route file (#2578)', () => {
it('finds the app\'s routes file when 20 test servers hold more routes', async () => {
const files: Record<string, string> = {
'package.json': EXPRESS_PACKAGE,
'src/routes.js': routes(['/orders', '/orders/:id', '/orders/:id/items', '/orders/:id/status']),
};
for (let k = 0; k < 20; k++) files[`tests/server${k}.test.js`] = routes(['/a', '/b', '/c', '/d', '/e'].map((p) => `/t${k}${p}`));
const g = await project(files);
expect(g.getTopRouteFile()).toEqual({ filePath: 'src/routes.js', routeCount: 4, totalRoutes: 4 });
}, 60_000);
it('measures the share against every route, not the 20 busiest files', async () => {
const files: Record<string, string> = {
'package.json': EXPRESS_PACKAGE,
'src/routes.js': routes(Array.from({ length: 30 }, (_, i) => `/r${i}`)),
};
for (let k = 0; k < 40; k++) files[`src/features/f${k}.js`] = routes([`/f${k}`]);
const g = await project(files);
// 30 of 70 routes: a 43% share, and the total is all 70.
expect(g.getTopRouteFile()).toEqual({ filePath: 'src/routes.js', routeCount: 30, totalRoutes: 70 });
}, 60_000);
it('answers null when routing is diffuse across more than 20 files', async () => {
const files: Record<string, string> = {
'package.json': EXPRESS_PACKAGE,
'src/routes.js': routes(Array.from({ length: 10 }, (_, i) => `/r${i}`)),
};
for (let k = 0; k < 40; k++) files[`src/features/f${k}.js`] = routes([`/f${k}`]);
const g = await project(files);
// 10 of 50 routes is 20%, under the 30% bar. Counting only the 20 busiest
// files made it 10 of 29.
expect(g.getTopRouteFile()).toBeNull();
}, 60_000);
});