pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
4,140 lines • 140 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
buildSymbolId
} from "./chunk-3MED75HT.js";
import {
createGitIdentityResolver,
resolveGitIdentity
} from "./chunk-3I5QFT3N.js";
import {
RUNTIME_CONFIG,
TreeSitterSymbolExtractor,
buildReverseDependencyIndexFromGraph,
getLSPService,
jsTsCandidatePaths,
rankFilesByReverseDependencyCentrality,
resolveAliasedImport,
resolveImportToFiles,
resolveProjectReferenceImport,
resolveWorkspacePackageImport,
symbolExtractionGrammar
} from "./chunk-TVN5N6GY.js";
import {
buildQualifiedName,
findOwnerName
} from "./chunk-5W3R6LFT.js";
import {
classifyTreeSitterWasmError,
getSharedTreeSitterClient,
logTreeSitterCacheStats,
withTreeSitterRoot
} from "./chunk-LW6XKDZQ.js";
import {
readWorkerHeapStatistics
} from "./chunk-AJVLWNFN.js";
import {
getReviewGraphMaxFilesDerived
} from "./chunk-LNAKEO63.js";
import {
collectSourceFilesWithBudgetAsync
} from "./chunk-6QIUX4EP.js";
import {
collectUntrackedIgnoredIds,
getProjectDataDir,
isStaleStageFile
} from "./chunk-VN2AXLEX.js";
import {
KIND_EXTENSIONS,
detectFileKind
} from "./chunk-5ZEJHV35.js";
import {
detectFileRole
} from "./chunk-DQ2NZ7C4.js";
import {
writeFileAtomic
} from "./chunk-2EECBNC5.js";
import {
compareOrdinal,
escapeRegExp,
incrementDegradationCount
} from "./chunk-N3YQJI6O.js";
import {
logLatency
} from "./chunk-5THXD6X5.js";
import {
createNdjsonLogger,
getGlobalPiLensLogDir,
getMaxLogSizeMB,
getTurnId,
isTestMode,
lazyEnvNumber
} from "./chunk-UK3CMEAL.js";
import {
FactStore
} from "./chunk-NMABWBZN.js";
import {
BoundedFifoMap,
BoundedLruCache
} from "./chunk-ABBOA7UD.js";
import {
isAtOrAboveHomeDir,
isUnderDir,
normalizeFilePath,
normalizeMapKey,
toProjectRelativePath
} from "./chunk-Q5U6FMDI.js";
// dist/clients/review-graph/builder.js
import { createHash as createHash2 } from "node:crypto";
import * as fs2 from "node:fs";
import * as path2 from "node:path";
import { Worker } from "node:worker_threads";
import { constants as zlibConstants, gunzipSync, gzipSync } from "node:zlib";
import { fileURLToPath } from "node:url";
// dist/clients/dispatch/facts/file-content.js
import * as fs from "node:fs/promises";
var fileContentProvider = {
id: "fact.file.content",
provides: ["file.content"],
requires: [],
appliesTo(_ctx) {
return true;
},
async run(ctx, store) {
let content;
try {
content = await fs.readFile(ctx.filePath, "utf-8");
} catch {
content = null;
}
store.setFileFact(ctx.filePath, "file.content", content);
}
};
// dist/clients/feature-hints.js
function normalizeHintInput(value) {
return value.replace(/\\/g, "/");
}
function hasHintToken(value, tokens) {
const escaped = tokens.map((token) => escapeRegExp(token));
const alternatives = escaped.join("|");
return new RegExp(`(?:^|[/_.\\-\\s])(?:${alternatives})(?=[/_.\\-\\s]|$)`, "i").test(value) || new RegExp(`(?:^|(?<=[a-z0-9]))(?:${tokens.flatMap((token) => [
token[0].toUpperCase() + token.slice(1),
...token === "openai" ? ["OpenAI"] : [],
...token === "github" ? ["GitHub"] : [],
...token === "gitlab" ? ["GitLab"] : []
]).join("|")})(?=[A-Z]|[/_.\\-\\s]|$)`).test(value);
}
function inferFeatureKind(nameOrPath) {
const normalized = normalizeHintInput(nameOrPath);
if (hasHintToken(normalized, [
"config",
"store",
"db",
"database",
"github",
"openai",
"sync",
"service"
])) {
return "service";
}
if (/(^|[/_.\-\s])(cli|command|bin)([/_.\-\s]|$)/i.test(normalized)) {
return "cli-command";
}
return "library";
}
function inferTrustBoundaries(nameOrPath) {
const normalized = normalizeHintInput(nameOrPath);
const boundaries = /* @__PURE__ */ new Set();
if (hasHintToken(normalized, [
"config",
"store",
"db",
"database",
"repo",
"repository",
"model",
"migration"
])) {
boundaries.add("filesystem");
boundaries.add("database");
}
if (hasHintToken(normalized, [
"github",
"gitlab",
"openai",
"anthropic",
"stripe",
"slack",
"sync",
"webhook",
"api",
"client"
])) {
boundaries.add("network");
boundaries.add("external-api");
boundaries.add("serialization");
}
if (/(^|[/_.\-\s])(cli|command|bin|exec|spawn|process|shell)([/_.\-\s]|$)/i.test(normalized)) {
boundaries.add("user-input");
boundaries.add("process-exec");
}
if (hasHintToken(normalized, [
"auth",
"login",
"token",
"session",
"oauth",
"jwt"
])) {
boundaries.add("auth");
boundaries.add("user-input");
}
return [...boundaries];
}
function featureHintMetadata(nameOrPath) {
return {
featureKind: inferFeatureKind(nameOrPath),
trustBoundaries: inferTrustBoundaries(nameOrPath)
};
}
// dist/clients/lsp-document-symbols.js
var LSP_DOCUMENT_SYMBOL_TIMEOUT_MS = 150;
var SYMBOL_KIND_NAMES = {
1: "file",
2: "module",
3: "namespace",
4: "package",
5: "class",
6: "method",
7: "property",
8: "field",
9: "constructor",
10: "enum",
11: "interface",
12: "function",
13: "variable",
14: "constant",
15: "string",
16: "number",
17: "boolean",
18: "array",
19: "object",
20: "key",
21: "null",
22: "enum-member",
23: "struct",
24: "event",
25: "operator",
26: "type-parameter"
};
function lspSymbolKindName(kind) {
return SYMBOL_KIND_NAMES[kind] ?? `lsp-symbol-${kind}`;
}
async function getOpenDocumentSymbols(filePath, timeoutMs = LSP_DOCUMENT_SYMBOL_TIMEOUT_MS) {
const spawned = await getLSPService().getWarmClientForFile(filePath);
if (!spawned || !spawned.client.isDocumentOpen(filePath) || !spawned.client.getOperationSupport().documentSymbol) {
return void 0;
}
let timer;
try {
return await Promise.race([
spawned.client.documentSymbol(filePath),
new Promise((resolve2) => {
timer = setTimeout(() => resolve2(void 0), timeoutMs);
})
]);
} catch {
return void 0;
} finally {
if (timer)
clearTimeout(timer);
}
}
function containerNameChain(symbol, allSymbols) {
const chain = [];
const seen = /* @__PURE__ */ new Set();
let container = symbol.containerName;
while (container && !seen.has(container) && chain.length < 10) {
chain.unshift(container);
seen.add(container);
container = allSymbols?.find((candidate) => candidate.name === container)?.containerName;
}
return chain;
}
// dist/clients/project-scan-policy.js
function collectProjectSourceFilesWithBudgetAsync(rootDir, options) {
return collectSourceFilesWithBudgetAsync(rootDir, options);
}
// dist/clients/review-graph-logger.js
import * as path from "node:path";
var REVIEW_GRAPH_LOG_DIR = getGlobalPiLensLogDir();
var REVIEW_GRAPH_LOG_FILE = path.join(REVIEW_GRAPH_LOG_DIR, "review-graph.log");
var writer = createNdjsonLogger({
filePath: REVIEW_GRAPH_LOG_FILE,
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
backupPath: `${REVIEW_GRAPH_LOG_FILE}.1`
});
function makeReviewGraphBuildMetadata(graph, options = {}) {
return {
...options.buildId === void 0 ? {} : { buildId: options.buildId },
...graph.buildGeneration === void 0 ? {} : { graphGeneration: graph.buildGeneration },
builtAt: graph.builtAt,
...options.projectSeq === void 0 ? {} : { projectSeq: options.projectSeq },
...options.seqHint === void 0 ? {} : { seqHint: options.seqHint },
...options.mode === void 0 ? {} : { mode: options.mode },
sourceFiles: options.sourceFileCount ?? graph.fileNodes.size,
...options.sourceFileCountTruncated || graph.persistCoverage?.sourceFilesTruncated ? { sourceFilesTruncated: true } : {},
...options.pathNormalizeCalls === void 0 ? {} : { pathNormalizeCalls: options.pathNormalizeCalls },
nodes: graph.nodes.size,
edges: graph.edges.length,
...graph.persistCoverage ? { persistCoverage: graph.persistCoverage } : {}
};
}
function logReviewGraph(entry) {
if (isTestMode()) {
return;
}
writer.log({
ts: (/* @__PURE__ */ new Date()).toISOString(),
...entry,
cwd: normalizeFilePath(entry.cwd),
pid: process.pid,
turnId: getTurnId()
});
}
function flushReviewGraphLogSync() {
writer.flushSync();
}
// dist/clients/review-graph/revision-drift.js
function formatReviewGraphRevisionDriftNote(drift) {
return `Graph was built at commit ${drift.stampedHead.slice(0, 8)}; HEAD is now ${drift.currentHead.slice(0, 8)}. Results below reflect the earlier revision \u2014 run pilens_rebuild or re-analyze to refresh.`;
}
// dist/clients/review-graph/shared-extraction-ir.js
import { createHash } from "node:crypto";
function reviewGraphIrContentHash(content) {
return createHash("sha256").update(content).digest("hex");
}
var MAX_ROOTS = 8;
var MAX_FILES_PER_ROOT = 2e4;
var roots = new BoundedFifoMap(MAX_ROOTS);
var accepted = 0;
var rejected = 0;
function rootKey(cwd) {
return normalizeMapKey(cwd);
}
function publishReviewGraphFileIr(cwd, entry) {
if (!entry.complete || !entry.structural)
return;
const key = rootKey(cwd);
let files = roots.get(key);
if (!files) {
files = new BoundedFifoMap(MAX_FILES_PER_ROOT);
roots.set(key, files);
}
const fileKey = normalizeMapKey(entry.filePath);
files.delete(fileKey);
files.set(fileKey, { ...entry, filePath: fileKey });
}
function getFreshReviewGraphFileIr(cwd, filePath, contentHash) {
const files = roots.get(rootKey(cwd));
const fileKey = normalizeMapKey(filePath);
const entry = files?.get(fileKey);
if (!entry)
return void 0;
if (entry.contentHash !== contentHash) {
rejected++;
return void 0;
}
files?.delete(fileKey);
accepted++;
return entry;
}
function clearReviewGraphFileIr(cwd) {
if (cwd === void 0)
roots.clear();
else
roots.delete(rootKey(cwd));
}
// dist/clients/review-graph/builder.js
var REVIEW_GRAPH_VERSION = "v8";
var MAIN_KINDS = /* @__PURE__ */ new Set([
"jsts",
"python",
"go",
"rust",
"ruby",
"cxx",
// Languages added in #152: WASMs + symbol queries now available
"java",
"kotlin",
"dart",
"elixir",
"csharp",
"php",
"swift",
"lua",
"ocaml",
"zig",
"shell"
]);
var MAIN_KIND_EXTENSIONS = Array.from(MAIN_KINDS).flatMap((kind) => KIND_EXTENSIONS[kind] ?? []);
var CHANGED_SYMBOLS_PREFIX = "session.reviewGraph.changedSymbols:";
var extractorCache = /* @__PURE__ */ new Map();
var REVIEW_GRAPH_MAX_WARM_WORKSPACES = 8;
var REVIEW_GRAPH_SOURCE_PATH_MEMO_ENTRIES = 16384;
var _sourcePathMemos = new BoundedLruCache(REVIEW_GRAPH_MAX_WARM_WORKSPACES);
function sourcePathMemo(cwd) {
const key = normalizeMapKey(path2.resolve(cwd));
const existing = _sourcePathMemos.get(key);
if (existing)
return existing;
const memo = {
cache: new BoundedLruCache(REVIEW_GRAPH_SOURCE_PATH_MEMO_ENTRIES),
normalizeCalls: { value: 0 }
};
_sourcePathMemos.set(key, memo);
return memo;
}
function normalizeGraphSourcePath(memo, raw) {
const cached = memo.cache.get(raw);
if (cached !== void 0)
return cached;
const normalized = normalizeMapKey(raw);
if (fs2.existsSync(raw))
memo.cache.set(raw, normalized);
memo.normalizeCalls.value++;
return normalized;
}
function _resetReviewGraphSourcePathMemoForTests() {
_sourcePathMemos.clear();
}
var _buildCache = /* @__PURE__ */ new Map();
var _workspaceGraphCache = /* @__PURE__ */ new Map();
var REVIEW_GRAPH_IDLE_EVICT_MS_DEFAULT = 20 * 6e4;
var _workspaceCacheEpoch = 0;
var _workspaceCacheEpochs = /* @__PURE__ */ new Map();
function workspaceCacheEpoch(key) {
return _workspaceCacheEpoch + (_workspaceCacheEpochs.get(key) ?? 0);
}
function reviewGraphIdleEvictMs() {
const value = Number.parseInt(process.env.PI_LENS_REVIEW_GRAPH_IDLE_EVICT_MS ?? "", 10);
return Number.isSafeInteger(value) && value > 0 ? value : REVIEW_GRAPH_IDLE_EVICT_MS_DEFAULT;
}
function clearWorkspaceGraphTimer(entry) {
if (entry.idleTimer)
clearTimeout(entry.idleTimer);
entry.idleTimer = void 0;
}
function buildCacheWorkspaceKey(cwd) {
return normalizeMapKey(path2.resolve(cwd));
}
function buildCacheKeyWorkspace(buildKey) {
const separator = buildKey.indexOf("|");
return separator >= 0 ? buildKey.slice(0, separator) : void 0;
}
function evictWorkspaceGraph(key, entry) {
if (_workspaceGraphCache.get(key) !== entry)
return;
clearWorkspaceGraphTimer(entry);
const buildWorkspace = buildCacheWorkspaceKey(key);
for (const buildKey of _buildCache.keys()) {
if (buildCacheKeyWorkspace(buildKey) === buildWorkspace) {
_buildCache.delete(buildKey);
}
}
_sourcePathMemos.delete(key);
_workspaceCacheEpochs.set(key, (_workspaceCacheEpochs.get(key) ?? 0) + 1);
_workspaceGraphCache.delete(key);
}
function scheduleWorkspaceGraphEviction(key, entry) {
clearWorkspaceGraphTimer(entry);
const epoch = workspaceCacheEpoch(key);
entry.idleTimer = setTimeout(() => {
entry.idleTimer = void 0;
if (_workspaceGraphCache.get(key) !== entry || workspaceCacheEpoch(key) !== epoch)
return;
evictWorkspaceGraph(key, entry);
}, reviewGraphIdleEvictMs());
entry.idleTimer.unref?.();
}
function touchWorkspaceGraph(key) {
const entry = _workspaceGraphCache.get(key);
if (!entry)
return;
entry.lastUsedAt = Date.now();
scheduleWorkspaceGraphEviction(key, entry);
}
var _wasmTrappedFiles = /* @__PURE__ */ new Map();
function stampWasmTrapped(entries, includeCharged) {
let stamped;
for (const [file, state] of _wasmTrappedFiles) {
if ((includeCharged || state === "retry") && entries.has(file)) {
stamped ??= new Map(entries);
stamped.set(file, "wasm-trapped");
}
}
return stamped ?? entries;
}
function stampWasmTrappedFiles(stored, includeCharged) {
const fileSignatures = stampWasmTrapped(stored.fileSignatures, includeCharged);
if (fileSignatures === stored.fileSignatures)
return stored;
return {
...stored,
signature: sourceSignatureFromMap(fileSignatures),
fileSignatures,
fileHashes: stored.fileHashes && stampWasmTrapped(stored.fileHashes, includeCharged)
};
}
function wasmTrappedFileCount(graph) {
let count = 0;
for (const file of _wasmTrappedFiles.keys()) {
if (graph.fileNodes.has(file))
count++;
}
return count;
}
function graphWasmTrapDetail(wasmTrappedFiles) {
return `review graph degraded \u2014 tree-sitter wasm runtime failure in ${wasmTrappedFiles} file(s)`;
}
function setWorkspaceGraph(key, entry, epoch) {
if (epoch !== void 0 && workspaceCacheEpoch(key) !== epoch)
return false;
const previous = _workspaceGraphCache.get(key);
if (previous)
clearWorkspaceGraphTimer(previous);
const boundedGraph = retainedGraph(key, entry.graph);
const resident = {
...stampWasmTrappedFiles(entry, false),
graph: boundedGraph,
lastUsedAt: Date.now()
};
_workspaceGraphCache.set(key, resident);
scheduleWorkspaceGraphEviction(key, resident);
while (_workspaceGraphCache.size > REVIEW_GRAPH_MAX_WARM_WORKSPACES) {
const victim = [..._workspaceGraphCache.entries()].sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)[0];
if (!victim)
break;
evictWorkspaceGraph(victim[0], victim[1]);
}
return true;
}
var _graphGenerationCounter = 0;
var _buildIdCounter = 0;
var SEQ_FASTPATH_MAX_CHANGES = 32;
var SEQ_FASTPATH_REVERIFY_MS = 5 * 6e4;
var SEQ_FASTPATH_REVERIFY_EVERY = 20;
function graphLogMetadata(graph, options = {}) {
return makeReviewGraphBuildMetadata(graph, options);
}
function seqFastpathEnabled() {
const raw = process.env.PI_LENS_GRAPH_SEQ_FASTPATH;
return raw !== "0" && raw !== "false";
}
var _lastGraphBuildInfo = {
reused: false,
mode: "full",
graphChanged: true
};
var _graphBuildInfoByGraph = /* @__PURE__ */ new WeakMap();
var _anyGraphStamped = false;
function setGraphBuildInfo(graph, info) {
_lastGraphBuildInfo = info;
_graphBuildInfoByGraph.set(graph, info);
_anyGraphStamped = true;
}
function getGraphBuildInfoForGraph(graph) {
return _graphBuildInfoByGraph.get(graph) ?? _lastGraphBuildInfo;
}
function graphBuildInfoIsTrustworthy(graph) {
return _graphBuildInfoByGraph.has(graph) || !_anyGraphStamped;
}
function updateGraphBuildInfo(graph, patch) {
const info = { ...getGraphBuildInfoForGraph(graph), ...patch };
setGraphBuildInfo(graph, info);
return info;
}
function clearGraphCache() {
_buildCache.clear();
}
function clearReviewGraphWorkspaceCache(cwd) {
if (cwd === void 0) {
_buildCache.clear();
for (const entry of _workspaceGraphCache.values())
clearWorkspaceGraphTimer(entry);
_workspaceGraphCache.clear();
_sourcePathMemos.clear();
_workspaceCacheEpoch++;
_sizeSkipVerdicts.clear();
_retainedGraphSites.clear();
} else {
const normalized = normalizeMapKey(cwd);
const buildWorkspace = buildCacheWorkspaceKey(cwd);
for (const key of _buildCache.keys()) {
if (buildCacheKeyWorkspace(key) === buildWorkspace) {
_buildCache.delete(key);
}
}
const entry = _workspaceGraphCache.get(normalized);
if (entry)
clearWorkspaceGraphTimer(entry);
_workspaceCacheEpochs.set(normalized, (_workspaceCacheEpochs.get(normalized) ?? 0) + 1);
_workspaceGraphCache.delete(normalized);
_sourcePathMemos.delete(normalized);
_sizeSkipVerdicts.delete(normalized);
}
_lastGraphBuildInfo = { reused: false, mode: "full", graphChanged: true };
_anyGraphStamped = false;
}
var _reviewGraphBuildGateForTests;
function _setReviewGraphBuildGateForTests(gate) {
_reviewGraphBuildGateForTests = gate;
}
var REVIEW_GRAPH_NODE_RESIDENT_BYTES = 450.5;
var REVIEW_GRAPH_EDGE_RESIDENT_BYTES = 243;
function estimateReviewGraphStoreBytes(totalNodes, totalEdges) {
return totalNodes * REVIEW_GRAPH_NODE_RESIDENT_BYTES + totalEdges * REVIEW_GRAPH_EDGE_RESIDENT_BYTES;
}
var GRAPH_MAX_IN_MEMORY_BYTES_DEFAULT = 512 * 1024 * 1024;
function graphMaxInMemoryBytes() {
const raw = Number(process.env.PI_LENS_GRAPH_MAX_IN_MEMORY_BYTES);
return Number.isFinite(raw) && raw > 0 ? raw : GRAPH_MAX_IN_MEMORY_BYTES_DEFAULT;
}
function retainedGraph(cwd, graph) {
const nodes = graph.nodes.size;
const edges = graph.edges.length;
const bytes = estimateReviewGraphStoreBytes(nodes, edges);
const budget = graphMaxInMemoryBytes();
if (bytes <= budget)
return graph;
const elementCap = Math.max(1, Math.floor((nodes + edges) * budget / bytes));
let capped = capGraphForPersist(cwd, graph, elementCap);
if (capped.nodes.size === 0 && nodes > 0) {
capped = headSliceGraph(graph, elementCap);
incrementDegradationCount({
kind: "review-graph-memory-cap-floor",
subject: cwd,
reason: `centrality selection retained 0 of ${nodes} nodes at cap ${elementCap}; fell back to a ${capped.nodes.size}-node head slice`
});
}
const cappedNodes = capped.nodes.size;
const cappedEdges = capped.edges.length;
capped.persistCoverage = {
...capped.persistCoverage ?? graphCoverage(capped, elementCap),
partial: true,
capTrimmed: true
};
incrementDegradationCount({
kind: "review-graph-memory-cap",
subject: cwd,
reason: `live graph ${nodes}n/${edges}e ~${Math.round(bytes / (1024 * 1024))}MiB over ${Math.round(budget / (1024 * 1024))}MiB budget; trimmed to ${cappedNodes}n/${cappedEdges}e ~${Math.round(estimateReviewGraphStoreBytes(cappedNodes, cappedEdges) / (1024 * 1024))}MiB`
});
return capped;
}
function headSliceGraph(graph, cap) {
const nodeBudget = Math.max(1, Math.floor(cap * graph.nodes.size / (graph.nodes.size + graph.edges.length)));
const keptIds = /* @__PURE__ */ new Set();
for (const [id] of graph.nodes) {
if (keptIds.size >= nodeBudget)
break;
keptIds.add(id);
}
const nodes = new Map([...graph.nodes].filter(([id]) => keptIds.has(id)));
const edgeBudget = Math.max(0, cap - nodes.size);
const edges = graph.edges.filter((edge) => keptIds.has(edge.from) && keptIds.has(edge.to)).slice(0, edgeBudget);
const sliced = {
...graph,
nodes,
edges,
edgesByFrom: /* @__PURE__ */ new Map(),
edgesByTo: /* @__PURE__ */ new Map(),
fileNodes: /* @__PURE__ */ new Map(),
symbolNodesByFile: /* @__PURE__ */ new Map(),
changedSymbolsByFile: /* @__PURE__ */ new Map(),
persistCoverage: void 0
};
rebuildIndexes(sliced);
return sliced;
}
function setSessionReviewGraphFact(cwd, facts, graph) {
const retained = retainedGraph(cwd, graph);
registerRetainedGraph(`fact:${cwd}`, retained);
facts.setSessionFact("session.reviewGraph", retained);
}
var _retainedGraphSites = /* @__PURE__ */ new Map();
function registerRetainedGraph(site, graph) {
for (const [key, ref] of _retainedGraphSites) {
if (ref.deref() === void 0)
_retainedGraphSites.delete(key);
}
_retainedGraphSites.set(site, new WeakRef(graph));
}
function getReviewGraphWorkspaceCacheSnapshot() {
let totalNodes = 0;
let totalEdges = 0;
const counted = /* @__PURE__ */ new Set();
const count = (graph) => {
if (counted.has(graph))
return;
counted.add(graph);
totalNodes += graph.nodes.size;
totalEdges += graph.edges.length;
};
for (const entry of _workspaceGraphCache.values())
count(entry.graph);
for (const [key, ref] of _retainedGraphSites) {
const graph = ref.deref();
if (graph === void 0) {
_retainedGraphSites.delete(key);
continue;
}
count(graph);
}
return {
cacheEntries: _workspaceGraphCache.size,
totalNodes,
totalEdges,
residentBytes: estimateReviewGraphStoreBytes(totalNodes, totalEdges)
};
}
function _getReviewGraphWorkspaceCacheKeysForTests() {
return [..._workspaceGraphCache.entries()].sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt).map(([key]) => key);
}
function _setReviewGraphWorkspaceEntryForTests(key, graph) {
setWorkspaceGraph(key, {
signature: "test",
fileSignatures: /* @__PURE__ */ new Map(),
graph
});
}
function _setSessionReviewGraphFactForTests(cwd, facts, graph) {
setSessionReviewGraphFact(cwd, facts, graph);
}
function _persistedCoverageForTests(coverage) {
return stripProcessLocalCoverage(coverage);
}
function _getReviewGraphWorkspaceGraphForTests(key) {
return _workspaceGraphCache.get(key)?.graph;
}
function _getReviewGraphCacheStateForTests(cwd) {
const cached = _workspaceGraphCache.get(normalizeMapKey(cwd));
if (!cached)
return void 0;
touchWorkspaceGraph(normalizeMapKey(cwd));
return {
signature: cached.signature,
fileSignatures: new Map(cached.fileSignatures),
fileHashes: cached.fileHashes ? new Map(cached.fileHashes) : void 0
};
}
function getReviewGraphCacheIdentity(cwd, graph) {
const cached = _workspaceGraphCache.get(normalizeMapKey(cwd));
if (!cached)
return void 0;
touchWorkspaceGraph(normalizeMapKey(cwd));
if (graph) {
if (cached.buildGeneration === void 0 && graph.buildGeneration === void 0) {
if (cached.graph !== graph)
return void 0;
} else if (cached.buildGeneration !== graph.buildGeneration) {
return void 0;
}
}
const version = graph?.version ?? cached.graph.version;
if (typeof version !== "string" || version.length === 0 || typeof cached.signature !== "string") {
return void 0;
}
return { version, signature: cached.signature };
}
var _cwdWorktreeCheckedCwds = /* @__PURE__ */ new Set();
function _resetCwdWorktreeMismatchLogForTests() {
_cwdWorktreeCheckedCwds.clear();
}
function logCwdWorktreeMismatchOnce(cwd, resolveIdentity = resolveGitIdentity) {
const key = normalizeMapKey(cwd);
if (_cwdWorktreeCheckedCwds.has(key))
return;
_cwdWorktreeCheckedCwds.add(key);
const identity = resolveIdentity(cwd);
if (!identity)
return;
if (identity.worktreeRoot === normalizeFilePath(path2.resolve(cwd)))
return;
logLatency({
type: "phase",
phase: "review_graph_cwd_worktree_mismatch",
filePath: cwd,
durationMs: 0,
metadata: { cwd, worktreeRoot: identity.worktreeRoot }
});
}
function getLastGraphBuildInfo() {
return _lastGraphBuildInfo;
}
function _setLastGraphBuildInfoForTests(info) {
_lastGraphBuildInfo = info;
}
function ensureIndexed(graph) {
if (graph.edges.length > 0 && graph.edgesByFrom.size === 0) {
rebuildIndexes(graph);
}
}
function getCachedReviewGraph(cwd) {
const key = normalizeMapKey(cwd);
if (getReviewGraphSizeSkipVerdict(cwd))
return void 0;
const cached = _workspaceGraphCache.get(key);
if (cached) {
touchWorkspaceGraph(key);
ensureIndexed(cached.graph);
return cached.graph;
}
const disk = loadPersistedGraph(cwd, {
verifyWorktreeIdentity: true,
allowPartial: true
});
if (!disk)
return void 0;
setWorkspaceGraph(key, {
signature: disk.signature,
fileSignatures: disk.fileSignatures,
fileHashes: disk.fileHashes,
graph: disk.graph,
// Durable half of the drift fact. The current-HEAD half is never cached
// beside it — see getReviewGraphRevisionDrift (#1961 review F3).
...disk.stampedHead ? { snapshotStampedHead: disk.stampedHead } : {}
});
return disk.graph;
}
function getReviewGraphRevisionDrift(cwd) {
const entry = _workspaceGraphCache.get(normalizeMapKey(cwd));
const stampedHead = entry?.snapshotStampedHead;
if (!stampedHead)
return void 0;
const current = resolveGitIdentity(cwd);
if (!current || current.headCommit === stampedHead)
return void 0;
return { stampedHead, currentHead: current.headCommit };
}
function makeCtx(filePath, cwd, facts) {
return {
filePath,
cwd,
kind: detectFileKind(filePath),
fileRole: detectFileRole(filePath),
pi: { getFlag: () => void 0 },
autofix: false,
deltaMode: false,
facts,
blockingOnly: false,
modifiedRanges: void 0,
hasTool: async () => false,
log: () => {
}
};
}
function createEmptyGraph() {
return {
version: REVIEW_GRAPH_VERSION,
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
nodes: /* @__PURE__ */ new Map(),
edges: [],
edgesByFrom: /* @__PURE__ */ new Map(),
edgesByTo: /* @__PURE__ */ new Map(),
fileNodes: /* @__PURE__ */ new Map(),
symbolNodesByFile: /* @__PURE__ */ new Map(),
changedSymbolsByFile: /* @__PURE__ */ new Map()
};
}
function cloneGraph(graph) {
return {
version: graph.version,
builtAt: graph.builtAt,
nodes: new Map(graph.nodes),
// Edges are immutable values: update paths replace/filter entries rather
// than mutating them, so copying the array is sufficient isolation.
edges: [...graph.edges],
edgesByFrom: /* @__PURE__ */ new Map(),
edgesByTo: /* @__PURE__ */ new Map(),
fileNodes: /* @__PURE__ */ new Map(),
symbolNodesByFile: /* @__PURE__ */ new Map(),
changedSymbolsByFile: new Map(graph.changedSymbolsByFile),
// Carry the partial-coverage marker so a clone can never silently pose as
// a complete graph (#936 review). The build path additionally refuses a
// partial base outright; this keeps any other cloner honest.
persistCoverage: graph.persistCoverage
};
}
function refreshGraphBuiltAt(graph) {
graph.builtAt = (/* @__PURE__ */ new Date()).toISOString();
}
function sourceSignatureEntry(file) {
try {
const stat = fs2.statSync(file);
return `${stat.size}:${stat.mtimeMs}`;
} catch {
return "missing";
}
}
var STAT_YIELD_EVERY = 100;
var yieldToLoop = () => new Promise((resolve2) => setImmediate(resolve2));
async function sourceSignatureMapAsync(files) {
const signatures = /* @__PURE__ */ new Map();
let sinceYield = 0;
for (const file of files) {
signatures.set(file, sourceSignatureEntry(file));
if (++sinceYield >= STAT_YIELD_EVERY) {
sinceYield = 0;
await yieldToLoop();
}
}
return signatures;
}
function sourceSignatureFromMap(signatures) {
return [...signatures.entries()].sort(([a], [b]) => compareOrdinal(a, b)).map(([file, signature]) => `${file}:${signature}`).join("|");
}
function contentHashEntry(file) {
try {
return createHash2("sha256").update(fs2.readFileSync(file)).digest("hex");
} catch {
return "missing";
}
}
async function confirmContentChanged(candidates, previousHashes) {
const prior = previousHashes ?? /* @__PURE__ */ new Map();
const hashes = new Map(prior);
const trulyChanged = [];
let sinceYield = 0;
for (const file of candidates) {
const hash = contentHashEntry(file);
hashes.set(file, hash);
if (prior.get(file) !== hash)
trulyChanged.push(file);
if (++sinceYield >= STAT_YIELD_EVERY) {
sinceYield = 0;
await yieldToLoop();
}
}
return { trulyChanged, hashes };
}
function diffSignatureMaps(previous, next) {
const added = [];
const changed = [];
for (const [file, signature] of next) {
const oldSignature = previous.get(file);
if (oldSignature === void 0)
added.push(file);
else if (oldSignature !== signature)
changed.push(file);
}
const removed = [];
for (const file of previous.keys()) {
if (!next.has(file))
removed.push(file);
}
return { added, removed, changed };
}
function getReviewGraphMaxFiles(cwd) {
const override = Number.parseInt(process.env.PI_LENS_REVIEW_GRAPH_MAX_FILES ?? "", 10);
return Number.isFinite(override) && override > 0 ? override : getReviewGraphMaxFilesDerived(cwd);
}
function getReviewGraphMaxFileBytes() {
const override = Number.parseInt(process.env.PI_LENS_REVIEW_GRAPH_MAX_FILE_BYTES ?? "", 10);
return Number.isFinite(override) && override > 0 ? override : RUNTIME_CONFIG.reviewGraph.maxFileBytes;
}
function isWithinReviewGraphSizeLimit(file) {
try {
return fs2.statSync(file).size <= getReviewGraphMaxFileBytes();
} catch {
return false;
}
}
var DEFAULT_REVIEW_GRAPH_SIZE_SKIP_TTL_MS = 15 * 6e4;
var _sizeSkipTtl = lazyEnvNumber("PI_LENS_REVIEW_GRAPH_SIZE_SKIP_TTL_MS", DEFAULT_REVIEW_GRAPH_SIZE_SKIP_TTL_MS);
var _resetReviewGraphSizeSkipTtlForTests = _sizeSkipTtl._resetForTests;
var _buildAttempts = /* @__PURE__ */ new Map();
function getLastReviewGraphBuildAttempt(cwd) {
return _buildAttempts.get(normalizeMapKey(cwd));
}
function _resetReviewGraphBuildAttemptsForTests() {
_buildAttempts.clear();
}
function recordBuildAttempt(cwd, outcome, reason, buildId) {
const key = normalizeMapKey(cwd);
const prior = _buildAttempts.get(key);
if (prior?.buildId !== void 0 && (buildId === void 0 || buildId < prior.buildId)) {
return;
}
_buildAttempts.set(key, {
when: (/* @__PURE__ */ new Date()).toISOString(),
outcome,
...buildId === void 0 ? {} : { buildId },
...reason ? { reason } : {}
});
}
function recordPersistFailure(cwd, reason, error, pending, workerState) {
const prior = _buildAttempts.get(normalizeMapKey(cwd));
const pendingBuildId = pending?.graphMetadata.buildId;
const belongsToCurrentBuild = pendingBuildId === void 0 || prior?.buildId === pendingBuildId;
if (belongsToCurrentBuild && (prior === void 0 || prior.outcome === "succeeded")) {
recordBuildAttempt(cwd, "succeeded", `graph built but persistence failed: ${error}`, pendingBuildId);
}
logReviewGraph({
cwd,
phase: "persist_failed",
reason,
error,
...pending ? {
observability: persistObservability(pending, {
status: "failed",
reason: workerState?.fallbackReason ?? reason,
workerStarted: workerState?.started,
workerCompleted: workerState?.completed,
...workerState?.fallbackReason ? { workerFallback: true } : {}
})
} : {}
});
}
var _sizeSkipVerdicts = /* @__PURE__ */ new Map();
function _resetReviewGraphSizeSkipVerdictsForTests() {
_sizeSkipVerdicts.clear();
}
function recordReviewGraphSizeSkip(cwd, sourceFileCount, maxFileCount) {
_sizeSkipVerdicts.set(normalizeMapKey(cwd), {
sourceFileCount,
maxFileCount,
sourceFileCountTruncated: true,
skippedAt: Date.now()
});
}
function clearReviewGraphSizeSkip(cwd) {
_sizeSkipVerdicts.delete(normalizeMapKey(cwd));
}
var REVIEW_GRAPH_SIZE_NEAR_MISS_RATIO = 0.05;
function isReviewGraphSizeNearMiss(sourceFileCount, maxFileCount) {
return sourceFileCount > maxFileCount && sourceFileCount <= maxFileCount * (1 + REVIEW_GRAPH_SIZE_NEAR_MISS_RATIO);
}
function getReviewGraphSizeSkipVerdict(cwd, now = Date.now()) {
const key = normalizeMapKey(cwd);
const verdict = _sizeSkipVerdicts.get(key);
if (!verdict)
return void 0;
if (now - verdict.skippedAt >= _sizeSkipTtl.get()) {
_sizeSkipVerdicts.delete(key);
return void 0;
}
return verdict;
}
var _reviewGraphEntryBudgetForTests;
function _setReviewGraphEntryBudgetForTests(maxScanEntries) {
_reviewGraphEntryBudgetForTests = maxScanEntries;
}
var _reviewGraphEntryCounterForTests;
function _setReviewGraphEntryCounterForTests(counter) {
_reviewGraphEntryCounterForTests = counter;
}
async function getGraphSourceFiles(cwd) {
const sourceMemo = sourcePathMemo(cwd);
const normalizeCallsBefore = sourceMemo.normalizeCalls.value;
const maxGraphFiles = getReviewGraphMaxFiles(cwd);
const { files: collected, entryBudgetExceeded } = await collectProjectSourceFilesWithBudgetAsync(cwd, {
// Only walk graph-relevant extensions so the cap counts what the graph
// keeps (post-filter), not JSON/YAML/MD noise it would discard anyway.
extensions: MAIN_KIND_EXTENSIONS,
maxFiles: maxGraphFiles + 1,
..._reviewGraphEntryBudgetForTests === void 0 ? {} : { maxScanEntries: _reviewGraphEntryBudgetForTests },
..._reviewGraphEntryCounterForTests === void 0 ? {} : { onEntryVisited: _reviewGraphEntryCounterForTests }
});
if (entryBudgetExceeded) {
logLatency({
type: "phase",
phase: "review_graph_source_walk_entry_budget",
filePath: cwd,
durationMs: 0,
metadata: { cwd, collectedFiles: collected.length }
});
}
if (collected.length > maxGraphFiles) {
return {
files: collected,
pathNormalizeCalls: sourceMemo.normalizeCalls.value - normalizeCallsBefore,
sourceFileCount: collected.length,
maxFileCount: maxGraphFiles,
entryBudgetExceeded
};
}
const result = [];
let sinceYield = 0;
for (const raw of collected) {
const file = normalizeGraphSourcePath(sourceMemo, raw);
const kind = detectFileKind(file);
if (!!kind && MAIN_KINDS.has(kind) && detectFileRole(file) !== "test" && isWithinReviewGraphSizeLimit(file)) {
result.push(file);
}
if (++sinceYield >= STAT_YIELD_EVERY) {
sinceYield = 0;
await yieldToLoop();
}
}
return {
files: result,
pathNormalizeCalls: sourceMemo.normalizeCalls.value - normalizeCallsBefore,
sourceFileCount: result.length,
maxFileCount: maxGraphFiles,
entryBudgetExceeded
};
}
function addNode(graph, node) {
const isNew = !graph.nodes.has(node.id);
graph.nodes.set(node.id, node);
if (node.kind === "file" && node.filePath) {
graph.fileNodes.set(node.filePath, node.id);
return;
}
if (isNew && node.kind === "symbol" && node.filePath) {
const ids = graph.symbolNodesByFile.get(node.filePath) ?? [];
ids.push(node.id);
graph.symbolNodesByFile.set(node.filePath, ids);
}
}
function indexEdge(graph, edge) {
const from = graph.edgesByFrom.get(edge.from) ?? [];
from.push(edge);
graph.edgesByFrom.set(edge.from, from);
const to = graph.edgesByTo.get(edge.to) ?? [];
to.push(edge);
graph.edgesByTo.set(edge.to, to);
}
function unindexEdge(graph, edge) {
const from = graph.edgesByFrom.get(edge.from);
if (from) {
_rebuildCounters.removeOwnedEdgePositions += from.length;
const at = from.indexOf(edge);
if (at >= 0)
from.splice(at, 1);
if (from.length === 0)
graph.edgesByFrom.delete(edge.from);
}
const to = graph.edgesByTo.get(edge.to);
if (to) {
_rebuildCounters.removeOwnedEdgePositions += to.length;
const at = to.indexOf(edge);
if (at >= 0)
to.splice(at, 1);
if (to.length === 0)
graph.edgesByTo.delete(edge.to);
}
}
function unindexEdges(graph, removedEdges) {
const fromIds = /* @__PURE__ */ new Set();
const toIds = /* @__PURE__ */ new Set();
for (const edge of removedEdges) {
fromIds.add(edge.from);
toIds.add(edge.to);
}
for (const fromId of fromIds) {
const bucket = graph.edgesByFrom.get(fromId);
if (!bucket)
continue;
const kept = [];
for (const edge of bucket) {
_rebuildCounters.removeOwnedEdgePositions++;
if (!removedEdges.has(edge))
kept.push(edge);
}
if (kept.length === 0)
graph.edgesByFrom.delete(fromId);
else
graph.edgesByFrom.set(fromId, kept);
}
for (const toId of toIds) {
const bucket = graph.edgesByTo.get(toId);
if (!bucket)
continue;
const kept = [];
for (const edge of bucket) {
_rebuildCounters.removeOwnedEdgePositions++;
if (!removedEdges.has(edge))
kept.push(edge);
}
if (kept.length === 0)
graph.edgesByTo.delete(toId);
else
graph.edgesByTo.set(toId, kept);
}
}
function addEdge(graph, edge) {
graph.edges.push(edge);
indexEdge(graph, edge);
}
function rebuildIndexes(graph) {
graph.edgesByFrom = /* @__PURE__ */ new Map();
graph.edgesByTo = /* @__PURE__ */ new Map();
graph.fileNodes = /* @__PURE__ */ new Map();
graph.symbolNodesByFile = /* @__PURE__ */ new Map();
for (const node of graph.nodes.values()) {
if (node.kind === "file" && node.filePath) {
graph.fileNodes.set(node.filePath, node.id);
}
if (node.kind === "symbol" && node.filePath) {
const ids = graph.symbolNodesByFile.get(node.filePath) ?? [];
ids.push(node.id);
graph.symbolNodesByFile.set(node.filePath, ids);
}
}
for (const edge of graph.edges) {
const from = graph.edgesByFrom.get(edge.from) ?? [];
from.push(edge);
graph.edgesByFrom.set(edge.from, from);
const to = graph.edgesByTo.get(edge.to) ?? [];
to.push(edge);
graph.edgesByTo.set(edge.to, to);
}
}
var GRAPH_CACHE_FILENAME = "review-graph.json.gz";
var LEGACY_GRAPH_CACHE_FILENAME = "review-graph.json";
var GRAPH_CHECKPOINT_FILENAME = "review-graph.checkpoint.json.gz";
function logSnapshotReadVerdict(cwd, phase, reason) {
const verdict = phase === "snapshot_read_dropped" ? "dropped" : "drifted";
const isRisingEdge = incrementDegradationCount({
kind: "review-graph-snapshot-read",
// Subject keeps BOTH discriminators, so aggregation still answers which
// workspace and which verdict after the detailed records stop.
subject: `${verdict}:${normalizeMapKey(cwd)}`,
reason
});
if (!isRisingEdge)
return;
logReviewGraph({ cwd, phase, reason });
}
function loadPersistedGraph(cwd, opts, resolveIdentity = resolveGitIdentity) {
const cacheDir = path2.join(getProjectDataDir(cwd), "cache");
const cachePath = path2.join(cacheDir, GRAPH_CACHE_FILENAME);
const legacyPath = path2.join(cacheDir, LEGACY_GRAPH_CACHE_FILENAME);
try {
const raw = fs2.existsSync(cachePath) ? gunzipSync(fs2.readFileSync(cachePath)).toString("utf-8") : fs2.readFileSync(legacyPath, "utf-8");
const data = JSON.parse(raw);
if (data.version !== REVIEW_GRAPH_VERSION || typeof data.signature !== "string" || typeof data.builtAt !== "string" || !Array.isArray(data.nodes) || !Array.isArray(data.edges))
return null;
if (data.coverage?.partial && !opts?.allowPartial)
return null;
let stampedHead;
if (opts?.verifyWorktreeIdentity && data.gitStamp) {
const current = resolveIdentity(cwd);
if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
logSnapshotReadVerdict(cwd, "snapshot_read_dropped", "worktree_mismatch");
return null;
}
if (current && current.headCommit !== data.gitStamp.headCommit) {
stampedHead = data.gitStamp.headCommit;
logSnapshotReadVerdict(cwd, "snapshot_read_drifted", "head_moved");
}
}
const graph = {
version: data.version,
builtAt: data.builtAt,
nodes: new Map(data.nodes),
edges: data.edges,
edgesByFrom: /* @__PURE__ */ new Map(),
edgesByTo: /* @__PURE__ */ new Map(),
fileNodes: /* @__PURE__ */ new Map(),
symbolNodesByFile: /* @__PURE__ */ new Map(),
changedSymbolsByFile: /* @__PURE__ */ new Map(),
persistCoverage: data.coverage
};
rebuildIndexes(graph);
return {
signature: data.signature,
fileSignatures: new Map(data.fileSignatures ?? []),
fileHashes: new Map(data.fileHashes ?? []),
graph,
...stampedHead ? { stampedHead } : {}
};
} catch {
return null;
}
}
function getPersistedReviewGraphVersion(cwd) {
const cacheDir = path2.join(getProjectDataDir(cwd), "cache");
const cachePath = path2.join(cacheDir, GRAPH_CACHE_FILENAME);
const legacyPath = path2.join(cacheDir, LEGACY_GRAPH_CACHE_FILENAME);
if (fs2.existsSync(cachePath)) {
let fd2;
try {
fd2 = fs2.openSync(cachePath, "r");
const compressed = Buffer.alloc(4096);
const n = fs2.readSync(fd2, compressed, 0, compressed.length, 0);
const head = gunzipSync(compressed.subarray(0, n), {
finishFlush: zlibConstants.Z_SYNC_FLUSH
}).toString("utf-8");
const match = head.match(/"version"\s*:\s*"([^"]+)"/);
return match ? match[1] : null;
} catch {
return null;
} finally {
if (fd2 !== void 0) {
try {
fs2.closeSync(fd2);
} catch {
}
}
}
}
let fd;
try {
fd = fs2.openSync(legacyPath, "r");
const buf = Buffer.alloc(200);
const n = fs2.readSync(fd, buf, 0, 200, 0);
const match = buf.toString("utf-8", 0, n).match(/"version"\s*:\s*"([^"]+)"/);
return match ? match[1] : null;
} catch {
return null;
} finally {
if (fd !== void 0) {
try {
fs2.closeSync(fd);
} catch {
}
}
}
}
function isReviewGraphMigrationNeeded(cwd) {
const version = getPersistedReviewGraphVersion(cwd);
return version !== null && version !== REVIEW_GRAPH_VERSION;
}
var GRAPH_PERSIST_DEBOUNCE_MS_DEFAULT = 1500;
var GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT = 5e5;
function graphPersistDebounceMs() {
const raw = Number(process.env.PI_LENS_GRAPH_PERSIST_DEBOUNCE_MS);
return Number.isFinite(raw) && raw >= 0 ? raw : GRAPH_PERSIST_DEBOUNCE_MS_DEFAULT;
}
function graphPersistMaxElements() {
const raw = Number(process.env.PI_LENS_GRAPH_PERSIST_MAX_ELEMENTS);
return Number.isFinite(raw) && raw > 0 ? raw : GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT;
}
var _pendingPersist = /* @__PURE__ */ new Map();
var _persistTimers = /* @__PURE__ */ new Map();
var _persistGenerations = /* @__PURE__ */ new Map();
var _workerRequests = /* @__PURE__ */ new Map();
var _persistWorker;
var _persistWorkerRequestId = 0;
var _persistAttemptId = 0;
var _workerDisabled = false;
var _persistWorkerUnavailableReason;
var _lastWorkerFallbackReasonForTests;
var _persistWorkerHeapStatistics = null;
function refreshReviewGraphPersistWorkerHeapStatistics() {
const worker = _persistWorker;
if (!worker || typeof worker.getHeapStatistics !== "function") {
return Promise.resolve();
}
return readWorkerHeapStatistics(worker).then((stats) => {
if (_persistWorker === worker)
_persistWorkerHeapStatistics = stats;
}).catch(() => {
});
}
function getReviewGraphPersistWorkerHeapStatistics() {
return _persistWorkerHeapStatistics;
}
var _checkpointGenerations = /* @__PURE__ */ new Map();
var _checkpointWorkerRequests = /* @__PURE__ */ new Map();
var _checkpointOffloadCountForTests = 0;
function persistedData(pending) {
return {
version: pending.graph.version,
builtAt: pending.graph.builtAt,
signature: pending.signature,
fileSignatures: Array.from(pending.fileSignatures.entries()),
fileHashes: pending.fileHashes ? Array.from(pending.fileHashes.entries()) : void 0,
nodes: Array.from(pending.graph.nodes.entries()),
edges: pending.graph.edges,
coverage: stripProcessLocalCoverage(pending.graph.persistCoverage),
gitStamp: pending.gitStamp
};
}
function stripProcessLocalCoverage(coverage) {
if (!coverage?.capTrimmed)
return coverage;
const { capTrimmed: _capTrimmed, ...persisted } = coverage;
return persisted;
}
function countRetainedSourceFiles(graph, sourceFilePaths) {
if (sourceFilePaths === void 0)
return graph.fileNodes.size;
const sourceKeys = new Set(sourceFilePaths);
let retained = 0;
for (const filePath of sourceKeys) {
if (graph.fileNodes.has(filePath))
retained++;
}
return retained;
}
function graphCoverage(graph, cap, sourceFileCount = graph.persistCoverage?.totalFiles ?? graph.fileNodes.size, sourceFilesTruncated = graph.persistCoverage?.sourceFilesTruncated === true, sourceFilePaths) {
const inherited = graph.persistCoverage;
return {
partial: inherited?.partial === true || sourceFilesTruncated,
cap,
totalNodes: graph.nodes.size,
totalEdges: graph.edges.length,
persistedNodes: graph.nodes.size,
persistedEdges: graph.edges.length,
totalFiles: sourceFileCount,
persistedFiles: countRetainedSourceFiles(graph, sourceFilePaths),
...sourceFilesTruncated ? { sourceFilesTruncated: true } : {},
...inherited?.inProgress ? { inProgress: true } : {}
};
}
function capGraphForPersist(cwd, graph, cap, options = {}) {
const totalElements = graph.nodes.size + graph.edges.length;
const nodeBudget = Math.max(1, Math.floor(cap * graph.nodes.size / totalElements));
const reverseDeps = buildReverseDependencyIndexFromGraph({ cwd, graph });
const rankedFiles = rankFilesByReverseDependencyCentrality(reverseDeps);
const indexNodesByFile = (fold) => {
const byFile = /* @__PURE__ */ new Map();
for (const [id, node] of graph.nodes) {
if (!node.filePath)
continue;
const fileKey = fold ? normalizeMapKey(node.filePath) : node.filePath;
const ids = byFile.get(fileKey) ?? [];
ids.push(id);
byFile.set(fileKey, ids);
}
return byFile;
};
const reachableNodes = (byFile) => {
let reached = 0;
for (const filePath of rankedFiles)
reached += byFile.get(filePath)?.length ?? 0;
return reached;
};
let placeableNodes = 0;
for (const node of graph.nodes.values())
if (node.filePath)
placeableNodes++;
let nodeIdsByFile = indexNodesByFile(false);
if (reachableNodes(nodeIdsByFile) < placeableNodes) {
const folded = indexNodesByFile(true);
if (reachableNodes(folded) > reachableNodes(nodeIdsByFile)) {
nodeIdsByFile = folded;
}
}
const keptIds = /* @__PURE__ */ new Set();
for (const filePath of rankedFiles) {
const ids = nodeIdsByFile.get(filePath) ?? [];
if (ids.length === 0)
continue;
const effectiveNodeBudget = keptIds.size === 0 ? Math.max(nodeBudget, Math.min(cap, ids.length)) : nodeBudget;
if (keptIds.size + ids.length > effectiveNodeBudget)
continue;
for (const id of ids)
keptIds.add(id);
}
const nodes = new Map([...graph.nodes].filter(([id]) => keptIds.has(id)));
const edgeBudget = Math.max(0, cap - nodes.size);
const edges = graph.edges.filter((edge) => keptIds.has(edge.from) && keptIds.has(edge.to)).slice(0, edgeBudget);
const coverage = {
partial: true,
cap,
totalNodes: graph.nodes.size,
totalEdges: graph.edges.length,
persistedNodes: nodes.size,
persistedEdges: edges.length,
totalFiles: options.sourceFileCount ?? graph.persistCoverage?.totalFiles ?? graph.fileNodes.size,
persistedFiles: 0,
...options.sourceFilesTruncated || graph.persistCoverage?.sourceFilesTruncated ? { sourceFilesTruncated: true } : {},
...graph.persistCoverage?.inProgress ? { inProgress: true } : {}
};
const capped = {
...graph,
nodes,
edges,
edgesByFrom: /* @__PURE__ */ new Map(),
edgesByTo: /* @__PURE__ */ new Map(),
fileNodes: /* @__PURE__ */ new Map(),
symbolNodesByFile: /* @__PURE__ */ new Map(),
changedSymbolsByFile: /* @__PURE__ */ new Map(),
persistCoverage: void 0
};
rebuildIndexes(capped);
coverage.persistedFiles = countRetainedSourceFiles(capped, options.sourceFilePaths);
capped.persistCoverage = coverage;
return capped;
}
function persistObservability(pending, patch) {
return {
graph: pending.graphMetadata,
persistence: { ...pending.persistenceMetadata, ...patch }
};
}
function logPersistSuccess(key, pending, stats, workerState = { started: false, completed: false }) {
logLatency({
type: "phase",
phase: "review_graph_persist",
filePath: pending.cachePath,
durationMs: stats.durationMs,
metadata: { elements: pending.elementCount, ...stats }
});
logReviewGraph({
cwd: key,
phase: "persist_succeeded",
elements: pending.elementCount,
...stats,
durationMs: stats.durationMs,
observability: persistObservability(pending, {
status: "succeeded",
workerStarted: workerState.started,
workerCompleted: workerState.completed,
...workerState.fallback ? { workerFallback: true } : {}
})
});
}
function writePendingOnMainThread(key, pending, reason, workerState = { started: false, completed: false }) {
const persistStarted = performance.now();
const serializeStarted = performance.now();
try {
const json = JSON.stringify(persistedData(pending));
const serializeMs = performance.now() - serializeStarted;
const rawBytes = Buffer.byteLength(json);
const writeStarted = performance.now();
const gzip = gzipSync(json);
fs2.mkdirSync(pending.cacheDir, { recursive: true });
writeFileAtomic(pending.cachePath, gzip, { bestEffort: false });
fs2.rmSync(path2.join(pending.cacheDir, LEGACY_GRAPH_CACHE_FILENAME), {
force: true
});
logPersistSuccess(key, pending, {
rawBytes,
gzBytes: gzip.byteLength,
serializeMs,
writeMs: performance.now() - writeStarted,
durationMs: performance.now() - persistStarted,
offloaded: false
}, {
started: workerState.started,
completed: workerState.completed,
fallback: Boolean(reason)
});
if (reason) {
_lastWorkerFallbackReasonForTests = reason;
logReviewGraph({
cwd: key,
phase: "worker_fallback",
reason: "worker_fallback",
error: reason,
offloaded: false,
observability: persistObservability(pending, {
status: "fallback",
workerStarted: workerState.started,
workerCompleted: workerState.completed,
workerFallback: true,
reason: workerState.fallbackReason ?? "worker_fallback"
})
});
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
recordPersistFailure(key, "cache_write_failed", message, pending, workerState);
}
}
function handleWorkerResult(result) {
const checkpoint = _checkpointWorkerRequests.get(result.id);
if (checkpoint) {
handleCheckpointWorkerResult(checkpoint, result);
return;
}
const request = _workerRequests.get(result.id);
if (!request) {
fs2.rm(result.stagePath, { force: true }, () => {
});
return;
}
_workerRequests.delete(result.id);
const { key, pending } = request;
const currentGeneration = _persistGenerations.get(key);
if (currentGeneration !== result.generation) {
try {
fs2.rmSync(result.stagePath, { force: true });
} catch (reapErr) {
logReviewGraph({
cwd: key,
phase: "persist_failed",
reason: `superseded-stage reap failed: ${String(reapErr)}`
});
}
logReviewGraph({
cwd: key,
phase: "persist_skipped",
reason: "superseded",
observability: persistObservability(pending, {
status: "superseded",
supersededByGeneration: currentGeneration,
reason: "newer_generation_scheduled",
workerStarted: true,
workerCompleted: true
})
});
return;
}
if (result.error || result.rawBytes === void 0 || result.gzBytes === void 0 || result.serializeMs === void 0 || result.writeMs === void 0 || result.durationMs === void 0) {
fs2.rm(result.stagePath, { force: true }, () => {
});
writePendingOnMainThread(key, pending, result.error ?? "invalid worker result", {
started: true,
completed: true,
fallbackReason: result.error ? "worker_error" : "invalid_worker_result"
});
return;
}
try {
fs2.renameSync(result.stagePath, pending.cachePath);
fs2.rmSync(path2.join(pending.cacheDir, LEGACY_GRAPH_CACHE_FILENAME), {
force: true
});
logPersistSuccess(key, pending, {
rawBytes: result.rawBytes,
gzBytes: result.gzBytes,
serializeMs: result.serializeMs,
writeMs: result.writeMs,
durationMs: result.durationMs,
offloaded: true
}, { started: true, completed: true });
} catch (err) {
fs2.rm(result.stagePath, { force: true }, () => {
});
writePendingOnMainThread(key, pending, err instanceof Error ? err.message : String(err), {
started: true,
completed: true,
fallbackReason: "promotion_failed"
});
}
}
function handleCheckpointWorkerResult(cp, result) {
_checkpointWorkerRequests.delete(result.id);
if (result.error || result.gzBytes === void 0) {
logReviewGraph({
cwd: cp.cwd,
phase: "checkpoint_write_failed",
reason: "worker_error",
error: result.error ?? "worker returned no gz metrics"
});
fs2.rm(result.stagePath, { force: true }, () => {
});
return;
}
if (_checkpointGenerations.get(cp.cwd) !== cp.generation) {
fs2.rm(result.stagePath, { force: true }, () => {
});
return;
}
try {
fs2.renameSync(result.stagePath, cp.checkpointPath);
logReviewGraph({
cwd: cp.cwd,
phase: "checkpoint_written",
nodes: cp.nodes,
edges: cp.edges,
processed: cp.processed,
target: cp.target,
offloaded: true
});
} catch (err) {
logReviewGraph({
cwd: cp.cwd,
phase: "checkpoint_write_failed",
reason: "promote_failed",
error: err instanceof Error ? err.message : String(err)
});
fs2.rm(result.stagePath, { force: true }, () => {
});
}
}
function handleWorkerDeath(reason) {
_persistWorkerUnavailableReason = reason;
_persistWorker = void 0;
_persistWorkerHeapStatistics = null;
_workerDisabled = true;
const requests = [..._workerRequests.values()];
_workerRequests.clear();
for (const { key, pending } of requests) {
if (_persistGenerations.get(key) !== pending.generation) {
logReviewGraph({
cwd: key,
phase: "persist_skipped",
reason: "superseded",
observability: persistObservability(pending, {
status: "superseded",
supersededByGeneration: _persistGenerations.get(key),
reason: "worker_death_after_newer_generation",
workerStarted: true,
workerCompleted: false
})
});
continue;
}
writePendingOnMainThread(key, pending, reason, {
started: true,
completed: false,
fallbackReason: "worker_death"
});
}
const checkpoints = [..._checkpointWorkerRequests.values()];
_checkpointWorkerRequests.clear();
for (const cp of checkpoints) {
logReviewGraph({
cwd: cp.cwd,
phase: "checkpoint_write_failed",
reason: "worker_death",
error: reason
});
fs2.rm(cp.stagePath, { force: true }, () => {
});
}
}
function resolvePersistWorkerPath() {
const candidates = [
new URL("./persist-worker.js", import.meta.url),
new URL("./workers/review-graph-persist-worker.js", import.meta.url),
new URL("../workers/review-graph-persist-worker.js", import.meta.url)
];
for (const url of candidates) {
try {
const resolved = fileURLToPath(url);
if (fs2.existsSync(resolved))
return resolved;
} catch {
}
}
return void 0;
}
function getPersistWorker() {
if (_workerDisabled)
return void 0;
if (_persistWorker)
return _persistWorker;
try {
const workerPath = resolvePersistWorkerPath();
if (workerPath === void 0) {
handleWorkerDeath("persist worker script not found in any layout");
return void 0;
}
const worker = new Worker(workerPath);
worker.on("message", handleWorkerResult);
worker.on("error", (err) => handleWorkerDeath(err.message));
worker.on("exit", (code) => {
if (_persistWorker !== worker)
return;
const hasPendingRequests = _workerRequests.size > 0 || _checkpointWorkerRequests.size > 0;
if (code !== 0 || hasPendingRequests) {
handleWorkerDeath(code !== 0 ? `persist worker exited with code ${code}` : "persist worker exited with pending requests");
} else {
_persistWorker = void 0;
_persistWorkerHeapStatistics = null;
}
});
worker.unref();
_persistWorker = worker;
return worker;
} catch (err) {
handleWorkerDeath(err instanceof Error ? err.message : String(err));
return void 0;
}
}
function writePending(key) {
const pending = _pendingPersist.get(key);
if (!pending)
return;
_pendingPersist.delete(key);
const timer = _persistTimers.get(key);
if (timer) {
clearTimeout(timer);
_persistTimers.delete(key);
}
const worker = getPersistWorker();
if (!worker) {
const unavailableReason = _persistWorkerUnavailableReason ?? "persist worker unavailable";
writePendingOnMainThread(key, pending, unavailableReason, {
started: false,
completed: false,
fallbackReason: unavailableReason
});
return;
}
const id = ++_persistWorkerRequestId;
const stagePath = `${pending.cachePath}.stage-${process.pid}-${pending.generation}`;
const request = {
id,
cwd: key,
generation: pending.generation,
stagePath,
data: persistedData(pending),
elements: pending.elementCount,
testDelayMs: process.env.NODE_ENV === "test" ? Number(process.env.PI_LENS_TEST_PERSIST_WORKER_DELAY_MS) || void 0 : void 0
};
_workerRequests.set(id, { key, pending });
worker.postMessage(request);
}
var _persistExitHookInstalled = false;
function flushPendingReviewGraphPersistsAtExit() {
const keys = /* @__PURE__ */ new Set([
..._pendingPersist.keys(),
...[..._workerRequests.values()].map((request) => request.key)
]);
for (const key of keys) {
flushReviewGraphPersist(key, "exit_hook");
}
flushReviewGraphLogSync();
void _persistWorker?.terminate();
}
function flushReviewGraphPersistsForExitForTests() {
flushPendingReviewGraphPersistsAtExit();
}
function ensurePersistExitHook() {
if (_persistExitHookInstalled)
return;
_persistExitHookInstalled = true;
process.once("exit", flushPendingReviewGraphPersistsAtExit);
}
var _sweptStageDirs = /* @__PURE__ */ new Set();
var REVIEW_GRAPH_ARTIFACT_PREFIX = "review-graph.";
function sweepStaleStageFiles(cacheDir) {
if (_sweptStageDirs.has(cacheDir))
return;
_sweptStageDirs.add(cacheDir);
fs2.readdir(cacheDir, (err, entries) => {
if (err)
return;
for (const entry of entries) {
if (!isStaleStageFile(entry, REVIEW_GRAPH_ARTIFACT_PREFIX))
continue;
fs2.rm(path2.join(cacheDir, entry), { force: true }, () => {
});
}
});
}
function _resetReviewGraphStageSweepForTests() {
_sweptStageDirs.clear();
}
function persistGraph(cwd, signature, fileSignatures, fileHashes, graph, options = {}) {
const totalElementCount = graph.nodes.size + graph.edges.length;
const cap = graphPersistMaxElements();
const sourceFileCount = options.sourceFileCount ?? fileSignatures.size;
const sourceFilePaths = options.sourceFilePaths ?? fileSignatures.keys();
const persistedGraph = totalElementCount > cap ? capGraphForPersist(cwd, graph, cap, {
sourceFileCount,
sourceFilesTruncated: options.sourceFilesTruncated,
sourceFilePaths
}) : {
...graph,
persistCoverage: graphCoverage(graph, cap, sourceFileCount, options.sourceFilesTruncated, sourceFilePaths)
};
const elementCount = persistedGraph.nodes.size + persistedGraph.edges.length;
const sourceWalkPartial = persistedGraph.persistCoverage?.sourceFilesTruncated === true;
let persistReason;
if (totalElementCount > cap) {
const coverage = persistedGraph.persistCoverage;
if (!coverage)
return;
logLatency({
type: "phase",
phase: "review_graph_persist",
filePath: cwd,
durationMs: 0,
metadata: {
partial: true,
totalElements: totalElementCount,
persistedElements: elementCount,
cap
}
});
persistReason = `persisted partial review graph (${elementCount}/${totalElementCount} elements; ${coverage.persistedNodes}/${coverage.totalNodes} nodes, ${coverage.persistedEdges}/${coverage.totalEdges} edges; ${cap} cap)`;
} else if (sourceWalkPartial) {
persistReason = "persisted partial review graph (source walk entry budget)";
}
const cacheDir = path2.join(getProjectDataDir(cwd), "cache");
const cachePath = path2.join(cacheDir, GRAPH_CACHE_FILENAME);
sweepStaleStageFiles(cacheDir);
const gitStamp = (options.resolveIdentity ?? resolveGitIdentity)(cwd);
const key = normalizeMapKey(cwd);
const priorGeneration = _persistGenerations.get(key);
const existingPending = _pendingPersist.get(key);
const existingTimer = _persistTimers.get(key);
const generation = (priorGeneration ?? 0) + 1;
const attemptId = ++_persistAttemptId;
const coalesced = Boolean(existingPending || existingTimer);
_persistGenerations.set(key, generation);
const graphMetadata = graphLogMetadata(persistedGraph, {
...options,
sourceFileCount,
sourceFileCountTruncated: persistedGraph.persistCoverage?.sourceFilesTruncated === true
});
const persistenceMetadata = {
generation,
attemptId,
status: "scheduled",
...priorGeneration === void 0 ? {} : {
supersededGeneration: priorGeneration,
coalesced,
reason: coalesced ? "debounced_coalescing" : "in_flight_supersession"
}
};
const pending = {
cacheDir,
cachePath,
// #3605: a restart re-extracts every trapped file.
...stampWasmTrappedFiles({ signature, fileSignatures, fileHashes }, true),
graph: persistedGraph,
gitStamp,
elementCount,
generation,
attemptId,
graphMetadata,
persistenceMetadata
};
if (existingPending) {
logReviewGraph({
cwd: key,
phase: "persist_skipped",
reason: "superseded",
observability: persistObservability(existingPending, {
status: "superseded",
supersededByGeneration: generation,
reason: "newer_generation_scheduled",
workerStarted: false,
workerCompleted: false
})
});
}
_pendingPersist.set(key, pending);
if (totalElementCount > cap || sourceWalkPartial) {
logReviewGraph({
cwd,
phase: "persist_partial",
reason: totalElementCount > cap ? "element_cap_exceeded" : "source_walk_entry_budget",
elements: totalElementCount,
persistedElements: elementCount,
cap,
observability: {
graph: graphMetadata,
persistence: persistenceMetadata
}
});
}
logReviewGraph({
cwd,
phase: "persist_scheduled",
elements: elementCount,
cap,
observability: {
graph: graphMetadata,
persistence: persistenceMetadata
}
});
ensurePersistExitHook();
const debounce = graphPersistDebounceMs();
const existing = _persistTimers.get(key);
if (existing)
clearTimeout(existing);
if (debounce === 0) {
writePending(key);
return persistReason;
}
const timer = setTimeout(() => writePending(key), debounce);
if (typeof timer.unref === "function")
timer.unref();
_persistTimers.set(key, timer);
return persistReason;
}
var GRAPH_CHECKPOINT_EVERY_FILES_DEFAULT = 250;
var GRAPH_CHECKPOINT_MIN_INTERVAL_MS_DEFAULT = 5e3;
function graphCheckpointEveryFiles() {
const raw = Number(process.env.PI_LENS_GRAPH_CHECKPOINT_EVERY_FILES);
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : GRAPH_CHECKPOINT_EVERY_FILES_DEFAULT;
}
function graphCheckpointMinIntervalMs() {
const raw = Number(process.env.PI_LENS_GRAPH_CHECKPOINT_MIN_INTERVAL_MS);
return Number.isFinite(raw) && raw >= 0 ? raw : GRAPH_CHECKPOINT_MIN_INTERVAL_MS_DEFAULT;
}
function reviewGraphCheckpointPath(cwd) {
return path2.join(getProjectDataDir(cwd), "cache", GRAPH_CHECKPOINT_FILENAME);
}
function hashIgnoredIds(ignoredIds) {
if (ignoredIds === void 0)
return "unavailable";
const joined = [...ignoredIds].sort((a, b) => compareOrdinal(a, b)).join("\0");
return createHash2("sha256").update(joined).digest("hex");
}
function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity = resolveGitIdentity) {
return {
version: REVIEW_GRAPH_VERSION,
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
inProgress: true,
targetFileCount,
processedFiles: Array.from(stampWasmTrapped(processedHashes, true)),
ignoredIdsHash: hashIgnoredIds(ignoredIds),
nodes: Array.from(graph.nodes.entries()),
edges: graph.edges,
gitStamp: resolveIdentity(cwd)
};
}
function writeReviewGraphCheckpoint(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity = resolveGitIdentity) {
const generation = (_checkpointGenerations.get(cwd) ?? 0) + 1;
_checkpointGenerations.set(cwd, generation);
let data;
try {
data = buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity);
} catch {
return;
}
const worker = _workerDisabled ? void 0 : getPersistWorker();
if (!worker) {
writeReviewGraphCheckpointSync(cwd, data, {
nodes: graph.nodes.size,
edges: graph.edges.length,
processed: processedHashes.size,
target: targetFileCount
});
return;
}
const checkpointPath = reviewGraphCheckpointPath(cwd);
const cacheDir = path2.dirname(checkpointPath);
try {
fs2.mkdirSync(cacheDir, { recursive: true });
} catch (err) {
logReviewGraph({
cwd,
phase: "checkpoint_write_failed",
reason: "mkdir_failed",
error: err instanceof Error ? err.message : String(err)
});
return;
}
sweepStaleStageFiles(cacheDir);
ensurePersistExitHook();
const id = ++_persistWorkerRequestId;
const stagePath = `${checkpointPath}.stage-${process.pid}-${generation}`;
_checkpointWorkerRequests.set(id, {
cwd,
generation,
checkpointPath,
stagePath,
nodes: graph.nodes.size,
edges: graph.edges.length,
processed: processedHashes.size,
target: targetFileCount
});
const request = {
id,
cwd,
generation,
stagePath,
data,
elements: graph.nodes.size + graph.edges.length,
testDelayMs: process.env.NODE_ENV === "test" ? Number(process.env.PI_LENS_TEST_PERSIST_WORKER_DELAY_MS) || void 0 : void 0
};
worker.postMessage(request);
_checkpointOffloadCountForTests++;
}
function writeReviewGraphCheckpointSync(cwd, data, counts) {
try {
const gzip = gzipSync(JSON.stringify(data));
fs2.mkdirSync(path2.dirname(reviewGraphCheckpointPath(cwd)), {
recursive: true
});
writeFileAtomic(reviewGraphCheckpointPath(cwd), gzip, { bestEffort: true });
logReviewGraph({
cwd,
phase: "checkpoint_written",
nodes: counts.nodes,
edges: counts.edges,
processed: counts.processed,
target: counts.target
});
} catch (err) {
logReviewGraph({
cwd,
phase: "checkpoint_write_failed",
reason: "sync_write_failed",
error: err instanceof Error ? err.message : String(err)
});
}
}
function deleteReviewGraphCheckpoint(cwd) {
_checkpointGenerations.set(cwd, (_checkpointGenerations.get(cwd) ?? 0) + 1);
fs2.rm(reviewGraphCheckpointPath(cwd), { force: true }, () => {
});
}
function loadReviewGraphCheckpoint(cwd, resolveIdentity = resolveGitIdentity) {
const checkpointPath = reviewGraphCheckpointPath(cwd);
let data;
try {
if (!fs2.existsSync(checkpointPath))
return null;
data = JSON.parse(gunzipSync(fs2.readFileSync(checkpointPath)).toString("utf-8"));
} catch {
logReviewGraph({ cwd, phase: "checkpoint_discarded", reason: "corrupt" });
deleteReviewGraphCheckpoint(cwd);
return null;
}
if (data.version !== REVIEW_GRAPH_VERSION || data.inProgress !== true) {
logReviewGraph({
cwd,
phase: "checkpoint_discarded",
reason: data.version !== REVIEW_GRAPH_VERSION ? "version_mismatch" : "not_in_progress"
});
deleteReviewGraphCheckpoint(cwd);
return null;
}
if (data.gitStamp) {
const current = resolveIdentity(cwd);
if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
logReviewGraph({
cwd,
phase: "checkpoint_discarded",
reason: "worktree_mismatch"
});
deleteReviewGraphCheckpoint(cwd);
return null;
}
}
const totalNodes = data.nodes.length;
const totalEdges = data.edges.length;
const graph = {
version: data.version,
builtAt: data.builtAt,
nodes: new Map(data.nodes),
edges: data.edges,
edgesByFrom: /* @__PURE__ */ new Map(),
edgesByTo: /* @__PURE__ */ new Map(),
fileNodes: /* @__PURE__ */ new Map(),
symbolNodesByFile: /* @__PURE__ */ new Map(),
changedSymbolsByFile: /* @__PURE__ */ new Map(),
persistCoverage: {
partial: true,
inProgress: true,
cap: 0,
totalNodes,
totalEdges,
persistedNodes: totalNodes,
persistedEdges: totalEdges,
totalFiles: data.targetFileCount,
persistedFiles: 0
}
};
rebuildIndexes(graph);
if (graph.persistCoverage) {
graph.persistCoverage.persistedFiles = countRetainedSourceFiles(graph, data.processedFiles.map(([filePath]) => filePath));
}
return {
graph,
processedHashes: new Map(data.processedFiles),
ignoredIdsHash: data.ignoredIdsHash,
targetFileCount: data.targetFileCount
};
}
function pruneOrphanNonFileNodes(graph) {
const referenced = /* @__PURE__ */ new Set();
for (const edge of graph.edges) {
referenced.add(edge.from);
referenced.add(edge.to);
}
for (const [id, node] of graph.nodes) {
if (!node.filePath && !referenced.has(id))
graph.nodes.delete(id);
}
}
async function tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds, resolveIdentity = resolveGitIdentity) {
const loaded = loadReviewGraphCheckpoint(cwd, resolveIdentity);
if (!loaded)
return null;
if (loaded.ignoredIdsHash !== hashIgnoredIds(ignoredIds)) {
logReviewGraph({
cwd,
phase: "checkpoint_discarded",
reason: "ignored_ids_mismatch",
processed: loaded.processedHashes.size,
target: filesToBuild.length
});
deleteReviewGraphCheckpoint(cwd);
return null;
}
const targetSet = new Set(filesToBuild);
for (const file of loaded.processedHashes.keys()) {
if (!targetSet.has(file)) {
logReviewGraph({
cwd,
phase: "checkpoint_discarded",
reason: "removed_file",
processed: loaded.processedHashes.size,
target: filesToBuild.length
});
deleteReviewGraphCheckpoint(cwd);
return null;
}
}
const reusableHashes = /* @__PURE__ */ new Map();
const stale = [];
let sinceYield = 0;
for (const [file, priorHash] of loaded.processedHashes) {
const currentHash = contentHashEntry(file);
if (currentHash === priorHash)
reusableHashes.set(file, currentHash);
else
stale.push(file);
if (++sinceYield >= STAT_YIELD_EVERY) {
sinceYield = 0;
await yieldToLoop();
}
}
if (reusableHashes.size === 0) {
logReviewGraph({
cwd,
phase: "checkpoint_discarded",
reason: "all_stale",
stale: stale.length,
target: filesToBuild.length
});
deleteReviewGraphCheckpoint(cwd);
return null;
}
const graph = loaded.graph;
const removedEdges = /* @__PURE__ */ new Set();
for (const file of stale)
removeFileOwnedGraphData(graph, file, removedEdges);
if (removedEdges.size > 0) {
unindexEdges(graph, removedEdges);
graph.edges = graph.edges.filter((edge) => !removedEdges.has(edge));
}
pruneOrphanNonFileNodes(graph);
graph.changedSymbolsByFile = /* @__PURE__ */ new Map();
const remaining = filesToBuild.filter((file) => !reusableHashes.has(file));
logReviewGraph({
cwd,
phase: "checkpoint_resumed",
reused: reusableHashes.size,
stale: stale.length,
remaining: remaining.length,
target: filesToBuild.length
});
return { graph, fileHashes: reusableHashes, remaining };
}
function _readReviewGraphCheckpointForTests(cwd) {
const loaded = loadReviewGraphCheckpoint(cwd);
if (!loaded)
return null;
return {
inProgress: loaded.graph.persistCoverage?.inProgress === true,
processedFiles: [...loaded.processedHashes.keys()],
nodeCount: loaded.graph.nodes.size,
edgeCount: loaded.graph.edges.length,
persistCoverage: loaded.graph.persistCoverage
};
}
function flushReviewGraphPersistsForTests() {
for (const key of _pendingPersist.keys()) {
const pending = _pendingPersist.get(key);
if (!pending)
continue;
_pendingPersist.delete(key);
const timer = _persistTimers.get(key);
if (timer)
clearTimeout(timer);
_persistTimers.delete(key);
writePendingOnMainThread(key, pending);
}
}
async function waitForReviewGraphPersistsForTests() {
for (let attempts = 0; attempts < 200 && (_workerRequests.size > 0 || _checkpointWorkerRequests.size > 0); attempts++) {
await new Promise((resolve2) => setTimeout(resolve2, 10));
}
}
async function terminateReviewGraphPersistWorkerForTests() {
const worker = _persistWorker;
if (worker)
await worker.terminate();
}
function resetReviewGraphPersistWorkerForTests() {
_workerDisabled = false;
_persistWorker = void 0;
_persistWorkerHeapStatistics = null;
_persistWorkerUnavailableReason = void 0;
_lastWorkerFallbackReasonForTests = void 0;
_checkpointWorkerRequests.clear();
_checkpointGenerations.clear();
_checkpointOffloadCountForTests = 0;
}
function getCheckpointOffloadCountForTests() {
return _checkpointOffloadCountForTests;
}
function getReviewGraphWorkerFallbackReasonForTests() {
return _lastWorkerFallbackReasonForTests;
}
function reviewGraphCachePath(cwd) {
return path2.join(getProjectDataDir(cwd), "cache", GRAPH_CACHE_FILENAME);
}
function flushReviewGraphPersist(cwd, source = "cli") {
const key = normalizeMapKey(cwd);
let pending = _pendingPersist.get(key);
const removedWorkerRequests = [];
let selectedWorkerRequest;
for (const [id, request] of _workerRequests) {
if (request.key !== key)
continue;
_workerRequests.delete(id);
removedWorkerRequests.push(request.pending);
if (!pending || request.pending.generation > pending.generation) {
pending = request.pending;
selectedWorkerRequest = request.pending;
}
}
for (const removed of removedWorkerRequests) {
if (!pending || removed.generation === pending.generation)
continue;
logReviewGraph({
cwd: key,
phase: "persist_skipped",
reason: "superseded",
observability: persistObservability(removed, {
status: "superseded",
supersededByGeneration: pending.generation,
reason: "forced_flush_newest_generation",
workerStarted: true,
workerCompleted: false
})
});
}
const currentGeneration = _persistGenerations.get(key);
if (pending && pending.generation !== currentGeneration) {
logReviewGraph({
cwd: key,
phase: "persist_skipped",
reason: "superseded",
observability: persistObservability(pending, {
status: "superseded",
supersededByGeneration: currentGeneration,
reason: "forced_flush_superseded",
workerStarted: true,
workerCompleted: false
})
});
return {
ok: false,
reason: `queued snapshot generation ${pending.generation} was superseded by generation ${currentGeneration}`
};
}
if (!pending) {
return {
ok: false,
reason: "no graph snapshot was queued for persistence"
};
}
_persistGenerations.set(key, Math.max(_persistGenerations.get(key) ?? 0, pending.generation) + 1);
_pendingPersist.delete(key);
const timer = _persistTimers.get(key);
if (timer) {
clearTimeout(timer);
_persistTimers.delete(key);
}
const startedAt = performance.now();
try {
const serializeStarted = performance.now();
const json = JSON.stringify(persistedData(pending));
const serializeMs = performance.now() - serializeStarted;
const rawBytes = Buffer.byteLength(json);
const writeStarted = performance.now();
const gzip = gzipSync(json);
fs2.mkdirSync(pending.cacheDir, { recursive: true });
writeFileAtomic(pending.cachePath, gzip, { bestEffort: false });
fs2.rmSync(path2.join(pending.cacheDir, LEGACY_GRAPH_CACHE_FILENAME), {
force: true
});
const writeMs = performance.now() - writeStarted;
logLatency({
type: "phase",
phase: "review_graph_persist",
filePath: pending.cachePath,
durationMs: performance.now() - startedAt,
metadata: {
elements: pending.elementCount,
rawBytes,
gzBytes: gzip.byteLength,
serializeMs,
writeMs,
offloaded: false
}
});
logReviewGraph({
cwd,
phase: "persist_succeeded",
durationMs: performance.now() - startedAt,
elements: pending.elementCount,
rawBytes,
gzBytes: gzip.byteLength,
serializeMs,
writeMs,
offloaded: false,
observability: persistObservability(pending, {
status: "succeeded",
reason: source === "exit_hook" ? "exit_flush" : "forced_flush",
workerStarted: selectedWorkerRequest !== void 0,
workerCompleted: false
})
});
return {
ok: true,
path: pending.cachePath,
bytes: gzip.byteLength,
elements: pending.elementCount,
coverage: pending.graph.persistCoverage
};
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
recordPersistFailure(cwd, source === "exit_hook" ? "exit_flush_failed" : "forced_flush_failed", reason, pending, selectedWorkerRequest ? {
started: true,
completed: false,
fallbackReason: "forced_flush_write_failed"
} : void 0);
return { ok: false, reason };
}
}
function localImportToFile(cwd, filePath, source, ignoredIds) {
if (!source.startsWith(".")) {
const aliased = resolveAliasedImport(cwd, source, path2.dirname(filePath));
const referenced = aliased.length ? [] : resolveProjectReferenceImport(cwd, source, path2.dirname(filePath));
const resolved = aliased.length ? aliased : referenced.length ? referenced : resolveWorkspacePackageImport(cwd, source);
for (const normalized of resolved) {
if (ignoredIds?.has(normalized))
continue;
return normalized;
}
return void 0;
}
const root = path2.resolve(cwd);
for (const candidate of jsTsCandidatePaths(filePath, source)) {
if (!fs2.existsSync(candidate))
continue;
const normalized = normalizeMapKey(candidate);
if (!isUnderDir(normalized, root))
continue;
if (ignoredIds?.has(normalized))
continue;
return normalized;
}
return void 0;
}
function upsertChangedSymbols(graph, facts, filePath) {
if (detectFileRole(filePath) === "test")
return;
const normalized = normalizeMapKey(filePath);
const changed = facts.getBoundedSessionFact(`${CHANGED_SYMBOLS_PREFIX}${normalized}`);
if (changed && changed.length > 0) {
graph.changedSymbolsByFile.set(normalized, [...changed]);
} else {
graph.changedSymbolsByFile.delete(normalized);
}
}
async function ensureReviewGraphFacts(filePath, cwd, facts, contentOverride) {
const ctx = makeCtx(filePath, cwd, facts);
if (contentOverride === void 0) {
await fileContentProvider.run(ctx, facts);
} else {
facts.setFileFact(filePath, "file.content", contentOverride);
}
try {
const [{ importFactProvider }, { functionFactProvider }] = await Promise.all([
import("./chunk-CGGAIP7J.js"),
import("./chunk-D6QS3U6M.js")
]);
await importFactProvider.run(ctx, facts);
await functionFactProvider.run(ctx, facts);
} catch (err) {
logReviewGraph({
cwd,
phase: "build_skipped",
reason: "structural_facts_disabled",
error: err?.message ?? String(err)
});
}
}
function addJsTsFile(graph, cwd, filePath, facts, ignoredIds) {
const normalized = normalizeMapKey(filePath);
const hintPath = toProjectRelativePath(normalized, cwd);
const content = facts.getFileFact(normalized, "file.content") ?? "";
const fileNodeId = `file:${normalized}`;
const warmCallCoverage = facts.getFileFact(normalized, "file.functionFactsCoverage") ?? "unavailable";
const importCoverage = facts.getFileFact(normalized, "file.importFactsCoverage") ?? "unavailable";
addNode(graph, {
id: fileNodeId,
kind: "file",
language: "jsts",
filePath: normalized,
metadata: {
lineCount: content.split("\n").length,
extractionCoverage: {
definitions: warmCallCoverage,
references: warmCallCoverage,
imports: importCoverage,
calls: warmCallCoverage
},
...featureHintMetadata(hintPath)
}
});
const imports = facts.getFileFact(normalized, "file.imports") ?? [];
const functions = facts.getFileFact(normalized, "file.functionSummaries") ?? [];
for (const entry of imports) {
const localFile = localImportToFile(cwd, normalized, entry.source, ignoredIds);
if (localFile) {
const targetId = `file:${localFile}`;
if (!graph.nodes.has(targetId)) {
addNode(graph, {
id: targetId,
kind: "file",
language: detectFileKind(localFile) ?? "jsts",
filePath: localFile
});
}
addEdge(graph, { from: fileNodeId, to: targetId, kind: "imports" });
} else {
const targetId = `${entry.source.startsWith(".") ? "module" : "external"}:${entry.source}`;
if (!graph.nodes.has(targetId)) {
addNode(graph, {
id: targetId,
kind: entry.source.startsWith(".") ? "module" : "external",
language: "jsts",
metadata: { source: entry.source }
});
}
addEdge(graph, { from: fileNodeId, to: targetId, kind: "imports" });
}
}
const importedNameToFile = /* @__PURE__ */ new Map();
for (const entry of imports) {
const localFile = localImportToFile(cwd, normalized, entry.source, ignoredIds);
if (!localFile)
continue;
for (const name of entry.names)
importedNameToFile.set(name, localFile);
if (entry.defaultName)
importedNameToFile.set(entry.defaultName, localFile);
}
const methodsByQualifiedName = /* @__PURE__ */ new Map();
for (const fn of functions) {
const symbolId = buildSymbolId(normalized, fn.name, "function", fn.line);
const qualifiedName = buildQualifiedName(fn.owner, fn.name);
if (qualifiedName) {
const existing = methodsByQualifiedName.get(qualifiedName) ?? [];
existing.push(symbolId);
methodsByQualifiedName.set(qualifiedName, existing);
}
addNode(graph, {
id: symbolId,
kind: "symbol",
language: "jsts",
filePath: normalized,
symbolName: fn.name,
symbolKind: "function",
...qualifiedName ? { qualifiedName } : {},
exported: new RegExp(String.raw`export\s+(?:async\s+)?(?:function|const|let|var)\s+${escapeRegExp(fn.name)}\b`).test(content),
metadata: {
line: fn.line,
endLine: fn.endLine,
column: fn.column,
cyclomaticComplexity: fn.cyclomaticComplexity,
maxNestingDepth: fn.maxNestingDepth,
isBoundaryWrapper: fn.isBoundaryWrapper,
isPassThroughWrapper: fn.isPassThroughWrapper,
...featureHintMetadata(`${fn.name} ${hintPath}`)
}
});
addEdge(graph, { from: fileNodeId, to: symbolId, kind: "contains" });
addEdge(graph, { from: fileNodeId, to: symbolId, kind: "defines" });
}
for (const fn of functions) {
const symbolId = buildSymbolId(normalized, fn.name, "function", fn.line);
const memberCallTexts = /* @__PURE__ */ new Set();
for (const site of fn.memberCallSites ?? []) {
const callText = `${site.receiver}.${site.method}`;
memberCallTexts.add(callText);
const receiverClass = fn.receiverTypes?.[site.receiver];
const candidates = receiverClass ? methodsByQualifiedName.get(`${receiverClass}.${site.method}`) ?? [] : [];
if (candidates.length === 1) {
addEdge(graph, {
from: symbolId,
to: candidates[0],
kind: "calls",
metadata: {
unresolvedName: callText,
receiver: site.receiver,
receiverType: receiverClass
},
resolution: "receiver-type"
});
continue;
}
if (candidates.length > 1) {
const qualifiedPlaceholderId = `symbol-qualified-name:${receiverClass}.${site.method}`;
if (!graph.nodes.has(qualifiedPlaceholderId)) {
addNode(graph, {
id: qualifiedPlaceholderId,
kind: "symbol",
language: "jsts",
symbolName: site.method,
qualifiedName: `${receiverClass}.${site.method}`,
metadata: {
unresolvedName: callText,
ambiguousCandidates: candidates.length
}
});
}
addEdge(graph, {
from: symbolId,
to: qualifiedPlaceholderId,
kind: "calls",
metadata: {
unresolvedName: callText,
receiver: site.receiver,
receiverType: receiverClass
},
resolution: "name-only"
});
continue;
}
const externalId = `external:${callText}`;
if (!graph.nodes.has(externalId)) {
addNode(graph, {
id: externalId,
kind: "external",
language: "jsts",
metadata: { unresolvedName: callText }
});
}
addEdge(graph, {
from: symbolId,
to: externalId,
kind: "calls",
metadata: { unresolvedName: callText }
});
}
for (const callee of fn.outgoingCalls) {
if (memberCallTexts.has(callee))
continue;
const targetId = callee.includes(".") ? `external:${callee}` : `symbol-name:${callee}`;
if (!graph.nodes.has(targetId)) {
addNode(graph, {
id: targetId,
kind: callee.includes(".") ? "external" : "symbol",
language: "jsts",
symbolName: callee.includes(".") ? void 0 : callee,
metadata: { unresolvedName: callee }
});
}
const importHintFile = !callee.includes(".") ? importedNameToFile.get(callee) : void 0;
addEdge(graph, {
from: symbolId,
to: targetId,
kind: "calls",
metadata: {
unresolvedName: callee,
...importHintFile ? { importHintFile } : {}
},
// A definite external call (`callee.includes(".")`) is never
// ambiguous — no in-project candidate to collide with, so no
// resolution marker. An in-project bare-name callee starts
// "name-only" and resolveDeferredSymbolEdges below may upgrade it
// to "import" (when importHintFile narrows it) or "exact" once
// every file has been added.
...callee.includes(".") ? {} : { resolution: "name-only" }
});
}
}
}
function mapKindToTreeSitterLanguage(kind, filePath) {
return symbolExtractionGrammar(kind, filePath);
}
async function getExtractor(languageId) {
if (extractorCache.has(languageId))
return extractorCache.get(languageId);
const client = getSharedTreeSitterClient();
if (!client)
return null;
const extractor = new TreeSitterSymbolExtractor(languageId, client);
const ok = await extractor.init();
if (!ok) {
extractorCache.set(languageId, null);
return null;
}
extractorCache.set(languageId, extractor);
return extractor;
}
async function extractTreeSitterSymbols(filePath, languageId, contentOverride) {
const empty = {
symbols: [],
refs: [],
imports: [],
coverage: {
definitions: "unavailable",
references: "unavailable",
imports: "unavailable"
}
};
if (contentOverride === null)
return empty;
const treeSitterClient = getSharedTreeSitterClient();
if (!treeSitterClient)
return empty;
const initialized = await treeSitterClient.init();
if (!initialized)
return empty;
const extractor = await getExtractor(languageId);
if (!extractor)
return empty;
const content = contentOverride ?? fs2.readFileSync(filePath, "utf-8");
const extracted = await treeSitterClient.withParsedTree(filePath, languageId, content, (tree) => extractor.extract(tree, filePath, content), "review-graph");
return extracted.parsed ? extracted.value : { ...empty, wasmTrap: extracted.wasmTrap };
}
async function captureReviewGraphStructuralIr(filePath, cwd, content, borrowed) {
const kind = detectFileKind(filePath);
if (!kind || !MAIN_KINDS.has(kind) || detectFileRole(filePath) === "test") {
return { complete: true };
}
if (kind === "jsts") {
const borrowable = borrowed.hasFileFact(filePath, "file.imports") && borrowed.hasFileFact(filePath, "file.reexports") && borrowed.hasFileFact(filePath, "file.functionSummaries") && borrowed.getFileFact(filePath, "file.content") === content;
let facts = borrowed;
if (!borrowable) {
const run = new FactStore("review-graph-run");
await ensureReviewGraphFacts(filePath, cwd, run, content);
facts = run;
}
const parsed = await withTreeSitterRoot(filePath, content, () => true, "review-graph-ir");
if (!parsed.parsed)
return { complete: false };
const functionCoverage = facts.getFileFact(filePath, "file.functionFactsCoverage") ?? "unavailable";
const importCoverage = facts.getFileFact(filePath, "file.importFactsCoverage") ?? "unavailable";
const coverage = {
definitions: functionCoverage,
references: functionCoverage,
imports: importCoverage,
calls: functionCoverage
};
if (Object.values(coverage).some((status) => status !== "complete")) {
return { complete: false };
}
return {
complete: true,
structural: {
kind: "jsts",
imports: facts.getFileFact(filePath, "file.imports") ?? [],
reexports: facts.getFileFact(filePath, "file.reexports") ?? [],
functionSummaries: facts.getFileFact(filePath, "file.functionSummaries") ?? [],
coverage
}
};
}
const languageId = mapKindToTreeSitterLanguage(kind, filePath);
if (!languageId)
return { complete: false };
const client = getSharedTreeSitterClient();
if (!client || !await client.init())
return { complete: false };
const extractor = await getExtractor(languageId);
if (!extractor)
return { complete: false };
const result = await client.withParsedTree(filePath, languageId, content, (tree) => extractor.extract(tree, filePath, content), "review-graph");
if (!result.parsed)
return { complete: false };
return {
complete: true,
structural: {
kind: "tree-sitter",
languageId,
extracted: result.value
}
};
}
var SYMBOL_KIND_SPECIFICITY = {
method: 2,
property: 2
};
function dedupeSamePositionSymbols(symbols) {
const bestByKey = /* @__PURE__ */ new Map();
for (const symbol of symbols) {
const key = `${symbol.name}0000${symbol.line}0000${symbol.column}`;
const existing = bestByKey.get(key);
if (!existing) {
bestByKey.set(key, symbol);
continue;
}
const existingScore = SYMBOL_KIND_SPECIFICITY[existing.kind] ?? 0;
const candidateScore = SYMBOL_KIND_SPECIFICITY[symbol.kind] ?? 0;
if (candidateScore > existingScore)
bestByKey.set(key, symbol);
}
return [...bestByKey.values()];
}
function addTreeSitterFile(graph, cwd, filePath, languageId, extracted, ignoredIds) {
const normalized = normalizeMapKey(filePath);
const hintPath = toProjectRelativePath(normalized, cwd);
const fileNodeId = `file:${normalized}`;
addNode(graph, {
id: fileNodeId,
kind: "file",
language: languageId,
filePath: normalized,
metadata: {
...featureHintMetadata(hintPath),
extractionCoverage: extracted.coverage
}
});
const dedupedSymbols = dedupeSamePositionSymbols(extracted.symbols);
const containers = dedupedSymbols.map((s) => ({
name: s.name,
startLine: s.line,
endLine: s.endLine ?? s.line
}));
for (const symbol of dedupedSymbols) {
const symbolId = buildSymbolId(normalized, symbol.name, symbol.kind, symbol.line);
const owner = findOwnerName(containers, symbol.line, symbol.endLine ?? symbol.line);
const qualifiedName = buildQualifiedName(owner, symbol.name);
addNode(graph, {
id: symbolId,
kind: "symbol",
language: languageId,
filePath: normalized,
symbolName: symbol.name,
symbolKind: symbol.kind,
...qualifiedName ? { qualifiedName } : {},
exported: symbol.isExported,
metadata: {
line: symbol.line,
endLine: symbol.endLine,
column: symbol.column,
signature: symbol.signature,
...featureHintMetadata(`${symbol.name} ${hintPath}`)
}
});
addEdge(graph, { from: fileNodeId, to: symbolId, kind: "contains" });
addEdge(graph, { from: fileNodeId, to: symbolId, kind: "defines" });
}
for (const ref of extracted.refs) {
const refName = ref.symbolName ?? ref.symbolId;
const targetId = `symbol-name:${refName}`;
if (!graph.nodes.has(targetId)) {
addNode(graph, {
id: targetId,
kind: "symbol",
language: languageId,
symbolName: refName,
metadata: { unresolvedName: refName }
});
}
addEdge(graph, {
from: fileNodeId,
to: targetId,
kind: "references",
metadata: {
line: ref.line,
column: ref.column,
referenceKind: ref.referenceKind ?? "unknown"
},
// Always starts bare-name-only (the extractor has no scope/type info);
// resolveDeferredSymbolEdges may upgrade this to "exact" below.
resolution: "name-only"
});
}
for (const imp of extracted.imports) {
const resolved = resolveImportToFiles(cwd, filePath, languageId, imp.source).filter((target) => !ignoredIds?.has(target));
if (resolved.length > 0) {
for (const target of resolved) {
const toNode = ensureFileNode(graph, target, cwd, mapKindToTreeSitterLanguage(detectFileKind(target), target) ?? languageId);
addEdge(graph, {
from: fileNodeId,
to: toNode,
kind: "imports",
metadata: { line: imp.line, source: imp.source }
});
}
continue;
}
const isRelative = imp.source.startsWith(".");
const targetId = `${isRelative ? "module" : "external"}:${imp.source}`;
if (!graph.nodes.has(targetId)) {
addNode(graph, {
id: targetId,
kind: isRelative ? "module" : "external",
language: languageId,
metadata: { source: imp.source }
});
}
addEdge(graph, {
from: fileNodeId,
to: targetId,
kind: "imports",
metadata: { line: imp.line }
});
}
}
function addLspFallbackSymbols(graph, filePath, languageId, symbols) {
const normalized = normalizeMapKey(filePath);
const fileNodeId = `file:${normalized}`;
let added = 0;
const flatOwnerIds = /* @__PURE__ */ new Map();
for (const symbol of symbols) {
const range = symbol.range ?? symbol.location?.range;
if (!range)
continue;
const kind = lspSymbolKindName(symbol.kind);
const id = buildSymbolId(normalized, symbol.name, kind, range.start.line + 1);
flatOwnerIds.set(symbol.name, id);
if (symbol.containerName) {
flatOwnerIds.set(`${symbol.containerName}.${symbol.name}`, id);
}
}
const visit = (items, parentId, ancestry) => {
for (const symbol of items) {
const range = symbol.range ?? symbol.location?.range;
if (!range)
continue;
const line = range.start.line + 1;
const kind = lspSymbolKindName(symbol.kind);
const symbolId = buildSymbolId(normalized, symbol.name, kind, line);
const owners = ancestry.length > 0 ? ancestry : containerNameChain(symbol, symbols);
const qualifiedName = owners.length > 0 ? [...owners, symbol.name].join(".") : void 0;
addNode(graph, {
id: symbolId,
kind: "symbol",
language: languageId,
filePath: normalized,
symbolName: symbol.name,
symbolKind: kind,
...qualifiedName ? { qualifiedName } : {},
provenance: "lsp",
metadata: {
line,
column: range.start.character,
endLine: range.end.line + 1,
...featureHintMetadata(`${symbol.name} ${normalized}`)
}
});
const resolvedParentId = ancestry.length === 0 && symbol.containerName ? flatOwnerIds.get(symbol.containerName) ?? parentId : parentId;
addEdge(graph, {
from: resolvedParentId,
to: symbolId,
kind: "contains"
});
addEdge(graph, { from: fileNodeId, to: symbolId, kind: "defines" });
added++;
if (symbol.children) {
visit(symbol.children, symbolId, [...owners, symbol.name]);
}
}
};
visit(symbols, fileNodeId, []);
return added;
}
function ensureFileNode(graph, filePath, cwd, languageId) {
const normalized = normalizeMapKey(filePath);
const hintPath = toProjectRelativePath(normalized, cwd);
const existing = graph.fileNodes.get(normalized);
if (existing)
return existing;
const fileNodeId = `file:${normalized}`;
addNode(graph, {
id: fileNodeId,
kind: "file",
language: languageId,
filePath: normalized,
metadata: featureHintMetadata(hintPath)
});
return fileNodeId;
}
function resolveCxxInclude(cwd, filePath, source) {
const candidates = [
path2.resolve(path2.dirname(filePath), source),
path2.resolve(cwd, source),
path2.resolve(cwd, "include", source),
path2.resolve(cwd, "src", source)
];
const root = path2.resolve(cwd);
for (const candidate of candidates) {
if (!candidate.startsWith(root + path2.sep) && candidate !== root)
continue;
if (fs2.existsSync(candidate) && detectFileKind(candidate) === "cxx") {
return normalizeMapKey(candidate);
}
}
return void 0;
}
function parseLocalCxxInclude(line) {
let i = 0;
while (i < line.length && (line[i] === " " || line[i] === " "))
i += 1;
if (line[i] !== "#")
return void 0;
i += 1;
while (i < line.length && (line[i] === " " || line[i] === " "))
i += 1;
if (!line.startsWith("include", i))
return void 0;
i += "include".length;
if (i >= line.length || line[i] !== " " && line[i] !== " ") {
return void 0;
}
while (i < line.length && (line[i] === " " || line[i] === " "))
i += 1;
if (line[i] !== '"')
return void 0;
i += 1;
const start = i;
while (i < line.length && line[i] !== '"')
i += 1;
if (i >= line.length || i === start)
return void 0;
return line.slice(start, i);
}
function addCxxIncludeEdges(graph, cwd, filePath, ignoredIds, contentOverride) {
let content = contentOverride;
if (content === void 0) {
try {
content = fs2.readFileSync(filePath, "utf-8");
} catch {
return;
}
}
if (content === null)
return;
const fromNode = ensureFileNode(graph, filePath, cwd, "cpp");
for (const line of content.split(/\r?\n/)) {
const source = parseLocalCxxInclude(line);
if (!source)
continue;
const target = resolveCxxInclude(cwd, filePath, source);
if (!target || ignoredIds?.has(target))
continue;
const languageId = mapKindToTreeSitterLanguage("cxx", target) ?? "cpp";
const toNode = ensureFileNode(graph, target, cwd, languageId);
addEdge(graph, {
from: fromNode,
to: toNode,
kind: "imports",
metadata: { source }
});
}
}
function removeFileOwnedGraphData(graph, filePath, removedEdges) {
const normalized = normalizeMapKey(filePath);
const fileNodeId = graph.fileNodes.get(normalized) ?? `file:${normalized}`;
const removedSymbolIds = new Set(graph.symbolNodesByFile.get(normalized));
const removedIds = new Set(removedSymbolIds);
if (graph.nodes.has(fileNodeId))
removedIds.add(fileNodeId);
const candidates = /* @__PURE__ */ new Set();
for (const id of removedIds) {
for (const edge of graph.edgesByFrom.get(id) ?? []) {
_rebuildCounters.removeOwnedEdgeVisits++;
candidates.add(edge);
}
for (const edge of graph.edgesByTo.get(id) ?? []) {
_rebuildCounters.removeOwnedEdgeVisits++;
candidates.add(edge);
}
}
const preservedIncomingSymbolEdges = [];
for (const edge of candidates) {
if (removedEdges.has(edge))
continue;
const fromRemoved = removedIds.has(edge.from);
if (!fromRemoved && edge.to === fileNodeId)
continue;
if (!fromRemoved && removedSymbolIds.has(edge.to)) {
preservedIncomingSymbolEdges.push({ ...edge });
}
removedEdges.add(edge);
}
for (const id of removedIds) {
const node = graph.nodes.get(id);
graph.nodes.delete(id);
if (!node?.filePath)
continue;
if (node.kind === "file") {
if (graph.fileNodes.get(node.filePath) === id) {
graph.fileNodes.delete(node.filePath);
}
} else if (node.kind === "symbol") {
const ids = graph.symbolNodesByFile.get(node.filePath);
if (!ids)
continue;
const at = ids.indexOf(id);
if (at >= 0)
ids.splice(at, 1);
if (ids.length === 0)
graph.symbolNodesByFile.delete(node.filePath);
}
}
return preservedIncomingSymbolEdges;
}
async function addFileToGraph(graph, cwd, file, ignoredIds, contentOverride) {
const kind = detectFileKind(file);
if (!kind || !MAIN_KINDS.has(kind))
return;
if (detectFileRole(file) === "test")
return;
const contentHash = typeof contentOverride === "string" ? reviewGraphIrContentHash(contentOverride) : void 0;
const sharedIr = contentHash ? getFreshReviewGraphFileIr(cwd, file, contentHash)?.structural : void 0;
if (kind === "jsts") {
const run = new FactStore("review-graph-run");
if (sharedIr?.kind === "jsts") {
run.setFileFact(file, "file.content", contentOverride ?? "");
run.setFileFact(file, "file.imports", sharedIr.imports);
run.setFileFact(file, "file.functionSummaries", sharedIr.functionSummaries);
run.setFileFact(file, "file.functionFactsCoverage", sharedIr.coverage.calls);
run.setFileFact(file, "file.importFactsCoverage", sharedIr.coverage.imports);
} else {
await ensureReviewGraphFacts(file, cwd, run, contentOverride);
}
addJsTsFile(graph, cwd, file, run, ignoredIds);
return;
}
const languageId = mapKindToTreeSitterLanguage(kind, file);
if (!languageId)
return;
const irExtracted = sharedIr?.kind === "tree-sitter" && sharedIr.languageId === languageId ? sharedIr.extracted : void 0;
const extracted = irExtracted ?? await extractTreeSitterSymbols(file, languageId, contentOverride);
if (extracted.wasmTrap)
_wasmTrappedFiles.set(file, extracted.wasmTrap);
else
_wasmTrappedFiles.delete(file);
addTreeSitterFile(graph, cwd, file, languageId, extracted, ignoredIds);
if (extracted.symbols.length === 0) {
const lspSymbols = await getOpenDocumentSymbols(file);
const added = lspSymbols ? addLspFallbackSymbols(graph, file, languageId, lspSymbols) : 0;
logReviewGraph({
phase: "lsp_symbol_fallback",
cwd,
reason: lspSymbols ? added > 0 ? "added" : "empty-response" : "unavailable-or-failed",
nodes: added
});
}
if (kind === "cxx") {
addCxxIncludeEdges(graph, cwd, file, ignoredIds, contentOverride);
}
}
var _rebuildCounters = {
restoreComparisons: 0,
importTargetEdgeScans: 0,
removeOwnedEdgeVisits: 0,
removeOwnedEdgePositions: 0
};
function _getReviewGraphRebuildCountersForTests() {
return { ..._rebuildCounters };
}
function _resetReviewGraphRebuildCountersForTests() {
_rebuildCounters.restoreComparisons = 0;
_rebuildCounters.importTargetEdgeScans = 0;
_rebuildCounters.removeOwnedEdgeVisits = 0;
_rebuildCounters.removeOwnedEdgePositions = 0;
}
function edgeIdentityKey(edge) {
_rebuildCounters.restoreComparisons++;
return JSON.stringify([edge.from, edge.kind, edge.metadata ?? {}]);
}
function restoreValidIncomingEdges(graph, edges) {
const seenByTarget = /* @__PURE__ */ new Map();
for (const edge of edges) {
if (!graph.nodes.has(edge.from) || !graph.nodes.has(edge.to))
continue;
let seen = seenByTarget.get(edge.to);
if (!seen) {
seen = /* @__PURE__ */ new Set();
for (const candidate of graph.edgesByTo.get(edge.to) ?? []) {
seen.add(edgeIdentityKey(candidate));
}
seenByTarget.set(edge.to, seen);
}
const key = edgeIdentityKey(edge);
if (seen.has(key))
continue;
seen.add(key);
addEdge(graph, edge);
}
}
function dedupeResolvedEdges(graph, replacements) {
const resolvedEdges = new Set(replacements.map(([, after]) => after));
const seenByTarget = /* @__PURE__ */ new Map();
const removed = /* @__PURE__ */ new Set();
for (const [, edge] of replacements) {
if (removed.has(edge))
continue;
let seen = seenByTarget.get(edge.to);
if (!seen) {
seen = /* @__PURE__ */ new Set();
for (const candidate of graph.edgesByTo.get(edge.to) ?? []) {
if (!resolvedEdges.has(candidate)) {
seen.add(edgeIdentityKey(candidate));
}
}
seenByTarget.set(edge.to, seen);
}
const key = edgeIdentityKey(edge);
if (seen.has(key)) {
removed.add(edge);
unindexEdge(graph, edge);
continue;
}
seen.add(key);
}
if (removed.size > 0) {
graph.edges = graph.edges.filter((edge) => !removed.has(edge));
}
}
var _graphImportChanges = /* @__PURE__ */ new WeakMap();
function getGraphImportChanges(graph) {
return _graphImportChanges.get(graph);
}
function importTargetsForFile(graph, filePath) {
const normalized = normalizeMapKey(filePath);
const fileNodeId = graph.fileNodes.get(normalized) ?? `file:${normalized}`;
const targets = /* @__PURE__ */ new Set();
for (const edge of graph.edgesByFrom.get(fileNodeId) ?? []) {
_rebuildCounters.importTargetEdgeScans++;
if (edge.kind !== "imports")
continue;
const target = graph.nodes.get(edge.to)?.filePath;
if (target)
targets.add(normalizeMapKey(target));
}
return [...targets].sort((a, b) => compareOrdinal(a, b));
}
async function updateGraphFiles(graph, cwd, files, facts, ignoredIds) {
rebuildIndexes(graph);
const prior = files.map((file) => ({
filePath: normalizeMapKey(file),
existedBefore: graph.fileNodes.has(normalizeMapKey(file)),
priorTargets: importTargetsForFile(graph, file)
}));
const preservedIncoming = [];
const removedEdges = /* @__PURE__ */ new Set();
for (const file of files) {
preservedIncoming.push(...removeFileOwnedGraphData(graph, file, removedEdges));
await addFileToGraph(graph, cwd, file, ignoredIds);
}
if (removedEdges.size > 0) {
unindexEdges(graph, removedEdges);
graph.edges = graph.edges.filter((edge) => !removedEdges.has(edge));
}
restoreValidIncomingEdges(graph, preservedIncoming);
resolveDeferredSymbolEdges(graph, false);
graph.changedSymbolsByFile.clear();
for (const file of files) {
upsertChangedSymbols(graph, facts, file);
}
return prior.map(({ filePath, existedBefore, priorTargets }) => ({
filePath,
existedBefore,
existsAfter: graph.fileNodes.has(filePath),
priorTargets,
newTargets: importTargetsForFile(graph, filePath)
}));
}
function resolveDeferredSymbolEdges(graph, rebuild = true) {
const symbolNameToIds = /* @__PURE__ */ new Map();
for (const node of graph.nodes.values()) {
if (node.kind !== "symbol" || !node.symbolName)
continue;
if (node.metadata?.unresolvedName)
continue;
const ids = symbolNameToIds.get(node.symbolName) ?? [];
ids.push(node.id);
symbolNameToIds.set(node.symbolName, ids);
}
const replacements = [];
graph.edges = graph.edges.map((edge) => {
const targetNode = graph.nodes.get(edge.to);
if (!targetNode?.metadata?.unresolvedName)
return edge;
const candidates = symbolNameToIds.get(targetNode.symbolName ?? "") ?? [];
const importHintFile = edge.metadata?.importHintFile;
if (importHintFile) {
const scoped = candidates.filter((id) => graph.nodes.get(id)?.filePath === importHintFile);
if (scoped.length === 1) {
const resolved = {
...edge,
to: scoped[0],
resolution: "import"
};
replacements.push([edge, resolved]);
return resolved;
}
}
if (candidates.length === 1) {
const resolved = {
...edge,
to: candidates[0],
resolution: "exact"
};
replacements.push([edge, resolved]);
return resolved;
}
return edge;
});
if (rebuild) {
rebuildIndexes(graph);
return;
}
if (replacements.length === 0)
return;
const affectedTargets = /* @__PURE__ */ new Set();
for (const [before, after] of replacements) {
const fromBucket = graph.edgesByFrom.get(before.from);
if (fromBucket) {
const at = fromBucket.indexOf(before);
if (at >= 0)
fromBucket[at] = after;
}
affectedTargets.add(before.to);
affectedTargets.add(after.to);
}
for (const target of affectedTargets)
graph.edgesByTo.set(target, []);
for (const edge of graph.edges) {
if (affectedTargets.has(edge.to))
graph.edgesByTo.get(edge.to)?.push(edge);
}
for (const target of affectedTargets) {
if (graph.edgesByTo.get(target)?.length === 0) {
graph.edgesByTo.delete(target);
}
}
dedupeResolvedEdges(graph, replacements);
}
function verifiedCacheFields(seqAtBuildStart) {
return {
builtAtProjectSeq: seqAtBuildStart,
lastFullVerifyMs: Date.now(),
fastPathSinceVerify: 0
};
}
async function tryIncrementalFromCache(cached, ctx) {
if (cached.fileSignatures.size === 0)
return void 0;
const { added, removed, changed } = diffSignatureMaps(cached.fileSignatures, ctx.fileSignatures);
if (removed.length > 0)
return void 0;
if (added.length === 0 && changed.length === 0)
return void 0;
const { trulyChanged, hashes } = await confirmContentChanged(changed, cached.fileHashes);
const filesToUpdate = [...added, ...trulyChanged];
if (filesToUpdate.length === 0) {
const generation2 = cached.buildGeneration ?? ++_graphGenerationCounter;
const graph2 = cloneGraph(cached.graph);
rebuildIndexes(graph2);
graph2.changedSymbolsByFile.clear();
for (const file of ctx.normalizedChanged) {
upsertChangedSymbols(graph2, ctx.facts, file);
}
setWorkspaceGraph(ctx.normalizedCwd, {
signature: ctx.signature,
fileSignatures: new Map(ctx.fileSignatures),
fileHashes: hashes,
graph: cloneGraph(cached.graph),
buildGeneration: generation2,
...verifiedCacheFields(ctx.seqAtBuildStart)
}, ctx.cacheEpoch);
setGraphBuildInfo(graph2, {
reused: true,
mode: "cached",
graphChanged: false
});
graph2.buildGeneration = generation2;
setSessionReviewGraphFact(ctx.cwd, ctx.facts, graph2);
return graph2;
}
for (const file of added) {
hashes.set(file, contentHashEntry(file));
}
const priorGeneration = cached.graph.buildGeneration;
const graph = cloneGraph(cached.graph);
const importChanges = await updateGraphFiles(graph, ctx.cwd, filesToUpdate, ctx.facts, ctx.ignoredIds);
refreshGraphBuiltAt(graph);
const generation = ++_graphGenerationCounter;
graph.buildGeneration = generation;
setWorkspaceGraph(ctx.normalizedCwd, {
signature: ctx.signature,
fileSignatures: new Map(ctx.fileSignatures),
fileHashes: hashes,
graph,
buildGeneration: generation,
...verifiedCacheFields(ctx.seqAtBuildStart)
}, ctx.cacheEpoch);
const persistReason = persistGraph(ctx.cwd, ctx.signature, ctx.fileSignatures, hashes, graph, {
buildId: ctx.buildId,
projectSeq: ctx.seqAtBuildStart,
seqHint: ctx.seqHint,
mode: ctx.mode,
resolveIdentity: ctx.resolveIdentity
});
setGraphBuildInfo(graph, {
reused: true,
mode: "incremental",
...persistReason ? { persistReason } : {},
graphChanged: true
});
_graphImportChanges.set(graph, {
fromGeneration: priorGeneration,
changes: importChanges
});
setSessionReviewGraphFact(ctx.cwd, ctx.facts, graph);
return graph;
}
function hasGraphKindExtension(file) {
const kind = detectFileKind(file);
return !!kind && MAIN_KINDS.has(kind) && detectFileRole(file) !== "test";
}
async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, ignoredIds, cacheEpoch, resolveIdentity) {
const cached = _workspaceGraphCache.get(normalizedCwd);
const cachedCoverage = cached?.graph.persistCoverage;
if (cachedCoverage?.partial && cachedCoverage.capTrimmed !== true) {
return { fallback: "partial-base" };
}
if (!cached || cached.builtAtProjectSeq === void 0) {
return { fallback: "no-seq" };
}
const now = Date.now();
const ageMs = cached.lastFullVerifyMs === void 0 ? Number.POSITIVE_INFINITY : now - cached.lastFullVerifyMs;
const sinceVerify = cached.fastPathSinceVerify ?? 0;
if (ageMs > SEQ_FASTPATH_REVERIFY_MS || sinceVerify >= SEQ_FASTPATH_REVERIFY_EVERY) {
return { fallback: "verify-due" };
}
const changedSet = new Set(seqHint.getFilesChangedSince(cached.builtAtProjectSeq).map((file) => normalizeMapKey(file)));
for (const file of normalizedChanged)
changedSet.add(file);
for (const file of _wasmTrappedFiles.keys()) {
if (cached.fileSignatures.has(file))
changedSet.add(file);
}
const changed = [...changedSet];
if (changed.length > SEQ_FASTPATH_MAX_CHANGES) {
return { fallback: "too-many-changes" };
}
const candidateFiles = [];
for (const file of changed) {
const known = cached.fileSignatures.has(file);
let existsOnDisk = false;
try {
existsOnDisk = fs2.statSync(file).isFile();
} catch {
existsOnDisk = false;
}
if (!existsOnDisk) {
if (known) {
return { fallback: "removed-file" };
}
continue;
}
if (!known) {
if (!hasGraphKindExtension(file))
continue;
}
candidateFiles.push(file);
}
const candidateStats = candidateFiles.map((file) => [file, sourceSignatureEntry(file)]);
const { trulyChanged, hashes } = await confirmContentChanged(candidateFiles, cached.fileHashes);
if (trulyChanged.length === 0) {
const graph2 = cloneGraph(cached.graph);
rebuildIndexes(graph2);
graph2.changedSymbolsByFile.clear();
for (const file of normalizedChanged) {
upsertChangedSymbols(graph2, facts, file);
}
const nextSignatures2 = new Map(cached.fileSignatures);
for (const [file, stat] of candidateStats) {
nextSignatures2.set(file, stat);
}
cached.signature = sourceSignatureFromMap(nextSignatures2);
cached.fileSignatures = nextSignatures2;
cached.fileHashes = hashes;
cached.builtAtProjectSeq = seqAtBuildStart;
cached.fastPathSinceVerify = sinceVerify + 1;
const generation2 = cached.buildGeneration ??= ++_graphGenerationCounter;
setGraphBuildInfo(graph2, {
reused: true,
mode: "seq-fastpath",
graphChanged: false
});
graph2.buildGeneration = generation2;
setSessionReviewGraphFact(cwd, facts, graph2);
return { graph: graph2 };
}
const filesToUpdate = trulyChanged;
const priorGeneration = cached.graph.buildGeneration;
const graph = cloneGraph(cached.graph);
let importChanges;
try {
importChanges = await updateGraphFiles(graph, cwd, filesToUpdate, facts, ignoredIds);
} catch {
return { fallback: "stat-error" };
}
refreshGraphBuiltAt(graph);
const nextSignatures = new Map(cached.fileSignatures);
for (const [file, stat] of candidateStats) {
nextSignatures.set(file, stat);
}
const nextSignature = sourceSignatureFromMap(nextSignatures);
const generation = ++_graphGenerationCounter;
graph.buildGeneration = generation;
setWorkspaceGraph(normalizedCwd, {
signature: nextSignature,
fileSignatures: nextSignatures,
fileHashes: hashes,
graph,
buildGeneration: generation,
// Build-start seq, not stamp-time: see verifiedCacheFields — a bump that
// interleaved during updateGraphFiles' awaits must be re-diffed next build.
builtAtProjectSeq: seqAtBuildStart,
lastFullVerifyMs: cached.lastFullVerifyMs,
fastPathSinceVerify: sinceVerify + 1
}, cacheEpoch);
const persistReason = persistGraph(cwd, nextSignature, nextSignatures, hashes, graph, {
buildId,
projectSeq: seqAtBuildStart,
seqHint: true,
mode: "seq-fastpath",
resolveIdentity
});
setGraphBuildInfo(graph, {
reused: true,
mode: "seq-fastpath",
...persistReason ? { persistReason } : {},
graphChanged: true
});
_graphImportChanges.set(graph, {
fromGeneration: priorGeneration,
changes: importChanges
});
setSessionReviewGraphFact(cwd, facts, graph);
return { graph };
}
async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
const resolveIdentity = createGitIdentityResolver();
const normalizedCwd = normalizeMapKey(cwd);
const cacheEpoch = workspaceCacheEpoch(normalizedCwd);
if (_reviewGraphBuildGateForTests)
await _reviewGraphBuildGateForTests();
const normalizedChanged = changedFiles.map((file) => normalizeMapKey(file));
const normalizedChangedSet = new Set(normalizedChanged);
logCwdWorktreeMismatchOnce(cwd, resolveIdentity);
if (isAtOrAboveHomeDir(path2.resolve(cwd))) {
const graph2 = createEmptyGraph();
for (const file of normalizedChanged) {
upsertChangedSymbols(graph2, facts, file);
}
setGraphBuildInfo(graph2, {
reused: false,
mode: "skipped",
skipReason: "unsafe_root",
// #459: never persisted/reused, same as the too_many_files skip below —
// treat as changed so dependents never trust stale derived state.
graphChanged: true
});
setSessionReviewGraphFact(cwd, facts, graph2);
return graph2;
}
const seqAtBuildStart = seqHint?.projectSeq();
const ignoredIdsPromise = collectUntrackedIgnoredIds(cwd);
let seqFastpathFallback;
if (seqHint && seqAtBuildStart !== void 0 && seqFastpathEnabled()) {
const fast = await trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, await ignoredIdsPromise, cacheEpoch, resolveIdentity);
if ("graph" in fast)
return fast.graph;
seqFastpathFallback = fast.fallback;
}
const sourceCollection = await getGraphSourceFiles(cwd);
const filesToBuild = sourceCollection.files;
const sourceFileCount = sourceCollection.sourceFileCount;
const pathNormalizeCalls = sourceCollection.pathNormalizeCalls;
const sourceFilesTruncated = sourceCollection.entryBudgetExceeded;
const ignoredIds = await ignoredIdsPromise;
const maxGraphFiles = sourceCollection.maxFileCount;
if (filesToBuild.length > maxGraphFiles) {
const graph2 = createEmptyGraph();
graph2.version = REVIEW_GRAPH_VERSION;
graph2.builtAt = (/* @__PURE__ */ new Date()).toISOString();
for (const file of normalizedChanged) {
upsertChangedSymbols(graph2, facts, file);
}
recordReviewGraphSizeSkip(cwd, sourceFileCount, maxGraphFiles);
logLatency({
type: "phase",
phase: "review_graph_size_skip",
filePath: cwd,
durationMs: 0,
metadata: {
cwd,
sourceFileCount,
maxFileCount: maxGraphFiles,
sourceFileCountLabel: `more than ${maxGraphFiles} files`,
sourceFileCountTruncated: true
}
});
if (isReviewGraphSizeNearMiss(sourceFileCount, maxGraphFiles)) {
logLatency({
type: "phase",
phase: "review_graph_size_near_miss",
filePath: cwd,
durationMs: 0,
metadata: {
cwd,
maxFileCount: maxGraphFiles,
sourceFileCount,
sourceFileCountLabel: `more than ${maxGraphFiles} files`,
sourceFileCountTruncated: true
}
});
}
setGraphBuildInfo(graph2, {
reused: false,
mode: "skipped",
skipReason: "too_many_files",
sourceFileCount,
sourceFileCountTruncated: sourceFilesTruncated || filesToBuild.length > maxGraphFiles,
maxFileCount: maxGraphFiles,
pathNormalizeCalls,
seqFastpathFallback,
// #459: a fresh empty graph is returned every call on this path (never
// persisted/reused) — treat it as changed so dependents never trust stale
// derived state across skip/unskip transitions. Deliberately NOT stamped
// with a buildGeneration: absent ⇒ derived caches rebuild every time.
graphChanged: true
});
setSessionReviewGraphFact(cwd, facts, graph2);
return graph2;
}
if (!sourceFilesTruncated)
clearReviewGraphSizeSkip(cwd);
const fileSignatures = await sourceSignatureMapAsync(filesToBuild);
const signature = sourceSignatureFromMap(fileSignatures);
let memCached = sourceFilesTruncated ? void 0 : _workspaceGraphCache.get(normalizedCwd);
const memCoverage = memCached?.graph.persistCoverage;
if (memCoverage?.partial && memCoverage.capTrimmed !== true) {
memCached = void 0;
}
if (memCached?.signature === signature) {
touchWorkspaceGraph(normalizedCwd);
const graph2 = cloneGraph(memCached.graph);
rebuildIndexes(graph2);
graph2.changedSymbolsByFile.clear();
for (const file of normalizedChanged) {
upsertChangedSymbols(graph2, facts, file);
}
Object.assign(memCached, verifiedCacheFields(seqAtBuildStart));
const generation2 = memCached.buildGeneration ??= ++_graphGenerationCounter;
setGraphBuildInfo(graph2, {
reused: true,
mode: "cached",
seqFastpathFallback,
graphChanged: false
});
graph2.buildGeneration = generation2;
setSessionReviewGraphFact(cwd, facts, graph2);
return graph2;
}
if (memCached) {
const incremental = await tryIncrementalFromCache(memCached, {
cwd,
buildId,
seqHint: seqHint !== void 0,
mode: "incremental",
normalizedCwd,
normalizedChanged,
fileSignatures,
signature,
facts,
seqAtBuildStart,
ignoredIds,
cacheEpoch,
resolveIdentity
});
if (incremental) {
updateGraphBuildInfo(incremental, { seqFastpathFallback });
return incremental;
}
}
const diskCached = sourceFilesTruncated ? null : loadPersistedGraph(cwd, void 0, resolveIdentity);
if (diskCached?.signature === signature) {
const graph2 = cloneGraph(diskCached.graph);
rebuildIndexes(graph2);
graph2.changedSymbolsByFile.clear();
for (const file of normalizedChanged) {
upsertChangedSymbols(graph2, facts, file);
}
const generation2 = ++_graphGenerationCounter;
setWorkspaceGraph(normalizedCwd, {
signature,
fileSignatures: new Map(fileSignatures),
fileHashes: diskCached.fileHashes,
graph: cloneGraph(diskCached.graph),
buildGeneration: generation2,
...verifiedCacheFields(seqAtBuildStart)
}, cacheEpoch);
setGraphBuildInfo(graph2, {
reused: true,
mode: "cached",
seqFastpathFallback,
graphChanged: false
});
graph2.buildGeneration = generation2;
setSessionReviewGraphFact(cwd, facts, graph2);
return graph2;
}
if (diskCached) {
const incremental = await tryIncrementalFromCache({
signature: diskCached.signature,
fileSignatures: diskCached.fileSignatures,
fileHashes: diskCached.fileHashes,
graph: diskCached.graph
}, {
cwd,
buildId,
seqHint: seqHint !== void 0,
mode: "incremental",
normalizedCwd,
normalizedChanged,
fileSignatures,
signature,
facts,
seqAtBuildStart,
ignoredIds,
resolveIdentity
});
if (incremental) {
updateGraphBuildInfo(incremental, { seqFastpathFallback });
return incremental;
}
}
const resumed = sourceFilesTruncated ? null : await tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds, resolveIdentity);
const graph = resumed?.graph ?? createEmptyGraph();
const filesToExtract = resumed?.remaining ?? filesToBuild;
const treeSitterClient = getSharedTreeSitterClient();
treeSitterClient?.ensureTreeCacheCapacity(filesToExtract.length);
const extractionStartedAt = Date.now();
const fileHashes = resumed?.fileHashes ?? /* @__PURE__ */ new Map();
const checkpointEvery = graphCheckpointEveryFiles();
const checkpointMinIntervalMs = graphCheckpointMinIntervalMs();
const testStopAfter = Number(process.env.PI_LENS_GRAPH_CHECKPOINT_TEST_STOP_AFTER);
let filesSinceCheckpoint = 0;
let lastCheckpointMs = Date.now();
let extractedCount = 0;
const extractFiles = async () => {
for (const file of filesToExtract) {
let content;
try {
const bytes = fs2.readFileSync(file);
content = bytes.toString("utf-8");
fileHashes.set(file, createHash2("sha256").update(bytes).digest("hex"));
} catch {
content = null;
fileHashes.set(file, "missing");
}
await addFileToGraph(graph, cwd, file, ignoredIds, content);
if (normalizedChangedSet.has(file)) {
upsertChangedSymbols(graph, facts, file);
}
extractedCount++;
filesSinceCheckpoint++;
if (Number.isFinite(testStopAfter) && testStopAfter > 0 && extractedCount >= testStopAfter) {
writeReviewGraphCheckpointSync(cwd, buildReviewGraphCheckpointData(cwd, graph, fileHashes, filesToBuild.length, ignoredIds, resolveIdentity), {
nodes: graph.nodes.size,
edges: graph.edges.length,
processed: fileHashes.size,
target: filesToBuild.length
});
throw new Error("__review_graph_checkpoint_test_abort__");
}
const remainingAfter = filesToExtract.length - extractedCount;
if (filesSinceCheckpoint >= checkpointEvery && remainingAfter >= checkpointEvery && Date.now() - lastCheckpointMs >= checkpointMinIntervalMs) {
writeReviewGraphCheckpoint(cwd, graph, fileHashes, filesToBuild.length, ignoredIds, resolveIdentity);
filesSinceCheckpoint = 0;
lastCheckpointMs = Date.now();
}
}
};
const extractAndDrainIr = async () => {
try {
await extractFiles();
} finally {
clearReviewGraphFileIr(cwd);
}
};
if (treeSitterClient) {
await treeSitterClient.withParseCacheMeasurement(extractAndDrainIr, (stats) => {
logTreeSitterCacheStats({
scope: "review_graph_full",
filePath: cwd,
fileCount: filesToBuild.length,
durationMs: Date.now() - extractionStartedAt,
stats,
// #1982: this scope never runs an ast-grep pass (extraction is
// tree-sitter only), so durationMs carries none and explicit
// zeros keep every cache_stats record uniformly parseable.
astGrep: { durationMs: 0, fileCount: 0 }
});
});
} else {
await extractAndDrainIr();
}
if (resumed) {
for (const file of normalizedChangedSet) {
if (resumed.fileHashes.has(file)) {
upsertChangedSymbols(graph, facts, file);
}
}
}
resolveDeferredSymbolEdges(graph);
graph.version = REVIEW_GRAPH_VERSION;
graph.builtAt = (/* @__PURE__ */ new Date()).toISOString();
graph.persistCoverage = sourceFilesTruncated ? graphCoverage(graph, graphPersistMaxElements(), sourceFileCount, true, filesToBuild) : void 0;
deleteReviewGraphCheckpoint(cwd);
const generation = ++_graphGenerationCounter;
const retained = retainedGraph(cwd, graph);
const wasCapped = retained !== graph;
const graphSnapshot = wasCapped ? retained : cloneGraph(graph);
if (!wasCapped)
rebuildIndexes(graphSnapshot);
graphSnapshot.buildGeneration = generation;
setWorkspaceGraph(normalizedCwd, {
signature,
fileSignatures: new Map(fileSignatures),
fileHashes,
graph: graphSnapshot,
buildGeneration: generation,
...verifiedCacheFields(seqAtBuildStart)
}, cacheEpoch);
const persistReason = persistGraph(cwd, signature, fileSignatures, fileHashes, graphSnapshot, {
buildId,
projectSeq: seqAtBuildStart,
seqHint: seqHint !== void 0,
mode: "full",
sourceFileCount,
sourceFilesTruncated,
resolveIdentity
});
setGraphBuildInfo(graph, {
reused: false,
mode: "full",
sourceFileCount,
sourceFileCountTruncated: sourceFilesTruncated,
pathNormalizeCalls,
seqFastpathFallback,
...persistReason ? { persistReason } : {},
graphChanged: true
});
graph.buildGeneration = generation;
setSessionReviewGraphFact(cwd, facts, graphSnapshot);
return graph;
}
function buildCacheKey(cwd, changedFiles) {
return `${buildCacheWorkspaceKey(cwd)}|${[...changedFiles].sort((a, b) => compareOrdinal(a, b)).join(",")}`;
}
function isGraphBuildInFlight(cwd, changedFiles = []) {
return _buildCache.has(buildCacheKey(cwd, changedFiles));
}
function buildOrUpdateGraph(cwd, changedFiles, facts, seqHint) {
const cacheKey = buildCacheKey(cwd, changedFiles);
const cached = _buildCache.get(cacheKey);
if (cached)
return cached;
const startedAt = Date.now();
const buildId = ++_buildIdCounter;
recordBuildAttempt(cwd, "running", void 0, buildId);
const startedProjectSeq = seqHint?.projectSeq();
logReviewGraph({
cwd,
phase: "build_started",
observability: {
graph: {
buildId,
...startedProjectSeq === void 0 ? {} : { projectSeq: startedProjectSeq },
...seqHint === void 0 ? {} : { seqHint: true },
nodes: 0,
edges: 0
}
}
});
const promise = _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId).then((graph) => {
let buildInfo = getGraphBuildInfoForGraph(graph);
const wasmTrappedFiles = wasmTrappedFileCount(graph);
if (wasmTrappedFiles > 0) {
buildInfo = updateGraphBuildInfo(graph, { wasmTrappedFiles });
}
if (buildInfo.mode === "skipped") {
const reason = buildInfo.skipReason ?? "skipped";
recordBuildAttempt(cwd, "skipped", reason, buildId);
logReviewGraph({
cwd,
phase: "build_skipped",
reason,
durationMs: Date.now() - startedAt,
observability: {
graph: graphLogMetadata(graph, {
buildId,
projectSeq: startedProjectSeq,
seqHint: seqHint !== void 0,
mode: buildInfo.mode,
sourceFileCount: buildInfo.sourceFileCount,
sourceFileCountTruncated: buildInfo.sourceFileCountTruncated,
pathNormalizeCalls: buildInfo.pathNormalizeCalls
})
}
});
} else {
const reason = buildInfo.persistReason;
recordBuildAttempt(cwd, "succeeded", reason, buildId);
logReviewGraph({
cwd,
phase: "build_succeeded",
durationMs: Date.now() - startedAt,
nodes: graph.nodes.size,
edges: graph.edges.length,
...reason ? { reason } : {},
observability: {
graph: graphLogMetadata(graph, {
buildId,
projectSeq: startedProjectSeq,
seqHint: seqHint !== void 0,
mode: buildInfo.mode,
sourceFileCount: buildInfo.sourceFileCount,
sourceFileCountTruncated: buildInfo.sourceFileCountTruncated,
pathNormalizeCalls: buildInfo.pathNormalizeCalls
})
}
});
}
return graph;
}).catch((err) => {
const reason = err instanceof Error ? err.message : String(err);
const wasmFailure = classifyTreeSitterWasmError(err);
recordBuildAttempt(cwd, "failed", reason, buildId);
logReviewGraph({
cwd,
phase: "build_failed",
// #3605: an internal wasm failure, not a failed build.
failureClass: wasmFailure ? `wasm-${wasmFailure}` : "error",
reason,
durationMs: Date.now() - startedAt,
error: reason,
observability: {
graph: {
buildId,
...seqHint === void 0 ? {} : { seqHint: true },
nodes: 0,
edges: 0
}
}
});
throw err;
});
_buildCache.set(cacheKey, promise);
const release = () => {
if (_buildCache.get(cacheKey) === promise)
_buildCache.delete(cacheKey);
};
promise.then(release, release);
return promise;
}
function extractSymbolsAndRefsFromGraph(graph) {
const allSymbols = /* @__PURE__ */ new Map();
const allRefs = /* @__PURE__ */ new Map();
const nodeSymbols = /* @__PURE__ */ new Map();
const coverage = {
totalEvidence: 0,
callsEvidence: 0,
referencesEvidence: 0,
eligibleEvidence: 0,
resolvedEvidence: 0,
unresolvedEvidence: 0,
typeOnlyEvidence: 0,
unsupportedEvidence: 0,
sameFileEvidence: 0,
duplicateEvidence: 0,
complete: graph.persistCoverage?.partial !== true && graph.persistCoverage?.inProgress !== true,
languages: {}
};
const numberMetadata = (node, key) => {
const value = node?.metadata?.[key];
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
};
const symbolKind = (node) => {
switch (node.symbolKind) {
case "class":
case "interface":
case "type":
case "variable":
case "method":
case "property":
case "function":
return node.symbolKind;
default:
return "function";
}
};
for (const node of graph.nodes.values()) {
if (node.kind !== "symbol" || !node.filePath || !node.symbolName)
continue;
const sym = {
id: node.id,
name: node.symbolName,
kind: symbolKind(node),
filePath: node.filePath,
line: numberMetadata(node, "line") ?? 1,
endLine: numberMetadata(node, "endLine"),
column: numberMetadata(node, "column") ?? 1,
isExported: node.exported === true
};
nodeSymbols.set(node.id, sym);
const list = allSymbols.get(node.filePath) ?? [];
list.push(sym);
allSymbols.set(node.filePath, list);
}
const addRef = (ref) => {
const list = allRefs.get(ref.filePath) ?? [];
list.push(ref);
allRefs.set(ref.filePath, list);
};
for (const edge of graph.edges) {
if (edge.kind !== "calls" && edge.kind !== "references")
continue;
coverage.totalEvidence++;
if (edge.kind === "calls")
coverage.callsEvidence++;
else
coverage.referencesEvidence++;
const fromNode = graph.nodes.get(edge.from);
const targetNode = graph.nodes.get(edge.to);
const callerFile = fromNode?.kind === "symbol" ? fromNode.filePath : fromNode?.kind === "file" ? fromNode.filePath : edge.from.startsWith("file:") ? edge.from.slice("file:".length) : void 0;
if (!callerFile) {
coverage.unsupportedEvidence++;
continue;
}
const metadata = edge.metadata ?? {};
const referenceKind = edge.kind === "calls" ? "call" : metadata.referenceKind === "call" ? "call" : metadata.referenceKind === "type" ? "type" : "unknown";
const resolution = edge.resolution ?? (targetNode && !targetNode.metadata?.unresolvedName ? "exact" : "unresolved");
const targetSymbol = nodeSymbols.get(edge.to);
const targetName = targetNode?.symbolName ?? (edge.to.startsWith("symbol-name:") ? edge.to.slice("symbol-name:".length) : void 0);
const line = typeof metadata.line === "number" ? metadata.line : numberMetadata(fromNode, "line") ?? 1;
const column = typeof metadata.column === "number" ? metadata.column : numberMetadata(fromNode, "column") ?? 1;
addRef({
symbolId: targetName ? `${callerFile}:${targetName}` : edge.to,
filePath: callerFile,
line,
column,
evidenceKind: edge.kind,
referenceKind,
targetId: edge.to,
callerSymbolId: fromNode?.kind === "symbol" ? fromNode.id : void 0,
resolution,
targetName,
targetKind: targetNode?.symbolKind,
targetFilePath: targetNode?.filePath,
targetLine: numberMetadata(targetNode, "line"),
targetColumn: numberMetadata(targetNode, "column")
});
if (referenceKind === "type")
coverage.typeOnlyEvidence++;
else if (referenceKind !== "call")
coverage.unsupportedEvidence++;
else if (!targetSymbol || resolution === "name-only" || resolution === "unresolved") {
coverage.unresolvedEvidence++;
} else {
coverage.eligibleEvidence++;
coverage.resolvedEvidence++;
}
}
let sawRelevantFileNode = false;
for (const node of graph.nodes.values()) {
if (node.kind !== "file")
continue;
sawRelevantFileNode = true;
const extraction = node.metadata?.extractionCoverage;
const statuses = extraction ? Object.values(extraction) : [];
const languageStatus = !extraction || statuses.length === 0 || statuses.includes("unavailable") ? "unavailable" : statuses.includes("partial") ? "partial" : "complete";
if (languageStatus !== "complete")
coverage.complete = false;
if (node.language) {
const prior = coverage.languages[node.language];
coverage.languages[node.language] = prior === "unavailable" || languageStatus === "unavailable" ? "unavailable" : prior === "partial" || languageStatus === "partial" ? "partial" : languageStatus;
}
}
if (!sawRelevantFileNode)
coverage.complete = false;
return { allSymbols, allRefs, coverage };
}
export {
fileContentProvider,
SYMBOL_KIND_NAMES,
formatReviewGraphRevisionDriftNote,
reviewGraphIrContentHash,
publishReviewGraphFileIr,
REVIEW_GRAPH_VERSION,
_resetReviewGraphSourcePathMemoForTests,
graphWasmTrapDetail,
getGraphBuildInfoForGraph,
graphBuildInfoIsTrustworthy,
clearGraphCache,
clearReviewGraphWorkspaceCache,
_setReviewGraphBuildGateForTests,
estimateReviewGraphStoreBytes,
getReviewGraphWorkspaceCacheSnapshot,
_getReviewGraphWorkspaceCacheKeysForTests,
_setReviewGraphWorkspaceEntryForTests,
_setSessionReviewGraphFactForTests,
_persistedCoverageForTests,
_getReviewGraphWorkspaceGraphForTests,
_getReviewGraphCacheStateForTests,
getReviewGraphCacheIdentity,
_resetCwdWorktreeMismatchLogForTests,
getLastGraphBuildInfo,
_setLastGraphBuildInfoForTests,
getCachedReviewGraph,
getReviewGraphRevisionDrift,
_resetReviewGraphSizeSkipTtlForTests,
getLastReviewGraphBuildAttempt,
_resetReviewGraphBuildAttemptsForTests,
_resetReviewGraphSizeSkipVerdictsForTests,
getReviewGraphSizeSkipVerdict,
_setReviewGraphEntryBudgetForTests,
_setReviewGraphEntryCounterForTests,
getGraphSourceFiles,
isReviewGraphMigrationNeeded,
GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT,
refreshReviewGraphPersistWorkerHeapStatistics,
getReviewGraphPersistWorkerHeapStatistics,
flushReviewGraphPersistsForExitForTests,
_resetReviewGraphStageSweepForTests,
_readReviewGraphCheckpointForTests,
flushReviewGraphPersistsForTests,
waitForReviewGraphPersistsForTests,
terminateReviewGraphPersistWorkerForTests,
resetReviewGraphPersistWorkerForTests,
getCheckpointOffloadCountForTests,
getReviewGraphWorkerFallbackReasonForTests,
reviewGraphCachePath,
flushReviewGraphPersist,
mapKindToTreeSitterLanguage,
captureReviewGraphStructuralIr,
_getReviewGraphRebuildCountersForTests,
_resetReviewGraphRebuildCountersForTests,
getGraphImportChanges,
isGraphBuildInFlight,
buildOrUpdateGraph,
extractSymbolsAndRefsFromGraph
};