weavatrix 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,847 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { atomicWriteFileSync } from "../graph/file-lock.js";
6
+ import { edgeProvenance } from "../graph/edge-provenance.js";
7
+ import { isStructuralRelation } from "../graph/relations.js";
8
+ import { createRepoBoundary, isPathInside } from "../repo-path.js";
9
+ import {
10
+ classifyTypeScriptReferenceUsage,
11
+ createTypeScriptLspClient,
12
+ typeScriptLspContract,
13
+ typeScriptProjectSafety,
14
+ } from "./typescript-lsp-provider.js";
15
+
16
+ export const PRECISION_OVERLAY_V = 3;
17
+ export const PRECISION_FILE = "precision.json";
18
+
19
+ const JS_TS_FILE = /\.(?:[cm]?[jt]sx?)$/i;
20
+ const endpoint = (value) => String(value && typeof value === "object" ? value.id : value);
21
+ const norm = (value) => String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
22
+ const graphMode = (graph) => ["full", "no-tests", "tests-only"].includes(graph?.graphBuildMode)
23
+ ? graph.graphBuildMode : "full";
24
+ const graphScope = (graph) => String(graph?.graphBuildScope || "");
25
+ const precisionMode = (graph) => graph?.graphPrecisionMode === "off" ? "off" : "lsp";
26
+ const providerContractFor = (graph) => precisionMode(graph) === "off" ? "off" : typeScriptLspContract();
27
+ const graphContractFor = (graph) => ({
28
+ extractorSchemaV: Number(graph?.extractorSchemaV) || 0,
29
+ ...(graph?.repositoryFreshnessBuilderVersion != null
30
+ ? {repositoryFreshnessBuilderVersion: String(graph.repositoryFreshnessBuilderVersion)} : {}),
31
+ ...(graph?.graphBuilderVersion != null ? {graphBuilderVersion: String(graph.graphBuilderVersion)} : {}),
32
+ ...(graph?.internalBuilderVersion != null ? {internalBuilderVersion: String(graph.internalBuilderVersion)} : {}),
33
+ });
34
+ const lineNumber = (value) => {
35
+ const match = /L(\d+)/.exec(String(value || ""));
36
+ return match ? Number(match[1]) : 0;
37
+ };
38
+
39
+ export function precisionPathForGraph(graphPath) {
40
+ return resolve(dirname(graphPath), PRECISION_FILE);
41
+ }
42
+
43
+ function sameRequest(actual, expected) {
44
+ if (!expected) return true;
45
+ return Number(actual?.maxSymbols) === Number(expected.maxSymbols)
46
+ && Number(actual?.maxReferences) === Number(expected.maxReferences)
47
+ && Number(actual?.maxLinks) === Number(expected.maxLinks);
48
+ }
49
+
50
+ function sameGraphContract(actual, graph) {
51
+ const expected = graphContractFor(graph);
52
+ if (!actual || typeof actual !== "object") return false;
53
+ return JSON.stringify(actual) === JSON.stringify(expected);
54
+ }
55
+
56
+ export function precisionOverlayMatches(overlay, graph, { request } = {}) {
57
+ return Number(overlay?.precisionOverlayV) === PRECISION_OVERLAY_V
58
+ && String(overlay?.baseGraphRevision || "") === String(graph?.graphRevision || "")
59
+ && String(overlay?.graphBuildMode || "full") === graphMode(graph)
60
+ && String(overlay?.graphBuildScope || "") === graphScope(graph)
61
+ && String(overlay?.precisionMode || "") === precisionMode(graph)
62
+ && String(overlay?.providerContract || "") === providerContractFor(graph)
63
+ && sameGraphContract(overlay?.graphContract, graph)
64
+ && sameRequest(overlay?.request, request);
65
+ }
66
+
67
+ export function readPrecisionOverlay(graphPath, graph) {
68
+ const path = precisionPathForGraph(graphPath);
69
+ if (!existsSync(path)) return null;
70
+ try {
71
+ const overlay = JSON.parse(readFileSync(path, "utf8"));
72
+ return precisionOverlayMatches(overlay, graph) ? overlay : null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ export function precisionSummary(overlay) {
79
+ if (!overlay) return {
80
+ state: "UNAVAILABLE",
81
+ provider: null,
82
+ verifiedEdges: 0,
83
+ candidates: 0,
84
+ queried: 0,
85
+ reason: "no revision-matched precision overlay",
86
+ };
87
+ const engine = Array.isArray(overlay.engines) ? overlay.engines[0] : null;
88
+ return {
89
+ state: String(overlay.state || "UNAVAILABLE"),
90
+ provider: engine?.provider || null,
91
+ providerVersion: engine?.version || null,
92
+ typescriptVersion: engine?.typescriptVersion || null,
93
+ verifiedEdges: Number(overlay.coverage?.verifiedEdges) || 0,
94
+ candidates: Number(overlay.coverage?.candidates) || 0,
95
+ selected: Number(overlay.coverage?.selected) || 0,
96
+ queried: Number(overlay.coverage?.queried) || 0,
97
+ references: Number(overlay.coverage?.references) || 0,
98
+ unclassifiedReferences: Number(overlay.coverage?.unclassifiedReferences) || 0,
99
+ referenceEvidence: Array.isArray(overlay.referenceEvidence) ? overlay.referenceEvidence.length : 0,
100
+ truncated: overlay.coverage?.truncated === true,
101
+ reason: overlay.reason || engine?.reason || null,
102
+ noReferenceSymbols: Array.isArray(overlay.noReferenceSymbols) ? overlay.noReferenceSymbols.length : 0,
103
+ };
104
+ }
105
+
106
+ // Precision evidence is revision-bound and lives beside graph.json. The static graph stays pristine
107
+ // for Git/history diffs; ordinary graph reads merge only an overlay that matches revision + mode.
108
+ export function mergePrecisionOverlay(graph, overlay) {
109
+ if (!precisionOverlayMatches(overlay, graph)) return {...graph, precision: precisionSummary(null)};
110
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
111
+ const ids = new Set(nodes.map((node) => String(node.id)));
112
+ const links = (Array.isArray(graph.links) ? graph.links : []).map((link) => ({...link}));
113
+ const exactLinks = Array.isArray(overlay.links) ? overlay.links : [];
114
+ for (const exact of exactLinks) {
115
+ const source = endpoint(exact.source);
116
+ const target = endpoint(exact.target);
117
+ if (!ids.has(source) || !ids.has(target) || source === target) continue;
118
+ const relation = String(exact.relation || "references");
119
+ const exactLine = Number.isInteger(exact.line) ? exact.line : null;
120
+ const exactCharacter = Number.isInteger(exact.character) ? exact.character : null;
121
+ let matched = false;
122
+ for (const link of links) {
123
+ // Static topology remains static. Deduplicate only an already-materialized semantic
124
+ // occurrence; never mutate an EXTRACTED/RESOLVED/INFERRED edge into EXACT_LSP.
125
+ if (edgeProvenance(link) !== "EXACT_LSP") continue;
126
+ if (endpoint(link.source) !== source || endpoint(link.target) !== target || String(link.relation || "") !== relation) continue;
127
+ if (exactLine != null && (!Number.isInteger(link.line) || link.line !== exactLine)) continue;
128
+ // Never bless a line-only static edge with an occurrence-specific semantic result. A single
129
+ // source line can contain both type-only and runtime uses of the same symbol.
130
+ if (exactCharacter != null && (!Number.isInteger(link.character) || link.character !== exactCharacter)) continue;
131
+ link.provenance = "EXACT_LSP";
132
+ link.confidence = "EXACT_LSP";
133
+ link.precisionProvider = String(exact.provider || "typescript-language-server");
134
+ if (exact.typeOnly === true) link.typeOnly = true;
135
+ else delete link.typeOnly;
136
+ if (exact.compileOnly === true) link.compileOnly = true;
137
+ else delete link.compileOnly;
138
+ matched = true;
139
+ break;
140
+ }
141
+ if (!matched) {
142
+ links.push({
143
+ source,
144
+ target,
145
+ relation: relation || "references",
146
+ provenance: "EXACT_LSP",
147
+ confidence: "EXACT_LSP",
148
+ precisionProvider: String(exact.provider || "typescript-language-server"),
149
+ ...(exact.typeOnly === true ? {typeOnly: true} : {}),
150
+ ...(exact.compileOnly === true ? {compileOnly: true} : {}),
151
+ ...(exactLine != null ? {line: exactLine} : {}),
152
+ ...(exactCharacter != null ? {character: exactCharacter} : {}),
153
+ ...(Number.isInteger(exact.endLine) ? {endLine: exact.endLine} : {}),
154
+ ...(Number.isInteger(exact.endCharacter) ? {endCharacter: exact.endCharacter} : {}),
155
+ });
156
+ }
157
+ }
158
+ return {
159
+ ...graph,
160
+ links,
161
+ precisionOverlayV: PRECISION_OVERLAY_V,
162
+ precision: precisionSummary(overlay),
163
+ precisionNoReferenceSymbols: Array.isArray(overlay.noReferenceSymbols)
164
+ ? overlay.noReferenceSymbols.filter((id) => ids.has(String(id))).map(String)
165
+ : [],
166
+ precisionReferenceEvidence: Array.isArray(overlay.referenceEvidence)
167
+ ? overlay.referenceEvidence.filter((evidence) => ids.has(endpoint(evidence.source))
168
+ && ids.has(endpoint(evidence.target)))
169
+ .map((evidence) => ({
170
+ source: endpoint(evidence.source),
171
+ target: endpoint(evidence.target),
172
+ ...(Number.isInteger(evidence.line) ? {line: evidence.line} : {}),
173
+ ...(Number.isInteger(evidence.character) ? {character: evidence.character} : {}),
174
+ classification: String(evidence.classification || "unknown"),
175
+ provider: String(evidence.provider || "typescript-language-server"),
176
+ }))
177
+ : [],
178
+ };
179
+ }
180
+
181
+ function repoFileFromLocation(repoRoot, location) {
182
+ if (location?.file) {
183
+ try {
184
+ const root = realpathSync.native(repoRoot);
185
+ const path = realpathSync.native(resolve(root, String(location.file)));
186
+ if (!isPathInside(root, path)) return null;
187
+ const rel = relative(root, path);
188
+ return rel && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) ? norm(rel) : null;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+ const uri = typeof location === "string" ? location : location?.uri || location?.targetUri;
194
+ if (!uri || !String(uri).startsWith("file:")) return null;
195
+ try {
196
+ const root = realpathSync.native(repoRoot);
197
+ const path = realpathSync.native(fileURLToPath(uri));
198
+ if (!isPathInside(root, path)) return null;
199
+ const rel = relative(root, path);
200
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
201
+ return norm(rel);
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ function locationStart(location) {
208
+ return location?.range?.start || location?.targetSelectionRange?.start || location?.targetRange?.start || null;
209
+ }
210
+
211
+ function symbolIndex(graph) {
212
+ const files = new Set();
213
+ const byFile = new Map();
214
+ for (const node of graph.nodes || []) {
215
+ const id = String(node.id);
216
+ const file = norm(node.source_file || (id.includes("#") ? id.slice(0, id.indexOf("#")) : id));
217
+ if (!file) continue;
218
+ if (!id.includes("#")) files.add(file);
219
+ else {
220
+ const start = lineNumber(node.source_location) || Number(node.selection_start?.line) + 1 || 0;
221
+ const end = lineNumber(node.source_end) || start;
222
+ if (!start) continue;
223
+ const sourceRange = node.source_range;
224
+ const hasRange = Number.isInteger(sourceRange?.start?.line)
225
+ && Number.isInteger(sourceRange?.start?.character)
226
+ && Number.isInteger(sourceRange?.end?.line)
227
+ && Number.isInteger(sourceRange?.end?.character);
228
+ const rows = byFile.get(file) || [];
229
+ rows.push({
230
+ id,
231
+ start,
232
+ end: Math.max(start, end),
233
+ ...(hasRange ? {range: sourceRange} : {}),
234
+ });
235
+ byFile.set(file, rows);
236
+ }
237
+ }
238
+ for (const rows of byFile.values()) rows.sort((a, b) => {
239
+ if (a.range && b.range) {
240
+ // The innermost containing range starts latest; equal starts end earliest.
241
+ return comparePosition(b.range.start, a.range.start)
242
+ || comparePosition(a.range.end, b.range.end)
243
+ || a.id.localeCompare(b.id);
244
+ }
245
+ return (a.end - a.start) - (b.end - b.start) || b.start - a.start || a.id.localeCompare(b.id);
246
+ });
247
+ return {files, byFile};
248
+ }
249
+
250
+ const comparePosition = (left, right) => left.line - right.line || left.character - right.character;
251
+
252
+ function sourceAt(index, file, position) {
253
+ if (!Number.isInteger(position?.line) || !Number.isInteger(position?.character)) {
254
+ return index.files.has(file) ? file : null;
255
+ }
256
+ const line = position.line + 1;
257
+ const rows = (index.byFile.get(file) || []).filter((row) => row.range
258
+ // LSP ranges are start-inclusive and end-exclusive.
259
+ ? comparePosition(row.range.start, position) <= 0 && comparePosition(position, row.range.end) < 0
260
+ // Legacy graphs have line-only ranges. Refuse boundary lines rather than assigning an import,
261
+ // declaration, or trailing token to a coincidentally adjacent symbol.
262
+ : row.start < line && line < row.end);
263
+ if (rows.length) {
264
+ const first = rows[0];
265
+ const span = first.range ? null : first.end - first.start;
266
+ const tied = rows.filter((row) => {
267
+ if (first.range || row.range) return Boolean(first.range && row.range
268
+ && comparePosition(first.range.start, row.range.start) === 0
269
+ && comparePosition(first.range.end, row.range.end) === 0);
270
+ return row.end - row.start === span;
271
+ });
272
+ if (tied.length === 1) return tied[0].id;
273
+ }
274
+ return index.files.has(file) ? file : null;
275
+ }
276
+
277
+ function eligibleTargets(graph, limit) {
278
+ const byId = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
279
+ const ranked = new Map();
280
+ const inbound = new Set();
281
+ for (const link of graph.links || []) {
282
+ const relation = String(link.relation || "");
283
+ if (!isStructuralRelation(relation)) inbound.add(endpoint(link.target));
284
+ if (isStructuralRelation(relation) || !["calls", "references", "inherits", "implements"].includes(relation)) continue;
285
+ if (edgeProvenance(link) === "EXACT_LSP") continue;
286
+ const target = endpoint(link.target);
287
+ const node = byId.get(target);
288
+ if (!node?.selection_start || !JS_TS_FILE.test(String(node.source_file || ""))) continue;
289
+ const score = (relation === "calls" ? 30 : relation === "inherits" || relation === "implements" ? 20 : 10)
290
+ + (edgeProvenance(link) === "INFERRED" ? 8 : 0);
291
+ ranked.set(target, Math.max(ranked.get(target) || 0, score));
292
+ }
293
+ // After ambiguous positive edges, spend the remaining bounded budget on genuinely orphaned
294
+ // internal callables. This is the only route to an exact no-reference dead-code result; public
295
+ // exports remain conservative because consumers may live outside this workspace.
296
+ const orphans = new Set();
297
+ for (const node of byId.values()) {
298
+ const id = String(node.id);
299
+ const visibility = String(node.visibility || "").toLowerCase();
300
+ if (!node.selection_start || !JS_TS_FILE.test(String(node.source_file || "")) || inbound.has(id)) continue;
301
+ if (node.exported === true || visibility === "public" || visibility === "protected") continue;
302
+ if (!/\(\)$/.test(String(node.label || "")) && !["function", "method", "constructor"].includes(String(node.symbol_kind || "").toLowerCase())) continue;
303
+ if (!ranked.has(id)) {
304
+ ranked.set(id, 4);
305
+ orphans.add(id);
306
+ }
307
+ }
308
+ const all = [...ranked.entries()]
309
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
310
+ const positive = all.filter(([id]) => !orphans.has(id));
311
+ const orphan = all.filter(([id]) => orphans.has(id));
312
+ const reserve = orphan.length ? Math.min(8, Math.ceil(limit / 4), limit) : 0;
313
+ const selected = positive.slice(0, Math.max(0, limit - reserve));
314
+ selected.push(...orphan.slice(0, reserve));
315
+ if (selected.length < limit) {
316
+ const selectedIds = new Set(selected.map(([id]) => id));
317
+ selected.push(...all.filter(([id]) => !selectedIds.has(id)).slice(0, limit - selected.length));
318
+ }
319
+ return {
320
+ targets: selected.map(([id]) => byId.get(id)),
321
+ total: all.length,
322
+ orphanIds: new Set(orphan.map(([id]) => id)),
323
+ };
324
+ }
325
+
326
+ function baseOverlay(graph, state, extra = {}) {
327
+ return {
328
+ precisionOverlayV: PRECISION_OVERLAY_V,
329
+ baseGraphRevision: String(graph.graphRevision || ""),
330
+ graphBuildMode: graphMode(graph),
331
+ graphBuildScope: graphScope(graph),
332
+ precisionMode: precisionMode(graph),
333
+ providerContract: providerContractFor(graph),
334
+ graphContract: graphContractFor(graph),
335
+ state,
336
+ engines: [],
337
+ coverage: {candidates: 0, selected: 0, queried: 0, references: 0, unclassifiedReferences: 0, verifiedEdges: 0, truncated: false},
338
+ links: [],
339
+ referenceEvidence: [],
340
+ noReferenceSymbols: [],
341
+ ...extra,
342
+ };
343
+ }
344
+
345
+ export function writePrecisionOverlay(graphPath, overlay) {
346
+ atomicWriteFileSync(precisionPathForGraph(graphPath), JSON.stringify(overlay), "utf8");
347
+ return overlay;
348
+ }
349
+
350
+ const SAFE_INVALIDATION_REASON = "repository changed while semantic precision was running";
351
+
352
+ // The graph builder calls this after its post-LSP repository snapshot check. Never preserve exact
353
+ // edges or no-reference proofs from a run that overlapped a source/configuration change.
354
+ export function invalidatePrecisionOverlay(graphPath, graph, reason = SAFE_INVALIDATION_REASON) {
355
+ if (!graphPath || !graph) throw new Error("precision invalidation requires graphPath and graph");
356
+ const previous = readPrecisionOverlay(graphPath, graph);
357
+ const safeReason = typeof reason === "string"
358
+ && reason.length > 0 && reason.length <= 160
359
+ && /^[A-Za-z0-9 _.,()-]+$/.test(reason)
360
+ ? reason
361
+ : SAFE_INVALIDATION_REASON;
362
+ const engines = (Array.isArray(previous?.engines) && previous.engines.length
363
+ ? previous.engines
364
+ : [{provider: "typescript-language-server", version: null, language: "typescript/javascript", capability: "textDocument/references"}])
365
+ .map((engine) => ({...engine, status: "PARTIAL"}));
366
+ return writePrecisionOverlay(graphPath, baseOverlay(graph, "PARTIAL", {
367
+ ...(previous?.request ? {request: previous.request} : {}),
368
+ reason: safeReason,
369
+ engines,
370
+ coverage: {candidates: 0, selected: 0, queried: 0, references: 0, unclassifiedReferences: 0, verifiedEdges: 0, truncated: true},
371
+ links: [],
372
+ referenceEvidence: [],
373
+ noReferenceSymbols: [],
374
+ }));
375
+ }
376
+
377
+ class PrecisionBudgetError extends Error {
378
+ constructor(message = "semantic precision deadline reached") {
379
+ super(message);
380
+ this.name = "PrecisionBudgetError";
381
+ }
382
+ }
383
+
384
+ class PrecisionLimitError extends Error {
385
+ constructor(message) {
386
+ super(message);
387
+ this.name = "PrecisionLimitError";
388
+ }
389
+ }
390
+
391
+ class PrecisionStaleGraphError extends Error {
392
+ constructor() {
393
+ super("repository content did not match the graph snapshot");
394
+ this.name = "PrecisionStaleGraphError";
395
+ }
396
+ }
397
+
398
+ class PrecisionStaleSemanticInputsError extends Error {
399
+ constructor() {
400
+ super("TypeScript project inputs changed while semantic precision was running");
401
+ this.name = "PrecisionStaleSemanticInputsError";
402
+ }
403
+ }
404
+
405
+ function graphJavaScriptUniverse(graph) {
406
+ const files = [...new Set((graph.nodes || [])
407
+ .filter((node) => {
408
+ const id = String(node?.id || "");
409
+ const file = norm(node?.source_file || id);
410
+ return id === file && JS_TS_FILE.test(file);
411
+ })
412
+ .map((node) => norm(node.source_file || node.id)))].sort();
413
+ const hashed = Object.keys(graph.fileHashes || {}).map(norm).filter((file) => JS_TS_FILE.test(file)).sort();
414
+ const fileSet = new Set(files);
415
+ const complete = graphMode(graph) === "full" && !graphScope(graph)
416
+ && files.length === hashed.length && hashed.every((file) => fileSet.has(file));
417
+ return {files, complete};
418
+ }
419
+
420
+ // Synchronous and bounded so callers can reject a persisted COMPLETE overlay before taking an
421
+ // auto-refresh/probe shortcut. The digest covers applicable repo-contained config chains,
422
+ // configured project paths, and the content of configured files omitted from the graph.
423
+ export function precisionSemanticInputs(repoRoot, graph, options = {}) {
424
+ const universe = graphJavaScriptUniverse(graph);
425
+ if (!universe.files.length) {
426
+ return {safe: false, reason: "NO_JAVASCRIPT_TYPESCRIPT_INPUTS", fingerprint: null, universe};
427
+ }
428
+ return {...typeScriptProjectSafety(repoRoot, universe.files, options), universe};
429
+ }
430
+
431
+ export function precisionSemanticInputsMatch(overlay, repoRoot, graph) {
432
+ const current = precisionSemanticInputs(repoRoot, graph);
433
+ return current.safe === true
434
+ && typeof current.fingerprint === "string"
435
+ && current.fingerprint.length > 0
436
+ && String(overlay?.semanticInputFingerprint || "") === current.fingerprint;
437
+ }
438
+
439
+ function publicSemanticSafetyReason(reason) {
440
+ return reason === "CONFIGURED_TSSERVER_PLUGINS"
441
+ ? "configured TypeScript language-service plugins are not allowed"
442
+ : "TypeScript project configuration could not be verified safely";
443
+ }
444
+
445
+ function boundedInteger(value, fallback, minimum, maximum) {
446
+ const number = Number(value);
447
+ return Math.max(minimum, Math.min(maximum, Number.isFinite(number) ? Math.floor(number) : fallback));
448
+ }
449
+
450
+ export async function buildLspPrecisionOverlay({
451
+ repoRoot,
452
+ graph,
453
+ graphPath,
454
+ mode = "lsp",
455
+ maxSymbols = Number(process.env.WEAVATRIX_PRECISION_MAX_SYMBOLS) || 32,
456
+ maxReferences = Number(process.env.WEAVATRIX_PRECISION_MAX_REFERENCES) || 2_048,
457
+ maxLinks = Number(process.env.WEAVATRIX_PRECISION_MAX_LINKS) || 2_048,
458
+ timeoutMs = Number(process.env.WEAVATRIX_PRECISION_TIMEOUT_MS) || 45_000,
459
+ clientFactory,
460
+ } = {}) {
461
+ if (!graph || !repoRoot) throw new Error("precision overlay requires repoRoot and graph");
462
+ const boundedMax = boundedInteger(maxSymbols, 32, 1, 64);
463
+ const boundedReferences = boundedInteger(maxReferences, 2_048, 1, 16_384);
464
+ const boundedLinks = boundedInteger(maxLinks, 2_048, 1, 16_384);
465
+ const boundedTimeout = boundedInteger(timeoutMs, 45_000, 100, 60_000);
466
+ const request = {maxSymbols: boundedMax, maxReferences: boundedReferences, maxLinks: boundedLinks};
467
+ if (mode === "off") {
468
+ const overlay = baseOverlay(graph, "OFF", {request, reason: "precision disabled by request"});
469
+ return graphPath ? writePrecisionOverlay(graphPath, overlay) : overlay;
470
+ }
471
+ // Configuration discovery is part of the same global budget as the provider. Its bounded
472
+ // directory host also receives this absolute deadline, so ignored include trees cannot consume
473
+ // unlimited synchronous work before the LSP timer starts.
474
+ const deadline = Date.now() + boundedTimeout;
475
+ const universe = graphJavaScriptUniverse(graph);
476
+ if (!universe.files.length) {
477
+ const overlay = baseOverlay(graph, "UNAVAILABLE", {
478
+ request,
479
+ reason: "semantic precision currently supports JavaScript and TypeScript repositories",
480
+ });
481
+ return graphPath ? writePrecisionOverlay(graphPath, overlay) : overlay;
482
+ }
483
+ const semanticInputs = precisionSemanticInputs(repoRoot, graph, {deadline});
484
+ if (!semanticInputs.safe) {
485
+ const overlay = baseOverlay(graph, "UNAVAILABLE", {
486
+ request,
487
+ reason: publicSemanticSafetyReason(semanticInputs.reason),
488
+ engines: [{
489
+ provider: "typescript-language-server",
490
+ version: null,
491
+ language: "typescript/javascript",
492
+ capability: "textDocument/references",
493
+ status: "UNAVAILABLE",
494
+ }],
495
+ });
496
+ return graphPath ? writePrecisionOverlay(graphPath, overlay) : overlay;
497
+ }
498
+ if (graphPath) {
499
+ const cached = readPrecisionOverlay(graphPath, graph);
500
+ if (cached?.state === "COMPLETE"
501
+ && precisionOverlayMatches(cached, graph, {request})
502
+ && cached.semanticInputFingerprint === semanticInputs.fingerprint) return cached;
503
+ }
504
+ const eligible = eligibleTargets(graph, boundedMax);
505
+ const targets = eligible.targets;
506
+ if (!targets.length) {
507
+ const overlay = baseOverlay(graph, "COMPLETE", {
508
+ request,
509
+ semanticInputFingerprint: semanticInputs.fingerprint,
510
+ reason: "no eligible JavaScript/TypeScript semantic targets",
511
+ });
512
+ return graphPath ? writePrecisionOverlay(graphPath, overlay) : overlay;
513
+ }
514
+
515
+ const makeClient = clientFactory || createTypeScriptLspClient;
516
+ let client;
517
+ const links = [];
518
+ const seen = new Set();
519
+ const evidenceSeen = new Set();
520
+ const index = symbolIndex(graph);
521
+ let queried = 0;
522
+ let references = 0;
523
+ let unclassifiedReferences = 0;
524
+ let errors = 0;
525
+ let truncated = eligible.total > boundedMax;
526
+ const noReferenceSymbols = [];
527
+ const referenceEvidence = [];
528
+ const opened = new Set();
529
+ const openedTexts = new Map();
530
+ const classificationTexts = new Map();
531
+ let classificationBytes = 0;
532
+ let openedBytes = 0;
533
+ let fullUniverseOpened = false;
534
+ let stop = false;
535
+ const nodesById = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
536
+ const boundary = createRepoBoundary(repoRoot);
537
+ const remaining = () => deadline - Date.now();
538
+ const ensureBudget = () => {
539
+ if (remaining() <= 0) throw new PrecisionBudgetError();
540
+ };
541
+ const awaitWithBudget = (operation) => {
542
+ ensureBudget();
543
+ const wait = remaining();
544
+ return new Promise((resolvePromise, rejectPromise) => {
545
+ const timer = setTimeout(() => rejectPromise(new PrecisionBudgetError()), wait);
546
+ Promise.resolve().then(operation).then(
547
+ (value) => { clearTimeout(timer); resolvePromise(value); },
548
+ (error) => { clearTimeout(timer); rejectPromise(error); },
549
+ );
550
+ });
551
+ };
552
+ const coverage = (verifiedEdges = links.length) => ({
553
+ candidates: eligible.total,
554
+ selected: targets.length,
555
+ queried,
556
+ references,
557
+ unclassifiedReferences,
558
+ verifiedEdges,
559
+ truncated,
560
+ });
561
+ try {
562
+ client = await awaitWithBudget(() => makeClient({repoRoot, timeoutMs: Math.max(100, remaining())}));
563
+ const verifiedSource = (relPath, maxBytes = 4 * 1024 * 1024) => {
564
+ const file = norm(relPath);
565
+ const expectedHash = graph.fileHashes?.[file];
566
+ if (!file || !/^[a-f0-9]{64}$/i.test(String(expectedHash || ""))) throw new PrecisionStaleGraphError();
567
+ const resolvedFile = boundary.resolve(file);
568
+ if (!resolvedFile.ok) throw new PrecisionStaleGraphError();
569
+ let size;
570
+ try { size = statSync(resolvedFile.path).size; } catch { throw new PrecisionStaleGraphError(); }
571
+ if (size > maxBytes) throw new PrecisionLimitError("precision source-read budget reached");
572
+ let body;
573
+ try { body = readFileSync(resolvedFile.path); } catch { throw new PrecisionStaleGraphError(); }
574
+ if (body.byteLength > maxBytes) throw new PrecisionLimitError("precision source-read budget reached");
575
+ if (createHash("sha256").update(body).digest("hex") !== expectedHash) throw new PrecisionStaleGraphError();
576
+ return {file, body, bytes: body.byteLength, text: body.toString("utf8")};
577
+ };
578
+ const ensureOpen = async (relPath) => {
579
+ const file = norm(relPath);
580
+ if (!file || opened.has(file)) return;
581
+ ensureBudget();
582
+ if (opened.size >= 96) throw new PrecisionLimitError("precision open-document limit reached");
583
+ // Re-read and re-hash here even when classification cached the file earlier: didOpen must
584
+ // always be immediately guarded by the graph snapshot hash.
585
+ const {bytes, text} = verifiedSource(file, Math.min(4 * 1024 * 1024, 32 * 1024 * 1024 - openedBytes));
586
+ if (bytes > 4 * 1024 * 1024) throw new PrecisionLimitError("precision document exceeds 4 MiB limit");
587
+ if (openedBytes + bytes > 32 * 1024 * 1024) throw new PrecisionLimitError("precision source-transfer budget reached");
588
+ await awaitWithBudget(() => client.openDocument(file, text));
589
+ opened.add(file);
590
+ openedTexts.set(file, text);
591
+ openedBytes += bytes;
592
+ };
593
+
594
+ const sourceForClassification = (relPath) => {
595
+ const file = norm(relPath);
596
+ if (openedTexts.has(file)) return openedTexts.get(file);
597
+ if (classificationTexts.has(file)) return classificationTexts.get(file);
598
+ if (classificationTexts.size >= 96) return null;
599
+ let source;
600
+ try {
601
+ source = verifiedSource(file, Math.min(4 * 1024 * 1024, 32 * 1024 * 1024 - classificationBytes));
602
+ } catch (error) {
603
+ if (error instanceof PrecisionLimitError) return null;
604
+ throw error;
605
+ }
606
+ classificationTexts.set(file, source.text);
607
+ classificationBytes += source.bytes;
608
+ return source.text;
609
+ };
610
+
611
+ const ensureFullUniverse = async () => {
612
+ if (fullUniverseOpened) return true;
613
+ if (!universe.complete) return false;
614
+ const additional = universe.files.filter((file) => !opened.has(file));
615
+ if (opened.size + additional.length > 96) {
616
+ truncated = true;
617
+ return false;
618
+ }
619
+ let projectedBytes = openedBytes;
620
+ for (const file of additional) {
621
+ ensureBudget();
622
+ if (!/^[a-f0-9]{64}$/i.test(String(graph.fileHashes?.[file] || ""))) throw new PrecisionStaleGraphError();
623
+ const resolvedFile = boundary.resolve(file);
624
+ if (!resolvedFile.ok) throw new PrecisionStaleGraphError();
625
+ let bytes;
626
+ try { bytes = statSync(resolvedFile.path).size; } catch { throw new PrecisionStaleGraphError(); }
627
+ if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes > 32 * 1024 * 1024) {
628
+ truncated = true;
629
+ return false;
630
+ }
631
+ projectedBytes += bytes;
632
+ }
633
+ for (const file of additional) await ensureOpen(file);
634
+ fullUniverseOpened = universe.files.every((file) => opened.has(file));
635
+ return fullUniverseOpened;
636
+ };
637
+
638
+ const requestReferences = (relPath, position) => awaitWithBudget(
639
+ () => client.references(relPath, position, false, Math.max(1, remaining())),
640
+ );
641
+
642
+ for (const target of targets) {
643
+ const relPath = norm(target.source_file);
644
+ let locations;
645
+ try {
646
+ await ensureOpen(relPath);
647
+ // In inferred JavaScript projects the language server only knows opened roots. Open the
648
+ // bounded static callers of this target so exact validation works without executing repo
649
+ // configuration or pretending the whole unconfigured workspace was indexed.
650
+ const supportFiles = [];
651
+ for (const link of graph.links || []) {
652
+ if (endpoint(link.target) !== String(target.id) || isStructuralRelation(link.relation)) continue;
653
+ const sourceId = endpoint(link.source);
654
+ const sourceFile = norm(nodesById.get(sourceId)?.source_file || (sourceId.includes("#") ? sourceId.slice(0, sourceId.indexOf("#")) : sourceId));
655
+ if (sourceFile && JS_TS_FILE.test(sourceFile) && sourceFile !== relPath && !supportFiles.includes(sourceFile)) supportFiles.push(sourceFile);
656
+ if (supportFiles.length >= 12) break;
657
+ }
658
+ for (const file of supportFiles) await ensureOpen(file);
659
+ if (universe.complete && universe.files.every((file) => opened.has(file))) fullUniverseOpened = true;
660
+ locations = await requestReferences(relPath, target.selection_start);
661
+ if (!Array.isArray(locations)) throw new Error("language server returned an invalid references result");
662
+ queried++;
663
+ if (locations.length === 0) {
664
+ ensureBudget();
665
+ const configRel = semanticInputs.fileConfigs?.[relPath];
666
+ const project = configRel ? semanticInputs.projects?.[configRel] : null;
667
+ const configuredFiles = [...new Set((project?.projectFiles || [])
668
+ .map(norm).filter((file) => JS_TS_FILE.test(file)))].sort();
669
+ const projectFiles = new Set(configuredFiles);
670
+ const projectExactlyCoversUniverse = universe.complete
671
+ && configuredFiles.length === universe.files.length
672
+ && universe.files.every((file) => projectFiles.has(file));
673
+ ensureBudget();
674
+ if (configRel && projectFiles.has(relPath) && projectExactlyCoversUniverse) {
675
+ const alreadyComplete = fullUniverseOpened;
676
+ if (alreadyComplete || await ensureFullUniverse()) {
677
+ // Opening the complete graph universe can move files from an inferred project into
678
+ // the configured one. Re-query the first empty response after that transition.
679
+ if (!alreadyComplete) locations = await requestReferences(relPath, target.selection_start);
680
+ if (!Array.isArray(locations)) throw new Error("language server returned an invalid references result");
681
+ if (locations.length === 0) noReferenceSymbols.push(String(target.id));
682
+ }
683
+ }
684
+ }
685
+ } catch (error) {
686
+ if (error instanceof PrecisionStaleGraphError) throw error;
687
+ if (error instanceof PrecisionBudgetError || error instanceof PrecisionLimitError || remaining() <= 0) {
688
+ truncated = true;
689
+ stop = true;
690
+ break;
691
+ }
692
+ errors++;
693
+ continue;
694
+ }
695
+ for (const location of locations) {
696
+ if (remaining() <= 0) {
697
+ truncated = true;
698
+ stop = true;
699
+ break;
700
+ }
701
+ if (references >= boundedReferences) {
702
+ truncated = true;
703
+ stop = true;
704
+ break;
705
+ }
706
+ references++;
707
+ const file = repoFileFromLocation(repoRoot, location);
708
+ const start = locationStart(location);
709
+ if (!file || !start || !Number.isInteger(start.line) || !Number.isInteger(start.character)) continue;
710
+ const source = sourceAt(index, file, start);
711
+ if (!source || source === String(target.id)) continue;
712
+ const targetId = String(target.id);
713
+ const exactLine = start.line + 1;
714
+ const exactCharacter = start.character;
715
+ const relation = "references";
716
+ const line = exactLine;
717
+ const targetFile = norm(target.source_file);
718
+ const moduleDependency = source === file && targetFile
719
+ ? (graph.links || []).find((link) => endpoint(link.source) === file
720
+ && endpoint(link.target) === targetFile
721
+ && ["imports", "re_exports"].includes(String(link.relation || ""))
722
+ && Number.isInteger(link.line) && link.line === exactLine)
723
+ : null;
724
+ const sourceText = sourceForClassification(file);
725
+ let usage = sourceText == null
726
+ ? "unknown"
727
+ : classifyTypeScriptReferenceUsage(file, sourceText, start);
728
+ if (usage === "unknown" && moduleDependency?.typeOnly === true) usage = "type";
729
+ if (usage === "unknown" && moduleDependency?.compileOnly === true) usage = "compile";
730
+ if (usage === "unknown") {
731
+ const evidenceKey = `${source}\0${targetId}\0${line}\0${exactCharacter}`;
732
+ if (!evidenceSeen.has(evidenceKey)) {
733
+ if (referenceEvidence.length >= boundedLinks) {
734
+ truncated = true;
735
+ stop = true;
736
+ break;
737
+ }
738
+ evidenceSeen.add(evidenceKey);
739
+ unclassifiedReferences++;
740
+ referenceEvidence.push({
741
+ source,
742
+ target: targetId,
743
+ line,
744
+ character: exactCharacter,
745
+ classification: "unknown",
746
+ provider: "typescript-language-server",
747
+ });
748
+ }
749
+ continue;
750
+ }
751
+ const key = `${source}\0${relation}\0${targetId}\0${line}\0${exactCharacter}`;
752
+ if (seen.has(key)) continue;
753
+ if (links.length >= boundedLinks) {
754
+ truncated = true;
755
+ stop = true;
756
+ break;
757
+ }
758
+ seen.add(key);
759
+ links.push({
760
+ source,
761
+ target: targetId,
762
+ relation,
763
+ line,
764
+ character: exactCharacter,
765
+ ...(Number.isInteger(location?.range?.end?.line) ? {endLine: location.range.end.line + 1} : {}),
766
+ ...(Number.isInteger(location?.range?.end?.character) ? {endCharacter: location.range.end.character} : {}),
767
+ provenance: "EXACT_LSP",
768
+ provider: "typescript-language-server",
769
+ ...(usage === "type" || moduleDependency?.typeOnly === true ? {typeOnly: true} : {}),
770
+ ...(usage === "compile" || moduleDependency?.compileOnly === true ? {compileOnly: true} : {}),
771
+ });
772
+ }
773
+ if (remaining() <= 0) {
774
+ truncated = true;
775
+ stop = true;
776
+ }
777
+ if (stop) break;
778
+ }
779
+ ensureBudget();
780
+ const semanticInputsAfter = precisionSemanticInputs(repoRoot, graph, {deadline});
781
+ ensureBudget();
782
+ if (!semanticInputsAfter.safe || semanticInputsAfter.fingerprint !== semanticInputs.fingerprint) {
783
+ throw new PrecisionStaleSemanticInputsError();
784
+ }
785
+ const state = errors || truncated || unclassifiedReferences ? "PARTIAL" : "COMPLETE";
786
+ const overlay = baseOverlay(graph, state, {
787
+ request,
788
+ engines: [{
789
+ provider: client.provider || "typescript-language-server",
790
+ version: client.version || null,
791
+ typescriptVersion: client.typescriptVersion || null,
792
+ typescriptSource: client.typescriptSource || null,
793
+ language: "typescript/javascript",
794
+ capability: "textDocument/references",
795
+ status: state,
796
+ }],
797
+ semanticInputFingerprint: semanticInputs.fingerprint,
798
+ coverage: coverage(),
799
+ links,
800
+ referenceEvidence,
801
+ noReferenceSymbols,
802
+ ...(errors ? {reason: `${errors} semantic request(s) failed or were refused`}
803
+ : truncated ? {reason: "semantic precision stopped at a configured safety limit"}
804
+ : unclassifiedReferences ? {reason: "some exact references could not be classified as runtime or type-only"} : {}),
805
+ });
806
+ return graphPath ? writePrecisionOverlay(graphPath, overlay) : overlay;
807
+ } catch (error) {
808
+ const stale = error instanceof PrecisionStaleGraphError;
809
+ const semanticInputsChanged = error instanceof PrecisionStaleSemanticInputsError;
810
+ const deadlineReached = error instanceof PrecisionBudgetError || remaining() <= 0;
811
+ const state = stale || semanticInputsChanged || deadlineReached ? "PARTIAL" : "UNAVAILABLE";
812
+ const overlay = baseOverlay(graph, state, {
813
+ request,
814
+ // Provider stderr and discovery errors can contain host paths. Persist a bounded status, never
815
+ // raw diagnostics, command lines, environment values, or absolute install/repository paths.
816
+ reason: stale
817
+ ? "repository content no longer matched the graph snapshot"
818
+ : semanticInputsChanged ? "TypeScript project inputs changed while semantic precision was running"
819
+ : deadlineReached ? "semantic precision stopped at its global deadline"
820
+ : error?.name === "LspTimeoutError" ? "bundled TypeScript language server timed out"
821
+ : "bundled TypeScript language server was unavailable",
822
+ engines: [{provider: "typescript-language-server", version: null, language: "typescript/javascript", capability: "textDocument/references", status: state}],
823
+ coverage: {
824
+ candidates: eligible.total,
825
+ selected: targets.length,
826
+ queried: stale || semanticInputsChanged ? 0 : queried,
827
+ references: stale || semanticInputsChanged ? 0 : references,
828
+ unclassifiedReferences: stale || semanticInputsChanged ? 0 : unclassifiedReferences,
829
+ verifiedEdges: 0,
830
+ truncated: truncated || stale || semanticInputsChanged || deadlineReached,
831
+ },
832
+ links: [],
833
+ noReferenceSymbols: [],
834
+ });
835
+ return graphPath ? writePrecisionOverlay(graphPath, overlay) : overlay;
836
+ } finally {
837
+ if (client) {
838
+ const closeBudget = Math.min(2_000, Math.max(0, remaining()));
839
+ if (closeBudget > 0 && client.close) {
840
+ try { await awaitWithBudget(() => client.close(closeBudget)); }
841
+ catch { client.kill?.(); }
842
+ } else {
843
+ client.kill?.();
844
+ }
845
+ }
846
+ }
847
+ }