fix(ui): show synthesis as a 'Linking dynamic dispatch' phase; mute node:sqlite warning spam (#1299)

Two first-run UX bugs surfaced by indexing a real 1,342-file C repo:

1. After 'Resolving refs' hit 100%, the ~40 dynamic-dispatch synthesis
   passes ran with no progress surface, so the bar sat frozen at 100%
   long enough to read as a hang (the C fn-pointer pass alone can hold
   for a while on C-heavy repos). Synthesis now reports per-pass
   progress through a new 'linking' IndexProgress phase, rendered as
   'Linking dynamic dispatch'. The step total is pinned by a test to
   the synthesizer's actual __mark() count so adding a pass without
   bumping it fails loudly.

2. node:sqlite's ExperimentalWarning is emitted once per THREAD, so the
   main process plus every parse worker printed it mid-index,
   interleaved with the progress UI. All launch paths now pass
   --disable-warning=ExperimentalWarning: both bundle launchers, the
   Windows npm-shim invocation, and the CLI self-relaunch
   (NODE_RUNTIME_FLAGS, deliberately excluded from the re-exec gate so
   an older installed launcher never triggers a pointless re-exec, and
   version-gated off nodes older than the flag).

Verified end-to-end on the same repo: zero warnings, live linking bar,
byte-identical graph (50,520 nodes / 148,232 edges). Full suite green.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-15 19:44:09 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 243ef1d3e2
commit ad5300a601
11 changed files with 239 additions and 35 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* Progress reporting for the callback-edge synthesis tail.
*
* Synthesis runs AFTER the resolution bar reaches 100%, so before this it had
* no progress surface at all — on synthesizer-heavy repos (e.g. large C
* codebases hitting the fn-pointer pass) the CLI sat frozen at
* "Resolving refs 100%" long enough that users concluded the index hung and
* killed it. These tests pin (a) that indexing emits the dedicated 'linking'
* phase with monotonic per-pass progress, and (b) that the advertised step
* total stays in sync with the synthesizer's actual pass list.
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph, IndexProgress } from '../src/index';
import { SYNTH_PROGRESS_STEPS } from '../src/resolution/callback-synthesizer';
describe('synthesis progress ("Linking dynamic dispatch" phase)', () => {
it('SYNTH_PROGRESS_STEPS matches the synthesizers actual __mark() step count', () => {
// The constant is cosmetic (progress denominator), but drift makes the bar
// end early or jump to 100% — adding a pass must bump it. Every step site
// calls __mark('<label>') with a string literal, so count those.
const src = fs.readFileSync(
path.join(__dirname, '../src/resolution/callback-synthesizer.ts'),
'utf8'
);
const stepSites = (src.match(/__mark\('/g) ?? []).length;
expect(SYNTH_PROGRESS_STEPS).toBe(stepSites);
});
it('indexing emits a monotonic linking phase ending at the full step count', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-synth-progress-'));
try {
fs.writeFileSync(path.join(dir, 'a.ts'), 'export function helper() { return 1; }\n');
fs.writeFileSync(
path.join(dir, 'b.ts'),
"import { helper } from './a';\nexport function main() { return helper(); }\n"
);
const events: IndexProgress[] = [];
const cg = await CodeGraph.init(dir, {
index: true,
onProgress: (p) => events.push(p),
});
await cg.close();
const linking = events.filter((e) => e.phase === 'linking');
expect(linking.length).toBeGreaterThan(0);
// Emitted up-front so the phase label flips as soon as synthesis starts…
expect(linking[0]!.current).toBe(0);
// …and every step reports against the same total, monotonically.
expect(linking.every((e) => e.total === SYNTH_PROGRESS_STEPS)).toBe(true);
for (let i = 1; i < linking.length; i++) {
expect(linking[i]!.current).toBeGreaterThanOrEqual(linking[i - 1]!.current);
}
expect(linking[linking.length - 1]!.current).toBe(SYNTH_PROGRESS_STEPS);
// The linking phase comes after resolution has finished.
const lastResolving = events.map((e) => e.phase).lastIndexOf('resolving');
const firstLinking = events.map((e) => e.phase).indexOf('linking');
expect(firstLinking).toBeGreaterThan(lastResolving);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});