Skip to content

Commit d697929

Browse files
committed
refactor(ci): let resolution gate reuse benchmark artifact
The pre-publish-benchmark job's `Run resolution benchmark` step builds codegraphs for ~34 language fixtures, computes precision/recall, and writes resolution-result.json. The `Gate on resolution thresholds` step that follows then ran the same vitest suite which independently copied every fixture and rebuilt the graphs again — doubling the most expensive slice of the publish pipeline. Extend the script's per-language LangResult to include falsePositiveEdges and falseNegativeEdges so the gate test has everything it needs for the existing precision/recall threshold assertions and failure messages. Refactor the gate test to consume that artifact when RESOLUTION_RESULT_JSON is set, falling back to the build-from-fixtures path when unset so devs can still run `npx vitest run tests/benchmarks/resolution/...` standalone. Wire the env var through the workflow's Gate step. Verified locally: gate test in artifact mode passes 170/170 in ~0.5s against an artifact produced by scripts/resolution-benchmark.ts, and the legacy build-from-fixtures path still passes for the javascript fixture. Closes #1052
1 parent a029d43 commit d697929

3 files changed

Lines changed: 85 additions & 18 deletions

File tree

.github/workflows/publish.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ jobs:
333333
334334
- name: Gate on resolution thresholds
335335
timeout-minutes: 30
336+
# Reuse the metrics produced by the previous step instead of rebuilding
337+
# every fixture from scratch (issue #1052). The gate test falls back to
338+
# the build-from-fixtures path when this env var is unset, so local
339+
# runs (`npx vitest run …`) still work standalone.
340+
env:
341+
RESOLUTION_RESULT_JSON: ${{ github.workspace }}/resolution-result.json
336342
run: npx vitest run tests/benchmarks/resolution/resolution-benchmark.test.ts --reporter=verbose
337343

338344
- name: Run tracer validation (same-file edge recall)

scripts/resolution-benchmark.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ interface LangResult {
6464
totalResolved: number;
6565
totalExpected: number;
6666
byMode: Record<string, ModeMetrics>;
67+
// Edge lists are included so the gate test can reuse this artifact
68+
// instead of rebuilding fixtures from scratch (see issue #1052).
69+
falsePositiveEdges: string[];
70+
falseNegativeEdges: string[];
6771
dynamicEdges?: number;
6872
dynamicConfirmed?: number;
6973
}
@@ -132,6 +136,8 @@ function computeMetrics(resolvedEdges: ResolvedEdge[], expectedEdges: ExpectedEd
132136
totalResolved: resolvedSet.size,
133137
totalExpected: expectedSet.size,
134138
byMode,
139+
falsePositiveEdges: [...falsePositives],
140+
falseNegativeEdges: [...falseNegatives],
135141
};
136142
}
137143

