Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-caches-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codegraphy-dev/extension": minor
---

Keep an existing Graph Cache current when files are saved, created, deleted, or renamed in VS Code.
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Interaction rules:

Indexing runs File Discovery, Tree-sitter Analysis, Plugin Analysis, and Graph Projection. JavaScript-family reexports are explicit Relationships; renamed exports are Alias Symbol Nodes, so calls can resolve through barrels to implementation Symbols across full and incremental Indexing. The Graph Cache stores unscoped analysis facts so Graph Scope can hide data without deleting it. Active Filters and Git ignored state exclude files from fresh analysis and the file budget; facts cached while those files were eligible remain reusable but stay out of the current graph. Expensive facts such as Symbol or plugin-owned tiers can load when their scope needs them and remain cached for reuse.

The VS Code extension runs Indexing only after an explicit Index or Re-index Workspace action. Saving, creating, deleting, or renaming a workspace file does not process source files or change the cached Relationship Graph. Opening the Graph View reads the last Graph Cache without warming analysis or updating stale inputs in the background. The Graph View keeps the current graph visible during an explicit Re-index and uses graph-local progress.
The VS Code extension creates the first Graph Cache only after an explicit Index Workspace action. After that cache exists, native VS Code save, create, delete, and rename events maintain it through targeted incremental Indexing. The Extension batches changed paths for 500 ms with a two-second maximum batch age and processes one batch at a time. It does not start a Core watcher, poll the workspace, or process source files during activation. Opening the Graph View reads the last Graph Cache without warming analysis. The Graph View keeps the current graph visible during an explicit Re-index and uses graph-local progress.

The separate `codegraphy watch` CLI command is an explicit foreground workflow for sessions that need a continuously current Graph Cache. It subscribes before initial synchronization, batches workspace changes for 500 ms with a two-second maximum batch age, preserves arrivals during active work, skips cache artifacts and paths excluded by active Filters, and flushes pending changes during shutdown. Graph Cache writers coordinate through operation-scoped exclusive transactions in a separate SQLite coordinator, which releases ownership automatically when a process terminates; no long-lived watcher ownership or heartbeat is required. Full and incremental updates acquire writer ownership before their final discovery and source verification and retain it through persistence, retrying when analyzed inputs were superseded. If an incremental write finds a corrupt Graph Cache, it rebuilds the complete database from current in-memory analysis under that same ownership instead of replaying a partial patch.

Expand Down
2 changes: 1 addition & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Run `codegraphy --help` for the command list and `codegraphy <command> --help` f
- Target Query: inspect one exact File or Symbol with prioritized declarations and bounded Relationships.
- Graph Query: list scoped Nodes and Edges, then use complete cached types by default for exact targeted relationships and bounded paths unless an invocation explicitly projects Node or Edge Types.

The core package exposes `indexCodeGraphyWorkspace` for one-shot Indexing and composes `createCodeGraphyWorkspaceCacheUpdater` with `subscribeCodeGraphyWorkspaceChanges` for the explicit foreground CLI watcher. The VS Code Extension uses Core only after an explicit Index or Re-index Workspace action; it does not subscribe to source-file changes.
The core package exposes `indexCodeGraphyWorkspace` for one-shot Indexing and composes `createCodeGraphyWorkspaceCacheUpdater` with `subscribeCodeGraphyWorkspaceChanges` for the explicit foreground CLI watcher. The VS Code Extension creates the first cache only after an explicit Index Workspace action. It then maps native VS Code file events to Core targeted refresh APIs through its Extension-host plugin pipeline; it does not start the Core watcher.

## Built-In Language Coverage

