From 10f95e5b4e40a96c18e0a31875baa3ddf2fb2031 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 10:24:34 -0700 Subject: [PATCH 1/5] Report plugin Filter exclusions --- .changeset/calm-filters-count.md | 6 + .../core/src/analysis/workspaceAnalyze.ts | 2 + .../core/src/analysis/workspaceDiscovery.ts | 1 + packages/core/src/discovery/contracts.ts | 2 + packages/core/src/discovery/file/service.ts | 7 ++ packages/core/src/indexing/state.ts | 2 + .../tests/analysis/workspaceAnalyze.test.ts | 8 ++ .../tests/analysis/workspaceDiscovery.test.ts | 1 + .../discovery/file/discovery.discover.test.ts | 34 ++++++ packages/core/tests/indexing/analysis.test.ts | 1 + .../extension/graphView/analysis/execution.ts | 1 + .../provider/analysis/handlers/messages.ts | 6 + .../graphView/provider/analysis/methods.ts | 1 + .../src/extension/pipeline/analysis/run.ts | 1 + .../src/extension/pipeline/analysisSource.ts | 7 ++ .../extension/pipeline/indexingState/model.ts | 2 + .../pipeline/service/cache/invalidation.ts | 1 + .../extension/pipeline/service/cachedGraph.ts | 6 + .../pipeline/service/graphDiscovery.ts | 1 + .../pipeline/service/pluginFacade.ts | 4 + .../pipeline/service/refresh/context.ts | 1 + .../service/refresh/modes/analysisScope.ts | 1 + .../service/refresh/modes/changedFiles.ts | 1 + .../refresh/modes/gitignoreMetadata.ts | 1 + .../service/refresh/modes/pluginFiles.ts | 1 + .../pipeline/service/runtime/discovery.ts | 1 + .../src/shared/protocol/extensionToWebview.ts | 1 + .../src/webview/app/shell/storeSelectors.ts | 2 + .../extension/src/webview/app/shell/view.tsx | 4 +- .../components/searchBar/field/model.ts | 3 +- .../searchBar/filters/countState.ts | 14 ++- .../components/searchBar/filters/popover.tsx | 10 +- .../searchBar/filters/popover/types.ts | 3 +- .../src/webview/store/initialState.ts | 1 + .../webview/store/messageHandlers/graph.ts | 6 + .../src/webview/store/messageTypes.ts | 1 + .../extension/src/webview/store/messages.ts | 5 + packages/extension/src/webview/store/state.ts | 1 + .../provider/analysis/handlers.test.ts | 8 +- ...pluginFilterAccounting.integration.test.ts | 111 ++++++++++++++++++ .../service/refresh/modes/pluginFiles.test.ts | 3 + .../service/runtime/discovery.test.ts | 2 + .../app/shell/view/visibleGraph.test.tsx | 7 +- .../searchBar/filters/popover.test.tsx | 14 ++- .../store/messageHandlers/graph/fixture.ts | 1 + .../messageHandlers/graph/settings.test.ts | 6 + 46 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 .changeset/calm-filters-count.md create mode 100644 packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts diff --git a/.changeset/calm-filters-count.md b/.changeset/calm-filters-count.md new file mode 100644 index 0000000000..12a206f3e4 --- /dev/null +++ b/.changeset/calm-filters-count.md @@ -0,0 +1,6 @@ +--- +"@codegraphy-dev/core": patch +"@codegraphy-dev/extension": patch +--- + +Report Filter exclusions before analysis separately from Nodes excluded in the Graph View. diff --git a/packages/core/src/analysis/workspaceAnalyze.ts b/packages/core/src/analysis/workspaceAnalyze.ts index 977c2a6792..517525cc3f 100644 --- a/packages/core/src/analysis/workspaceAnalyze.ts +++ b/packages/core/src/analysis/workspaceAnalyze.ts @@ -41,6 +41,7 @@ export interface WorkspacePipelineAnalysisSource { _completeGraphData?: IGraphData; _lastDiscoveredDirectories: string[]; _lastDiscoveredFiles: IDiscoveredFile[]; + _lastFilterExcludedPaths?: string[]; _lastFileAnalysis: Map; _lastFileConnections: Map; _lastGitIgnoredPaths?: string[]; @@ -164,6 +165,7 @@ export async function analyzeWorkspaceWithAnalyzer( source._lastFileConnections = analysisResult.fileConnections; source._lastDiscoveredDirectories = discoveryResult.directories ?? []; source._lastDiscoveredFiles = discoveryResult.files; + source._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; source._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; source._lastWorkspaceRoot = workspaceRoot; diff --git a/packages/core/src/analysis/workspaceDiscovery.ts b/packages/core/src/analysis/workspaceDiscovery.ts index 45fa7b578c..30b2cb0e4e 100644 --- a/packages/core/src/analysis/workspaceDiscovery.ts +++ b/packages/core/src/analysis/workspaceDiscovery.ts @@ -10,6 +10,7 @@ export interface WorkspacePipelineDiscoveryResult { directories?: string[]; durationMs: number; files: TFile[]; + filterExcludedPaths: string[]; gitIgnoredPaths?: string[]; limitReached: boolean; totalFound: number; diff --git a/packages/core/src/discovery/contracts.ts b/packages/core/src/discovery/contracts.ts index 93da533cd0..94a10ef017 100644 --- a/packages/core/src/discovery/contracts.ts +++ b/packages/core/src/discovery/contracts.ts @@ -49,6 +49,8 @@ export interface IDiscoveredFile { export interface IDiscoveryResult { /** Eligible discovered files, capped by maxFiles */ files: IDiscoveredFile[]; + /** Workspace file paths excluded by active Filters, excluding Git ignored state. */ + filterExcludedPaths: string[]; /** Discovered directory paths relative to the workspace root */ directories: string[]; /** Discovered file and directory paths reported by Git as ignored */ diff --git a/packages/core/src/discovery/file/service.ts b/packages/core/src/discovery/file/service.ts index cbfe8ef3e3..679a14f23e 100644 --- a/packages/core/src/discovery/file/service.ts +++ b/packages/core/src/discovery/file/service.ts @@ -305,6 +305,12 @@ export class FileDiscovery { directories, ) : new Set(); + const filterExcludedPaths = candidateFiles + .filter(file => ( + !gitIgnoredPaths.has(file.relativePath) + && matchesAnyPattern(file.relativePath, filterPatterns) + )) + .map(file => file.relativePath); const eligibleFiles = candidateFiles.filter(file => ( !matchesAnyPattern(file.relativePath, filterPatterns) && !gitIgnoredPaths.has(file.relativePath) @@ -339,6 +345,7 @@ export class FileDiscovery { cacheFilePaths, cachePathPrefixes: gitWorkspacePaths?.ignoredPathPrefixes ?? [], directories: eligibleDirectories, + filterExcludedPaths, gitIgnoredPaths: [ ...gitIgnoredPaths, ...(gitWorkspacePaths?.ignoredPathPrefixes ?? []), diff --git a/packages/core/src/indexing/state.ts b/packages/core/src/indexing/state.ts index f49dd477a6..d4dcde442a 100644 --- a/packages/core/src/indexing/state.ts +++ b/packages/core/src/indexing/state.ts @@ -8,6 +8,7 @@ export interface WorkspaceIndexEngineState { cache: IWorkspaceAnalysisCache; discoveredDirectories: string[]; discoveredFiles: IDiscoveredFile[]; + filterExcludedPaths: string[]; gitIgnoredPaths: string[]; fileAnalysis: Map; fileConnections: Map; @@ -22,6 +23,7 @@ export function createWorkspaceIndexEngineState( cache, discoveredDirectories: [], discoveredFiles: [], + filterExcludedPaths: [], gitIgnoredPaths: [], fileAnalysis: new Map(), fileConnections: new Map(), diff --git a/packages/core/tests/analysis/workspaceAnalyze.test.ts b/packages/core/tests/analysis/workspaceAnalyze.test.ts index 360efb3f3e..88f7ee572a 100644 --- a/packages/core/tests/analysis/workspaceAnalyze.test.ts +++ b/packages/core/tests/analysis/workspaceAnalyze.test.ts @@ -36,6 +36,7 @@ function createSource() { _completeGraphData: undefined as IGraphData | undefined, _lastDiscoveredDirectories: [] as string[], _lastDiscoveredFiles: [] as IDiscoveredFile[], + _lastFilterExcludedPaths: [] as string[], _lastFileAnalysis: new Map(), _lastFileConnections: new Map(), _lastGitIgnoredPaths: [] as string[], @@ -51,6 +52,7 @@ function createDependencies() { directories: [] as string[], durationMs: 3, files: [] as IDiscoveredFile[], + filterExcludedPaths: [] as string[], gitIgnoredPaths: [] as string[], limitReached: false, totalFound: 0, @@ -134,6 +136,7 @@ describe('pipeline/analysis/analyze', () => { directories: ['src/new-folder'], durationMs: 4, files, + filterExcludedPaths: ['dist/app.js'], gitIgnoredPaths: ['src/index.ts'], limitReached: false, totalFound: 1, @@ -185,6 +188,7 @@ describe('pipeline/analysis/analyze', () => { new Set(['plugin.python']), ); expect(source._lastDiscoveredFiles).toEqual(files); + expect(source._lastFilterExcludedPaths).toEqual(['dist/app.js']); expect(source._lastDiscoveredDirectories).toEqual(['src/new-folder']); expect(source._lastGitIgnoredPaths).toEqual(['src/index.ts']); expect(source._lastFileAnalysis).toBe(fileAnalysis); @@ -254,6 +258,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 5, files: [] as IDiscoveredFile[], + filterExcludedPaths: [] as string[], limitReached: true, totalFound: 27, }); @@ -276,6 +281,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 4, files, + filterExcludedPaths: [] as string[], limitReached: false, totalFound: 1, }); @@ -330,6 +336,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 4, files: [] as IDiscoveredFile[], + filterExcludedPaths: [] as string[], limitReached: false, totalFound: 0, }; @@ -358,6 +365,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 2, files: [] as IDiscoveredFile[], + filterExcludedPaths: [] as string[], limitReached: false, totalFound: 0, }); diff --git a/packages/core/tests/analysis/workspaceDiscovery.test.ts b/packages/core/tests/analysis/workspaceDiscovery.test.ts index d946107ff7..4913be95d6 100644 --- a/packages/core/tests/analysis/workspaceDiscovery.test.ts +++ b/packages/core/tests/analysis/workspaceDiscovery.test.ts @@ -10,6 +10,7 @@ describe('pipeline/discovery', () => { const discover = vi.fn(async () => ({ durationMs: 2, files: ['src/index.ts'], + filterExcludedPaths: [], limitReached: false, totalFound: 1, })); diff --git a/packages/core/tests/discovery/file/discovery.discover.test.ts b/packages/core/tests/discovery/file/discovery.discover.test.ts index 72e5597236..035454dc3f 100644 --- a/packages/core/tests/discovery/file/discovery.discover.test.ts +++ b/packages/core/tests/discovery/file/discovery.discover.test.ts @@ -83,9 +83,43 @@ describe('FileDiscovery discover', () => { }); expect(result.files.map(file => file.relativePath)).toEqual([path.join('src', 'app.ts')]); + expect(result.filterExcludedPaths).toEqual([path.join('filtered', 'one.ts')]); + expect(result.filterExcludedPaths).not.toContain(path.join('ignored', 'file-0.ts')); expect(onProgress.mock.calls.map(([progress]) => progress.current)).toEqual([1]); }); + it('accounts for mixed TypeScript, Godot, and Unity plugin Filters separately from Git ignored state', async () => { + initGitRepo(); + createFile('.gitignore', 'ignored/**\n'); + createFile('src/app.ts'); + createFile('dist/app.js'); + createFile('.godot/editor/project_metadata.cfg'); + createFile('Assets/Player.prefab'); + createFile('Assets/Player.prefab.meta'); + createFile('ProjectSettings/ProjectSettings.asset'); + createFile('ignored/generated.ts'); + + const result = await discovery.discover({ + rootPath: tempDir, + filter: [ + '**/dist/**', + '**/.godot/**', + '**/*.meta', + '**/[Pp]roject[Ss]ettings/**', + ], + }); + + expect(result.filterExcludedPaths).toEqual([ + '.godot/editor/project_metadata.cfg', + 'Assets/Player.prefab.meta', + 'ProjectSettings/ProjectSettings.asset', + ]); + expect(result.files.map(file => file.relativePath)).toContain('src/app.ts'); + expect(result.files.map(file => file.relativePath)).toContain('Assets/Player.prefab'); + expect(result.gitIgnoredPaths).toContain('ignored'); + expect(result.filterExcludedPaths).not.toContain('ignored/generated.ts'); + }); + it('uses Git paths without recursively reading ignored or filtered workspace trees', async () => { initGitRepo(); createFile('.gitignore', 'ignored/**\n'); diff --git a/packages/core/tests/indexing/analysis.test.ts b/packages/core/tests/indexing/analysis.test.ts index 0d8d2ab1eb..955b8a810c 100644 --- a/packages/core/tests/indexing/analysis.test.ts +++ b/packages/core/tests/indexing/analysis.test.ts @@ -51,6 +51,7 @@ describe('indexing/analysis', () => { discoveryResult: { durationMs: 1, files, + filterExcludedPaths: [], directories: [], gitIgnoredPaths: [], limitReached: false, diff --git a/packages/extension/src/extension/graphView/analysis/execution.ts b/packages/extension/src/extension/graphView/analysis/execution.ts index 2a3010d660..ebc6749759 100644 --- a/packages/extension/src/extension/graphView/analysis/execution.ts +++ b/packages/extension/src/extension/graphView/analysis/execution.ts @@ -12,6 +12,7 @@ interface GraphViewAnalyzerLike { initialize(): Promise; syncWorkspacePlugins(): Promise; hasIndex(): boolean; + getFilterExcludedFileCount?(): number; getIndexStatus?(): { freshness: CodeGraphyIndexFreshness; detail: string; diff --git a/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts b/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts index fb80fdb1dd..6be03d8fa7 100644 --- a/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts +++ b/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts @@ -18,6 +18,12 @@ export function sendGraphDataUpdated( source._disabledPlugins, ); source._sendMessage({ type: 'GRAPH_DATA_UPDATED', payload: graphData }); + source._sendMessage({ + type: 'FILTER_ACCOUNTING_UPDATED', + payload: { + excludedFileCount: source._analyzer?.getFilterExcludedFileCount?.() ?? 0, + }, + }); } export const sendGraphIndexStatusUpdated: ( diff --git a/packages/extension/src/extension/graphView/provider/analysis/methods.ts b/packages/extension/src/extension/graphView/provider/analysis/methods.ts index 82e1955763..a924692a4a 100644 --- a/packages/extension/src/extension/graphView/provider/analysis/methods.ts +++ b/packages/extension/src/extension/graphView/provider/analysis/methods.ts @@ -31,6 +31,7 @@ export interface GraphViewProviderWorkspaceReadyRegistryLike { interface GraphViewProviderAnalysisAnalyzerLike { registry?: GraphViewProviderWorkspaceReadyRegistryLike; + getFilterExcludedFileCount?(): number; } export interface GraphViewProviderAnalysisMethodsSource { diff --git a/packages/extension/src/extension/pipeline/analysis/run.ts b/packages/extension/src/extension/pipeline/analysis/run.ts index 833a9b2064..897e93f398 100644 --- a/packages/extension/src/extension/pipeline/analysis/run.ts +++ b/packages/extension/src/extension/pipeline/analysis/run.ts @@ -35,6 +35,7 @@ export function runWorkspacePipelineAnalysis( directories: result.directories, durationMs: result.durationMs, files: result.files, + filterExcludedPaths: result.filterExcludedPaths, gitIgnoredPaths: result.gitIgnoredPaths, limitReached: result.limitReached, totalFound: result.totalFound ?? result.files.length, diff --git a/packages/extension/src/extension/pipeline/analysisSource.ts b/packages/extension/src/extension/pipeline/analysisSource.ts index 54a386d164..93cb27cb1b 100644 --- a/packages/extension/src/extension/pipeline/analysisSource.ts +++ b/packages/extension/src/extension/pipeline/analysisSource.ts @@ -46,6 +46,7 @@ export interface WorkspacePipelineSourceOwner { _completeGraphData: IGraphData; _lastDiscoveredDirectories: string[]; _lastDiscoveredFiles: IDiscoveredFile[]; + _lastFilterExcludedPaths: string[]; _lastFileAnalysis: Map; _lastFileConnections: Map; _lastGitIgnoredPaths: string[]; @@ -135,6 +136,12 @@ export function createWorkspacePipelineAnalysisSource( owner._lastDiscoveredDirectories = directories; }, }, + _lastFilterExcludedPaths: { + get: () => owner._lastFilterExcludedPaths, + set: (paths: string[]) => { + owner._lastFilterExcludedPaths = paths; + }, + }, _lastFileConnections: { get: () => owner._lastFileConnections, set: (fileConnections: Map) => { diff --git a/packages/extension/src/extension/pipeline/indexingState/model.ts b/packages/extension/src/extension/pipeline/indexingState/model.ts index abf0d72bb4..ceb70b5b3f 100644 --- a/packages/extension/src/extension/pipeline/indexingState/model.ts +++ b/packages/extension/src/extension/pipeline/indexingState/model.ts @@ -58,6 +58,8 @@ export abstract class WorkspacePipelineEngineStateBase { protected set _lastDiscoveredFiles(value: IDiscoveredFile[]) { this._engineState.discoveredFiles = value; } + protected get _lastFilterExcludedPaths(): string[] { return this._engineState.filterExcludedPaths; } + protected set _lastFilterExcludedPaths(value: string[]) { this._engineState.filterExcludedPaths = value; } protected get _lastGitIgnoredPaths(): string[] { return this._engineState.gitIgnoredPaths; } protected set _lastGitIgnoredPaths(value: string[]) { this._engineState.gitIgnoredPaths = value; } protected get _lastWorkspaceRoot(): string { return this._engineState.workspaceRoot; } diff --git a/packages/extension/src/extension/pipeline/service/cache/invalidation.ts b/packages/extension/src/extension/pipeline/service/cache/invalidation.ts index 2b4f59591a..05049f7f87 100644 --- a/packages/extension/src/extension/pipeline/service/cache/invalidation.ts +++ b/packages/extension/src/extension/pipeline/service/cache/invalidation.ts @@ -20,6 +20,7 @@ export function invalidateWorkspacePipelineFiles( cache: state.cache, discoveredDirectories: [], discoveredFiles: [], + filterExcludedPaths: [], gitIgnoredPaths: [], fileAnalysis: state.lastFileAnalysis, fileConnections: state.lastFileConnections, diff --git a/packages/extension/src/extension/pipeline/service/cachedGraph.ts b/packages/extension/src/extension/pipeline/service/cachedGraph.ts index 195625355e..a2d18f618c 100644 --- a/packages/extension/src/extension/pipeline/service/cachedGraph.ts +++ b/packages/extension/src/extension/pipeline/service/cachedGraph.ts @@ -60,6 +60,12 @@ export abstract class WorkspacePipelineCachedGraphFacade extends WorkspacePipeli ...this._getEffectivePluginFilterPatterns(disabledPlugins), ]; const gitIgnoredPaths = new Set(cachedDiscovery.gitIgnoredPaths); + this._lastFilterExcludedPaths = cachedDiscovery.files + .filter(file => ( + !gitIgnoredPaths.has(file.relativePath) + && matchesAnyPattern(file.relativePath, activeFilterPatterns) + )) + .map(file => file.relativePath); const eligibleFiles = cachedDiscovery.files.filter(file => ( !gitIgnoredPaths.has(file.relativePath) && !matchesAnyPattern(file.relativePath, activeFilterPatterns) diff --git a/packages/extension/src/extension/pipeline/service/graphDiscovery.ts b/packages/extension/src/extension/pipeline/service/graphDiscovery.ts index 3cdefcb11f..e985d0132b 100644 --- a/packages/extension/src/extension/pipeline/service/graphDiscovery.ts +++ b/packages/extension/src/extension/pipeline/service/graphDiscovery.ts @@ -37,6 +37,7 @@ export abstract class WorkspacePipelineGraphDiscoveryFacade extends WorkspacePip this._lastDiscoveredDirectories = discoveryResult.directories ?? []; this._lastDiscoveredFiles = discoveryResult.files; + this._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; this._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; this._lastFileAnalysis = new Map(); this._lastFileConnections = fileConnections; diff --git a/packages/extension/src/extension/pipeline/service/pluginFacade.ts b/packages/extension/src/extension/pipeline/service/pluginFacade.ts index d42363477f..cd64e334d2 100644 --- a/packages/extension/src/extension/pipeline/service/pluginFacade.ts +++ b/packages/extension/src/extension/pipeline/service/pluginFacade.ts @@ -79,6 +79,10 @@ export abstract class WorkspacePipelinePluginFacade extends WorkspacePipelineInt return getPipelinePluginFilterGroups(this._registry, disabledPlugins); } + getFilterExcludedFileCount(): number { + return this._lastFilterExcludedPaths.length; + } + protected _getEffectiveCustomFilterPatterns(filterPatterns: string[]): string[] { return getEffectiveCustomFilterPatterns(this._config, filterPatterns); } diff --git a/packages/extension/src/extension/pipeline/service/refresh/context.ts b/packages/extension/src/extension/pipeline/service/refresh/context.ts index 950fd8af7e..23567f16f9 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/context.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/context.ts @@ -27,6 +27,7 @@ export interface RefreshFacadeContext _captureRefreshState(): WorkspacePipelineRefreshState; _getWorkspaceRoot(): string | undefined; _lastGitIgnoredPaths: string[]; + _lastFilterExcludedPaths: string[]; _persistCache(): void; _persistCachePatch(patch: WorkspacePipelineCachePatch): Promise; _persistIndexMetadata(resolvedChangedFilePaths?: readonly string[]): Promise; diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts index ef7f05308c..9500b133e0 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts @@ -34,6 +34,7 @@ export async function refreshAnalysisScopeForFacade( signal: input.signal, workspaceRoot, }); + facade._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; facade._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; if (canReuseCurrentAnalysisForScope({ diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts index 05838aba56..36fad26a91 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts @@ -136,6 +136,7 @@ async function getChangedFileDiscoveryState( files: discovered.discoveryResult.files, limitReached: discovered.discoveryResult.limitReached, }; + facade._lastFilterExcludedPaths = discovered.discoveryResult.filterExcludedPaths; facade._lastGitIgnoredPaths = discovered.discoveryResult.gitIgnoredPaths ?? []; return discoveryResult; } diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts index 64b991aca5..5684c4f4cc 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts @@ -31,6 +31,7 @@ export async function refreshGitignoreMetadataForFacade( facade._lastDiscoveredDirectories = discoveryResult.directories ?? []; facade._lastDiscoveredFiles = discoveryResult.files; + facade._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; facade._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; facade._lastWorkspaceRoot = workspaceRoot; diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts index 4c797515d6..e9188a16d0 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts @@ -31,6 +31,7 @@ export async function refreshPluginFilesForFacade( signal: input.signal, workspaceRoot, }); + facade._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; facade._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; return refreshWorkspacePipelinePluginFiles(createWorkspaceIndexRefreshSource( diff --git a/packages/extension/src/extension/pipeline/service/runtime/discovery.ts b/packages/extension/src/extension/pipeline/service/runtime/discovery.ts index f2dc975e4f..6f561d52a4 100644 --- a/packages/extension/src/extension/pipeline/service/runtime/discovery.ts +++ b/packages/extension/src/extension/pipeline/service/runtime/discovery.ts @@ -18,6 +18,7 @@ export function createWorkspacePipelineDiscoveryDependencies( directories: result.directories, durationMs: result.durationMs, files: result.files, + filterExcludedPaths: result.filterExcludedPaths, gitIgnoredPaths: result.gitIgnoredPaths, limitReached: result.limitReached, totalFound: result.totalFound ?? result.files.length, diff --git a/packages/extension/src/shared/protocol/extensionToWebview.ts b/packages/extension/src/shared/protocol/extensionToWebview.ts index 57a8646794..a8cf57dfd6 100644 --- a/packages/extension/src/shared/protocol/extensionToWebview.ts +++ b/packages/extension/src/shared/protocol/extensionToWebview.ts @@ -24,6 +24,7 @@ export interface IGraphNodeMetricsUpdate { export type ExtensionToWebviewMessage = | { type: 'GRAPH_DATA_UPDATED'; payload: IGraphData } + | { type: 'FILTER_ACCOUNTING_UPDATED'; payload: { excludedFileCount: number } } | { type: 'GRAPH_NODE_METRICS_UPDATED'; payload: { nodes: IGraphNodeMetricsUpdate[] } } | { type: 'APP_BOOTSTRAP_COMPLETE' } | { diff --git a/packages/extension/src/webview/app/shell/storeSelectors.ts b/packages/extension/src/webview/app/shell/storeSelectors.ts index 9c98064a7f..438bc50722 100644 --- a/packages/extension/src/webview/app/shell/storeSelectors.ts +++ b/packages/extension/src/webview/app/shell/storeSelectors.ts @@ -15,6 +15,7 @@ export function useAppState() { const searchOptions = useGraphStore(s => s.searchOptions); const legends = useGraphStore(s => s.legends); const filterPatterns = useGraphStore(s => s.filterPatterns); + const filterExcludedFileCount = useGraphStore(s => s.filterExcludedFileCount); const pluginFilterPatterns = useGraphStore(s => s.pluginFilterPatterns); const pluginFilterGroups = useGraphStore(s => s.pluginFilterGroups); const disabledCustomFilterPatterns = useGraphStore(s => s.disabledCustomFilterPatterns); @@ -40,6 +41,7 @@ export function useAppState() { searchOptions, legends, filterPatterns, + filterExcludedFileCount, pluginFilterPatterns, pluginFilterGroups, disabledCustomFilterPatterns, diff --git a/packages/extension/src/webview/app/shell/view.tsx b/packages/extension/src/webview/app/shell/view.tsx index ac2dc4316b..8ed657bb84 100644 --- a/packages/extension/src/webview/app/shell/view.tsx +++ b/packages/extension/src/webview/app/shell/view.tsx @@ -44,6 +44,7 @@ export default function App({ graphPhysicsPreparation }: AppShellProps): React.R searchOptions, legends, filterPatterns, + filterExcludedFileCount, pluginFilterPatterns, pluginFilterGroups, disabledCustomFilterPatterns, @@ -205,7 +206,8 @@ export default function App({ graphPhysicsPreparation }: AppShellProps): React.R customPatterns: filterPatterns, disabledCustomPatterns: disabledCustomFilterPatterns, disabledPluginPatterns: disabledPluginFilterPatterns, - excludedCount, + excludedFileCount: filterExcludedFileCount, + excludedNodeCount: excludedCount, onDisabledCustomPatternsChange: setDisabledCustomFilterPatterns, onDisabledPluginPatternsChange: setDisabledPluginFilterPatterns, buttonRef: filterButtonRef, diff --git a/packages/extension/src/webview/components/searchBar/field/model.ts b/packages/extension/src/webview/components/searchBar/field/model.ts index c93144d7d3..217cd7a67d 100644 --- a/packages/extension/src/webview/components/searchBar/field/model.ts +++ b/packages/extension/src/webview/components/searchBar/field/model.ts @@ -28,7 +28,8 @@ export interface SearchBarProps { disabledCustomPatterns: string[]; disabledPluginPatterns: string[]; customPatterns: string[]; - excludedCount: number; + excludedFileCount: number; + excludedNodeCount: number; onDisabledCustomPatternsChange: (patterns: string[]) => void; onDisabledPluginPatternsChange: (patterns: string[]) => void; onOpenChange: (open: boolean) => void; diff --git a/packages/extension/src/webview/components/searchBar/filters/countState.ts b/packages/extension/src/webview/components/searchBar/filters/countState.ts index 506845f41b..c9329476cf 100644 --- a/packages/extension/src/webview/components/searchBar/filters/countState.ts +++ b/packages/extension/src/webview/components/searchBar/filters/countState.ts @@ -43,10 +43,18 @@ export function getFilterCountState({ return { kind: 'idle', label: null }; } -export function formatExcludedCount(count: number): string { +export function formatExcludedFileCount(count: number): string { if (count === 1) { - return '1 excluded from graph'; + return 'Before analysis: 1 workspace file excluded'; } - return `${count} excluded from graph`; + return `Before analysis: ${count} workspace files excluded`; +} + +export function formatExcludedNodeCount(count: number): string { + if (count === 1) { + return 'In Graph View: 1 Node excluded'; + } + + return `In Graph View: ${count} Nodes excluded`; } diff --git a/packages/extension/src/webview/components/searchBar/filters/popover.tsx b/packages/extension/src/webview/components/searchBar/filters/popover.tsx index 1e6de2ca42..af97655df6 100644 --- a/packages/extension/src/webview/components/searchBar/filters/popover.tsx +++ b/packages/extension/src/webview/components/searchBar/filters/popover.tsx @@ -10,7 +10,7 @@ import { getDisabledFilterPatterns, getEnabledFilterCount, } from './model'; -import { formatExcludedCount } from './countState'; +import { formatExcludedFileCount, formatExcludedNodeCount } from './countState'; import { handleAddPatterns } from './popover/actions'; import { CustomFiltersSection } from './popover/customSection'; import { useFilterDraftState } from './popover/draftState'; @@ -23,7 +23,8 @@ export function FilterPopover({ disabledCustomPatterns, disabledPluginPatterns, customPatterns, - excludedCount, + excludedFileCount, + excludedNodeCount, onDisabledCustomPatternsChange, onDisabledPluginPatternsChange, onOpenChange, @@ -106,7 +107,7 @@ export function FilterPopover({ className="h-7 px-2 text-xs" aria-expanded={isOpen} aria-label={`Filters, ${enabledCount} enabled`} - title={formatExcludedCount(excludedCount)} + title={`${formatExcludedFileCount(excludedFileCount)}\n${formatExcludedNodeCount(excludedNodeCount)}`} onClick={() => setOpen(!isOpen)} > @@ -123,7 +124,8 @@ export function FilterPopover({

Filters

{enabledCount} enabled -

{formatExcludedCount(excludedCount)}

+

{formatExcludedFileCount(excludedFileCount)}

+

{formatExcludedNodeCount(excludedNodeCount)}

diff --git a/packages/extension/src/webview/components/searchBar/filters/popover/types.ts b/packages/extension/src/webview/components/searchBar/filters/popover/types.ts index a180ab7cb0..3043e6fabd 100644 --- a/packages/extension/src/webview/components/searchBar/filters/popover/types.ts +++ b/packages/extension/src/webview/components/searchBar/filters/popover/types.ts @@ -6,7 +6,8 @@ export interface FilterPopoverProps { disabledCustomPatterns: string[]; disabledPluginPatterns: string[]; customPatterns: string[]; - excludedCount: number; + excludedFileCount: number; + excludedNodeCount: number; onDisabledCustomPatternsChange: (patterns: string[]) => void; onDisabledPluginPatternsChange: (patterns: string[]) => void; onOpenChange?: (open: boolean) => void; diff --git a/packages/extension/src/webview/store/initialState.ts b/packages/extension/src/webview/store/initialState.ts index 232fe4a887..187929f452 100644 --- a/packages/extension/src/webview/store/initialState.ts +++ b/packages/extension/src/webview/store/initialState.ts @@ -37,6 +37,7 @@ export const INITIAL_STATE: GraphStateFields = { optimisticLegendUpdates: {}, optimisticUserLegends: null, filterPatterns: [], + filterExcludedFileCount: 0, pluginFilterPatterns: [], pluginFilterGroups: [], disabledCustomFilterPatterns: [], diff --git a/packages/extension/src/webview/store/messageHandlers/graph.ts b/packages/extension/src/webview/store/messageHandlers/graph.ts index dc8e13d6b3..1fc96114ab 100644 --- a/packages/extension/src/webview/store/messageHandlers/graph.ts +++ b/packages/extension/src/webview/store/messageHandlers/graph.ts @@ -80,3 +80,9 @@ export function handleFilterPatternsUpdated( disabledPluginFilterPatterns: message.payload.disabledPluginPatterns, }; } + +export function handleFilterAccountingUpdated( + message: Extract, +): PartialState { + return { filterExcludedFileCount: message.payload.excludedFileCount }; +} diff --git a/packages/extension/src/webview/store/messageTypes.ts b/packages/extension/src/webview/store/messageTypes.ts index da8c21b6b9..d633e457ba 100644 --- a/packages/extension/src/webview/store/messageTypes.ts +++ b/packages/extension/src/webview/store/messageTypes.ts @@ -52,6 +52,7 @@ export interface IStoreFields { optimisticLegendUpdates: PendingGroupUpdates; optimisticUserLegends: PendingUserGroupsUpdate | null; filterPatterns: string[]; + filterExcludedFileCount: number; pluginFilterPatterns: string[]; pluginFilterGroups: IPluginFilterPatternGroup[]; disabledCustomFilterPatterns: string[]; diff --git a/packages/extension/src/webview/store/messages.ts b/packages/extension/src/webview/store/messages.ts index 577174613c..4d916a6b91 100644 --- a/packages/extension/src/webview/store/messages.ts +++ b/packages/extension/src/webview/store/messages.ts @@ -9,6 +9,7 @@ import { handleSettingsUpdated, handleLegendsUpdated, handleFilterPatternsUpdated, + handleFilterAccountingUpdated, handleDepthModeUpdated, handlePhysicsSettingsUpdated, handleDepthLimitUpdated, @@ -73,6 +74,10 @@ export const MESSAGE_HANDLERS: Record< ), FILTER_PATTERNS_UPDATED: (msg) => handleFilterPatternsUpdated(msg as Extract), + FILTER_ACCOUNTING_UPDATED: (msg) => + handleFilterAccountingUpdated( + msg as Extract, + ), PHYSICS_SETTINGS_UPDATED: (msg) => handlePhysicsSettingsUpdated(msg as Extract), DEPTH_LIMIT_UPDATED: (msg) => diff --git a/packages/extension/src/webview/store/state.ts b/packages/extension/src/webview/store/state.ts index ea04729a56..880fbd23df 100644 --- a/packages/extension/src/webview/store/state.ts +++ b/packages/extension/src/webview/store/state.ts @@ -56,6 +56,7 @@ export interface GraphState { optimisticLegendUpdates: PendingGroupUpdates; optimisticUserLegends: PendingUserGroupsUpdate | null; filterPatterns: string[]; + filterExcludedFileCount: number; pluginFilterPatterns: string[]; pluginFilterGroups: IPluginFilterPatternGroup[]; disabledCustomFilterPatterns: string[]; diff --git a/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts b/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts index 47c9c531af..c6aa36d844 100644 --- a/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts +++ b/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts @@ -73,7 +73,9 @@ describe('graphView/provider/analysis/handlers', () => { }); it('builds execution handlers that update provider state and delegate callbacks', () => { - const source = createSource(); + const source = createSource({ + _analyzer: { getFilterExcludedFileCount: vi.fn(() => 3) } as never, + }); const dependencies = createDependencies(); const callbacks = { isAnalysisStale: vi.fn(() => true), @@ -114,6 +116,10 @@ describe('graphView/provider/analysis/handlers', () => { type: 'GRAPH_DATA_UPDATED', payload: graphData, }); + expect(source._sendMessage).toHaveBeenCalledWith({ + type: 'FILTER_ACCOUNTING_UPDATED', + payload: { excludedFileCount: 3 }, + }); expect(handlerHarness.sendGraphControlsUpdated).toHaveBeenCalledWith( source._rawGraphData, source._analyzer, diff --git a/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts b/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts new file mode 100644 index 0000000000..875f085e3c --- /dev/null +++ b/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts @@ -0,0 +1,111 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { z } from 'zod'; +import { FileDiscovery, parseCodeGraphyPluginPackageManifest } from '@codegraphy-dev/core'; +import { getWorkspacePipelinePluginFilterPatterns } from '../../../src/extension/pipeline/plugins/bootstrap/filters'; +import { unknownRecordSchema } from '../../../src/shared/values'; + +interface FilterPluginInfo { + plugin: { + id: string; + name: string; + defaultFilters: string[]; + }; +} + +const tempDirectories: string[] = []; + +function createMixedWorkspace(): string { + const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraphy-filter-accounting-')); + tempDirectories.push(workspaceRoot); + execFileSync('git', ['init', '-q'], { cwd: workspaceRoot }); + + const files: Record = { + '.gitignore': 'ignored/**\n', + 'src/app.ts': 'export const app = true;\n', + '.next/app.js': 'export const app = true;\n', + 'scripts/player.gd': 'extends Node\n', + 'scripts/player.gd.uid': 'uid://player\n', + 'Assets/Player.prefab': '%YAML 1.1\n', + 'Assets/Player.prefab.meta': 'guid: player\n', + 'ignored/ignored.meta': 'guid: ignored\n', + }; + + for (const [relativePath, content] of Object.entries(files)) { + const filePath = path.join(workspaceRoot, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); + } + + return workspaceRoot; +} + +function readCorePlugin(packageName: string): FilterPluginInfo { + const packageJsonPath = path.resolve(__dirname, `../../../../${packageName}/package.json`); + const manifest = parseCodeGraphyPluginPackageManifest( + JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')), + ); + if (!manifest) throw new Error(`Invalid plugin package: ${packageName}`); + const descriptor = manifest.plugins.find(plugin => plugin.host === 'core'); + if (!descriptor) throw new Error(`Core plugin descriptor not found: ${packageName}`); + const metadata = unknownRecordSchema.parse(descriptor.data); + const defaultFilters = z.array(z.string()).parse(metadata.defaultFilters); + if (!descriptor.id) throw new Error(`Core plugin ID not found: ${packageName}`); + if (!descriptor.name) throw new Error(`Core plugin name not found: ${packageName}`); + + return { + plugin: { + id: descriptor.id, + name: descriptor.name, + defaultFilters, + }, + }; +} + +afterEach(() => { + for (const directory of tempDirectories.splice(0)) { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); + +describe('plugin Filter accounting', () => { + it('tracks shipped TypeScript, Godot, and Unity defaults as plugins become active', async () => { + const workspaceRoot = createMixedWorkspace(); + const plugins = [ + readCorePlugin('plugin-typescript'), + readCorePlugin('plugin-godot'), + readCorePlugin('plugin-unity'), + ]; + const source = { list: () => plugins }; + const discovery = new FileDiscovery(); + + const typescriptOnly = await discovery.discover({ + rootPath: workspaceRoot, + filter: getWorkspacePipelinePluginFilterPatterns( + source, + new Set(['codegraphy.gdscript', 'codegraphy.unity']), + ), + }); + expect(typescriptOnly.filterExcludedPaths).toEqual(['.next/app.js']); + + const allPlugins = await discovery.discover({ + rootPath: workspaceRoot, + filter: getWorkspacePipelinePluginFilterPatterns(source), + }); + expect(allPlugins.filterExcludedPaths).toEqual([ + '.next/app.js', + 'Assets/Player.prefab.meta', + 'scripts/player.gd.uid', + ]); + expect(allPlugins.files.map(file => file.relativePath)).toEqual(expect.arrayContaining([ + 'Assets/Player.prefab', + 'scripts/player.gd', + 'src/app.ts', + ])); + expect(allPlugins.gitIgnoredPaths).toContain('ignored'); + expect(allPlugins.filterExcludedPaths).not.toContain('ignored/ignored.meta'); + }); +}); diff --git a/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts b/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts index 503a1241e9..0f560cb074 100644 --- a/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts +++ b/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts @@ -54,6 +54,7 @@ function createFacade( _lastDiscoveredFiles: [], _lastFileAnalysis: new Map(), _lastFileConnections: new Map(), + _lastFilterExcludedPaths: ['old-filtered.ts'], _lastGitIgnoredPaths: ['old-ignore'], _lastGraphData: createGraph('last'), _lastWorkspaceRoot: '/workspace', @@ -122,6 +123,7 @@ describe('extension/pipeline/service/refresh/modes/pluginFiles', () => { discoveryResult: { directories: undefined, files, + filterExcludedPaths: ['.next/app.js'], gitIgnoredPaths: undefined, }, } as never); @@ -149,6 +151,7 @@ describe('extension/pipeline/service/refresh/modes/pluginFiles', () => { const getPluginFilterPatterns = vi.mocked(discoverRefreshWorkspaceFiles).mock.calls[0][0].getPluginFilterPatterns; expect(getPluginFilterPatterns(disabledPlugins)).toEqual(['plugin/**']); expect(facade.getPluginFilterPatterns).toHaveBeenCalledWith(disabledPlugins); + expect(facade._lastFilterExcludedPaths).toEqual(['.next/app.js']); expect(facade._lastGitIgnoredPaths).toEqual([]); expect(createWorkspaceIndexRefreshSource).toHaveBeenCalledWith(facade, disabledPlugins); expect(facade._registry.list).toHaveBeenCalledOnce(); diff --git a/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts b/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts index aac42bab0c..f0db8ae815 100644 --- a/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts +++ b/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts @@ -25,6 +25,7 @@ describe('pipeline/service/discovery', () => { durationMs: 12, files: [{ relativePath: 'src/a.ts' }], cacheFilePaths: ['src/a.ts', 'example-python/app.py'], + filterExcludedPaths: ['dist/app.js'], gitIgnoredPaths: ['example-python/app.py'], limitReached: false, totalFound: undefined, @@ -39,6 +40,7 @@ describe('pipeline/service/discovery', () => { directories: ['src/new-folder'], durationMs: 12, files: [{ relativePath: 'src/a.ts' }], + filterExcludedPaths: ['dist/app.js'], gitIgnoredPaths: ['example-python/app.py'], limitReached: false, totalFound: 1, diff --git a/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx b/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx index fdf94090b4..a57713d2b3 100644 --- a/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx +++ b/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx @@ -149,6 +149,7 @@ describe('App visible graph',()=>{ { pluginId: 'plugin.one', pluginName: 'Plugin One', patterns: ['**/generated/**'] }, ], pluginFilterPatterns: ['**/generated/**'], + filterExcludedFileCount: 3, disabledPluginFilterPatterns: [], showOrphans: true, }); @@ -156,11 +157,13 @@ describe('App visible graph',()=>{ render(); fireEvent.click(screen.getByRole('button', { name: 'Filters, 1 enabled' })); - expect(screen.getByText('1 excluded from graph')).toBeInTheDocument(); + expect(screen.getByText('Before analysis: 3 workspace files excluded')).toBeInTheDocument(); + expect(screen.getByText('In Graph View: 1 Node excluded')).toBeInTheDocument(); fireEvent.click(screen.getByLabelText('Disable plugin Plugin One filters')); - expect(screen.getByText('0 excluded from graph')).toBeInTheDocument(); + expect(screen.getByText('Before analysis: 3 workspace files excluded')).toBeInTheDocument(); + expect(screen.getByText('In Graph View: 0 Nodes excluded')).toBeInTheDocument(); }); it('should hide graph corner controls while a right-side popup is open', async () => { diff --git a/packages/extension/tests/webview/searchBar/filters/popover.test.tsx b/packages/extension/tests/webview/searchBar/filters/popover.test.tsx index d19b56b95b..1fe785b741 100644 --- a/packages/extension/tests/webview/searchBar/filters/popover.test.tsx +++ b/packages/extension/tests/webview/searchBar/filters/popover.test.tsx @@ -14,7 +14,8 @@ function renderPopover(overrides: Partial { expect(screen.getByText('Filters')).toBeInTheDocument(); expect(screen.getByText('2 enabled')).toBeInTheDocument(); - expect(screen.getByText('4 excluded from graph')).toBeInTheDocument(); + expect(screen.getByText('Before analysis: 4 workspace files excluded')).toBeInTheDocument(); + expect(screen.getByText('In Graph View: 2 Nodes excluded')).toBeInTheDocument(); expect(screen.getByText('Custom')).toBeInTheDocument(); expect(screen.getByText('Plugin defaults')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Filters, 2 enabled' }).className).toContain('bg-secondary'); }); + it('reports pre-analysis File and Graph View Node exclusions without reconciling overlap', () => { + renderPopover({ excludedFileCount: 1, excludedNodeCount: 1 }); + + expect(screen.getByText('Before analysis: 1 workspace file excluded')).toBeInTheDocument(); + expect(screen.getByText('In Graph View: 1 Node excluded')).toBeInTheDocument(); + expect(screen.queryByText(/2 .*excluded/)).not.toBeInTheDocument(); + }); + it('opens and closes from its own trigger when uncontrolled', () => { const props = renderPopover({ onOpenChange: vi.fn(), open: undefined }); diff --git a/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts b/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts index d2833f4d09..25e37a39b4 100644 --- a/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts +++ b/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts @@ -37,6 +37,7 @@ export function createState( optimisticLegendUpdates: {}, optimisticUserLegends: null, filterPatterns: [], + filterExcludedFileCount: 0, pluginFilterPatterns: [], pluginFilterGroups: [], disabledCustomFilterPatterns: [], diff --git a/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts b/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts index a77d7fe1db..6a28500b66 100644 --- a/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts +++ b/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts @@ -5,6 +5,7 @@ import { handleDepthLimitUpdated, handleDepthModeUpdated, handleDirectionSettingsUpdated, + handleFilterAccountingUpdated, handleFilterPatternsUpdated, handleLegendsUpdated, handleMaxFilesUpdated, @@ -43,6 +44,11 @@ describe('graph message handlers: settings',()=>{ disabledCustomFilterPatterns: ['custom/**'], disabledPluginFilterPatterns: [], }); + + expect(handleFilterAccountingUpdated({ + type: 'FILTER_ACCOUNTING_UPDATED', + payload: { excludedFileCount: 3 }, + })).toEqual({ filterExcludedFileCount: 3 }); }); it('maps depth, direction, physics, labels, max-files, and active-file payloads', () => { From 648b09a7e8493c70ba3e38c76c82cc4de7ab5cce Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 10:50:28 -0700 Subject: [PATCH 2/5] Persist Filter exclusion accounting --- .changeset/calm-filters-count.md | 2 +- .../core/src/analysis/workspaceAnalyze.ts | 6 +- .../core/src/analysis/workspaceDiscovery.ts | 3 +- packages/core/src/discovery/contracts.ts | 12 ++- packages/core/src/discovery/file/service.ts | 10 ++- packages/core/src/index.ts | 1 + packages/core/src/indexing/engineGraph.ts | 1 + packages/core/src/indexing/engineSetup.ts | 1 + packages/core/src/indexing/metadata.ts | 3 + packages/core/src/indexing/state.ts | 6 +- packages/core/src/indexing/workspace.ts | 1 + packages/core/src/workspace/meta.ts | 22 +++++- .../tests/analysis/workspaceAnalyze.test.ts | 20 +++-- .../tests/analysis/workspaceDiscovery.test.ts | 2 +- .../discovery/file/discovery.discover.test.ts | 18 +++-- packages/core/tests/indexing/analysis.test.ts | 2 +- .../tests/workspace/meta.concurrent.test.ts | 1 + packages/core/tests/workspace/meta.test.ts | 15 ++-- .../extension/graphView/analysis/execution.ts | 8 +- .../src/extension/graphView/controls/send.ts | 4 + .../controls/send/definitions/contracts.ts | 2 + .../controls/send/definitions/snapshot.ts | 3 + .../provider/analysis/handlers/messages.ts | 6 -- .../graphView/provider/analysis/methods.ts | 3 +- .../graphView/provider/plugin/methods.ts | 4 +- .../graphView/provider/settingsState.ts | 2 + .../src/extension/pipeline/analysis/run.ts | 2 +- .../src/extension/pipeline/analysisSource.ts | 12 +-- .../extension/pipeline/indexingState/model.ts | 9 ++- .../pipeline/service/base/internal.ts | 1 + .../extension/pipeline/service/cache/index.ts | 2 + .../pipeline/service/cache/invalidation.ts | 2 +- .../extension/pipeline/service/cachedGraph.ts | 10 +-- .../pipeline/service/graphDiscovery.ts | 2 +- .../pipeline/service/pluginFacade.ts | 5 +- .../pipeline/service/refresh/context.ts | 8 +- .../service/refresh/modes/analysisScope.ts | 2 +- .../service/refresh/modes/changedFiles.ts | 2 +- .../refresh/modes/gitignoreMetadata.ts | 6 +- .../service/refresh/modes/pluginFiles.ts | 2 +- .../pipeline/service/runtime/discovery.ts | 2 +- .../src/extension/repoSettings/meta.ts | 7 +- .../src/shared/graphControls/contracts.ts | 2 + .../src/shared/protocol/extensionToWebview.ts | 1 - .../src/webview/app/shell/storeSelectors.ts | 4 +- .../extension/src/webview/app/shell/view.tsx | 4 +- .../components/searchBar/field/model.ts | 3 +- .../searchBar/filters/countState.ts | 9 ++- .../components/searchBar/filters/popover.tsx | 6 +- .../searchBar/filters/popover/types.ts | 3 +- .../src/webview/store/initialState.ts | 2 +- .../webview/store/messageHandlers/graph.ts | 6 -- .../store/messageHandlers/graphControls.ts | 1 + .../messageHandlers/graphControls/patch.ts | 1 + .../src/webview/store/messageTypes.ts | 3 +- .../extension/src/webview/store/messages.ts | 5 -- packages/extension/src/webview/store/state.ts | 3 +- .../graphView/analysis/execution/fixtures.ts | 1 + .../analysis/execution/incremental.test.ts | 1 + .../graphView/analysis/lifecycle.test.ts | 1 + .../controls/definitions/snapshot.test.ts | 13 ++++ .../extension/graphView/controls/send.test.ts | 14 +++- .../provider/analysis/handlers.test.ts | 13 ++-- .../graphView/provider/settingsState.test.ts | 10 ++- .../settingsStateDefaultDependencies.test.ts | 10 ++- .../extension/pipeline/lifecycle.test.ts | 15 ++-- ...pluginFilterAccounting.integration.test.ts | 17 +++-- .../pipeline/service/cache/index.test.ts | 11 ++- .../pipeline/service/cachedGraph.test.ts | 41 ++++++++++ .../refresh/modes/gitignoreMetadata.test.ts | 18 ++--- .../service/refresh/modes/pluginFiles.test.ts | 10 ++- .../service/runtime/discovery.test.ts | 4 +- .../repoSettings/freshness/index.test.ts | 3 +- .../repoSettings/freshness/reasons.test.ts | 3 +- .../filter-accounting.spec.ts | 75 +++++++++++++++++++ .../app/shell/view/visibleGraph.test.tsx | 2 +- .../searchBar/filters/popover.test.tsx | 15 +++- .../messageHandlers/graph/controls.test.ts | 13 +++- .../store/messageHandlers/graph/fixture.ts | 2 +- .../messageHandlers/graph/settings.test.ts | 5 -- 80 files changed, 420 insertions(+), 162 deletions(-) create mode 100644 packages/extension/tests/playwright-vscode/filter-accounting.spec.ts diff --git a/.changeset/calm-filters-count.md b/.changeset/calm-filters-count.md index 12a206f3e4..cff4326505 100644 --- a/.changeset/calm-filters-count.md +++ b/.changeset/calm-filters-count.md @@ -3,4 +3,4 @@ "@codegraphy-dev/extension": patch --- -Report Filter exclusions before analysis separately from Nodes excluded in the Graph View. +Persist discovery-time Filter and Git-ignore accounting, restore it from the Graph Cache, and report Filter-excluded workspace files separately from Nodes excluded in the Graph View. diff --git a/packages/core/src/analysis/workspaceAnalyze.ts b/packages/core/src/analysis/workspaceAnalyze.ts index 517525cc3f..a248b66b5c 100644 --- a/packages/core/src/analysis/workspaceAnalyze.ts +++ b/packages/core/src/analysis/workspaceAnalyze.ts @@ -1,5 +1,5 @@ import type { IFileAnalysisResult, IPluginNodeType } from '@codegraphy-dev/plugin-api'; -import type { IDiscoveredFile } from '../discovery/contracts'; +import type { IDiscoveredFile, WorkspaceFilterAccounting } from '../discovery/contracts'; import type { IGraphData } from '../graph/contracts'; import { throwIfWorkspaceAnalysisAborted } from './abort'; import type { IWorkspaceFileAnalysisResult } from './fileAnalysis'; @@ -41,7 +41,7 @@ export interface WorkspacePipelineAnalysisSource { _completeGraphData?: IGraphData; _lastDiscoveredDirectories: string[]; _lastDiscoveredFiles: IDiscoveredFile[]; - _lastFilterExcludedPaths?: string[]; + _filterAccounting: WorkspaceFilterAccounting; _lastFileAnalysis: Map; _lastFileConnections: Map; _lastGitIgnoredPaths?: string[]; @@ -165,7 +165,7 @@ export async function analyzeWorkspaceWithAnalyzer( source._lastFileConnections = analysisResult.fileConnections; source._lastDiscoveredDirectories = discoveryResult.directories ?? []; source._lastDiscoveredFiles = discoveryResult.files; - source._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; + source._filterAccounting = discoveryResult.filterAccounting; source._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; source._lastWorkspaceRoot = workspaceRoot; diff --git a/packages/core/src/analysis/workspaceDiscovery.ts b/packages/core/src/analysis/workspaceDiscovery.ts index 30b2cb0e4e..4a7b934b3a 100644 --- a/packages/core/src/analysis/workspaceDiscovery.ts +++ b/packages/core/src/analysis/workspaceDiscovery.ts @@ -1,3 +1,4 @@ +import type { WorkspaceFilterAccounting } from '../discovery/contracts'; import { DEFAULT_EXCLUDE } from '../discovery/pathMatching'; export interface WorkspacePipelineDiscoveryConfig { @@ -10,7 +11,7 @@ export interface WorkspacePipelineDiscoveryResult { directories?: string[]; durationMs: number; files: TFile[]; - filterExcludedPaths: string[]; + filterAccounting: Extract; gitIgnoredPaths?: string[]; limitReached: boolean; totalFound: number; diff --git a/packages/core/src/discovery/contracts.ts b/packages/core/src/discovery/contracts.ts index 94a10ef017..f2b79d79b4 100644 --- a/packages/core/src/discovery/contracts.ts +++ b/packages/core/src/discovery/contracts.ts @@ -43,14 +43,22 @@ export interface IDiscoveredFile { gitIgnored?: boolean; } +export type WorkspaceFilterAccounting = + | { + kind: 'current'; + excludedFileCount: number; + gitIgnoredPathCount: number; + } + | { kind: 'unavailable' }; + /** * Result of a discovery operation. */ export interface IDiscoveryResult { /** Eligible discovered files, capped by maxFiles */ files: IDiscoveredFile[]; - /** Workspace file paths excluded by active Filters, excluding Git ignored state. */ - filterExcludedPaths: string[]; + /** Filter and Git ignored state accounting captured by this discovery. */ + filterAccounting: Extract; /** Discovered directory paths relative to the workspace root */ directories: string[]; /** Discovered file and directory paths reported by Git as ignored */ diff --git a/packages/core/src/discovery/file/service.ts b/packages/core/src/discovery/file/service.ts index 679a14f23e..00f735eac9 100644 --- a/packages/core/src/discovery/file/service.ts +++ b/packages/core/src/discovery/file/service.ts @@ -305,12 +305,12 @@ export class FileDiscovery { directories, ) : new Set(); - const filterExcludedPaths = candidateFiles + const filterExcludedFileCount = candidateFiles .filter(file => ( !gitIgnoredPaths.has(file.relativePath) && matchesAnyPattern(file.relativePath, filterPatterns) )) - .map(file => file.relativePath); + .length; const eligibleFiles = candidateFiles.filter(file => ( !matchesAnyPattern(file.relativePath, filterPatterns) && !gitIgnoredPaths.has(file.relativePath) @@ -345,7 +345,11 @@ export class FileDiscovery { cacheFilePaths, cachePathPrefixes: gitWorkspacePaths?.ignoredPathPrefixes ?? [], directories: eligibleDirectories, - filterExcludedPaths, + filterAccounting: { + kind: 'current', + excludedFileCount: filterExcludedFileCount, + gitIgnoredPathCount: gitIgnoredPaths.size + (gitWorkspacePaths?.ignoredPathPrefixes.length ?? 0), + }, gitIgnoredPaths: [ ...gitIgnoredPaths, ...(gitWorkspacePaths?.ignoredPathPrefixes ?? []), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 41ab7558c9..07bf427a4f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -111,6 +111,7 @@ export type { IDiscoveredFile, IDiscoveryOptions, IDiscoveryResult, + WorkspaceFilterAccounting, } from './discovery/contracts'; export { createAbortError, throwIfAborted } from './discovery/abort'; export { FileDiscovery } from './discovery/file/service'; diff --git a/packages/core/src/indexing/engineGraph.ts b/packages/core/src/indexing/engineGraph.ts index 3ee4584fa2..d323ec91d8 100644 --- a/packages/core/src/indexing/engineGraph.ts +++ b/packages/core/src/indexing/engineGraph.ts @@ -77,6 +77,7 @@ async function persistMetadata(runtime: WorkspaceEngineRuntime): Promise { registry: state.registry, }); await persistWorkspaceIndexMetadata({ + filterAccounting: state.filterAccounting, pluginBuildSignature: createWorkspaceIndexPluginBuildSignature(state.loadedPackagePlugins), pluginSignature, failedPluginIds: state.failedPluginIds, diff --git a/packages/core/src/indexing/engineSetup.ts b/packages/core/src/indexing/engineSetup.ts index f0187935a9..98a268457b 100644 --- a/packages/core/src/indexing/engineSetup.ts +++ b/packages/core/src/indexing/engineSetup.ts @@ -80,6 +80,7 @@ export async function discoverWorkspaceEngineFiles( }); state.discoveredDirectories = state.discoveryResult.directories ?? []; state.discoveredFiles = state.discoveryResult.files; + state.filterAccounting = state.discoveryResult.filterAccounting; } export function createWorkspaceEngineDisabledPlugins(runtime: WorkspaceEngineRuntime): Set { diff --git a/packages/core/src/indexing/metadata.ts b/packages/core/src/indexing/metadata.ts index 55fb016472..b8930004dd 100644 --- a/packages/core/src/indexing/metadata.ts +++ b/packages/core/src/indexing/metadata.ts @@ -9,6 +9,7 @@ import { createCodeGraphyWorkspaceSettingsSignature, } from '../workspace/signatures'; import type { CodeGraphyWorkspaceSettings } from '../workspace/settings'; +import type { WorkspaceFilterAccounting } from '../discovery/contracts'; import type { IndexCodeGraphyWorkspacePlugin } from './contracts'; function runtimeSignaturePlugins(registry: CorePluginRegistry): IPlugin[] { @@ -70,6 +71,7 @@ export function createWorkspaceIndexPluginBuildSignature( } export async function persistWorkspaceIndexMetadata(input: { + filterAccounting: WorkspaceFilterAccounting; pluginSignature: string | null; pluginBuildSignature: string | null; failedPluginIds: ReadonlySet; @@ -79,6 +81,7 @@ export async function persistWorkspaceIndexMetadata(input: { workspaceRoot: string; }): Promise { await persistCodeGraphyWorkspaceIndexMetadata(input.workspaceRoot, { + filterAccounting: input.filterAccounting, pluginSignature: input.pluginSignature, pluginBuildSignature: input.pluginBuildSignature, failedPluginIds: [...input.failedPluginIds].sort((left, right) => left.localeCompare(right)), diff --git a/packages/core/src/indexing/state.ts b/packages/core/src/indexing/state.ts index d4dcde442a..93b0539bb5 100644 --- a/packages/core/src/indexing/state.ts +++ b/packages/core/src/indexing/state.ts @@ -1,14 +1,14 @@ import { createEmptyWorkspaceAnalysisCache, type IWorkspaceAnalysisCache } from '../analysis/cache'; import type { IProjectedConnection } from '../analysis/projectedConnection'; import type { IFileAnalysisResult } from '@codegraphy-dev/plugin-api'; -import type { IDiscoveredFile } from '../discovery/contracts'; +import type { IDiscoveredFile, WorkspaceFilterAccounting } from '../discovery/contracts'; import type { IGraphData } from '../graph/contracts'; export interface WorkspaceIndexEngineState { cache: IWorkspaceAnalysisCache; discoveredDirectories: string[]; discoveredFiles: IDiscoveredFile[]; - filterExcludedPaths: string[]; + filterAccounting: WorkspaceFilterAccounting; gitIgnoredPaths: string[]; fileAnalysis: Map; fileConnections: Map; @@ -23,7 +23,7 @@ export function createWorkspaceIndexEngineState( cache, discoveredDirectories: [], discoveredFiles: [], - filterExcludedPaths: [], + filterAccounting: { kind: 'unavailable' }, gitIgnoredPaths: [], fileAnalysis: new Map(), fileConnections: new Map(), diff --git a/packages/core/src/indexing/workspace.ts b/packages/core/src/indexing/workspace.ts index a4815ea514..c9774954e1 100644 --- a/packages/core/src/indexing/workspace.ts +++ b/packages/core/src/indexing/workspace.ts @@ -383,6 +383,7 @@ export async function indexCodeGraphyWorkspace( }, recovery); } await persistWorkspaceIndexMetadata({ + filterAccounting: discoveryResult.filterAccounting, pluginBuildSignature, pluginSignature, failedPluginIds, diff --git a/packages/core/src/workspace/meta.ts b/packages/core/src/workspace/meta.ts index c5bce63409..fba401e871 100644 --- a/packages/core/src/workspace/meta.ts +++ b/packages/core/src/workspace/meta.ts @@ -3,6 +3,7 @@ import * as path from 'node:path'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { WORKSPACE_ANALYSIS_CACHE_VERSION } from '../analysis/cache'; +import type { WorkspaceFilterAccounting } from '../discovery/contracts'; import { looseStringArraySchema } from '../values'; import { getWorkspaceMetaPath } from './paths'; import { getWorkspaceAnalysisDatabasePath } from '../graphCache/database/storage'; @@ -13,7 +14,7 @@ import { } from '../graphCache/database/writeCoordination/model'; export interface CodeGraphyWorkspaceMeta { - version: 1; + version: 2; lastIndexedAt: string | null; lastIndexedCommit?: string | null; pluginSignature: string | null; @@ -22,11 +23,21 @@ export interface CodeGraphyWorkspaceMeta { analysisVersion: string | null; pendingChangedFiles: string[]; failedPluginIds: string[]; + filterAccounting: WorkspaceFilterAccounting; } const optionalNullableStringSchema = z.union([z.string(), z.null()]).optional().catch(undefined); +const filterAccountingSchema = z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('current'), + excludedFileCount: z.number().int().nonnegative(), + gitIgnoredPathCount: z.number().int().nonnegative(), + }), + z.object({ kind: z.literal('unavailable') }), +]); const codeGraphyWorkspaceMetaSchema = z.looseObject({ + version: z.literal(2), analysisVersion: optionalNullableStringSchema, lastIndexedAt: optionalNullableStringSchema, lastIndexedCommit: optionalNullableStringSchema, @@ -35,6 +46,7 @@ const codeGraphyWorkspaceMetaSchema = z.looseObject({ pluginSignature: optionalNullableStringSchema, pluginBuildSignature: optionalNullableStringSchema, settingsSignature: optionalNullableStringSchema, + filterAccounting: filterAccountingSchema, }).transform((meta): CodeGraphyWorkspaceMeta => ({ ...createDefaultCodeGraphyWorkspaceMeta(), ...(meta.analysisVersion !== undefined ? { analysisVersion: meta.analysisVersion } : {}), @@ -45,12 +57,13 @@ const codeGraphyWorkspaceMetaSchema = z.looseObject({ ...(meta.settingsSignature !== undefined ? { settingsSignature: meta.settingsSignature } : {}), pendingChangedFiles: meta.pendingChangedFiles, failedPluginIds: meta.failedPluginIds, - version: 1, + filterAccounting: meta.filterAccounting, + version: 2, })); export function createDefaultCodeGraphyWorkspaceMeta(): CodeGraphyWorkspaceMeta { return { - version: 1, + version: 2, lastIndexedAt: null, lastIndexedCommit: null, pluginSignature: null, @@ -59,6 +72,7 @@ export function createDefaultCodeGraphyWorkspaceMeta(): CodeGraphyWorkspaceMeta analysisVersion: WORKSPACE_ANALYSIS_CACHE_VERSION, pendingChangedFiles: [], failedPluginIds: [], + filterAccounting: { kind: 'unavailable' }, }; } @@ -148,6 +162,7 @@ export async function persistCodeGraphyWorkspaceIndexMetadata( pluginBuildSignature?: string | null; settingsSignature: string; failedPluginIds?: readonly string[]; + filterAccounting: WorkspaceFilterAccounting; resolvedChangedFilePaths?: readonly string[]; }, ): Promise { @@ -171,5 +186,6 @@ export async function persistCodeGraphyWorkspaceIndexMetadata( failedPluginIds: metadata.failedPluginIds === undefined ? previous.failedPluginIds : [...metadata.failedPluginIds], + filterAccounting: metadata.filterAccounting, })); } diff --git a/packages/core/tests/analysis/workspaceAnalyze.test.ts b/packages/core/tests/analysis/workspaceAnalyze.test.ts index 88f7ee572a..5aaf662659 100644 --- a/packages/core/tests/analysis/workspaceAnalyze.test.ts +++ b/packages/core/tests/analysis/workspaceAnalyze.test.ts @@ -36,7 +36,7 @@ function createSource() { _completeGraphData: undefined as IGraphData | undefined, _lastDiscoveredDirectories: [] as string[], _lastDiscoveredFiles: [] as IDiscoveredFile[], - _lastFilterExcludedPaths: [] as string[], + _filterAccounting: { kind: 'unavailable' as const }, _lastFileAnalysis: new Map(), _lastFileConnections: new Map(), _lastGitIgnoredPaths: [] as string[], @@ -52,7 +52,7 @@ function createDependencies() { directories: [] as string[], durationMs: 3, files: [] as IDiscoveredFile[], - filterExcludedPaths: [] as string[], + filterAccounting: { kind: 'current' as const, excludedFileCount: 0, gitIgnoredPathCount: 0 }, gitIgnoredPaths: [] as string[], limitReached: false, totalFound: 0, @@ -136,7 +136,7 @@ describe('pipeline/analysis/analyze', () => { directories: ['src/new-folder'], durationMs: 4, files, - filterExcludedPaths: ['dist/app.js'], + filterAccounting: { kind: 'current', excludedFileCount: 1, gitIgnoredPathCount: 1 }, gitIgnoredPaths: ['src/index.ts'], limitReached: false, totalFound: 1, @@ -188,7 +188,11 @@ describe('pipeline/analysis/analyze', () => { new Set(['plugin.python']), ); expect(source._lastDiscoveredFiles).toEqual(files); - expect(source._lastFilterExcludedPaths).toEqual(['dist/app.js']); + expect(source._filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 1, + gitIgnoredPathCount: 1, + }); expect(source._lastDiscoveredDirectories).toEqual(['src/new-folder']); expect(source._lastGitIgnoredPaths).toEqual(['src/index.ts']); expect(source._lastFileAnalysis).toBe(fileAnalysis); @@ -258,7 +262,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 5, files: [] as IDiscoveredFile[], - filterExcludedPaths: [] as string[], + filterAccounting: { kind: 'current', excludedFileCount: 0, gitIgnoredPathCount: 0 }, limitReached: true, totalFound: 27, }); @@ -281,7 +285,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 4, files, - filterExcludedPaths: [] as string[], + filterAccounting: { kind: 'current', excludedFileCount: 0, gitIgnoredPathCount: 0 }, limitReached: false, totalFound: 1, }); @@ -336,7 +340,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 4, files: [] as IDiscoveredFile[], - filterExcludedPaths: [] as string[], + filterAccounting: { kind: 'current' as const, excludedFileCount: 0, gitIgnoredPathCount: 0 }, limitReached: false, totalFound: 0, }; @@ -365,7 +369,7 @@ describe('pipeline/analysis/analyze', () => { directories: [], durationMs: 2, files: [] as IDiscoveredFile[], - filterExcludedPaths: [] as string[], + filterAccounting: { kind: 'current', excludedFileCount: 0, gitIgnoredPathCount: 0 }, limitReached: false, totalFound: 0, }); diff --git a/packages/core/tests/analysis/workspaceDiscovery.test.ts b/packages/core/tests/analysis/workspaceDiscovery.test.ts index 4913be95d6..fff4db6035 100644 --- a/packages/core/tests/analysis/workspaceDiscovery.test.ts +++ b/packages/core/tests/analysis/workspaceDiscovery.test.ts @@ -10,7 +10,7 @@ describe('pipeline/discovery', () => { const discover = vi.fn(async () => ({ durationMs: 2, files: ['src/index.ts'], - filterExcludedPaths: [], + filterAccounting: { kind: 'current' as const, excludedFileCount: 0, gitIgnoredPathCount: 0 }, limitReached: false, totalFound: 1, })); diff --git a/packages/core/tests/discovery/file/discovery.discover.test.ts b/packages/core/tests/discovery/file/discovery.discover.test.ts index 035454dc3f..c2374735c0 100644 --- a/packages/core/tests/discovery/file/discovery.discover.test.ts +++ b/packages/core/tests/discovery/file/discovery.discover.test.ts @@ -83,8 +83,11 @@ describe('FileDiscovery discover', () => { }); expect(result.files.map(file => file.relativePath)).toEqual([path.join('src', 'app.ts')]); - expect(result.filterExcludedPaths).toEqual([path.join('filtered', 'one.ts')]); - expect(result.filterExcludedPaths).not.toContain(path.join('ignored', 'file-0.ts')); + expect(result.filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 1, + gitIgnoredPathCount: 27, + }); expect(onProgress.mock.calls.map(([progress]) => progress.current)).toEqual([1]); }); @@ -109,15 +112,14 @@ describe('FileDiscovery discover', () => { ], }); - expect(result.filterExcludedPaths).toEqual([ - '.godot/editor/project_metadata.cfg', - 'Assets/Player.prefab.meta', - 'ProjectSettings/ProjectSettings.asset', - ]); + expect(result.filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 3, + gitIgnoredPathCount: 2, + }); expect(result.files.map(file => file.relativePath)).toContain('src/app.ts'); expect(result.files.map(file => file.relativePath)).toContain('Assets/Player.prefab'); expect(result.gitIgnoredPaths).toContain('ignored'); - expect(result.filterExcludedPaths).not.toContain('ignored/generated.ts'); }); it('uses Git paths without recursively reading ignored or filtered workspace trees', async () => { diff --git a/packages/core/tests/indexing/analysis.test.ts b/packages/core/tests/indexing/analysis.test.ts index 955b8a810c..3d6e4b5fbb 100644 --- a/packages/core/tests/indexing/analysis.test.ts +++ b/packages/core/tests/indexing/analysis.test.ts @@ -51,7 +51,7 @@ describe('indexing/analysis', () => { discoveryResult: { durationMs: 1, files, - filterExcludedPaths: [], + filterAccounting: { kind: 'current', excludedFileCount: 0, gitIgnoredPathCount: 0 }, directories: [], gitIgnoredPaths: [], limitReached: false, diff --git a/packages/core/tests/workspace/meta.concurrent.test.ts b/packages/core/tests/workspace/meta.concurrent.test.ts index d1d1ccf185..debc2134d6 100644 --- a/packages/core/tests/workspace/meta.concurrent.test.ts +++ b/packages/core/tests/workspace/meta.concurrent.test.ts @@ -88,6 +88,7 @@ describe('workspace metadata concurrency', () => { reportOwnership?.(); await ownedCommitGate; await persistCodeGraphyWorkspaceIndexMetadata(workspaceRoot, { + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, pluginSignature: 'plugins-sha', settingsSignature: 'settings-sha', resolvedChangedFilePaths: ['src/resolved.ts'], diff --git a/packages/core/tests/workspace/meta.test.ts b/packages/core/tests/workspace/meta.test.ts index 4a9f7f050f..f4ccb2cbd5 100644 --- a/packages/core/tests/workspace/meta.test.ts +++ b/packages/core/tests/workspace/meta.test.ts @@ -42,7 +42,7 @@ describe('workspace/meta', () => { it('writes and reads workspace metadata', () => { const workspaceRoot = createTempWorkspace(); const meta = { - version: 1 as const, + version: 2 as const, lastIndexedAt: '2026-04-08T19:00:00.000Z', lastIndexedCommit: 'abc123', pluginSignature: 'codegraphy.markdown@1.0.0', @@ -51,6 +51,7 @@ describe('workspace/meta', () => { analysisVersion: null, pendingChangedFiles: ['src/index.ts'], failedPluginIds: ['acme.failed'], + filterAccounting: { kind: 'current' as const, excludedFileCount: 3, gitIgnoredPathCount: 2 }, }; writeWorkspaceMetaFixture(workspaceRoot, meta); @@ -110,6 +111,7 @@ describe('workspace/meta', () => { }); await persistCodeGraphyWorkspaceIndexMetadata(workspaceRoot, { + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, pluginSignature: 'plugins-sha', settingsSignature: 'settings-sha', resolvedChangedFilePaths: ['src/resolved.ts'], @@ -122,7 +124,7 @@ describe('workspace/meta', () => { }); }); - it('filters invalid persisted metadata fields through the workspace meta schema', () => { + it('requires re-indexing when persisted metadata cannot provide current accounting', () => { const workspaceRoot = createTempWorkspace(); const metaPath = getWorkspaceMetaPath(workspaceRoot); @@ -141,14 +143,7 @@ describe('workspace/meta', () => { 'utf8', ); - expect(readCodeGraphyWorkspaceMeta(workspaceRoot)).toEqual({ - ...createDefaultCodeGraphyWorkspaceMeta(), - analysisVersion: null, - pluginSignature: 'plugins-sha', - pendingChangedFiles: ['src/app.ts', 'src/index.ts'], - failedPluginIds: ['acme.failed'], - version: 1, - }); + expect(readCodeGraphyWorkspaceMeta(workspaceRoot)).toEqual(createDefaultCodeGraphyWorkspaceMeta()); }); it('falls back to defaults when metadata JSON is invalid', () => { diff --git a/packages/extension/src/extension/graphView/analysis/execution.ts b/packages/extension/src/extension/graphView/analysis/execution.ts index ebc6749759..3538d30d76 100644 --- a/packages/extension/src/extension/graphView/analysis/execution.ts +++ b/packages/extension/src/extension/graphView/analysis/execution.ts @@ -1,5 +1,9 @@ import type { IGraphData } from '../../../shared/graph/contracts'; -import type { AnalysisCacheTier, DiagnosticEventInput } from '@codegraphy-dev/core'; +import type { + AnalysisCacheTier, + DiagnosticEventInput, + WorkspaceFilterAccounting, +} from '@codegraphy-dev/core'; import { publishAnalysisFailure } from './execution/publish'; import { prepareGraphViewAnalysis } from './execution/prepare'; import { runGraphViewAnalysis } from './execution/run'; @@ -12,7 +16,7 @@ interface GraphViewAnalyzerLike { initialize(): Promise; syncWorkspacePlugins(): Promise; hasIndex(): boolean; - getFilterExcludedFileCount?(): number; + getFilterAccounting(): WorkspaceFilterAccounting; getIndexStatus?(): { freshness: CodeGraphyIndexFreshness; detail: string; diff --git a/packages/extension/src/extension/graphView/controls/send.ts b/packages/extension/src/extension/graphView/controls/send.ts index bfd011084d..cfe7fe81c8 100644 --- a/packages/extension/src/extension/graphView/controls/send.ts +++ b/packages/extension/src/extension/graphView/controls/send.ts @@ -55,6 +55,9 @@ export function sendGraphControlsUpdated( disabledPlugins: ReadonlySet = new Set(), ): void { const registry = analyzer?.registry; + const filterAccounting = analyzer + ? analyzer.getFilterAccounting() + : { kind: 'unavailable' as const }; const controlsGraphData = filterDisabledPluginEdgesForControls(graphData, disabledPlugins); const filePaths = controlsGraphData.nodes .filter(isFileNode) @@ -67,6 +70,7 @@ export function sendGraphControlsUpdated( controlsGraphData, readNodeTypes(registry, disabledPlugins), readEdgeTypes(registry, disabledPlugins), + filterAccounting, readGraphScopeCapabilities(registry, filePaths, disabledPlugins), ), ), diff --git a/packages/extension/src/extension/graphView/controls/send/definitions/contracts.ts b/packages/extension/src/extension/graphView/controls/send/definitions/contracts.ts index 80b3136b0a..ecd2bcd719 100644 --- a/packages/extension/src/extension/graphView/controls/send/definitions/contracts.ts +++ b/packages/extension/src/extension/graphView/controls/send/definitions/contracts.ts @@ -1,4 +1,5 @@ import type { GraphEdgeKind } from '../../../../../shared/graph/contracts'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; export interface GraphNodeTypeLike { id: string; @@ -29,6 +30,7 @@ export interface GraphTypeDescriptionLike { export interface GraphControlsAnalyzerLike { registry?: unknown; + getFilterAccounting(): WorkspaceFilterAccounting; } export interface GraphControlsConfigurationLike { diff --git a/packages/extension/src/extension/graphView/controls/send/definitions/snapshot.ts b/packages/extension/src/extension/graphView/controls/send/definitions/snapshot.ts index de29a0878c..122a40fd00 100644 --- a/packages/extension/src/extension/graphView/controls/send/definitions/snapshot.ts +++ b/packages/extension/src/extension/graphView/controls/send/definitions/snapshot.ts @@ -1,5 +1,6 @@ import type { IGraphData } from '../../../../../shared/graph/contracts'; import type { IGraphControlsSnapshot } from '../../../../../shared/graphControls/contracts'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import { mergeEdgeTypes, mergeNodeTypes } from './merge'; import { resolveNodeColors, @@ -17,6 +18,7 @@ export function captureGraphControlsSnapshot( graphData: IGraphData, pluginNodeTypes: GraphNodeTypeLike[], pluginEdgeTypes: GraphEdgeTypeLike[], + filterAccounting: WorkspaceFilterAccounting, graphScopeCapabilities: GraphScopeCapabilitiesLike = { nodeTypes: [], edgeTypes: [] }, ): IGraphControlsSnapshot { const configuredNodeColors = config.get>('nodeColors', {}) ?? {}; @@ -31,6 +33,7 @@ export function captureGraphControlsSnapshot( const edgeTypes = mergeEdgeTypes(graphData, pluginEdgeTypes, graphScopeCapabilities.edgeTypes); return { + filterAccounting, nodeTypes, edgeTypes, nodeColors: resolveNodeColors(nodeTypes, configuredNodeColors), diff --git a/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts b/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts index 6be03d8fa7..fb80fdb1dd 100644 --- a/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts +++ b/packages/extension/src/extension/graphView/provider/analysis/handlers/messages.ts @@ -18,12 +18,6 @@ export function sendGraphDataUpdated( source._disabledPlugins, ); source._sendMessage({ type: 'GRAPH_DATA_UPDATED', payload: graphData }); - source._sendMessage({ - type: 'FILTER_ACCOUNTING_UPDATED', - payload: { - excludedFileCount: source._analyzer?.getFilterExcludedFileCount?.() ?? 0, - }, - }); } export const sendGraphIndexStatusUpdated: ( diff --git a/packages/extension/src/extension/graphView/provider/analysis/methods.ts b/packages/extension/src/extension/graphView/provider/analysis/methods.ts index a924692a4a..effdbceae6 100644 --- a/packages/extension/src/extension/graphView/provider/analysis/methods.ts +++ b/packages/extension/src/extension/graphView/provider/analysis/methods.ts @@ -1,4 +1,5 @@ import type { IGraphData } from '../../../../shared/graph/contracts'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import type { ExtensionToWebviewMessage } from '../../../../shared/protocol/extensionToWebview'; import type { GraphViewIndexingProgress } from '../../analysis/execution'; import type { GraphViewProviderAnalysisState } from '../../analysis/lifecycle'; @@ -31,7 +32,7 @@ export interface GraphViewProviderWorkspaceReadyRegistryLike { interface GraphViewProviderAnalysisAnalyzerLike { registry?: GraphViewProviderWorkspaceReadyRegistryLike; - getFilterExcludedFileCount?(): number; + getFilterAccounting(): WorkspaceFilterAccounting; } export interface GraphViewProviderAnalysisMethodsSource { diff --git a/packages/extension/src/extension/graphView/provider/plugin/methods.ts b/packages/extension/src/extension/graphView/provider/plugin/methods.ts index 94e5585ace..fb643b4f15 100644 --- a/packages/extension/src/extension/graphView/provider/plugin/methods.ts +++ b/packages/extension/src/extension/graphView/provider/plugin/methods.ts @@ -4,6 +4,7 @@ import type { ViewRegistry } from '../../../../core/views/registry'; import type { IGraphData } from '../../../../shared/graph/contracts'; import type { ExtensionToWebviewMessage } from '../../../../shared/protocol/extensionToWebview'; import type { IGroup } from '../../../../shared/settings/groups'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import { sendGraphViewPluginWebviewInjections } from '../../webview/plugins/contributionDispatch'; import { sendGraphViewDecorations, @@ -19,7 +20,8 @@ const DEFAULT_DEPTH_LIMIT = 1; type GraphViewPluginAnalyzerLike = NonNullable[0]> - & NonNullable[0]>; + & NonNullable[0]> + & { getFilterAccounting(): WorkspaceFilterAccounting }; type GraphViewDecorationManagerLike = Parameters[0]; diff --git a/packages/extension/src/extension/graphView/provider/settingsState.ts b/packages/extension/src/extension/graphView/provider/settingsState.ts index 05fce6810b..920886d55e 100644 --- a/packages/extension/src/extension/graphView/provider/settingsState.ts +++ b/packages/extension/src/extension/graphView/provider/settingsState.ts @@ -13,9 +13,11 @@ import { captureGraphViewSettingsSnapshot } from '../settings/snapshot'; import { sendGraphViewProviderAllSettings, sendGraphViewProviderSettings } from '../settings/lifecycle'; import { sendGraphControlsUpdated } from '../controls/send'; import type { IPluginFilterPatternGroup } from '../../../shared/protocol/extensionToWebview'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import { resolveCssSnippetStylesheets } from '../cssSnippets/resolve'; interface GraphViewProviderSettingsAnalyzerLike { + getFilterAccounting(): WorkspaceFilterAccounting; getPluginFilterPatterns(): string[]; getPluginFilterGroups?(disabledPlugins?: ReadonlySet): IPluginFilterPatternGroup[]; registry?: unknown; diff --git a/packages/extension/src/extension/pipeline/analysis/run.ts b/packages/extension/src/extension/pipeline/analysis/run.ts index 897e93f398..edd7de08ba 100644 --- a/packages/extension/src/extension/pipeline/analysis/run.ts +++ b/packages/extension/src/extension/pipeline/analysis/run.ts @@ -35,7 +35,7 @@ export function runWorkspacePipelineAnalysis( directories: result.directories, durationMs: result.durationMs, files: result.files, - filterExcludedPaths: result.filterExcludedPaths, + filterAccounting: result.filterAccounting, gitIgnoredPaths: result.gitIgnoredPaths, limitReached: result.limitReached, totalFound: result.totalFound ?? result.files.length, diff --git a/packages/extension/src/extension/pipeline/analysisSource.ts b/packages/extension/src/extension/pipeline/analysisSource.ts index 93cb27cb1b..b58b98a3ff 100644 --- a/packages/extension/src/extension/pipeline/analysisSource.ts +++ b/packages/extension/src/extension/pipeline/analysisSource.ts @@ -2,7 +2,7 @@ import type { IProjectedConnection, IFileAnalysisResult, } from '../../core/plugins/types/contracts'; -import type { IDiscoveredFile } from '@codegraphy-dev/core'; +import type { IDiscoveredFile, WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import type { EventBus } from '../events/bus'; import type { PluginRegistry } from '../../core/plugins/registry/manager'; import type { FileDiscovery } from '@codegraphy-dev/core'; @@ -46,7 +46,7 @@ export interface WorkspacePipelineSourceOwner { _completeGraphData: IGraphData; _lastDiscoveredDirectories: string[]; _lastDiscoveredFiles: IDiscoveredFile[]; - _lastFilterExcludedPaths: string[]; + _filterAccounting: WorkspaceFilterAccounting; _lastFileAnalysis: Map; _lastFileConnections: Map; _lastGitIgnoredPaths: string[]; @@ -136,10 +136,10 @@ export function createWorkspacePipelineAnalysisSource( owner._lastDiscoveredDirectories = directories; }, }, - _lastFilterExcludedPaths: { - get: () => owner._lastFilterExcludedPaths, - set: (paths: string[]) => { - owner._lastFilterExcludedPaths = paths; + _filterAccounting: { + get: () => owner._filterAccounting, + set: (accounting: WorkspaceFilterAccounting) => { + owner._filterAccounting = accounting; }, }, _lastFileConnections: { diff --git a/packages/extension/src/extension/pipeline/indexingState/model.ts b/packages/extension/src/extension/pipeline/indexingState/model.ts index ceb70b5b3f..db02ce43cc 100644 --- a/packages/extension/src/extension/pipeline/indexingState/model.ts +++ b/packages/extension/src/extension/pipeline/indexingState/model.ts @@ -3,6 +3,7 @@ import { createWorkspaceIndexEngineState, FileDiscovery, type IDiscoveredFile, + type WorkspaceFilterAccounting, type WorkspaceIndexEngineState, } from '@codegraphy-dev/core'; import type { IFileAnalysisResult, IProjectedConnection } from '../../../core/plugins/types/contracts'; @@ -58,8 +59,12 @@ export abstract class WorkspacePipelineEngineStateBase { protected set _lastDiscoveredFiles(value: IDiscoveredFile[]) { this._engineState.discoveredFiles = value; } - protected get _lastFilterExcludedPaths(): string[] { return this._engineState.filterExcludedPaths; } - protected set _lastFilterExcludedPaths(value: string[]) { this._engineState.filterExcludedPaths = value; } + protected get _filterAccounting(): WorkspaceFilterAccounting { + return this._engineState.filterAccounting; + } + protected set _filterAccounting(value: WorkspaceFilterAccounting) { + this._engineState.filterAccounting = value; + } protected get _lastGitIgnoredPaths(): string[] { return this._engineState.gitIgnoredPaths; } protected set _lastGitIgnoredPaths(value: string[]) { this._engineState.gitIgnoredPaths = value; } protected get _lastWorkspaceRoot(): string { return this._engineState.workspaceRoot; } diff --git a/packages/extension/src/extension/pipeline/service/base/internal.ts b/packages/extension/src/extension/pipeline/service/base/internal.ts index f33ac49e1a..6b373a2030 100644 --- a/packages/extension/src/extension/pipeline/service/base/internal.ts +++ b/packages/extension/src/extension/pipeline/service/base/internal.ts @@ -247,6 +247,7 @@ export abstract class WorkspacePipelineInternalBase extends WorkspacePipelineSta ): Promise { const workspaceRoot = this._getWorkspaceRoot(); const dependencies = { + getFilterAccounting: () => this._filterAccounting, getCurrentCommitSha: () => workspaceRoot ? this._getCurrentCommitShaSync(workspaceRoot) : null, getPluginSignature: () => this._getPluginSignature(), diff --git a/packages/extension/src/extension/pipeline/service/cache/index.ts b/packages/extension/src/extension/pipeline/service/cache/index.ts index 494fa3bbd3..ab5708e410 100644 --- a/packages/extension/src/extension/pipeline/service/cache/index.ts +++ b/packages/extension/src/extension/pipeline/service/cache/index.ts @@ -11,6 +11,7 @@ interface WorkspacePipelineSignatureDependencies { interface WorkspacePipelinePersistIndexDependencies extends WorkspacePipelineSignatureDependencies { + getFilterAccounting(): Parameters[1]['filterAccounting']; getCurrentCommitSha?: () => Promise | string | null; persistIndexMetadata?: ( workspaceRoot: string, @@ -48,6 +49,7 @@ export async function persistWorkspacePipelineIndexMetadata( try { const currentCommitSha = await dependencies.getCurrentCommitSha?.(); await (dependencies.persistIndexMetadata ?? persistCodeGraphyWorkspaceIndexMetadata)(workspaceRoot, { + filterAccounting: dependencies.getFilterAccounting(), ...(dependencies.getCurrentCommitSha ? { lastIndexedCommit: currentCommitSha ?? null } : {}), diff --git a/packages/extension/src/extension/pipeline/service/cache/invalidation.ts b/packages/extension/src/extension/pipeline/service/cache/invalidation.ts index 05049f7f87..90a0bcbe82 100644 --- a/packages/extension/src/extension/pipeline/service/cache/invalidation.ts +++ b/packages/extension/src/extension/pipeline/service/cache/invalidation.ts @@ -20,7 +20,7 @@ export function invalidateWorkspacePipelineFiles( cache: state.cache, discoveredDirectories: [], discoveredFiles: [], - filterExcludedPaths: [], + filterAccounting: { kind: 'unavailable' }, gitIgnoredPaths: [], fileAnalysis: state.lastFileAnalysis, fileConnections: state.lastFileConnections, diff --git a/packages/extension/src/extension/pipeline/service/cachedGraph.ts b/packages/extension/src/extension/pipeline/service/cachedGraph.ts index a2d18f618c..266dcc09f1 100644 --- a/packages/extension/src/extension/pipeline/service/cachedGraph.ts +++ b/packages/extension/src/extension/pipeline/service/cachedGraph.ts @@ -9,6 +9,7 @@ import { projectFileAnalysisConnections, throwIfWorkspaceAnalysisAborted, } from '@codegraphy-dev/core'; +import { readCodeGraphyWorkspaceMeta } from '@codegraphy-dev/core'; import type { IGraphData } from '../../../shared/graph/contracts'; import { collectCachedDirectoryPaths, @@ -55,17 +56,12 @@ export abstract class WorkspacePipelineCachedGraphFacade extends WorkspacePipeli cachedFilePaths, ); + this._filterAccounting = readCodeGraphyWorkspaceMeta(workspaceRoot).filterAccounting; + const gitIgnoredPaths = new Set(cachedDiscovery.gitIgnoredPaths); const activeFilterPatterns = [ ...this._getEffectiveCustomFilterPatterns(filterPatterns), ...this._getEffectivePluginFilterPatterns(disabledPlugins), ]; - const gitIgnoredPaths = new Set(cachedDiscovery.gitIgnoredPaths); - this._lastFilterExcludedPaths = cachedDiscovery.files - .filter(file => ( - !gitIgnoredPaths.has(file.relativePath) - && matchesAnyPattern(file.relativePath, activeFilterPatterns) - )) - .map(file => file.relativePath); const eligibleFiles = cachedDiscovery.files.filter(file => ( !gitIgnoredPaths.has(file.relativePath) && !matchesAnyPattern(file.relativePath, activeFilterPatterns) diff --git a/packages/extension/src/extension/pipeline/service/graphDiscovery.ts b/packages/extension/src/extension/pipeline/service/graphDiscovery.ts index e985d0132b..560965aa8c 100644 --- a/packages/extension/src/extension/pipeline/service/graphDiscovery.ts +++ b/packages/extension/src/extension/pipeline/service/graphDiscovery.ts @@ -37,7 +37,7 @@ export abstract class WorkspacePipelineGraphDiscoveryFacade extends WorkspacePip this._lastDiscoveredDirectories = discoveryResult.directories ?? []; this._lastDiscoveredFiles = discoveryResult.files; - this._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; + this._filterAccounting = discoveryResult.filterAccounting; this._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; this._lastFileAnalysis = new Map(); this._lastFileConnections = fileConnections; diff --git a/packages/extension/src/extension/pipeline/service/pluginFacade.ts b/packages/extension/src/extension/pipeline/service/pluginFacade.ts index cd64e334d2..8337f9019c 100644 --- a/packages/extension/src/extension/pipeline/service/pluginFacade.ts +++ b/packages/extension/src/extension/pipeline/service/pluginFacade.ts @@ -1,4 +1,5 @@ import type { IPluginFilterPatternGroup } from '../../../shared/protocol/extensionToWebview'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import { hasWorkspacePipelineIndex } from './cache/index'; import { WorkspacePipelineInternalBase } from './base/internal'; import { getWorkspacePipelineIndexStatus } from './indexStatus'; @@ -79,8 +80,8 @@ export abstract class WorkspacePipelinePluginFacade extends WorkspacePipelineInt return getPipelinePluginFilterGroups(this._registry, disabledPlugins); } - getFilterExcludedFileCount(): number { - return this._lastFilterExcludedPaths.length; + getFilterAccounting(): WorkspaceFilterAccounting { + return this._filterAccounting; } protected _getEffectiveCustomFilterPatterns(filterPatterns: string[]): string[] { diff --git a/packages/extension/src/extension/pipeline/service/refresh/context.ts b/packages/extension/src/extension/pipeline/service/refresh/context.ts index 23567f16f9..169e0eb8cb 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/context.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/context.ts @@ -1,4 +1,8 @@ -import type { AnalysisCacheTier, FileDiscovery } from '@codegraphy-dev/core'; +import type { + AnalysisCacheTier, + FileDiscovery, + WorkspaceFilterAccounting, +} from '@codegraphy-dev/core'; import type { Configuration } from '../../../config/reader'; import type { PluginRegistry } from '../../../../core/plugins/registry/manager'; import type { IGraphData } from '../../../../shared/graph/contracts'; @@ -27,7 +31,7 @@ export interface RefreshFacadeContext _captureRefreshState(): WorkspacePipelineRefreshState; _getWorkspaceRoot(): string | undefined; _lastGitIgnoredPaths: string[]; - _lastFilterExcludedPaths: string[]; + _filterAccounting: WorkspaceFilterAccounting; _persistCache(): void; _persistCachePatch(patch: WorkspacePipelineCachePatch): Promise; _persistIndexMetadata(resolvedChangedFilePaths?: readonly string[]): Promise; diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts index 9500b133e0..deff12fba2 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/analysisScope.ts @@ -34,7 +34,7 @@ export async function refreshAnalysisScopeForFacade( signal: input.signal, workspaceRoot, }); - facade._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; + facade._filterAccounting = discoveryResult.filterAccounting; facade._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; if (canReuseCurrentAnalysisForScope({ diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts index 36fad26a91..6ba69aa765 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/changedFiles.ts @@ -136,7 +136,7 @@ async function getChangedFileDiscoveryState( files: discovered.discoveryResult.files, limitReached: discovered.discoveryResult.limitReached, }; - facade._lastFilterExcludedPaths = discovered.discoveryResult.filterExcludedPaths; + facade._filterAccounting = discovered.discoveryResult.filterAccounting; facade._lastGitIgnoredPaths = discovered.discoveryResult.gitIgnoredPaths ?? []; return discoveryResult; } diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts index 5684c4f4cc..c85937ca37 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/gitignoreMetadata.ts @@ -31,13 +31,11 @@ export async function refreshGitignoreMetadataForFacade( facade._lastDiscoveredDirectories = discoveryResult.directories ?? []; facade._lastDiscoveredFiles = discoveryResult.files; - facade._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; + facade._filterAccounting = discoveryResult.filterAccounting; facade._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; facade._lastWorkspaceRoot = workspaceRoot; - void facade._persistIndexMetadata().catch(error => { - console.warn('[CodeGraphy] Failed to persist gitignore metadata refresh.', error); - }); + await facade._persistIndexMetadata(); const eligibleFilePaths = new Set( discoveryResult.files.map(file => file.relativePath), diff --git a/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts b/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts index e9188a16d0..5e357c1ee1 100644 --- a/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts +++ b/packages/extension/src/extension/pipeline/service/refresh/modes/pluginFiles.ts @@ -31,7 +31,7 @@ export async function refreshPluginFilesForFacade( signal: input.signal, workspaceRoot, }); - facade._lastFilterExcludedPaths = discoveryResult.filterExcludedPaths; + facade._filterAccounting = discoveryResult.filterAccounting; facade._lastGitIgnoredPaths = discoveryResult.gitIgnoredPaths ?? []; return refreshWorkspacePipelinePluginFiles(createWorkspaceIndexRefreshSource( diff --git a/packages/extension/src/extension/pipeline/service/runtime/discovery.ts b/packages/extension/src/extension/pipeline/service/runtime/discovery.ts index 6f561d52a4..7f2e3b3c6d 100644 --- a/packages/extension/src/extension/pipeline/service/runtime/discovery.ts +++ b/packages/extension/src/extension/pipeline/service/runtime/discovery.ts @@ -18,7 +18,7 @@ export function createWorkspacePipelineDiscoveryDependencies( directories: result.directories, durationMs: result.durationMs, files: result.files, - filterExcludedPaths: result.filterExcludedPaths, + filterAccounting: result.filterAccounting, gitIgnoredPaths: result.gitIgnoredPaths, limitReached: result.limitReached, totalFound: result.totalFound ?? result.files.length, diff --git a/packages/extension/src/extension/repoSettings/meta.ts b/packages/extension/src/extension/repoSettings/meta.ts index 7921a8ec6a..623c5c0cb1 100644 --- a/packages/extension/src/extension/repoSettings/meta.ts +++ b/packages/extension/src/extension/repoSettings/meta.ts @@ -2,16 +2,18 @@ import * as path from 'node:path'; import { createDefaultCodeGraphyWorkspaceMeta, readCodeGraphyWorkspaceMeta, + type WorkspaceFilterAccounting, } from '@codegraphy-dev/core'; export interface ICodeGraphyRepoMeta { - version: 1; + version: 2; lastIndexedAt: string | null; lastIndexedCommit: string | null; pluginBuildSignature: string | null; pluginSignature: string | null; settingsSignature: string | null; pendingChangedFiles: string[]; + filterAccounting: WorkspaceFilterAccounting; } export function createDefaultCodeGraphyRepoMeta(): ICodeGraphyRepoMeta { @@ -28,12 +30,13 @@ export function readCodeGraphyRepoMeta(workspaceRoot: string): ICodeGraphyRepoMe function toRepoMeta(meta: ReturnType): ICodeGraphyRepoMeta { return { - version: 1, + version: 2, lastIndexedAt: meta.lastIndexedAt, lastIndexedCommit: meta.lastIndexedCommit ?? null, pluginBuildSignature: meta.pluginBuildSignature, pluginSignature: meta.pluginSignature, settingsSignature: meta.settingsSignature, pendingChangedFiles: [...meta.pendingChangedFiles], + filterAccounting: meta.filterAccounting, }; } diff --git a/packages/extension/src/shared/graphControls/contracts.ts b/packages/extension/src/shared/graphControls/contracts.ts index 4c4608c4a4..78710385ab 100644 --- a/packages/extension/src/shared/graphControls/contracts.ts +++ b/packages/extension/src/shared/graphControls/contracts.ts @@ -2,6 +2,7 @@ import type { GraphEdgeKind, NodeType, } from '../graph/contracts'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; export interface IGraphNodeTypeDefinition { id: NodeType; @@ -38,6 +39,7 @@ export interface IGraphTypeDescription { } export interface IGraphControlsSnapshot { + filterAccounting: WorkspaceFilterAccounting; nodeTypes: IGraphNodeTypeDefinition[]; edgeTypes: IGraphEdgeTypeDefinition[]; nodeColors: Record; diff --git a/packages/extension/src/shared/protocol/extensionToWebview.ts b/packages/extension/src/shared/protocol/extensionToWebview.ts index a8cf57dfd6..57a8646794 100644 --- a/packages/extension/src/shared/protocol/extensionToWebview.ts +++ b/packages/extension/src/shared/protocol/extensionToWebview.ts @@ -24,7 +24,6 @@ export interface IGraphNodeMetricsUpdate { export type ExtensionToWebviewMessage = | { type: 'GRAPH_DATA_UPDATED'; payload: IGraphData } - | { type: 'FILTER_ACCOUNTING_UPDATED'; payload: { excludedFileCount: number } } | { type: 'GRAPH_NODE_METRICS_UPDATED'; payload: { nodes: IGraphNodeMetricsUpdate[] } } | { type: 'APP_BOOTSTRAP_COMPLETE' } | { diff --git a/packages/extension/src/webview/app/shell/storeSelectors.ts b/packages/extension/src/webview/app/shell/storeSelectors.ts index 438bc50722..bede3542dc 100644 --- a/packages/extension/src/webview/app/shell/storeSelectors.ts +++ b/packages/extension/src/webview/app/shell/storeSelectors.ts @@ -15,7 +15,7 @@ export function useAppState() { const searchOptions = useGraphStore(s => s.searchOptions); const legends = useGraphStore(s => s.legends); const filterPatterns = useGraphStore(s => s.filterPatterns); - const filterExcludedFileCount = useGraphStore(s => s.filterExcludedFileCount); + const filterAccounting = useGraphStore(s => s.filterAccounting); const pluginFilterPatterns = useGraphStore(s => s.pluginFilterPatterns); const pluginFilterGroups = useGraphStore(s => s.pluginFilterGroups); const disabledCustomFilterPatterns = useGraphStore(s => s.disabledCustomFilterPatterns); @@ -41,7 +41,7 @@ export function useAppState() { searchOptions, legends, filterPatterns, - filterExcludedFileCount, + filterAccounting, pluginFilterPatterns, pluginFilterGroups, disabledCustomFilterPatterns, diff --git a/packages/extension/src/webview/app/shell/view.tsx b/packages/extension/src/webview/app/shell/view.tsx index 8ed657bb84..ca1873965e 100644 --- a/packages/extension/src/webview/app/shell/view.tsx +++ b/packages/extension/src/webview/app/shell/view.tsx @@ -44,7 +44,7 @@ export default function App({ graphPhysicsPreparation }: AppShellProps): React.R searchOptions, legends, filterPatterns, - filterExcludedFileCount, + filterAccounting, pluginFilterPatterns, pluginFilterGroups, disabledCustomFilterPatterns, @@ -206,7 +206,7 @@ export default function App({ graphPhysicsPreparation }: AppShellProps): React.R customPatterns: filterPatterns, disabledCustomPatterns: disabledCustomFilterPatterns, disabledPluginPatterns: disabledPluginFilterPatterns, - excludedFileCount: filterExcludedFileCount, + filterAccounting, excludedNodeCount: excludedCount, onDisabledCustomPatternsChange: setDisabledCustomFilterPatterns, onDisabledPluginPatternsChange: setDisabledPluginFilterPatterns, diff --git a/packages/extension/src/webview/components/searchBar/field/model.ts b/packages/extension/src/webview/components/searchBar/field/model.ts index 217cd7a67d..2d1154d373 100644 --- a/packages/extension/src/webview/components/searchBar/field/model.ts +++ b/packages/extension/src/webview/components/searchBar/field/model.ts @@ -5,6 +5,7 @@ import type React from 'react'; import type { IPluginFilterPatternGroup } from '../../../../shared/protocol/extensionToWebview'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; export interface SearchOptions { matchCase: boolean; @@ -28,7 +29,7 @@ export interface SearchBarProps { disabledCustomPatterns: string[]; disabledPluginPatterns: string[]; customPatterns: string[]; - excludedFileCount: number; + filterAccounting: WorkspaceFilterAccounting; excludedNodeCount: number; onDisabledCustomPatternsChange: (patterns: string[]) => void; onDisabledPluginPatternsChange: (patterns: string[]) => void; diff --git a/packages/extension/src/webview/components/searchBar/filters/countState.ts b/packages/extension/src/webview/components/searchBar/filters/countState.ts index c9329476cf..afab6c86b6 100644 --- a/packages/extension/src/webview/components/searchBar/filters/countState.ts +++ b/packages/extension/src/webview/components/searchBar/filters/countState.ts @@ -1,3 +1,5 @@ +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; + export interface FilterCountInput { excludedCount: number; filterVisibleCount: number; @@ -43,7 +45,12 @@ export function getFilterCountState({ return { kind: 'idle', label: null }; } -export function formatExcludedFileCount(count: number): string { +export function formatExcludedFileCount(accounting: WorkspaceFilterAccounting): string { + if (accounting.kind === 'unavailable') { + return 'Before analysis: re-index to calculate excluded workspace files'; + } + + const count = accounting.excludedFileCount; if (count === 1) { return 'Before analysis: 1 workspace file excluded'; } diff --git a/packages/extension/src/webview/components/searchBar/filters/popover.tsx b/packages/extension/src/webview/components/searchBar/filters/popover.tsx index af97655df6..fee19dbeda 100644 --- a/packages/extension/src/webview/components/searchBar/filters/popover.tsx +++ b/packages/extension/src/webview/components/searchBar/filters/popover.tsx @@ -23,7 +23,7 @@ export function FilterPopover({ disabledCustomPatterns, disabledPluginPatterns, customPatterns, - excludedFileCount, + filterAccounting, excludedNodeCount, onDisabledCustomPatternsChange, onDisabledPluginPatternsChange, @@ -107,7 +107,7 @@ export function FilterPopover({ className="h-7 px-2 text-xs" aria-expanded={isOpen} aria-label={`Filters, ${enabledCount} enabled`} - title={`${formatExcludedFileCount(excludedFileCount)}\n${formatExcludedNodeCount(excludedNodeCount)}`} + title={`${formatExcludedFileCount(filterAccounting)}\n${formatExcludedNodeCount(excludedNodeCount)}`} onClick={() => setOpen(!isOpen)} > @@ -124,7 +124,7 @@ export function FilterPopover({

Filters

{enabledCount} enabled
-

{formatExcludedFileCount(excludedFileCount)}

+

{formatExcludedFileCount(filterAccounting)}

{formatExcludedNodeCount(excludedNodeCount)}

diff --git a/packages/extension/src/webview/components/searchBar/filters/popover/types.ts b/packages/extension/src/webview/components/searchBar/filters/popover/types.ts index 3043e6fabd..4d318ff39b 100644 --- a/packages/extension/src/webview/components/searchBar/filters/popover/types.ts +++ b/packages/extension/src/webview/components/searchBar/filters/popover/types.ts @@ -1,12 +1,13 @@ import type React from 'react'; import type { IPluginFilterPatternGroup } from '../../../../../shared/protocol/extensionToWebview'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; export interface FilterPopoverProps { buttonRef?: React.Ref; disabledCustomPatterns: string[]; disabledPluginPatterns: string[]; customPatterns: string[]; - excludedFileCount: number; + filterAccounting: WorkspaceFilterAccounting; excludedNodeCount: number; onDisabledCustomPatternsChange: (patterns: string[]) => void; onDisabledPluginPatternsChange: (patterns: string[]) => void; diff --git a/packages/extension/src/webview/store/initialState.ts b/packages/extension/src/webview/store/initialState.ts index 187929f452..7362933e2a 100644 --- a/packages/extension/src/webview/store/initialState.ts +++ b/packages/extension/src/webview/store/initialState.ts @@ -37,7 +37,7 @@ export const INITIAL_STATE: GraphStateFields = { optimisticLegendUpdates: {}, optimisticUserLegends: null, filterPatterns: [], - filterExcludedFileCount: 0, + filterAccounting: { kind: 'unavailable' as const }, pluginFilterPatterns: [], pluginFilterGroups: [], disabledCustomFilterPatterns: [], diff --git a/packages/extension/src/webview/store/messageHandlers/graph.ts b/packages/extension/src/webview/store/messageHandlers/graph.ts index 1fc96114ab..dc8e13d6b3 100644 --- a/packages/extension/src/webview/store/messageHandlers/graph.ts +++ b/packages/extension/src/webview/store/messageHandlers/graph.ts @@ -80,9 +80,3 @@ export function handleFilterPatternsUpdated( disabledPluginFilterPatterns: message.payload.disabledPluginPatterns, }; } - -export function handleFilterAccountingUpdated( - message: Extract, -): PartialState { - return { filterExcludedFileCount: message.payload.excludedFileCount }; -} diff --git a/packages/extension/src/webview/store/messageHandlers/graphControls.ts b/packages/extension/src/webview/store/messageHandlers/graphControls.ts index fe02944193..6518b2d7ea 100644 --- a/packages/extension/src/webview/store/messageHandlers/graphControls.ts +++ b/packages/extension/src/webview/store/messageHandlers/graphControls.ts @@ -35,6 +35,7 @@ export function handleGraphControlsUpdated( if (!state) { return { graphNodeTypes: message.payload.nodeTypes, + filterAccounting: message.payload.filterAccounting, graphEdgeTypes: message.payload.edgeTypes, nodeColors: message.payload.nodeColors, nodeVisibility: message.payload.nodeVisibility, diff --git a/packages/extension/src/webview/store/messageHandlers/graphControls/patch.ts b/packages/extension/src/webview/store/messageHandlers/graphControls/patch.ts index ad58f76774..6b10843b2f 100644 --- a/packages/extension/src/webview/store/messageHandlers/graphControls/patch.ts +++ b/packages/extension/src/webview/store/messageHandlers/graphControls/patch.ts @@ -13,6 +13,7 @@ export function createGraphControlsStatePatch( ): PartialState { const next: PartialState = {}; + assignChangedGraphControl(next, 'filterAccounting', state.filterAccounting, payload.filterAccounting); assignChangedGraphControl(next, 'graphNodeTypes', state.graphNodeTypes, payload.nodeTypes); assignChangedGraphControl(next, 'graphEdgeTypes', state.graphEdgeTypes, payload.edgeTypes); assignChangedGraphControl(next, 'nodeColors', state.nodeColors, payload.nodeColors); diff --git a/packages/extension/src/webview/store/messageTypes.ts b/packages/extension/src/webview/store/messageTypes.ts index d633e457ba..d4b1c7acfa 100644 --- a/packages/extension/src/webview/store/messageTypes.ts +++ b/packages/extension/src/webview/store/messageTypes.ts @@ -12,6 +12,7 @@ import type { import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; import type { IPhysicsSettings } from '../../shared/settings/physics'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import type { PendingGroupUpdates, PendingUserGroupsUpdate, @@ -52,7 +53,7 @@ export interface IStoreFields { optimisticLegendUpdates: PendingGroupUpdates; optimisticUserLegends: PendingUserGroupsUpdate | null; filterPatterns: string[]; - filterExcludedFileCount: number; + filterAccounting: WorkspaceFilterAccounting; pluginFilterPatterns: string[]; pluginFilterGroups: IPluginFilterPatternGroup[]; disabledCustomFilterPatterns: string[]; diff --git a/packages/extension/src/webview/store/messages.ts b/packages/extension/src/webview/store/messages.ts index 4d916a6b91..577174613c 100644 --- a/packages/extension/src/webview/store/messages.ts +++ b/packages/extension/src/webview/store/messages.ts @@ -9,7 +9,6 @@ import { handleSettingsUpdated, handleLegendsUpdated, handleFilterPatternsUpdated, - handleFilterAccountingUpdated, handleDepthModeUpdated, handlePhysicsSettingsUpdated, handleDepthLimitUpdated, @@ -74,10 +73,6 @@ export const MESSAGE_HANDLERS: Record< ), FILTER_PATTERNS_UPDATED: (msg) => handleFilterPatternsUpdated(msg as Extract), - FILTER_ACCOUNTING_UPDATED: (msg) => - handleFilterAccountingUpdated( - msg as Extract, - ), PHYSICS_SETTINGS_UPDATED: (msg) => handlePhysicsSettingsUpdated(msg as Extract), DEPTH_LIMIT_UPDATED: (msg) => diff --git a/packages/extension/src/webview/store/state.ts b/packages/extension/src/webview/store/state.ts index 880fbd23df..6c86444a2b 100644 --- a/packages/extension/src/webview/store/state.ts +++ b/packages/extension/src/webview/store/state.ts @@ -17,6 +17,7 @@ import type { import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; import type { IPhysicsSettings } from '../../shared/settings/physics'; +import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import type { PendingGroupUpdates, PendingUserGroupsUpdate, @@ -56,7 +57,7 @@ export interface GraphState { optimisticLegendUpdates: PendingGroupUpdates; optimisticUserLegends: PendingUserGroupsUpdate | null; filterPatterns: string[]; - filterExcludedFileCount: number; + filterAccounting: WorkspaceFilterAccounting; pluginFilterPatterns: string[]; pluginFilterGroups: IPluginFilterPatternGroup[]; disabledCustomFilterPatterns: string[]; diff --git a/packages/extension/tests/extension/graphView/analysis/execution/fixtures.ts b/packages/extension/tests/extension/graphView/analysis/execution/fixtures.ts index bde935fef0..4946bda518 100644 --- a/packages/extension/tests/extension/graphView/analysis/execution/fixtures.ts +++ b/packages/extension/tests/extension/graphView/analysis/execution/fixtures.ts @@ -26,6 +26,7 @@ export function createExecutionAnalyzer( initialize: vi.fn(async () => undefined), syncWorkspacePlugins: vi.fn(async () => undefined), hasIndex: vi.fn(() => true), + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), hasRecoverableGraphState: vi.fn(() => true), analyze: vi.fn(async () => ({ nodes: [], edges: [] })), refreshIndex: vi.fn(async () => ({ nodes: [], edges: [] })), diff --git a/packages/extension/tests/extension/graphView/analysis/execution/incremental.test.ts b/packages/extension/tests/extension/graphView/analysis/execution/incremental.test.ts index f112955607..8fa294ddc6 100644 --- a/packages/extension/tests/extension/graphView/analysis/execution/incremental.test.ts +++ b/packages/extension/tests/extension/graphView/analysis/execution/incremental.test.ts @@ -13,6 +13,7 @@ function createState( analyzer: { analyze: vi.fn(), hasIndex: vi.fn(() => true), + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), hasRecoverableGraphState: vi.fn(() => true), initialize: vi.fn(), loadCachedGraph: vi.fn(async () => ({ nodes: [], edges: [] })), diff --git a/packages/extension/tests/extension/graphView/analysis/lifecycle.test.ts b/packages/extension/tests/extension/graphView/analysis/lifecycle.test.ts index f0f6a55786..7af5397dd7 100644 --- a/packages/extension/tests/extension/graphView/analysis/lifecycle.test.ts +++ b/packages/extension/tests/extension/graphView/analysis/lifecycle.test.ts @@ -31,6 +31,7 @@ function createAnalyzer(overrides: Partial undefined), syncWorkspacePlugins: vi.fn(async () => undefined), hasIndex: vi.fn(() => true), + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), hasRecoverableGraphState: vi.fn(() => true), analyze: vi.fn(async () => ({ nodes: [], edges: [] })), registry: { diff --git a/packages/extension/tests/extension/graphView/controls/definitions/snapshot.test.ts b/packages/extension/tests/extension/graphView/controls/definitions/snapshot.test.ts index 66619f9a96..7489f9b233 100644 --- a/packages/extension/tests/extension/graphView/controls/definitions/snapshot.test.ts +++ b/packages/extension/tests/extension/graphView/controls/definitions/snapshot.test.ts @@ -16,6 +16,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: [] }, ); @@ -56,6 +57,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: ['symbol:function', 'symbol:struct', 'symbol:enum', 'symbol:constant'], edgeTypes: [], @@ -108,6 +110,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: ['import', 'reexport'] }, ); @@ -140,6 +143,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: ['symbol:function'], edgeTypes: [] }, ); @@ -182,6 +186,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [ 'symbol:function', @@ -254,6 +259,7 @@ describe('extension/graphView/controls/snapshot', () => { defaultVisible: true, }, ], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: ['import', 'plugin:route'] }, ); @@ -289,6 +295,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: ['reference'] }, ); @@ -317,6 +324,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, ); expect(snapshot.edgeTypes.map((edgeType) => edgeType.id)).toEqual([ @@ -347,6 +355,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: ['include', 'call', 'contains', 'inherit', 'overrides'] }, ); @@ -382,6 +391,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: ['using', 'type', 'call', 'inherit', 'implements', 'contains'] }, ); @@ -421,6 +431,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: [] }, ); @@ -482,6 +493,7 @@ describe('extension/graphView/controls/snapshot', () => { }, }, ], + { kind: 'unavailable' }, { nodeTypes: ['route'], edgeTypes: ['plugin:route'] }, ); @@ -541,6 +553,7 @@ describe('extension/graphView/controls/snapshot', () => { }, [], [], + { kind: 'unavailable' }, { nodeTypes: [], edgeTypes: [] }, ); diff --git a/packages/extension/tests/extension/graphView/controls/send.test.ts b/packages/extension/tests/extension/graphView/controls/send.test.ts index fd36cafe61..071c1b487d 100644 --- a/packages/extension/tests/extension/graphView/controls/send.test.ts +++ b/packages/extension/tests/extension/graphView/controls/send.test.ts @@ -20,6 +20,7 @@ describe('extension/graphView/controls/send', () => { ], }, { + getFilterAccounting: () => ({ kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }), registry: { listNodeTypes: () => [ null, @@ -55,6 +56,11 @@ describe('extension/graphView/controls/send', () => { const payload = sendMessage.mock.calls[0][0].payload; expect(payload).toBeDefined(); + expect(payload.filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 3, + gitIgnoredPathCount: 2, + }); expect(payload.nodeTypes.some((nodeType: { id: string }) => nodeType.id === 'bad-node')).toBe(false); expect(payload.edgeTypes.some((edgeType: { id: string }) => edgeType.id === 'bad-edge')).toBe(false); }); @@ -68,6 +74,7 @@ describe('extension/graphView/controls/send', () => { edges: [{ id: 'src/app.ts->src/lib.ts#import', from: 'src/app.ts', to: 'src/lib.ts', kind: 'import', sources: [] }], }, { + getFilterAccounting: () => ({ kind: 'unavailable' }), registry: { listNodeTypes: 'not-a-function', listEdgeTypes: () => 'not-an-array', @@ -92,6 +99,7 @@ describe('extension/graphView/controls/send', () => { edges: [], }, { + getFilterAccounting: () => ({ kind: 'unavailable' }), registry: { listEdgeTypes: () => [ { id: 'plugin:route', label: 'Route', defaultColor: '#10B981', defaultVisible: true }, @@ -137,6 +145,7 @@ describe('extension/graphView/controls/send', () => { ], }, { + getFilterAccounting: () => ({ kind: 'unavailable' }), registry: { listEdgeTypes: () => [], listGraphScopeCapabilities: () => ({ @@ -181,7 +190,7 @@ describe('extension/graphView/controls/send', () => { nodes: [{ id: 'game/player.gd', label: 'Player', color: '#111111', nodeType: 'file' }], edges: [], }, - { registry }, + { getFilterAccounting: () => ({ kind: 'unavailable' }), registry }, sendMessage, { get: (_key: string, defaultValue: T): T => defaultValue }, disabledPlugins, @@ -212,6 +221,7 @@ describe('extension/graphView/controls/send', () => { ], }, { + getFilterAccounting: () => ({ kind: 'unavailable' }), registry: { listEdgeTypes: () => [], listGraphScopeCapabilities: () => ({ nodeTypes: [], edgeTypes: [] }), @@ -246,12 +256,14 @@ describe('extension/graphView/controls/send', () => { const payload = sendMessage.mock.calls[0][0].payload; expect(payload).toBeDefined(); + expect(payload.filterAccounting).toEqual({ kind: 'unavailable' }); expect(payload.nodeTypes.map((nodeType: { id: string }) => nodeType.id)).toEqual(STRUCTURAL_NODE_TYPE_IDS); expect(payload.edgeTypes.some((edgeType: { id: string }) => edgeType.id === 'plugin:route')).toBe(false); }); it('keeps the snapshot payload intact when building the outbound message', () => { const snapshot: IGraphControlsSnapshot = { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [{ id: 'file', label: 'File', defaultColor: '#A1A1AA', defaultVisible: true }], edgeTypes: [{ id: 'import', label: 'Import', defaultColor: '#60A5FA', defaultVisible: true }], nodeColors: { file: '#A1A1AA' }, diff --git a/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts b/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts index c6aa36d844..52fcd6f878 100644 --- a/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts +++ b/packages/extension/tests/extension/graphView/provider/analysis/handlers.test.ts @@ -74,7 +74,11 @@ describe('graphView/provider/analysis/handlers', () => { it('builds execution handlers that update provider state and delegate callbacks', () => { const source = createSource({ - _analyzer: { getFilterExcludedFileCount: vi.fn(() => 3) } as never, + _analyzer: { getFilterAccounting: vi.fn(() => ({ + kind: 'current', + excludedFileCount: 3, + gitIgnoredPathCount: 2, + })) } as never, }); const dependencies = createDependencies(); const callbacks = { @@ -116,10 +120,6 @@ describe('graphView/provider/analysis/handlers', () => { type: 'GRAPH_DATA_UPDATED', payload: graphData, }); - expect(source._sendMessage).toHaveBeenCalledWith({ - type: 'FILTER_ACCOUNTING_UPDATED', - payload: { excludedFileCount: 3 }, - }); expect(handlerHarness.sendGraphControlsUpdated).toHaveBeenCalledWith( source._rawGraphData, source._analyzer, @@ -157,6 +157,7 @@ describe('graphView/provider/analysis/handlers', () => { sendMessage({ type: 'GRAPH_CONTROLS_UPDATED', payload: { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [], edgeTypes: [], nodeColors: {}, @@ -185,6 +186,7 @@ describe('graphView/provider/analysis/handlers', () => { expect(source._sendMessage).toHaveBeenCalledWith({ type: 'GRAPH_CONTROLS_UPDATED', payload: { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [], edgeTypes: [], nodeColors: {}, @@ -195,6 +197,7 @@ describe('graphView/provider/analysis/handlers', () => { expect(source._sendMessage).toHaveBeenNthCalledWith(1, { type: 'GRAPH_CONTROLS_UPDATED', payload: { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [], edgeTypes: [], nodeColors: {}, diff --git a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts index 6f14db84a6..30436aab2c 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts @@ -265,7 +265,10 @@ describe('graphView/provider/settingsState', () => { it('sends all settings with the current state, analyzer filters, and side-effect callbacks', () => { const source = createSource({ - _analyzer: { getPluginFilterPatterns: vi.fn(() => ['plugin/**']) }, + _analyzer: { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + getPluginFilterPatterns: vi.fn(() => ['plugin/**']), + }, }); const snapshot = { snapshot: true } as never; const message = { @@ -311,7 +314,10 @@ describe('graphView/provider/settingsState', () => { const source = createSource({ _userGroups: [{ id: 'group.current' } as never], _filterPatterns: ['current/**'], - _analyzer: { getPluginFilterPatterns: vi.fn(() => ['plugin/**']) }, + _analyzer: { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + getPluginFilterPatterns: vi.fn(() => ['plugin/**']), + }, }); const snapshotGroups = [{ id: 'group.snapshot' } as never]; const snapshotFilterPatterns = ['snapshot/**']; diff --git a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts index 8cdadbd23e..a4fb744390 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts @@ -198,7 +198,10 @@ describe('graphView/provider/settingsState default dependencies', () => { it('sends settings through the default provider message bridge', () => { const source = createSource({ - _analyzer: { getPluginFilterPatterns: vi.fn(() => ['plugin/**']) }, + _analyzer: { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + getPluginFilterPatterns: vi.fn(() => ['plugin/**']), + }, }); const settingsMessage = { type: 'SETTINGS_UPDATED', @@ -253,7 +256,10 @@ describe('graphView/provider/settingsState default dependencies', () => { it('sends all settings through the default snapshot bridge', () => { const source = createSource({ - _analyzer: { getPluginFilterPatterns: vi.fn(() => ['plugin/**']) }, + _analyzer: { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + getPluginFilterPatterns: vi.fn(() => ['plugin/**']), + }, }); const allSettingsMessage = { type: 'ALL_SETTINGS_UPDATED', diff --git a/packages/extension/tests/extension/pipeline/lifecycle.test.ts b/packages/extension/tests/extension/pipeline/lifecycle.test.ts index 9916f9be9e..809ef0e6f4 100644 --- a/packages/extension/tests/extension/pipeline/lifecycle.test.ts +++ b/packages/extension/tests/extension/pipeline/lifecycle.test.ts @@ -367,13 +367,14 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { } as unknown as vscode.ExtensionContext); vi.spyOn(repoMetaModule, 'readCodeGraphyRepoMeta').mockReturnValue({ - version: 1, + version: 2, lastIndexedAt: '2026-04-08T00:00:00.000Z', lastIndexedCommit: 'abc123', pluginBuildSignature: 'plugin-build-signature', pluginSignature: 'plugin-signature', settingsSignature: 'settings-signature', pendingChangedFiles: [], + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, }); vi.spyOn( analyzer as unknown as { @@ -396,13 +397,14 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { expect(analyzer.hasIndex()).toBe(true); vi.spyOn(repoMetaModule, 'readCodeGraphyRepoMeta').mockReturnValue({ - version: 1, + version: 2, lastIndexedAt: null, lastIndexedCommit: null, pluginBuildSignature: null, pluginSignature: null, settingsSignature: null, pendingChangedFiles: [], + filterAccounting: { kind: 'unavailable' }, }); expect(analyzer.hasIndex()).toBe(false); }); @@ -543,7 +545,7 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { }, } as unknown as vscode.ExtensionContext); writeWorkspaceMetaFixture(workspaceRoot, { - version: 1, + version: 2, lastIndexedAt: '2026-04-08T00:00:00.000Z', pluginBuildSignature: null, pluginSignature: 'old-plugin-signature', @@ -551,6 +553,7 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { analysisVersion: WORKSPACE_ANALYSIS_CACHE_VERSION, pendingChangedFiles: [], failedPluginIds: [], + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, }); vi.spyOn( analyzer as unknown as { @@ -584,7 +587,7 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { }, } as unknown as vscode.ExtensionContext); writeWorkspaceMetaFixture(workspaceRoot, { - version: 1, + version: 2, lastIndexedAt: '2026-04-08T00:00:00.000Z', pluginBuildSignature: null, pluginSignature: 'plugin-signature', @@ -592,6 +595,7 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { analysisVersion: WORKSPACE_ANALYSIS_CACHE_VERSION, pendingChangedFiles: [], failedPluginIds: [], + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, }); vi.spyOn( analyzer as unknown as { @@ -625,7 +629,7 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { }, } as unknown as vscode.ExtensionContext); writeWorkspaceMetaFixture(workspaceRoot, { - version: 1, + version: 2, lastIndexedAt: '2026-04-08T00:00:00.000Z', pluginBuildSignature: null, pluginSignature: 'plugin-signature', @@ -633,6 +637,7 @@ describe('WorkspacePipeline lifecycle', { timeout: 30000 }, () => { analysisVersion: 'old-analysis-version', pendingChangedFiles: [], failedPluginIds: [], + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, }); vi.spyOn( analyzer as unknown as { diff --git a/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts b/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts index 875f085e3c..52605996c4 100644 --- a/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts +++ b/packages/extension/tests/extension/pipeline/pluginFilterAccounting.integration.test.ts @@ -89,23 +89,26 @@ describe('plugin Filter accounting', () => { new Set(['codegraphy.gdscript', 'codegraphy.unity']), ), }); - expect(typescriptOnly.filterExcludedPaths).toEqual(['.next/app.js']); + expect(typescriptOnly.filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 1, + gitIgnoredPathCount: 2, + }); const allPlugins = await discovery.discover({ rootPath: workspaceRoot, filter: getWorkspacePipelinePluginFilterPatterns(source), }); - expect(allPlugins.filterExcludedPaths).toEqual([ - '.next/app.js', - 'Assets/Player.prefab.meta', - 'scripts/player.gd.uid', - ]); + expect(allPlugins.filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 3, + gitIgnoredPathCount: 2, + }); expect(allPlugins.files.map(file => file.relativePath)).toEqual(expect.arrayContaining([ 'Assets/Player.prefab', 'scripts/player.gd', 'src/app.ts', ])); expect(allPlugins.gitIgnoredPaths).toContain('ignored'); - expect(allPlugins.filterExcludedPaths).not.toContain('ignored/ignored.meta'); }); }); diff --git a/packages/extension/tests/extension/pipeline/service/cache/index.test.ts b/packages/extension/tests/extension/pipeline/service/cache/index.test.ts index 3a9d059fe6..5c516b9132 100644 --- a/packages/extension/tests/extension/pipeline/service/cache/index.test.ts +++ b/packages/extension/tests/extension/pipeline/service/cache/index.test.ts @@ -17,13 +17,14 @@ describe('pipeline/service/cache/index', () => { const tempRoots = new Set(); const meta = (overrides: Partial = {}): ICodeGraphyRepoMeta => ({ - version: 1, + version: 2, lastIndexedAt: '2026-01-01T00:00:00.000Z', lastIndexedCommit: 'abc123', pluginBuildSignature: null, pluginSignature: 'plugin-signature', settingsSignature: 'settings-signature', pendingChangedFiles: [], + filterAccounting: { kind: 'unavailable' }, ...overrides, }); @@ -120,6 +121,7 @@ describe('pipeline/service/cache/index', () => { const warn = vi.fn(); const persistIndexMetadata = vi.fn(); const dependencies = { + getFilterAccounting: vi.fn(() => ({ kind: 'current' as const, excludedFileCount: 3, gitIgnoredPathCount: 2 })), getPluginBuildSignature: vi.fn(() => 'next-plugin-build-signature'), getPluginSignature: vi.fn(() => 'next-plugin-signature'), getSettingsSignature: vi.fn(() => 'next-settings-signature'), @@ -130,6 +132,7 @@ describe('pipeline/service/cache/index', () => { await persistWorkspacePipelineIndexMetadata('/workspace', dependencies); expect(persistIndexMetadata).toHaveBeenCalledWith('/workspace', { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, pluginBuildSignature: 'next-plugin-build-signature', pluginSignature: 'next-plugin-signature', settingsSignature: 'next-settings-signature', @@ -140,6 +143,7 @@ describe('pipeline/service/cache/index', () => { it('records the current commit when index metadata is persisted', async () => { const persistIndexMetadata = vi.fn(); const dependencies = { + getFilterAccounting: vi.fn(() => ({ kind: 'current' as const, excludedFileCount: 3, gitIgnoredPathCount: 2 })), getCurrentCommitSha: vi.fn(() => 'def456'), getPluginBuildSignature: vi.fn(() => 'next-plugin-build-signature'), getPluginSignature: vi.fn(() => 'next-plugin-signature'), @@ -151,6 +155,7 @@ describe('pipeline/service/cache/index', () => { await persistWorkspacePipelineIndexMetadata('/workspace', dependencies); expect(persistIndexMetadata).toHaveBeenCalledWith('/workspace', { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, lastIndexedCommit: 'def456', pluginBuildSignature: 'next-plugin-build-signature', pluginSignature: 'next-plugin-signature', @@ -166,6 +171,7 @@ describe('pipeline/service/cache/index', () => { })); const persistIndexMetadata = vi.fn(); const dependencies = { + getFilterAccounting: vi.fn(() => ({ kind: 'current' as const, excludedFileCount: 3, gitIgnoredPathCount: 2 })), getPluginBuildSignature: vi.fn(() => 'plugin-build-signature'), getPluginSignature: vi.fn(() => 'plugin-signature'), getSettingsSignature: vi.fn(() => 'settings-signature'), @@ -176,6 +182,7 @@ describe('pipeline/service/cache/index', () => { await persistWorkspacePipelineIndexMetadata('/workspace', dependencies); expect(persistIndexMetadata).toHaveBeenCalledWith('/workspace', { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, pluginBuildSignature: 'plugin-build-signature', pluginSignature: 'plugin-signature', settingsSignature: 'settings-signature', @@ -189,6 +196,7 @@ describe('pipeline/service/cache/index', () => { throw error; }); const dependencies = { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), getPluginBuildSignature: vi.fn(() => 'next-plugin-build-signature'), getPluginSignature: vi.fn(() => 'next-plugin-signature'), getSettingsSignature: vi.fn(() => 'next-settings-signature'), @@ -214,6 +222,7 @@ describe('pipeline/service/cache/index', () => { const warn = vi.fn(); await expect(persistWorkspacePipelineIndexMetadata('/workspace', { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), getPluginBuildSignature: vi.fn(() => null), getPluginSignature: vi.fn(() => null), getSettingsSignature: vi.fn(() => 'settings-signature'), diff --git a/packages/extension/tests/extension/pipeline/service/cachedGraph.test.ts b/packages/extension/tests/extension/pipeline/service/cachedGraph.test.ts index a32ac54d87..fc85c74dfc 100644 --- a/packages/extension/tests/extension/pipeline/service/cachedGraph.test.ts +++ b/packages/extension/tests/extension/pipeline/service/cachedGraph.test.ts @@ -3,6 +3,7 @@ import { type AnalysisCacheTier, hasRequiredAnalysisCacheTiers, projectFileAnalysisConnections, + readCodeGraphyWorkspaceMeta, throwIfWorkspaceAnalysisAborted, type FileDiscovery, type IDiscoveredFile, @@ -19,6 +20,7 @@ vi.mock('@codegraphy-dev/core', async (importOriginal) => { ...actual, hasRequiredAnalysisCacheTiers: vi.fn(), projectFileAnalysisConnections: vi.fn(), + readCodeGraphyWorkspaceMeta: vi.fn(actual.readCodeGraphyWorkspaceMeta), throwIfWorkspaceAnalysisAborted: vi.fn(), }; }); @@ -146,6 +148,7 @@ interface CachedGraphState { _lastFileConnections: Map; _lastGitIgnoredPaths: string[]; _lastWorkspaceRoot: string; + _filterAccounting: import('@codegraphy-dev/core').WorkspaceFilterAccounting; } function cachedGraphState(facade: TestCachedGraphFacade): CachedGraphState { @@ -175,6 +178,17 @@ describe('extension/pipeline/service/cachedGraph', () => { vi.clearAllMocks(); vi.mocked(hasWorkspacePipelineIndex).mockReturnValue(false); vi.mocked(hasRequiredAnalysisCacheTiers).mockReturnValue(true); + vi.mocked(readCodeGraphyWorkspaceMeta).mockReturnValue({ + version: 2, + lastIndexedAt: null, + pluginSignature: null, + pluginBuildSignature: null, + settingsSignature: null, + analysisVersion: null, + pendingChangedFiles: [], + failedPluginIds: [], + filterAccounting: { kind: 'unavailable' }, + }); setupCachedDiscovery(); }); @@ -232,6 +246,33 @@ describe('extension/pipeline/service/cachedGraph', () => { }); + it('restores authoritative discovery accounting when excluded files never entered the cache', async () => { + vi.mocked(readCodeGraphyWorkspaceMeta).mockReturnValue({ + version: 2, + lastIndexedAt: '2026-08-06T12:00:00.000Z', + pluginSignature: 'plugins', + pluginBuildSignature: 'build', + settingsSignature: 'settings', + analysisVersion: 'analysis', + pendingChangedFiles: [], + failedPluginIds: [], + filterAccounting: { kind: 'current', excludedFileCount: 4, gitIgnoredPathCount: 2 }, + }); + const restartedFacade = new TestCachedGraphFacade(); + + await restartedFacade.loadCachedGraph(); + + expect(cachedGraphState(restartedFacade)._filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 4, + gitIgnoredPathCount: 2, + }); + expect(createCachedWorkspaceDiscoveryState).toHaveBeenCalledWith( + '/workspace', + ['src/cached.ts'], + ); + }); + it('marks replayed state recoverable only when it came from a persisted index', async () => { const facade = new TestCachedGraphFacade(); diff --git a/packages/extension/tests/extension/pipeline/service/refresh/modes/gitignoreMetadata.test.ts b/packages/extension/tests/extension/pipeline/service/refresh/modes/gitignoreMetadata.test.ts index 8e692aed92..6583c11b93 100644 --- a/packages/extension/tests/extension/pipeline/service/refresh/modes/gitignoreMetadata.test.ts +++ b/packages/extension/tests/extension/pipeline/service/refresh/modes/gitignoreMetadata.test.ts @@ -52,6 +52,7 @@ function createFacade( _lastDiscoveredFiles: [createFile('old.ts')], _lastFileAnalysis: new Map([['src/a.ts', { filePath: '/workspace/src/a.ts' }]]), _lastFileConnections: new Map([['src/a.ts', [{ from: 'src/a.ts', to: 'src/b.ts' }]]]), + _filterAccounting: { kind: 'current', excludedFileCount: 1, gitIgnoredPathCount: 1 }, _lastGitIgnoredPaths: ['old-ignore'], _lastGraphData: createGraph('last'), _lastWorkspaceRoot: '/old-workspace', @@ -95,19 +96,15 @@ describe('extension/pipeline/service/refresh/modes/gitignoreMetadata', () => { const files = [createFile('src/new.ts')]; const disabledPlugins = new Set(['plugin.disabled']); const signal = new AbortController().signal; - const persistError = new Error('metadata failed'); - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); const facade = createFacade({ _buildGraphDataFromAnalysis: vi.fn(() => graph), - _persistIndexMetadata: vi.fn(async () => { - throw persistError; - }), }); vi.mocked(discoverRefreshWorkspaceFiles).mockResolvedValue({ config: { showOrphans: false }, discoveryResult: { directories: undefined, files, + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, gitIgnoredPaths: undefined, }, } as never); @@ -119,7 +116,6 @@ describe('extension/pipeline/service/refresh/modes/gitignoreMetadata', () => { signal, }), ).resolves.toBe(graph); - await Promise.resolve(); expect(discoverRefreshWorkspaceFiles).toHaveBeenCalledWith({ configReader: facade._config, @@ -135,21 +131,21 @@ describe('extension/pipeline/service/refresh/modes/gitignoreMetadata', () => { expect(facade.getPluginFilterPatterns).toHaveBeenCalledWith(disabledPlugins); expect(facade._lastDiscoveredDirectories).toEqual([]); expect(facade._lastDiscoveredFiles).toBe(files); + expect(facade._filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 3, + gitIgnoredPathCount: 2, + }); expect(facade._lastGitIgnoredPaths).toEqual([]); expect(facade._lastWorkspaceRoot).toBe('/workspace'); expect(facade._lastFileAnalysis).toEqual(new Map()); expect(facade._lastFileConnections).toEqual(new Map()); expect(facade._persistIndexMetadata).toHaveBeenCalledOnce(); - expect(warn).toHaveBeenCalledWith( - '[CodeGraphy] Failed to persist gitignore metadata refresh.', - persistError, - ); expect(facade._buildGraphDataFromAnalysis).toHaveBeenCalledWith( new Map(), '/workspace', false, disabledPlugins, ); - warn.mockRestore(); }); }); diff --git a/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts b/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts index 0f560cb074..308c946a29 100644 --- a/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts +++ b/packages/extension/tests/extension/pipeline/service/refresh/modes/pluginFiles.test.ts @@ -54,7 +54,7 @@ function createFacade( _lastDiscoveredFiles: [], _lastFileAnalysis: new Map(), _lastFileConnections: new Map(), - _lastFilterExcludedPaths: ['old-filtered.ts'], + _filterAccounting: { kind: 'current', excludedFileCount: 1, gitIgnoredPathCount: 1 }, _lastGitIgnoredPaths: ['old-ignore'], _lastGraphData: createGraph('last'), _lastWorkspaceRoot: '/workspace', @@ -123,7 +123,7 @@ describe('extension/pipeline/service/refresh/modes/pluginFiles', () => { discoveryResult: { directories: undefined, files, - filterExcludedPaths: ['.next/app.js'], + filterAccounting: { kind: 'current', excludedFileCount: 1, gitIgnoredPathCount: 0 }, gitIgnoredPaths: undefined, }, } as never); @@ -151,7 +151,11 @@ describe('extension/pipeline/service/refresh/modes/pluginFiles', () => { const getPluginFilterPatterns = vi.mocked(discoverRefreshWorkspaceFiles).mock.calls[0][0].getPluginFilterPatterns; expect(getPluginFilterPatterns(disabledPlugins)).toEqual(['plugin/**']); expect(facade.getPluginFilterPatterns).toHaveBeenCalledWith(disabledPlugins); - expect(facade._lastFilterExcludedPaths).toEqual(['.next/app.js']); + expect(facade._filterAccounting).toEqual({ + kind: 'current', + excludedFileCount: 1, + gitIgnoredPathCount: 0, + }); expect(facade._lastGitIgnoredPaths).toEqual([]); expect(createWorkspaceIndexRefreshSource).toHaveBeenCalledWith(facade, disabledPlugins); expect(facade._registry.list).toHaveBeenCalledOnce(); diff --git a/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts b/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts index f0db8ae815..6d0ab8692b 100644 --- a/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts +++ b/packages/extension/tests/extension/pipeline/service/runtime/discovery.test.ts @@ -25,7 +25,7 @@ describe('pipeline/service/discovery', () => { durationMs: 12, files: [{ relativePath: 'src/a.ts' }], cacheFilePaths: ['src/a.ts', 'example-python/app.py'], - filterExcludedPaths: ['dist/app.js'], + filterAccounting: { kind: 'current' as const, excludedFileCount: 1, gitIgnoredPathCount: 1 }, gitIgnoredPaths: ['example-python/app.py'], limitReached: false, totalFound: undefined, @@ -40,7 +40,7 @@ describe('pipeline/service/discovery', () => { directories: ['src/new-folder'], durationMs: 12, files: [{ relativePath: 'src/a.ts' }], - filterExcludedPaths: ['dist/app.js'], + filterAccounting: { kind: 'current', excludedFileCount: 1, gitIgnoredPathCount: 1 }, gitIgnoredPaths: ['example-python/app.py'], limitReached: false, totalFound: 1, diff --git a/packages/extension/tests/extension/repoSettings/freshness/index.test.ts b/packages/extension/tests/extension/repoSettings/freshness/index.test.ts index e7bea6042c..7e52d63d9c 100644 --- a/packages/extension/tests/extension/repoSettings/freshness/index.test.ts +++ b/packages/extension/tests/extension/repoSettings/freshness/index.test.ts @@ -6,13 +6,14 @@ import { evaluateCodeGraphyIndexStatus } from '../../../../src/extension/repoSet import type { ICodeGraphyRepoMeta } from '../../../../src/extension/repoSettings/meta'; const indexedMeta: ICodeGraphyRepoMeta = { - version: 1, + version: 2, lastIndexedAt: '2026-04-27T12:00:00.000Z', lastIndexedCommit: 'abc123', pluginBuildSignature: 'plugin-build', pluginSignature: 'plugins', settingsSignature: 'settings', pendingChangedFiles: [], + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, }; describe('repoSettings/freshness/index', () => { diff --git a/packages/extension/tests/extension/repoSettings/freshness/reasons.test.ts b/packages/extension/tests/extension/repoSettings/freshness/reasons.test.ts index aa6eabd6ac..380e7d5182 100644 --- a/packages/extension/tests/extension/repoSettings/freshness/reasons.test.ts +++ b/packages/extension/tests/extension/repoSettings/freshness/reasons.test.ts @@ -3,13 +3,14 @@ import { collectStaleReasons } from '../../../../src/extension/repoSettings/fres import type { ICodeGraphyRepoMeta } from '../../../../src/extension/repoSettings/meta'; const indexedMeta: ICodeGraphyRepoMeta = { - version: 1, + version: 2, lastIndexedAt: '2026-04-27T12:00:00.000Z', lastIndexedCommit: 'abc123', pluginBuildSignature: 'plugin-build', pluginSignature: 'plugins', settingsSignature: 'settings', pendingChangedFiles: [], + filterAccounting: { kind: 'current', excludedFileCount: 2, gitIgnoredPathCount: 1 }, }; describe('repoSettings/freshness/reasons', () => { diff --git a/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts b/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts new file mode 100644 index 0000000000..da3528fc43 --- /dev/null +++ b/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts @@ -0,0 +1,75 @@ +import { expect, test } from '@playwright/test'; +import fs from 'node:fs'; +import path from 'node:path'; +import { clickToolbarButton, getGraphCounts } from '../acceptance/graphView/canvas'; +import { + copyExampleWorkspace, + createWorkspaceTempRoot, +} from '../acceptance/graphView/workspace'; +import { + cleanupVSCode, + launchVSCodeWithWorkspace, + openGraphView, + waitForGraphFrame, +} from '../acceptance/graphView/vscode'; + +test('reports plugin default Filter effects in an Extension Development Host', async ({}, testInfo) => { + const workspaceTempRoot = createWorkspaceTempRoot(); + const workspacePath = copyExampleWorkspace(workspaceTempRoot, 'example-typescript', { + pluginPackages: [], + }); + const filteredFilePath = path.join(workspacePath, '.next', 'generated.js'); + fs.mkdirSync(path.dirname(filteredFilePath), { recursive: true }); + fs.writeFileSync(filteredFilePath, 'export const generated = true;\n'); + const proofDirectory = process.env.CODEGRAPHY_VISUAL_PROOF_DIR ?? testInfo.outputDir; + fs.mkdirSync(proofDirectory, { recursive: true }); + const vscode = await launchVSCodeWithWorkspace(workspacePath, { + pluginPackageRelativePaths: ['packages/plugin-typescript'], + }); + + try { + await openGraphView(vscode.page); + const frame = await waitForGraphFrame(vscode.page); + await indexWorkspace(frame); + const beforeCounts = await getGraphCounts(frame); + + await frame.getByRole('button', { name: /^Filters,/ }).click(); + await expect(frame.getByText('Before analysis: 0 workspace files excluded')).toBeVisible(); + await expect(frame.getByText('In Graph View: 0 Nodes excluded')).toBeVisible(); + await vscode.page.screenshot({ + path: path.join(proofDirectory, 'trello-242-before.png'), + }); + + await frame.getByRole('button', { name: /^Filters,/ }).click(); + await clickToolbarButton(frame, 'Plugins'); + await frame.getByRole('switch', { name: 'TypeScript/JavaScript' }).click(); + await expect(frame.getByRole('switch', { name: 'TypeScript/JavaScript' })).toBeChecked(); + await expect(frame.getByRole('progressbar', { name: 'Indexing progress' })).toBeHidden({ timeout: 30_000 }); + await clickToolbarButton(frame, 'Plugins'); + await frame.getByTitle('Re-index Workspace').click(); + await expect(frame.getByRole('progressbar', { name: 'Indexing progress' })).toBeHidden({ timeout: 30_000 }); + await expect.poll(() => getGraphCounts(frame)).toEqual({ + nodes: beforeCounts.nodes - 1, + edges: expect.any(Number), + }); + + await frame.getByRole('button', { name: /^Filters,/ }).click(); + await expect(frame.getByText('Before analysis: 1 workspace file excluded')).toBeVisible(); + await expect(frame.getByText('In Graph View: 0 Nodes excluded')).toBeVisible(); + await vscode.page.screenshot({ + path: path.join(proofDirectory, 'trello-242-after.png'), + }); + } finally { + await cleanupVSCode(vscode); + fs.rmSync(workspaceTempRoot, { recursive: true, force: true }); + } +}); + +async function indexWorkspace(frame: Parameters[0]): Promise { + const indexButton = frame.getByRole('button', { name: 'Index Workspace' }); + await expect(indexButton).toBeVisible(); + await indexButton.click(); + await expect( + frame.getByRole('progressbar', { name: 'Indexing progress' }), + ).toBeHidden({ timeout: 30_000 }); +} diff --git a/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx b/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx index a57713d2b3..af5699ee42 100644 --- a/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx +++ b/packages/extension/tests/webview/app/shell/view/visibleGraph.test.tsx @@ -149,7 +149,7 @@ describe('App visible graph',()=>{ { pluginId: 'plugin.one', pluginName: 'Plugin One', patterns: ['**/generated/**'] }, ], pluginFilterPatterns: ['**/generated/**'], - filterExcludedFileCount: 3, + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 1 }, disabledPluginFilterPatterns: [], showOrphans: true, }); diff --git a/packages/extension/tests/webview/searchBar/filters/popover.test.tsx b/packages/extension/tests/webview/searchBar/filters/popover.test.tsx index 1fe785b741..44b5e6a04b 100644 --- a/packages/extension/tests/webview/searchBar/filters/popover.test.tsx +++ b/packages/extension/tests/webview/searchBar/filters/popover.test.tsx @@ -14,7 +14,7 @@ function renderPopover(overrides: Partial { }); it('reports pre-analysis File and Graph View Node exclusions without reconciling overlap', () => { - renderPopover({ excludedFileCount: 1, excludedNodeCount: 1 }); + renderPopover({ + filterAccounting: { kind: 'current', excludedFileCount: 1, gitIgnoredPathCount: 1 }, + excludedNodeCount: 1, + }); expect(screen.getByText('Before analysis: 1 workspace file excluded')).toBeInTheDocument(); expect(screen.getByText('In Graph View: 1 Node excluded')).toBeInTheDocument(); expect(screen.queryByText(/2 .*excluded/)).not.toBeInTheDocument(); }); + it('does not report zero when persisted discovery accounting is unavailable', () => { + renderPopover({ filterAccounting: { kind: 'unavailable' } }); + + expect(screen.getByText( + 'Before analysis: re-index to calculate excluded workspace files', + )).toBeInTheDocument(); + }); + it('opens and closes from its own trigger when uncontrolled', () => { const props = renderPopover({ onOpenChange: vi.fn(), open: undefined }); diff --git a/packages/extension/tests/webview/store/messageHandlers/graph/controls.test.ts b/packages/extension/tests/webview/store/messageHandlers/graph/controls.test.ts index f58a62ba7e..b108c076dd 100644 --- a/packages/extension/tests/webview/store/messageHandlers/graph/controls.test.ts +++ b/packages/extension/tests/webview/store/messageHandlers/graph/controls.test.ts @@ -36,6 +36,7 @@ describe('graph message handlers: controls',()=>{ it('maps graph controls and favorites payloads', () => { const controls: IGraphControlsSnapshot = { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [{ id: 'file', label: 'File', defaultColor: '#A1A1AA', defaultVisible: true }], edgeTypes: [{ id: 'import', label: 'Import', defaultColor: '#64748B', defaultVisible: true }], nodeColors: { file: '#A1A1AA' }, @@ -47,6 +48,7 @@ describe('graph message handlers: controls',()=>{ type: 'GRAPH_CONTROLS_UPDATED', payload: controls, })).toEqual({ + filterAccounting: controls.filterAccounting, graphNodeTypes: controls.nodeTypes, graphEdgeTypes: controls.edgeTypes, nodeColors: controls.nodeColors, @@ -64,6 +66,7 @@ describe('graph message handlers: controls',()=>{ it('skips graph controls updates when extension echoes the current controls', () => { const controls: IGraphControlsSnapshot = { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [{ id: 'file', label: 'File', defaultColor: '#A1A1AA', defaultVisible: true }], edgeTypes: [{ id: 'import', label: 'Import', defaultColor: '#64748B', defaultVisible: true }], nodeColors: { file: '#A1A1AA' }, @@ -71,6 +74,7 @@ describe('graph message handlers: controls',()=>{ edgeVisibility: { import: false }, }; const state = createState({ + filterAccounting: controls.filterAccounting, graphNodeTypes: controls.nodeTypes, graphEdgeTypes: controls.edgeTypes, nodeColors: controls.nodeColors, @@ -78,6 +82,7 @@ describe('graph message handlers: controls',()=>{ edgeVisibility: controls.edgeVisibility, }); const echoedControls: IGraphControlsSnapshot = { + filterAccounting: { ...controls.filterAccounting }, nodeTypes: [...controls.nodeTypes], edgeTypes: [...controls.edgeTypes], nodeColors: { ...controls.nodeColors }, @@ -93,6 +98,7 @@ describe('graph message handlers: controls',()=>{ it('returns only changed graph control fields when extension echoes partial changes', () => { const controls: IGraphControlsSnapshot = { + filterAccounting: { kind: 'current', excludedFileCount: 3, gitIgnoredPathCount: 2 }, nodeTypes: [{ id: 'file', label: 'File', defaultColor: '#A1A1AA', defaultVisible: true }], edgeTypes: [{ id: 'import', label: 'Import', defaultColor: '#64748B', defaultVisible: true }], nodeColors: { file: '#A1A1AA' }, @@ -100,6 +106,7 @@ describe('graph message handlers: controls',()=>{ edgeVisibility: { import: false }, }; const state = createState({ + filterAccounting: controls.filterAccounting, graphNodeTypes: controls.nodeTypes, graphEdgeTypes: controls.edgeTypes, nodeColors: controls.nodeColors, @@ -108,6 +115,7 @@ describe('graph message handlers: controls',()=>{ }); const nextEdgeVisibility = { import: true }; const echoedControls: IGraphControlsSnapshot = { + filterAccounting: { kind: 'current', excludedFileCount: 4, gitIgnoredPathCount: 2 }, nodeTypes: [...controls.nodeTypes], edgeTypes: [...controls.edgeTypes], nodeColors: { ...controls.nodeColors }, @@ -118,6 +126,9 @@ describe('graph message handlers: controls',()=>{ expect(handleGraphControlsUpdated( { type: 'GRAPH_CONTROLS_UPDATED', payload: echoedControls }, { getState: () => state }, - )).toEqual({ edgeVisibility: nextEdgeVisibility }); + )).toEqual({ + edgeVisibility: nextEdgeVisibility, + filterAccounting: { kind: 'current', excludedFileCount: 4, gitIgnoredPathCount: 2 }, + }); }); }); diff --git a/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts b/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts index 25e37a39b4..a4f8869edb 100644 --- a/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts +++ b/packages/extension/tests/webview/store/messageHandlers/graph/fixture.ts @@ -37,7 +37,7 @@ export function createState( optimisticLegendUpdates: {}, optimisticUserLegends: null, filterPatterns: [], - filterExcludedFileCount: 0, + filterAccounting: { kind: 'unavailable' }, pluginFilterPatterns: [], pluginFilterGroups: [], disabledCustomFilterPatterns: [], diff --git a/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts b/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts index 6a28500b66..8e105e9449 100644 --- a/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts +++ b/packages/extension/tests/webview/store/messageHandlers/graph/settings.test.ts @@ -5,7 +5,6 @@ import { handleDepthLimitUpdated, handleDepthModeUpdated, handleDirectionSettingsUpdated, - handleFilterAccountingUpdated, handleFilterPatternsUpdated, handleLegendsUpdated, handleMaxFilesUpdated, @@ -45,10 +44,6 @@ describe('graph message handlers: settings',()=>{ disabledPluginFilterPatterns: [], }); - expect(handleFilterAccountingUpdated({ - type: 'FILTER_ACCOUNTING_UPDATED', - payload: { excludedFileCount: 3 }, - })).toEqual({ filterExcludedFileCount: 3 }); }); it('maps depth, direction, physics, labels, max-files, and active-file payloads', () => { From 292cde162bd85abd633c658c2cd5a41fc9e79cd9 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 10:53:28 -0700 Subject: [PATCH 3/5] Focus Filter accounting visual proof --- .../tests/playwright-vscode/filter-accounting.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts b/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts index da3528fc43..1877cd1511 100644 --- a/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts +++ b/packages/extension/tests/playwright-vscode/filter-accounting.spec.ts @@ -36,7 +36,7 @@ test('reports plugin default Filter effects in an Extension Development Host', a await frame.getByRole('button', { name: /^Filters,/ }).click(); await expect(frame.getByText('Before analysis: 0 workspace files excluded')).toBeVisible(); await expect(frame.getByText('In Graph View: 0 Nodes excluded')).toBeVisible(); - await vscode.page.screenshot({ + await frame.locator('[data-codegraphy-surface="app"]').screenshot({ path: path.join(proofDirectory, 'trello-242-before.png'), }); @@ -56,7 +56,7 @@ test('reports plugin default Filter effects in an Extension Development Host', a await frame.getByRole('button', { name: /^Filters,/ }).click(); await expect(frame.getByText('Before analysis: 1 workspace file excluded')).toBeVisible(); await expect(frame.getByText('In Graph View: 0 Nodes excluded')).toBeVisible(); - await vscode.page.screenshot({ + await frame.locator('[data-codegraphy-surface="app"]').screenshot({ path: path.join(proofDirectory, 'trello-242-after.png'), }); } finally { From 2976c3f32f3018a917f514719cd816e3184b1247 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 10:54:18 -0700 Subject: [PATCH 4/5] Cover Filter accounting bootstrap --- .../graphView/provider/settingsState.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts index 30436aab2c..4f4e6eaa0b 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts @@ -266,7 +266,11 @@ describe('graphView/provider/settingsState', () => { it('sends all settings with the current state, analyzer filters, and side-effect callbacks', () => { const source = createSource({ _analyzer: { - getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + getFilterAccounting: vi.fn(() => ({ + kind: 'current' as const, + excludedFileCount: 4, + gitIgnoredPathCount: 2, + })), getPluginFilterPatterns: vi.fn(() => ['plugin/**']), }, }); @@ -306,6 +310,16 @@ describe('graphView/provider/settingsState', () => { expect(source._computeMergedGroups).toHaveBeenCalledOnce(); expect(source._sendGroupsUpdated).toHaveBeenCalledOnce(); expect(source._sendMessage).toHaveBeenCalledWith(message); + expect(source._sendMessage).toHaveBeenCalledWith(expect.objectContaining({ + type: 'GRAPH_CONTROLS_UPDATED', + payload: expect.objectContaining({ + filterAccounting: { + kind: 'current', + excludedFileCount: 4, + gitIgnoredPathCount: 2, + }, + }), + })); expect(source._userGroups).toEqual([{ id: 'group.updated' }]); expect(source._filterPatterns).toEqual(['updated/**']); }); From 59f26e6447db267a24639f3a35829a1daef90b30 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 08:00:19 -0700 Subject: [PATCH 5/5] test(extension): align filter accounting fixtures --- .../graphViewProvider.errorHandling.test.ts | 4 ++++ .../graphViewProvider.lifecycle.test.ts | 22 +++++++++++++++---- .../graphViewProvider.viewState.test.ts | 2 ++ .../tests/extension/repoSettings/meta.test.ts | 18 +++++++++------ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/extension/tests/extension/graphViewProvider.errorHandling.test.ts b/packages/extension/tests/extension/graphViewProvider.errorHandling.test.ts index 3f61d24de0..f144a20674 100644 --- a/packages/extension/tests/extension/graphViewProvider.errorHandling.test.ts +++ b/packages/extension/tests/extension/graphViewProvider.errorHandling.test.ts @@ -47,6 +47,7 @@ describe('GraphViewProvider error handling', () => { hasIndex(): boolean; initialize(): Promise; discoverGraph(): Promise; + getFilterAccounting(): { kind: 'unavailable' }; registry: { notifyPostAnalyze(graph: IGraphData): void }; }; })._analyzer = { @@ -54,6 +55,7 @@ describe('GraphViewProvider error handling', () => { hasIndex: () => true, initialize: vi.fn().mockResolvedValue(undefined), discoverGraph: vi.fn().mockResolvedValue({ nodes: [], edges: [] }), + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), registry: { notifyPostAnalyze: () => {}, }, @@ -78,6 +80,7 @@ describe('GraphViewProvider error handling', () => { (provider as unknown as { _analyzer: { analyze: () => Promise; + getFilterAccounting(): { kind: 'unavailable' }; registry: { extensionPlugins: { list(): unknown[]; @@ -89,6 +92,7 @@ describe('GraphViewProvider error handling', () => { }; })._analyzer = { analyze: vi.fn().mockRejectedValueOnce(new Error('analysis failed')), + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), registry: { extensionPlugins: { list: () => [], diff --git a/packages/extension/tests/extension/graphViewProvider.lifecycle.test.ts b/packages/extension/tests/extension/graphViewProvider.lifecycle.test.ts index b3093d7937..4fd9ff38c6 100644 --- a/packages/extension/tests/extension/graphViewProvider.lifecycle.test.ts +++ b/packages/extension/tests/extension/graphViewProvider.lifecycle.test.ts @@ -139,8 +139,16 @@ describe('GraphViewProvider lifecycle', () => { vi.spyOn(internals._fileInfoMethods, '_sendFavorites').mockImplementation(() => {}); (provider as unknown as { - _analyzer: { clearCache: () => void; hasIndex: () => boolean }; - })._analyzer = { clearCache, hasIndex: () => true }; + _analyzer: { + clearCache: () => void; + getFilterAccounting(): { kind: 'unavailable' }; + hasIndex: () => boolean; + }; + })._analyzer = { + clearCache, + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + hasIndex: () => true, + }; await provider.clearCacheAndRefresh(); @@ -229,9 +237,15 @@ describe('GraphViewProvider lifecycle', () => { vi.spyOn(internals._pluginMethods, '_sendGroupsUpdated').mockImplementation(() => {}); (provider as unknown as { - _analyzer: { analyze: () => Promise }; + _analyzer: { + analyze: () => Promise; + getFilterAccounting(): { kind: 'unavailable' }; + }; _analyzerInitialized: boolean; - })._analyzer = { analyze: vi.fn() }; + })._analyzer = { + analyze: vi.fn(), + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), + }; (provider as unknown as { _analyzerInitialized: boolean })._analyzerInitialized = true; (provider as unknown as { _analysisRequestId: number })._analysisRequestId = 1; diff --git a/packages/extension/tests/extension/graphViewProvider.viewState.test.ts b/packages/extension/tests/extension/graphViewProvider.viewState.test.ts index c680a46a64..d908fd2c39 100644 --- a/packages/extension/tests/extension/graphViewProvider.viewState.test.ts +++ b/packages/extension/tests/extension/graphViewProvider.viewState.test.ts @@ -292,6 +292,7 @@ describe('GraphViewProvider view state and internal helpers', () => { const notifyGraphRebuild = vi.fn(); (provider as unknown as { _analyzer: { + getFilterAccounting(): { kind: 'unavailable' }; rebuildGraph: typeof rebuildGraph; registry: { list(): []; @@ -302,6 +303,7 @@ describe('GraphViewProvider view state and internal helpers', () => { }; }; })._analyzer = { + getFilterAccounting: vi.fn(() => ({ kind: 'unavailable' as const })), rebuildGraph, registry: { list: () => [], diff --git a/packages/extension/tests/extension/repoSettings/meta.test.ts b/packages/extension/tests/extension/repoSettings/meta.test.ts index f02120d27c..b836f3a1bc 100644 --- a/packages/extension/tests/extension/repoSettings/meta.test.ts +++ b/packages/extension/tests/extension/repoSettings/meta.test.ts @@ -47,7 +47,7 @@ describe('extension/repoSettings/meta', () => { expect(path.basename(getCodeGraphyRepoMetaPath('/workspace/project'))).toBe('meta.json'); }); - it('merges partial persisted meta with the default fields', () => { + it('resets incomplete persisted metadata to the current default snapshot', () => { const workspaceRoot = createTempWorkspace(); tempDirectories.push(workspaceRoot); const metaPath = getCodeGraphyRepoMetaPath(workspaceRoot); @@ -63,13 +63,14 @@ describe('extension/repoSettings/meta', () => { ); expect(readCodeGraphyRepoMeta(workspaceRoot)).toEqual({ - version: 1, + version: 2, lastIndexedAt: null, - lastIndexedCommit: 'abc123', + lastIndexedCommit: null, pluginBuildSignature: null, pluginSignature: null, settingsSignature: null, - pendingChangedFiles: ['src/app.ts'], + pendingChangedFiles: [], + filterAccounting: { kind: 'unavailable' }, }); }); @@ -82,7 +83,9 @@ describe('extension/repoSettings/meta', () => { fs.writeFileSync( metaPath, JSON.stringify({ + version: 2, lastIndexedCommit: 'abc123', + filterAccounting: { kind: 'unavailable' }, }, null, 2), 'utf8', ); @@ -110,13 +113,14 @@ describe('extension/repoSettings/meta', () => { ); expect(readCodeGraphyRepoMeta(workspaceRoot)).toEqual({ - version: 1, + version: 2, lastIndexedAt: null, - lastIndexedCommit: 'abc123', + lastIndexedCommit: null, pluginBuildSignature: null, pluginSignature: null, settingsSignature: null, - pendingChangedFiles: ['src/app.ts', 'src/index.ts'], + pendingChangedFiles: [], + filterAccounting: { kind: 'unavailable' }, }); });