mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
Resolves PR #1693 review M2, M3, L1, L5 in a single parse-impl.ts pass: M2 - Monotonic progress through deferred phase (no more "stuck at 82%") Previously the deferred resolution stages (imports, heritage, routes, calls) all emitted percent: 82 — the UI looked frozen for the duration of the deferred work, which on large repos is several seconds to minutes and visually identical to the hang PR #1693 set out to fix. Redistributed: parse phase: 20-70 (was 20-82) imports: 70-75 heritage: 75-80 routes: 80-85 calls: 85-95 Each deferred stage now advances through its own band via the existing per-batch progress callback. Skipped stages (zero deferred input) leave their band as a no-op jump - the next stage still starts at its own band, preserving strict monotonicity. The "no parseable files" early return now jumps to 95 (was 82), and the duplicate "Parsing N files..." announcement is suppressed when totalParseable === 0 to avoid a non-monotonic 95 -> 20 regression that pre-existed (uncovered by the new monotonic test). M3 - Throughput log gated on `--verbose`, not just NODE_ENV=development The per-chunk files/s log was gated on `isDev`, so operators running `gitnexus analyze --verbose` in a production install never saw it. Now fires when (isDev || isVerboseIngestionEnabled()) — matches the documented promise that `--verbose` shows tuning observability. L1 - Typo rename: `chunkChunkStartMs` -> `chunkStartMs` L5 - `buildExportedTypeMapFromGraph` runs BEFORE `seedCrossFileReceiverTypes` Previously the seeding branch was reached with `exportedTypeMap.size === 0` in the worker path (the map was only built far below, AFTER the seeding branch), so the seed dead-coded itself silently and call resolution never got the cross-file receiver-type enrichment. Now the map is populated from the in-progress graph before the seed call; the post-parse builder remains as a defensive sequential-path fallback, guarded by `size === 0` so we don't pay the cost twice on the worker path. Net win: cross-file CALLS edges that previously had no receiver type now get enriched. New test: parse-impl-progress-monotonic.test.ts Asserts the emitted percent stream is strictly non-decreasing across the parse + deferred phases, and that the deferred band (>=70) is actually reached. Also pins the "no parseable files" path to exactly [95] so the 95 -> 20 regression we just fixed can't re-emerge.
120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
/**
|
|
* U4 (M2) — Monotonic progress through the parse + deferred-extraction phases.
|
|
*
|
|
* Before this fix, parse-impl emitted `percent: 82` for every progress
|
|
* update during the deferred resolution stages (imports, heritage, routes,
|
|
* calls). The UI sat at 82 for the duration of the deferred work — on real
|
|
* repos, several seconds to minutes — looking exactly like a hang, which is
|
|
* the user-facing symptom PR #1693 set out to fix.
|
|
*
|
|
* After M2, parse phase covers 20-70 and deferred extraction covers 70-95
|
|
* across four labelled sub-bands. This test runs `runChunkedParseAndResolve`
|
|
* on a small temp repo via the deterministic sequential-fallback path
|
|
* (`skipWorkers: true`) and asserts the recorded percent stream is strictly
|
|
* non-decreasing AND reaches the deferred band (>=70) before returning.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { runChunkedParseAndResolve } from '../../src/core/ingestion/pipeline-phases/parse-impl.js';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
|
|
|
function makeTempRepo(files: Record<string, string>): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-impl-progress-'));
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
const abs = path.join(dir, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, content);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
function scanned(repo: string, files: string[]) {
|
|
return files.map((rel) => ({
|
|
path: rel,
|
|
size: fs.statSync(path.join(repo, rel)).size,
|
|
}));
|
|
}
|
|
|
|
describe('parse-impl progress monotonicity (U4 M2)', () => {
|
|
let repoPath = '';
|
|
|
|
beforeEach(() => {
|
|
repoPath = makeTempRepo({
|
|
'a.ts': `export function foo() { return 1; }\n`,
|
|
'b.ts': `import { foo } from './a';\nexport function bar() { return foo(); }\n`,
|
|
'c.ts': `import { bar } from './b';\nexport class Baz { run() { return bar(); } }\n`,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (repoPath && fs.existsSync(repoPath)) {
|
|
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('emits a strictly non-decreasing percent stream and reaches the deferred band', async () => {
|
|
const graph = createKnowledgeGraph();
|
|
const files = ['a.ts', 'b.ts', 'c.ts'];
|
|
const percents: number[] = [];
|
|
|
|
await runChunkedParseAndResolve(
|
|
graph,
|
|
scanned(repoPath, files),
|
|
files,
|
|
files.length,
|
|
repoPath,
|
|
Date.now(),
|
|
(p) => {
|
|
if (typeof p.percent === 'number') percents.push(p.percent);
|
|
},
|
|
{ skipWorkers: true },
|
|
);
|
|
|
|
// Must have emitted at least one progress update.
|
|
expect(percents.length).toBeGreaterThan(0);
|
|
|
|
// Strict monotonic non-decreasing across the whole stream.
|
|
for (let i = 1; i < percents.length; i++) {
|
|
expect(percents[i]).toBe(Math.max(percents[i - 1], percents[i]));
|
|
}
|
|
|
|
// The parse phase advances through 20-70; the deferred extraction band
|
|
// covers 70-95. On a 3-file fixture with imports + heritage + calls,
|
|
// we should observe at least one percent value in the 70-95 band so the
|
|
// monotonic-advance behavior is exercised, not just the parse half.
|
|
const reachedDeferredBand = percents.some((p) => p >= 70 && p <= 95);
|
|
expect(reachedDeferredBand).toBe(true);
|
|
|
|
// The final emitted percent must land at or below the post-parse ceiling
|
|
// (95). The orchestrator (run-analyze) drives 95-100 itself; parse-impl
|
|
// never emits >95.
|
|
expect(percents[percents.length - 1]).toBe(Math.min(percents[percents.length - 1], 95));
|
|
});
|
|
|
|
it('emits percent 95 (not 82) when there are no parseable files to skip past the parse band', async () => {
|
|
const graph = createKnowledgeGraph();
|
|
// No parseable files: empty scanned list, empty parseable list.
|
|
const percents: number[] = [];
|
|
|
|
await runChunkedParseAndResolve(
|
|
graph,
|
|
[],
|
|
[],
|
|
0,
|
|
repoPath,
|
|
Date.now(),
|
|
(p) => {
|
|
if (typeof p.percent === 'number') percents.push(p.percent);
|
|
},
|
|
{ skipWorkers: true },
|
|
);
|
|
|
|
// The early-return path must emit 95 (the new post-deferred ceiling),
|
|
// not the stale 82 it used before M2 — otherwise downstream phases
|
|
// would visibly regress percent on the next update.
|
|
expect(percents).toEqual([95]);
|
|
});
|
|
});
|