Expand Down
2 changes: 1 addition & 1 deletion packages/extension/docs/boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The extension source tree is intentionally feature-first inside each runtime bou
- `src/extension/graphView/` and `src/extension/graphViewProvider.ts` own VS Code host orchestration for the Graph View.
- `src/extension/pipeline/` owns extension-facing adapters over `@codegraphy-dev/core` pipeline APIs and extension-only orchestration.
- `src/extension/repoSettings/` owns repo-local settings, freshness decisions, and Graph Cache trust state.
- `src/extension/workspaceFiles/` owns workspace file watching and refresh scheduling.
- `src/extension/workspaceFiles/` owns native VS Code file-event adapters and bounded refresh scheduling.
- `@codegraphy-dev/core` owns File Discovery, Tree-sitter Analysis, headless plugin analysis, cache reads/writes, Graph Projection inputs, and agent-readable Graph Query.
- `@codegraphy-dev/graph-renderer` owns WebGPU drawing and deterministic WebAssembly physics/layout. The extension owns how users configure and interact with it, including settings, persistence, plugins, selection, hover, picking, and context menus.
- `src/shared/visibleGraph/` owns the host/webview shared projection that turns scoped, filtered, and searched graph data into the Visible Graph.
Expand Down
18 changes: 10 additions & 8 deletions packages/extension/docs/graph-cache-lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Graph Cache Lifecycle

The VS Code extension changes workspace source facts only after an explicit **Index Workspace** or **Re-index Workspace** action. This keeps repository analysis off the shared extension host until the user requests it.
The VS Code extension creates workspace source facts only after an explicit **Index Workspace** action. After the first Graph Cache exists, native VS Code file events keep it current through targeted updates.

## Startup