tests/benchmarks/resolution/resolution-benchmark.test.ts

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
* per language and per resolution mode.
99
*
1010
* CI gate: fails if precision or recall drops below per-language thresholds.
11+
*
12+
* **Artifact mode (CI):** when `RESOLUTION_RESULT_JSON` points at a result
13+
* file produced by `scripts/resolution-benchmark.ts`, the suite reads those
14+
* pre-computed metrics and skips the fixture rebuild — avoiding the duplicate
15+
* work that doubled pre-publish CI time (issue #1052). Local runs without
16+
* the env var fall back to the build-from-fixtures path.
1117
*/
1218

1319
import fs from 'node:fs';
@@ -262,6 +268,52 @@ function formatReport(lang: string, metrics: BenchmarkMetrics): string {
262268
return lines.join('\n');
263269
}
264270

271+
// ── Artifact loading (CI dedup, issue #1052) ─────────────────────────────
272+
273+
const ARTIFACT_PATH = process.env.RESOLUTION_RESULT_JSON;
274+
275+
interface ArtifactLangResult {
276+
precision: number;
277+
recall: number;
278+
truePositives: number;
279+
falsePositives: number;
280+
falseNegatives: number;
281+
totalResolved: number;
282+
totalExpected: number;
283+
byMode: Record<string, ModeMetrics>;
284+
falsePositiveEdges?: string[];
285+
falseNegativeEdges?: string[];
286+
}
287+
288+
function loadArtifact(artifactPath: string): Record<string, ArtifactLangResult> {
289+
if (!fs.existsSync(artifactPath)) {
290+
throw new Error(
291+
`RESOLUTION_RESULT_JSON=${artifactPath} not found — run scripts/resolution-benchmark.ts first.`,
292+
);
293+
}
294+
return JSON.parse(fs.readFileSync(artifactPath, 'utf-8'));
295+
}
296+
297+
function metricsFromArtifact(lang: string, raw: ArtifactLangResult): BenchmarkMetrics {
298+
if (!Array.isArray(raw.falsePositiveEdges) || !Array.isArray(raw.falseNegativeEdges)) {
299+
throw new Error(
300+
`Resolution artifact for ${lang} is missing falsePositiveEdges/falseNegativeEdges — regenerate with the current resolution-benchmark.ts.`,
301+
);
302+
}
303+
return {
304+
precision: raw.precision,
305+
recall: raw.recall,
306+
truePositives: raw.truePositives,
307+
falsePositives: raw.falsePositives,
308+
falseNegatives: raw.falseNegatives,
309+
totalResolved: raw.totalResolved,
310+
totalExpected: raw.totalExpected,
311+
byMode: raw.byMode,
312+
falsePositiveEdges: raw.falsePositiveEdges,
313+
falseNegativeEdges: raw.falseNegativeEdges,
314+
};
315+
}
316+
265317
// ── Tests ────────────────────────────────────────────────────────────────
266318

267319
function discoverFixtures(): string[] {
@@ -276,7 +328,11 @@ function discoverFixtures(): string[] {
276328
return languages;
277329
}
278330

279-
const languages = discoverFixtures();
331+
const artifact = ARTIFACT_PATH ? loadArtifact(ARTIFACT_PATH) : null;
332+
// In artifact mode, drive the suite from the keys in the artifact so we never
333+
// silently skip a language the script reported. In local mode, discover from
334+
// the filesystem like before.
335+
const languages = artifact ? Object.keys(artifact).sort() : discoverFixtures();
280336

281337
/** Stores all results for the final summary */
282338
const allResults: Record<string, BenchmarkMetrics> = {};
@@ -309,22 +365,24 @@ describe('Call Resolution Precision/Recall', () => {
309365

310366
for (const lang of languages) {
311367
describe(lang, () => {
312-
let fixtureDir: string;
313-
let resolvedEdges: ResolvedEdge[];
314-
let expectedEdges: ExpectedEdge[];
368+
let fixtureDir: string | null = null;
315369
let metrics: BenchmarkMetrics;
316370

317371
beforeAll(async () => {
318-
fixtureDir = copyFixture(lang);
319-
await buildFixtureGraph(fixtureDir);
372+
if (artifact) {
373+
metrics = metricsFromArtifact(lang, artifact[lang]);
374+
} else {
375+
fixtureDir = copyFixture(lang);
376+
await buildFixtureGraph(fixtureDir);
320377

321-
resolvedEdges = extractResolvedEdges(fixtureDir);
378+
const resolvedEdges = extractResolvedEdges(fixtureDir) as ResolvedEdge[];
322379

323-
const manifestPath = path.join(FIXTURES_DIR, lang, 'expected-edges.json');
324-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
325-
expectedEdges = manifest.edges;
380+
const manifestPath = path.join(FIXTURES_DIR, lang, 'expected-edges.json');
381+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
382+
const expectedEdges: ExpectedEdge[] = manifest.edges;
326383

327-
metrics = computeMetrics(resolvedEdges, expectedEdges);
384+
metrics = computeMetrics(resolvedEdges, expectedEdges);
385+
}
328386
allResults[lang] = metrics;
329387
}, 60_000);
330388

@@ -334,16 +392,13 @@ describe('Call Resolution Precision/Recall', () => {
334392
}
335393
});
336394

337-
test('builds graph successfully', () => {
338-
expect(resolvedEdges).toBeDefined();
339-
expect(Array.isArray(resolvedEdges)).toBe(true);
340-
// Some languages may have 0 resolved call edges if resolution isn't
341-
// implemented yet — that's okay, the precision/recall tests will
342-
// catch it at the appropriate threshold level.
395+
test('metrics are populated', () => {
396+
expect(metrics).toBeDefined();
397+
expect(metrics.totalResolved).toBeGreaterThanOrEqual(0);
343398
});
344399

345400
test('expected edges manifest is non-empty', () => {
346-
expect(expectedEdges.length).toBeGreaterThan(0);
401+
expect(metrics.totalExpected).toBeGreaterThan(0);
347402
});
348403

349404
test('precision meets threshold', () => {

0 commit comments

Comments
 (0)