mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
* feat(progress): add per-language progress reporting to scope-resolution phase (#1741) The scope-resolution phase (which can run 74+ minutes on large Java/Kotlin repos) previously emitted zero progress updates, causing the CLI progress bar to freeze at ~49% with a stale "Parsing code" label — making users think the tool was stuck. - Add `scopeResolution` to PipelinePhase type and PHASE_LABELS - Add `onProgress` callback to `runScopeResolution` with per-file updates during the extract loop and sub-phase boundary markers (building scope model, resolving references, emitting edges) - Wire progress through `scopeResolutionPhase` with pre-counted file totals, per-language labels, and pipeline-wide percent mapping (90-95 internal) - Bump mro/communities/processes percent ranges to 95-100 to maintain monotonic progress after scope resolution - Add `scopeResolution` to mro's deps (latent ordering fix: mro reads EXTENDS edges that scope resolution writes via preEmitInheritanceEdges) * fix(progress): clamp overallRatio, fire final extract event, fix mro @deps JSDoc - Clamp overallRatio to [0,1] so percent never exceeds 95 when readFileContents drops files (langFileCount < totalScopeFiles) - Fire onProgress for the last file in the extract loop even when files.length is not divisible by progressInterval - Update mro @deps JSDoc to include scopeResolution * fix(progress): ensure bar redraws at every state transition - Fire initial 'extracting' event at file 0 so the sub-phase label appears immediately, not after progressInterval files - Emit a completion event at percent 95 when scope resolution finishes so the bar definitively reaches the phase ceiling before mro starts * feat(progress): improve UX with human-readable elapsed, language counter, cleaner labels - Format elapsed time as "5m 12s" / "1h 20m" instead of raw "(312s)" for all pipeline phases (CLI-wide improvement) - Add language counter "[1/3]" to scope-resolution detail so users know how many languages remain and which is active - Rename sub-phases for clarity: "building scope model" → "analyzing types", "emitting edges" → "linking symbols" - Remove nested parentheses from detail strings for cleaner display - Expand scope-resolution percent range from 5 to 8 points (90-98 internal → 54-59% display) for more visible bar motion - Re-allocate mro (98), communities (98-99), processes (99-100) * feat(progress): typed sub-phases, i18n locales, and test coverage - Extract ScopeResolutionSubPhase union type with exhaustive switch guard so adding a sub-phase without updating phase.ts is a compile error - Add scopeResolution key to en and zh-CN locale files so the web UI shows translated labels instead of raw message fallback - Extract formatElapsed to its own module with 7 boundary-value tests (0s, 59s, 60s, 3599s, 3600s, 3661s, 7323s) - Add runScopeResolution onProgress integration test proving sub-phase order (extracting → analyzing types → resolving references → linking symbols) and the 0-file early-return path --------- Co-authored-by: Test <test@example.com>
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { ParsedFile, ScopeId, Scope } from 'gitnexus-shared';
|
|
import {
|
|
runScopeResolution,
|
|
type ScopeResolutionSubPhase,
|
|
} from '../../../src/core/ingestion/scope-resolution/pipeline/run.js';
|
|
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
|
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
|
|
import type { ScopeResolver } from '../../../src/core/ingestion/scope-resolution/contract/scope-resolver.js';
|
|
|
|
const mkScope = (id: ScopeId, filePath: string): Scope => ({
|
|
id,
|
|
parent: null,
|
|
kind: 'Module',
|
|
range: { startLine: 1, startCol: 0, endLine: 10, endCol: 0 },
|
|
filePath,
|
|
bindings: new Map(),
|
|
ownedDefs: [],
|
|
imports: [],
|
|
typeBindings: new Map(),
|
|
});
|
|
|
|
const mkFile = (filePath: string): ParsedFile => ({
|
|
filePath,
|
|
moduleScope: `scope:${filePath}#module`,
|
|
scopes: [mkScope(`scope:${filePath}#module`, filePath)],
|
|
parsedImports: [],
|
|
localDefs: [],
|
|
referenceSites: [],
|
|
});
|
|
|
|
const stubProvider = {
|
|
language: 'python' as const,
|
|
languageProvider: {} as ScopeResolver['languageProvider'],
|
|
importEdgeReason: 'test',
|
|
populateOwners: () => {},
|
|
resolveImportTarget: () => null,
|
|
mergeBindings: (existing: unknown) => existing,
|
|
buildMro: () => new Map(),
|
|
propagatesReturnTypesAcrossImports: false,
|
|
} as unknown as ScopeResolver;
|
|
|
|
describe('runScopeResolution onProgress', () => {
|
|
it('emits sub-phases in order for a 3-file input', () => {
|
|
const files = [
|
|
{ path: 'a.py', content: '' },
|
|
{ path: 'b.py', content: '' },
|
|
{ path: 'c.py', content: '' },
|
|
];
|
|
const preExtracted = new Map<string, ParsedFile>();
|
|
for (const f of files) preExtracted.set(f.path, mkFile(f.path));
|
|
|
|
const calls: { subPhase: ScopeResolutionSubPhase; current: number; total: number }[] = [];
|
|
const onProgress = (subPhase: ScopeResolutionSubPhase, current: number, total: number) => {
|
|
calls.push({ subPhase, current, total });
|
|
};
|
|
|
|
runScopeResolution(
|
|
{
|
|
graph: createKnowledgeGraph(),
|
|
model: createSemanticModel(),
|
|
files,
|
|
preExtractedParsedFiles: preExtracted,
|
|
onProgress,
|
|
},
|
|
stubProvider,
|
|
);
|
|
|
|
const subPhases = calls.map((c) => c.subPhase);
|
|
expect(subPhases).toContain('extracting');
|
|
expect(subPhases).toContain('analyzing types');
|
|
expect(subPhases).toContain('resolving references');
|
|
expect(subPhases).toContain('linking symbols');
|
|
|
|
const extractCalls = calls.filter((c) => c.subPhase === 'extracting');
|
|
expect(extractCalls.length).toBeGreaterThan(0);
|
|
expect(extractCalls[0].total).toBe(3);
|
|
expect(extractCalls[0].current).toBe(0);
|
|
expect(extractCalls[extractCalls.length - 1].current).toBe(3);
|
|
|
|
const analyzeIdx = subPhases.indexOf('analyzing types');
|
|
const resolveIdx = subPhases.indexOf('resolving references');
|
|
const linkIdx = subPhases.indexOf('linking symbols');
|
|
expect(analyzeIdx).toBeLessThan(resolveIdx);
|
|
expect(resolveIdx).toBeLessThan(linkIdx);
|
|
});
|
|
|
|
it('emits only extracting (0, 0) then returns early for 0-file input', () => {
|
|
const calls: { subPhase: ScopeResolutionSubPhase; current: number; total: number }[] = [];
|
|
const onProgress = (subPhase: ScopeResolutionSubPhase, current: number, total: number) => {
|
|
calls.push({ subPhase, current, total });
|
|
};
|
|
|
|
const stats = runScopeResolution(
|
|
{
|
|
graph: createKnowledgeGraph(),
|
|
model: createSemanticModel(),
|
|
files: [],
|
|
onProgress,
|
|
},
|
|
stubProvider,
|
|
);
|
|
|
|
expect(stats.filesProcessed).toBe(0);
|
|
expect(calls).toEqual([{ subPhase: 'extracting', current: 0, total: 0 }]);
|
|
});
|
|
});
|