Expand All @@ -11,21 +11,23 @@ flowchart TD
A["Graph View opens"] --> B{"Readable Graph Cache exists?"}
B -->|"yes"| C["Render cached Relationship Graph"]
B -->|"no"| D["Render the unindexed workspace state"]
C --> E["Wait for an explicit Re-index Workspace action"]
C --> E["Wait for VS Code file events or an explicit Re-index"]
D --> F["Wait for an explicit Index Workspace action"]
E --> G["Run Indexing and replace the graph"]
E --> G["Update the Graph Cache and replace the graph"]
F --> G
```

A stale Graph Cache remains visible and useful. Freshness can tell the user that cached facts differ from the workspace, but it does not authorize background Indexing.
A stale Graph Cache remains visible and useful. Opening the Graph View does not authorize source analysis. A later native file event can update the existing cache.

## Workspace changes

Saving, creating, changing, deleting, or renaming files does not process source files or alter the cached Relationship Graph. Settings and display actions may re-project already indexed facts; they must not analyze changed source files.
After the first Graph Cache exists, the Extension responds to native VS Code save, create, delete, and rename events. It deduplicates paths, waits 500 ms after the latest event, and starts a batch after at most two seconds. It processes one batch at a time and retains events that arrive during active work.

Users choose **Re-index Workspace** when they want the Extension to rediscover files, run built-in and Extension-host plugin analysis, project the complete Relationship Graph, and replace the Graph Cache.
Each batch uses the targeted Core refresh path through the existing Extension plugin host. A source-file event refreshes only the affected files. A `.gitignore` or `.codegraphy/settings.json` event can refresh discovery metadata. Generated `.codegraphy` cache files do not trigger another update.

The separate foreground `codegraphy watch` command belongs to the Core CLI. It can maintain the same workspace cache during an explicit terminal session, but the Extension does not launch or own that process.
Users can still choose **Re-index Workspace** to rediscover and analyze the complete workspace. A queued file batch waits for an active Re-index to finish before it runs.

The separate foreground `codegraphy watch` command belongs to the Core CLI. The Extension does not launch that process, subscribe through Core watch mode, or poll the workspace.

## Progress UI

Expand All @@ -36,4 +38,4 @@ The whole-view loading state is only for the first graph payload. During an expl
- disable only actions that cannot run safely during Indexing;
- replace the graph payload when the new data is ready.

This explicit lifecycle prevents source analysis from competing with editor work or changing the graph before the user requests it.
The Extension status bar reports queued, updating, and failed cache updates. An idle Extension does not run source analysis or retain update timers.
2 changes: 2 additions & 0 deletions packages/extension/src/extension/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { registerConfigHandler } from './config/listener';
import { initializeCurrentCodeGraphyConfiguration } from './repoSettings/current';
import { registerCommands } from './commands/register';
import { registerEditorChangeHandler } from './workspaceFiles/editorSync';
import { registerWorkspaceCacheUpdates } from './workspaceFiles/cacheUpdates/register';
import { createCodeGraphyAgentUriHandler } from './agentBridge/uri';
import type { GraphQueryRequest, GraphQueryResult } from '@codegraphy-dev/core';
import { getCodeGraphyConfiguration } from './repoSettings/current';
Expand Down Expand Up @@ -58,6 +59,7 @@ export function activate(context: vscode.ExtensionContext): CodeGraphyAPI {

registerConfigHandler(context, provider);
registerEditorChangeHandler(context, provider);
registerWorkspaceCacheUpdates(context, provider);
registerCommands(context, provider);
diagnostics.emit({
area: 'extension.lifecycle',
Expand Down
11 changes: 10 additions & 1 deletion packages/extension/src/extension/graphView/analysis/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { prepareGraphViewAnalysis } from './execution/prepare';
import { runGraphViewAnalysis } from './execution/run';
import type { CodeGraphyIndexFreshness } from '../../repoSettings/freshness';

export type GraphViewAnalysisMode = 'load' | 'index' | 'refresh';
export type GraphViewAnalysisMode = 'load' | 'index' | 'incremental' | 'refresh';
export type GraphViewIndexingProgress = { phase: string; current: number; total: number };

interface GraphViewAnalyzerLike {
Expand All @@ -24,6 +24,14 @@ interface GraphViewAnalyzerLike {
requiredAnalysisCacheTiers?: readonly AnalysisCacheTier[];
},
): Promise<IGraphData>;
hasLoadedGraphState?(): boolean;
refreshChangedFiles?(
filePaths: readonly string[],
filterPatterns?: string[],
disabledPlugins?: Set<string>,
signal?: AbortSignal,
onProgress?: (progress: GraphViewIndexingProgress) => void,
): Promise<IGraphData>;
analyze(
filterPatterns?: string[],
disabledPlugins?: Set<string>,
Expand All @@ -50,6 +58,7 @@ export interface GraphViewAnalysisExecutionState {
analyzerInitPromise: Promise<void> | undefined;
installedPluginActivationPromise?: Promise<void>;
mode: GraphViewAnalysisMode;
changedFilePaths?: readonly string[];
filterPatterns: string[];
disabledPlugins: Set<string>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { IGraphData } from '../../../../shared/graph/contracts';
import type {
GraphViewAnalysisExecutionState,
GraphViewIndexingProgress,
} from '../execution';
import { EMPTY_GRAPH_DATA } from './publish';

export async function refreshGraphViewChangedFiles(
signal: AbortSignal,
state: GraphViewAnalysisExecutionState,
forwardProgress: (progress: GraphViewIndexingProgress) => void,
): Promise<IGraphData> {
const analyzer = state.analyzer;
if (!analyzer?.refreshChangedFiles) {
return EMPTY_GRAPH_DATA;
}

if (!analyzer.hasLoadedGraphState?.()) {
await analyzer.loadCachedGraph?.(
state.filterPatterns,
state.disabledPlugins,
signal,
);
}

return analyzer.refreshChangedFiles(
state.changedFilePaths ?? [],
state.filterPatterns,
state.disabledPlugins,
signal,
forwardProgress,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { EMPTY_GRAPH_DATA } from './publish';
import {
refreshGraphViewRawData,
} from './refresh';
import {
refreshGraphViewChangedFiles,
} from './incremental';
import {
loadCachedGraphViewRawData,
} from './load/analyzerData';
Expand Down Expand Up @@ -39,6 +42,11 @@ const GRAPH_VIEW_RAW_DATA_LOADERS: Record<GraphViewRawDataRoute, (
) => Promise<IGraphData>> = {
cached: loadCachedGraphViewRawDataOnly,
empty: async () => EMPTY_GRAPH_DATA,
incremental: context => refreshGraphViewChangedFiles(
context.signal,
context.state,
context.forwardProgress,
),
refresh: loadRefreshedGraphViewRawData,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { GraphViewAnalysisExecutionState } from '../../execution';
export type GraphViewRawDataLoadRoute =
| 'cached'
| 'empty'
| 'incremental'
| 'refresh';

export interface GraphViewRawDataLoadDecision {
Expand All @@ -21,5 +22,9 @@ export function selectGraphViewRawDataLoadDecision(
};
}

if (mode === 'incremental') {
return { route: 'incremental' };
}

return { route: 'refresh' };
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { createGraphViewIndexProgressCoalescer } from './progress/coalescer';
const ANALYSIS_PHASE_BY_MODE: Record<GraphViewAnalysisMode, string> = {
load: 'Loading Graph',
index: 'Indexing Workspace',
incremental: 'Updating Graph Cache',
refresh: 'Refreshing Index',
};
export function createGraphViewAnalysisProgressForwarder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DiagnosticEventInput } from '@codegraphy-dev/core';
export interface GraphViewAnalysisRequestState {
analysisController: AbortController | undefined;
analysisRequestId: number;
propagateErrors?: boolean;
}

export interface GraphViewAnalysisRequestHandlers {
Expand Down Expand Up @@ -74,6 +75,9 @@ export async function runGraphViewAnalysisRequest(
},
});
handlers.logError('[CodeGraphy] Analysis failed:', error);
if (state.propagateErrors) {
throw error;
}
}
} finally {
if (state.analysisController === controller) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ export function createGraphViewProviderDoAnalyzeAndSendData(
dependencies: GraphViewProviderAnalysisMethodDependencies,
delegates: GraphViewProviderAnalysisDelegateCalls,
mode: GraphViewAnalysisMode,
): (signal: AbortSignal, requestId: number) => Promise<void> {
return async (signal: AbortSignal, requestId: number): Promise<void> => {
const state = createGraphViewProviderAnalysisState(source, mode);
): (
signal: AbortSignal,
requestId: number,
changedFilePaths?: readonly string[],
) => Promise<void> {
return async (
signal: AbortSignal,
requestId: number,
changedFilePaths?: readonly string[],
): Promise<void> => {
const state = createGraphViewProviderAnalysisState(source, mode, changedFilePaths);

await dependencies.executeAnalysis(
signal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export interface GraphViewProviderAnalysisMethodsSource {
export interface GraphViewProviderAnalysisMethods {
_loadAndSendData(): Promise<void>;
_indexAndSendData(): Promise<void>;
_updateChangedFilesAndSendData(
filePaths: readonly string[],
signal?: AbortSignal,
): Promise<void>;
_refreshAndSendData(): Promise<void>;
_doLoadAndSendData(signal: AbortSignal, requestId: number): Promise<void>;
_markWorkspaceReady(graph: IGraphData, disabledPlugins?: ReadonlySet<string>): void;
Expand Down Expand Up @@ -135,6 +139,19 @@ export function createGraphViewProviderAnalysisMethods(
_doIndexAndSendData,
'index',
);
const _doUpdateChangedFilesAndSendData = createGraphViewProviderDoAnalyzeAndSendData(
source,
dependencies,
delegates,
'incremental',
);
const _updateChangedFilesAndSendData = createGraphViewProviderAnalyzeAndSendData(
source,
dependencies,
delegates,
_doUpdateChangedFilesAndSendData,
'incremental',
);
const _doRefreshAndSendData = createGraphViewProviderDoAnalyzeAndSendData(
source,
dependencies,
Expand All @@ -157,6 +174,26 @@ export function createGraphViewProviderAnalysisMethods(
await _loadAndSendData();
},
_indexAndSendData: () => fullIndexAnalysis.runFullIndexAnalysis(_indexAndSendData),
_updateChangedFilesAndSendData: async (filePaths, signal) => {
await fullIndexAnalysis.waitForFullIndexAnalysis();
if (signal?.aborted) {
return;
}

const abortUpdate = (): void => {
source._analysisController?.abort();
};
signal?.addEventListener('abort', abortUpdate, { once: true });
try {
const update = _updateChangedFilesAndSendData(filePaths);
if (signal?.aborted) {
abortUpdate();
}
await update;
} finally {
signal?.removeEventListener('abort', abortUpdate);
}
},
_refreshAndSendData: () => fullIndexAnalysis.runFullIndexAnalysis(_refreshAndSendData),
_doLoadAndSendData,
_markWorkspaceReady,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@ export function createGraphViewProviderAnalyzeAndSendData(
source: GraphViewProviderAnalysisMethodsSource,
dependencies: GraphViewProviderAnalysisMethodDependencies,
delegates: Pick<GraphViewProviderAnalysisDelegateCalls, 'callIsAbortError'>,
doAnalyzeAndSendData: (signal: AbortSignal, requestId: number) => Promise<void>,
doAnalyzeAndSendData: (
signal: AbortSignal,
requestId: number,
changedFilePaths?: readonly string[],
) => Promise<void>,
mode: GraphViewAnalysisMode,
): () => Promise<void> {
return async (): Promise<void> => {
const state = createGraphViewProviderAnalysisState(source, mode);
): (changedFilePaths?: readonly string[]) => Promise<void> {
return async (changedFilePaths?: readonly string[]): Promise<void> => {
const state = createGraphViewProviderAnalysisState(source, mode, changedFilePaths);

await dependencies.runAnalysisRequest(
state,
createGraphViewProviderAnalysisRequestHandlers(source, dependencies, {
executeAnalysis: (signal, requestId) => doAnalyzeAndSendData(signal, requestId),
executeAnalysis: (signal, requestId) => changedFilePaths
? doAnalyzeAndSendData(signal, requestId, changedFilePaths)
: doAnalyzeAndSendData(signal, requestId),
isAbortError: error => delegates.callIsAbortError(error),
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface GraphViewProviderWorkspaceReadyState {
export function createGraphViewProviderAnalysisState(
source: GraphViewProviderAnalysisMethodsSource,
mode: GraphViewAnalysisMode,
changedFilePaths?: readonly string[],
): GraphViewProviderAnalysisState {
return {
get analysisController() {
Expand Down Expand Up @@ -47,6 +48,8 @@ export function createGraphViewProviderAnalysisState(
return source._installedPluginActivationPromise;
},
mode,
propagateErrors: mode === 'incremental',
...(changedFilePaths ? { changedFilePaths } : {}),
get filterPatterns() {
return source._filterPatterns;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ interface GraphViewProviderPublicMethodsOwner {

export interface GraphViewProviderPublicMethods {
refresh: () => Promise<void>;
updateWorkspaceFiles: (
filePaths: readonly string[],
signal?: AbortSignal,
) => Promise<void>;
refreshIndex: () => Promise<void>;
hydrateGraphScope: () => Promise<boolean>;
hydratePluginGraphScope: (pluginIds: readonly string[]) => Promise<boolean>;
Expand Down Expand Up @@ -83,6 +87,9 @@ export function assignGraphViewProviderPublicMethods(
target: GraphViewProviderPublicMethodsTarget,
): void {
target.refresh = () => target._methodContainers.refresh.refresh();
target.updateWorkspaceFiles = (filePaths, signal) => signal
? target._methodContainers.analysis._updateChangedFilesAndSendData(filePaths, signal)
: target._methodContainers.analysis._updateChangedFilesAndSendData(filePaths);
target.refreshIndex = () => target._methodContainers.refresh.refreshIndex();
target.hydrateGraphScope = () => target._methodContainers.refresh.hydrateGraphScope();
target.hydratePluginGraphScope = pluginIds =>
Expand Down
Loading