weavatrix 0.2.4 → 0.2.6

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,238 @@
1
+ import {readFileSync, statSync} from 'node:fs'
2
+ import {boundedInteger} from '../bounds.js'
3
+ import {resolveRepoPath} from '../repo-path.js'
4
+ import {isStructuralRelation} from '../graph/relations.js'
5
+ import {querySymbolPrecision} from '../precision/symbol-query.js'
6
+ import {toolResult} from './tool-result.mjs'
7
+ import {degreeOf, isSymbol, labelOf, resolveNodeInfo} from './graph-context.mjs'
8
+
9
+ const MAX_EXCERPT_CHARS = 4_000
10
+ const MAX_LOCATION_SAMPLES = 5
11
+ const MAX_GRAPH_OCCURRENCES = 100
12
+
13
+ const sourceLine = (node) => {
14
+ const match = /L(\d+)/.exec(String(node?.source_location || ''))
15
+ return match ? Number(match[1]) : 1
16
+ }
17
+
18
+ function candidateNodes(g, query, limit = 20) {
19
+ const text = String(query || '').trim().toLowerCase()
20
+ if (!text) return []
21
+ const exact = g.byLabel.get(text)
22
+ const nodes = exact?.length ? exact : g.nodes.filter((node) =>
23
+ String(node.id).toLowerCase().includes(text) || String(node.label || '').toLowerCase().includes(text))
24
+ return nodes.slice(0, limit).map((node) => ({
25
+ id: String(node.id), label: String(node.label || node.id), file: node.source_file || null,
26
+ ...(node.symbol_space ? {space: node.symbol_space} : {}),
27
+ }))
28
+ }
29
+
30
+ function excerpt(repoRoot, file, focusLine, contextLines) {
31
+ const resolved = resolveRepoPath(repoRoot, file)
32
+ if (!resolved.ok) return null
33
+ let size
34
+ try { size = statSync(resolved.path).size } catch { return null }
35
+ if (size > 2 * 1024 * 1024) return null
36
+ let lines
37
+ try { lines = readFileSync(resolved.path, 'utf8').split(/\r?\n/) } catch { return null }
38
+ const focus = Math.max(1, Math.min(lines.length, Number(focusLine) || 1))
39
+ const startLine = Math.max(1, focus - contextLines)
40
+ const endLine = Math.min(lines.length, focus + contextLines)
41
+ const rawText = lines.slice(startLine - 1, endLine).join('\n')
42
+ return {
43
+ file,
44
+ focusLine: focus,
45
+ startLine,
46
+ endLine,
47
+ text: rawText.length > MAX_EXCERPT_CHARS ? `${rawText.slice(0, MAX_EXCERPT_CHARS)}\n… [excerpt truncated]` : rawText,
48
+ truncated: rawText.length > MAX_EXCERPT_CHARS,
49
+ }
50
+ }
51
+
52
+ function reverseImpact(g, targetId, depth = 3, cap = 40) {
53
+ const seen = new Map([[targetId, 0]])
54
+ const queue = [targetId]
55
+ for (let cursor = 0; cursor < queue.length; cursor++) {
56
+ const current = queue[cursor]
57
+ const currentDepth = seen.get(current)
58
+ if (currentDepth >= depth) continue
59
+ for (const edge of g.inn.get(current) || []) {
60
+ if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
61
+ const id = String(edge.id)
62
+ if (seen.has(id)) continue
63
+ seen.set(id, currentDepth + 1)
64
+ queue.push(id)
65
+ }
66
+ }
67
+ return [...seen.entries()]
68
+ .filter(([id]) => id !== targetId)
69
+ .map(([id, distance]) => ({id, label: labelOf(g, id), distance, degree: degreeOf(g, id)}))
70
+ .sort((left, right) => left.distance - right.distance || right.degree - left.degree)
71
+ .slice(0, cap)
72
+ }
73
+
74
+ function graphOccurrences(g, targetId) {
75
+ const all = (g.inn.get(targetId) || [])
76
+ .filter((edge) => !isStructuralRelation(edge.relation) && edge.barrelProxy !== true)
77
+ .map((edge) => ({
78
+ source: String(edge.id),
79
+ label: labelOf(g, edge.id),
80
+ relation: edge.relation || 'references',
81
+ provenance: edge.provenance || 'UNKNOWN',
82
+ ...(Number.isInteger(edge.line) ? {line: edge.line} : {}),
83
+ ...(edge.typeOnly === true ? {typeOnly: true} : {}),
84
+ ...(edge.compileOnly === true ? {compileOnly: true} : {}),
85
+ }))
86
+ return {total: all.length, shown: all.slice(0, MAX_GRAPH_OCCURRENCES)}
87
+ }
88
+
89
+ function groupLocations(g, locations, cap) {
90
+ const groups = new Map()
91
+ for (const location of locations) {
92
+ const source = String(location.source || location.file || '')
93
+ if (!source) continue
94
+ const node = g.byId.get(source)
95
+ const file = String(location.file || node?.source_file || (isSymbol(source) ? source.split('#')[0] : source))
96
+ let group = groups.get(source)
97
+ if (!group) {
98
+ group = {id: source, label: String(node?.label || source), file, count: 0, classifications: {}, locations: []}
99
+ groups.set(source, group)
100
+ }
101
+ group.count++
102
+ const classification = String(location.classification || 'unknown')
103
+ group.classifications[classification] = (group.classifications[classification] || 0) + 1
104
+ if (group.locations.length < MAX_LOCATION_SAMPLES) group.locations.push({
105
+ line: location.line,
106
+ character: location.character,
107
+ ...(Number.isInteger(location.endLine) ? {endLine: location.endLine} : {}),
108
+ ...(Number.isInteger(location.endCharacter) ? {endCharacter: location.endCharacter} : {}),
109
+ classification,
110
+ })
111
+ }
112
+ const all = [...groups.values()].sort((left, right) => right.count - left.count || left.file.localeCompare(right.file) || left.id.localeCompare(right.id))
113
+ return {all, shown: all.slice(0, cap)}
114
+ }
115
+
116
+ function textFor(result) {
117
+ if (result.status === 'NOT_FOUND') return `No symbol found matching "${result.query}".`
118
+ if (result.status === 'AMBIGUOUS') return [
119
+ `Symbol "${result.query}" is ambiguous (${result.candidates.length} candidate(s) shown). Supply an exact node ID:`,
120
+ ...result.candidates.map((candidate) => ` ${candidate.label} [${candidate.id}]`),
121
+ ].join('\n')
122
+ const definition = result.definition
123
+ const lines = [
124
+ `Symbol inspection: ${definition.label} [${definition.id}]`,
125
+ `Definition: ${definition.file}:${definition.line}`,
126
+ `Evidence: ${result.evidence.state}${result.evidence.cached ? ' (cache hit)' : ''}; ${result.exact.occurrences} exact reference occurrence(s) in ${result.exact.files} file(s) / ${result.exact.containers} container(s).`,
127
+ `Graph: ${result.graph.occurrenceTotal} direct occurrence edge(s) (${result.graph.occurrences.length} shown); ${result.graph.impact.length} dependent(s) shown.`,
128
+ ]
129
+ if (result.evidence.reason) lines.push(`Completeness: ${result.evidence.reason}`)
130
+ if (result.exact.zeroReferences) lines.push('Exact result: zero non-declaration references in the completely covered project universe.')
131
+ if (result.exact.groups.length) {
132
+ lines.push('Reference containers:')
133
+ for (const group of result.exact.groups) lines.push(` ${group.count} site(s) ${group.label} [${group.id}]`)
134
+ }
135
+ if (result.source.definition) {
136
+ const source = result.source.definition
137
+ lines.push('', `Definition source (${source.file}:${source.startLine}-${source.endLine}):`, source.text)
138
+ }
139
+ for (const source of result.source.callers) {
140
+ lines.push('', `Caller source (${source.file}:${source.startLine}-${source.endLine}, focus ${source.focusLine}):`, source.text)
141
+ }
142
+ return lines.join('\n')
143
+ }
144
+
145
+ export async function tInspectSymbol(g, args = {}, ctx = {}) {
146
+ const query = String(args.label || '').trim()
147
+ const info = resolveNodeInfo(g, query)
148
+ if (!info.node) return toolResult(textFor({status: 'NOT_FOUND', query}), {status: 'NOT_FOUND', query})
149
+ if (info.matches > 1 && !g.byId.has(query)) {
150
+ const result = {status: 'AMBIGUOUS', query, candidates: candidateNodes(g, query)}
151
+ return toolResult(textFor(result), result, {completeness: {status: 'blocked', reason: 'ambiguous symbol'}})
152
+ }
153
+ const node = info.node
154
+ const targetId = String(node.id)
155
+ if (!isSymbol(targetId)) {
156
+ const result = {status: 'UNSUPPORTED', query, candidates: [{id: targetId, label: node.label || targetId, file: node.source_file || targetId}]}
157
+ return toolResult('inspect_symbol requires a symbol node, not a file node.', result)
158
+ }
159
+ const maxReferences = boundedInteger(args.max_references, 1_000, 1, 5_000)
160
+ const maxContainers = boundedInteger(args.max_containers, 15, 1, 50)
161
+ const contextLines = boundedInteger(args.context_lines, 8, 0, 40)
162
+ const timeoutMs = boundedInteger(args.timeout_ms, 30_000, 1_000, 60_000)
163
+ const precision = ['auto', 'graph', 'lsp'].includes(args.precision) ? args.precision : 'auto'
164
+ const shouldQuery = precision === 'lsp' || (precision === 'auto' && g.graphPrecisionMode !== 'off')
165
+ let overlay = null
166
+ let cached = false
167
+ let elapsedMs = 0
168
+ let queryError = null
169
+ if (shouldQuery) {
170
+ try {
171
+ const queryResult = await querySymbolPrecision({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, targetId, maxReferences, timeoutMs})
172
+ overlay = queryResult.overlay
173
+ cached = queryResult.cached
174
+ elapsedMs = queryResult.elapsedMs
175
+ } catch (error) {
176
+ queryError = error?.message || 'exact symbol query failed'
177
+ }
178
+ }
179
+ const locations = Array.isArray(overlay?.locations) ? overlay.locations : []
180
+ const grouped = groupLocations(g, locations, maxContainers)
181
+ const files = new Set(locations.map((location) => String(location.file || '')).filter(Boolean))
182
+ let state = 'GRAPH_ONLY'
183
+ if (overlay?.state === 'COMPLETE') state = 'EXACT'
184
+ else if (overlay?.state === 'PARTIAL') state = 'PARTIAL'
185
+ else if (overlay?.state === 'OFF') state = 'OFF'
186
+ else if (shouldQuery) state = 'UNAVAILABLE'
187
+ const reason = queryError || overlay?.reason || (shouldQuery ? null : 'semantic precision was not requested')
188
+ const graphRefs = graphOccurrences(g, targetId)
189
+ const impact = reverseImpact(g, targetId)
190
+ const definition = {
191
+ id: targetId,
192
+ label: String(node.label || targetId),
193
+ kind: String(node.symbol_kind || 'symbol'),
194
+ space: String(node.symbol_space || 'value'),
195
+ exported: node.exported === true,
196
+ file: String(node.source_file || targetId.split('#')[0]),
197
+ line: sourceLine(node),
198
+ ...(node.source_range ? {range: node.source_range} : {}),
199
+ ...(node.complexity ? {complexity: node.complexity} : {}),
200
+ }
201
+ const definitionSource = excerpt(ctx.repoRoot, definition.file, definition.line, contextLines)
202
+ const callerSources = grouped.shown.slice(0, 5).map((group) => excerpt(ctx.repoRoot, group.file, group.locations[0]?.line || 1, contextLines)).filter(Boolean)
203
+ const result = {
204
+ status: 'OK',
205
+ definition,
206
+ evidence: {state, cached, elapsedMs, reason, provider: overlay?.engines?.[0]?.provider || null},
207
+ exact: {
208
+ occurrences: locations.length,
209
+ occurrencesWithDefinition: locations.length + 1,
210
+ files: files.size,
211
+ containers: grouped.all.length,
212
+ groups: grouped.shown,
213
+ zeroReferences: overlay?.noReferenceSymbols?.includes(targetId) === true,
214
+ capped: overlay?.coverage?.truncated === true || grouped.all.length > grouped.shown.length,
215
+ },
216
+ graph: {
217
+ occurrences: graphRefs.shown,
218
+ occurrenceTotal: graphRefs.total,
219
+ occurrencesCapped: graphRefs.total > graphRefs.shown.length,
220
+ impact,
221
+ },
222
+ source: {definition: definitionSource, callers: callerSources},
223
+ }
224
+ const warnings = state === 'PARTIAL' || state === 'UNAVAILABLE'
225
+ ? [{code: 'SYMBOL_PRECISION_INCOMPLETE', message: reason || `semantic precision is ${state.toLowerCase()}`}]
226
+ : []
227
+ return toolResult(textFor(result), result, {
228
+ warnings,
229
+ completeness: {
230
+ status: state === 'EXACT' && !result.exact.capped ? 'complete' : state.toLowerCase(),
231
+ referenceLimit: maxReferences,
232
+ containerLimit: maxContainers,
233
+ returnedContainers: grouped.shown.length,
234
+ locationSamplesPerContainer: MAX_LOCATION_SAMPLES,
235
+ graphOccurrenceLimit: MAX_GRAPH_OCCURRENCES,
236
+ },
237
+ })
238
+ }
@@ -425,7 +425,7 @@ export class StdioLspClient {
425
425
  await this.writeMessage({jsonrpc: JSON_RPC_VERSION, method, params})
426
426
  }
427
427
 
428
- async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.4'}} = {}) {
428
+ async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.6'}} = {}) {
429
429
  if (this.state !== 'running') throw new Error(`LSP initialize is invalid in state=${this.state}`)
430
430
  const result = await this.request('initialize', {
431
431
  processId: process.pid,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { boundedInteger } from "../bounds.js";
5
6
  import { atomicWriteFileSync } from "../graph/file-lock.js";
6
7
  import { edgeProvenance } from "../graph/edge-provenance.js";
7
8
  import { isStructuralRelation } from "../graph/relations.js";
@@ -274,8 +275,16 @@ function sourceAt(index, file, position) {
274
275
  return index.files.has(file) ? file : null;
275
276
  }
276
277
 
277
- function eligibleTargets(graph, limit) {
278
+ function eligibleTargets(graph, limit, requestedIds = null) {
278
279
  const byId = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
280
+ if (Array.isArray(requestedIds) && requestedIds.length) {
281
+ const ids = [...new Set(requestedIds.map(String))];
282
+ const targets = ids
283
+ .map((id) => byId.get(id))
284
+ .filter((node) => node?.selection_start && JS_TS_FILE.test(String(node.source_file || "")))
285
+ .slice(0, limit);
286
+ return {targets, total: ids.length, orphanIds: new Set()};
287
+ }
279
288
  const ranked = new Map();
280
289
  const inbound = new Set();
281
290
  for (const link of graph.links || []) {
@@ -442,11 +451,6 @@ function publicSemanticSafetyReason(reason) {
442
451
  : "TypeScript project configuration could not be verified safely";
443
452
  }
444
453
 
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
454
  export async function buildLspPrecisionOverlay({
451
455
  repoRoot,
452
456
  graph,
@@ -456,6 +460,7 @@ export async function buildLspPrecisionOverlay({
456
460
  maxReferences = Number(process.env.WEAVATRIX_PRECISION_MAX_REFERENCES) || 2_048,
457
461
  maxLinks = Number(process.env.WEAVATRIX_PRECISION_MAX_LINKS) || 2_048,
458
462
  timeoutMs = Number(process.env.WEAVATRIX_PRECISION_TIMEOUT_MS) || 45_000,
463
+ targetIds,
459
464
  clientFactory,
460
465
  } = {}) {
461
466
  if (!graph || !repoRoot) throw new Error("precision overlay requires repoRoot and graph");
@@ -501,7 +506,7 @@ export async function buildLspPrecisionOverlay({
501
506
  && precisionOverlayMatches(cached, graph, {request})
502
507
  && cached.semanticInputFingerprint === semanticInputs.fingerprint) return cached;
503
508
  }
504
- const eligible = eligibleTargets(graph, boundedMax);
509
+ const eligible = eligibleTargets(graph, boundedMax, targetIds);
505
510
  const targets = eligible.targets;
506
511
  if (!targets.length) {
507
512
  const overlay = baseOverlay(graph, "COMPLETE", {
@@ -525,6 +530,8 @@ export async function buildLspPrecisionOverlay({
525
530
  let truncated = eligible.total > boundedMax;
526
531
  const noReferenceSymbols = [];
527
532
  const referenceEvidence = [];
533
+ const exactLocations = [];
534
+ const collectLocations = Array.isArray(targetIds) && targetIds.length > 0;
528
535
  const opened = new Set();
529
536
  const openedTexts = new Map();
530
537
  const classificationTexts = new Map();
@@ -708,7 +715,6 @@ export async function buildLspPrecisionOverlay({
708
715
  const start = locationStart(location);
709
716
  if (!file || !start || !Number.isInteger(start.line) || !Number.isInteger(start.character)) continue;
710
717
  const source = sourceAt(index, file, start);
711
- if (!source || source === String(target.id)) continue;
712
718
  const targetId = String(target.id);
713
719
  const exactLine = start.line + 1;
714
720
  const exactCharacter = start.character;
@@ -727,6 +733,20 @@ export async function buildLspPrecisionOverlay({
727
733
  : classifyTypeScriptReferenceUsage(file, sourceText, start);
728
734
  if (usage === "unknown" && moduleDependency?.typeOnly === true) usage = "type";
729
735
  if (usage === "unknown" && moduleDependency?.compileOnly === true) usage = "compile";
736
+ if (collectLocations) exactLocations.push({
737
+ file,
738
+ source: source || file,
739
+ target: targetId,
740
+ line: exactLine,
741
+ character: exactCharacter,
742
+ ...(Number.isInteger(location?.range?.end?.line) ? {endLine: location.range.end.line + 1} : {}),
743
+ ...(Number.isInteger(location?.range?.end?.character) ? {endCharacter: location.range.end.character} : {}),
744
+ classification: usage,
745
+ });
746
+ if (!source || source === targetId) {
747
+ if (usage === "unknown") unclassifiedReferences++;
748
+ continue;
749
+ }
730
750
  if (usage === "unknown") {
731
751
  const evidenceKey = `${source}\0${targetId}\0${line}\0${exactCharacter}`;
732
752
  if (!evidenceSeen.has(evidenceKey)) {
@@ -798,6 +818,7 @@ export async function buildLspPrecisionOverlay({
798
818
  coverage: coverage(),
799
819
  links,
800
820
  referenceEvidence,
821
+ ...(collectLocations ? {locations: exactLocations} : {}),
801
822
  noReferenceSymbols,
802
823
  ...(errors ? {reason: `${errors} semantic request(s) failed or were refused`}
803
824
  : truncated ? {reason: "semantic precision stopped at a configured safety limit"}
@@ -0,0 +1,133 @@
1
+ import {existsSync, readFileSync, statSync} from 'node:fs'
2
+ import {dirname, resolve} from 'node:path'
3
+ import {boundedInteger} from '../bounds.js'
4
+ import {atomicWriteFileSync, withFileLock} from '../graph/file-lock.js'
5
+ import {
6
+ buildLspPrecisionOverlay,
7
+ precisionOverlayMatches,
8
+ precisionSemanticInputsMatch,
9
+ } from './lsp-overlay.js'
10
+
11
+ export const SYMBOL_PRECISION_CACHE_V = 1
12
+ export const SYMBOL_PRECISION_CACHE_FILE = 'precision-symbols.json'
13
+
14
+ const MAX_CACHE_ENTRIES = 32
15
+ const MAX_CACHE_BYTES = 8 * 1024 * 1024
16
+ const MAX_CACHE_READ_BYTES = 16 * 1024 * 1024
17
+ const inFlight = new Map()
18
+
19
+ export function symbolPrecisionCachePath(graphPath) {
20
+ return resolve(dirname(graphPath), SYMBOL_PRECISION_CACHE_FILE)
21
+ }
22
+
23
+ function loadRawGraph(graphPath) {
24
+ const graph = JSON.parse(readFileSync(graphPath, 'utf8'))
25
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.links)) {
26
+ throw new Error('the active graph is not a valid Weavatrix graph')
27
+ }
28
+ return graph
29
+ }
30
+
31
+ function emptyCache() {
32
+ return {symbolPrecisionCacheV: SYMBOL_PRECISION_CACHE_V, entries: []}
33
+ }
34
+
35
+ function readCache(path) {
36
+ if (!existsSync(path)) return emptyCache()
37
+ try {
38
+ if (statSync(path).size > MAX_CACHE_READ_BYTES) return emptyCache()
39
+ const value = JSON.parse(readFileSync(path, 'utf8'))
40
+ if (Number(value?.symbolPrecisionCacheV) !== SYMBOL_PRECISION_CACHE_V || !Array.isArray(value.entries)) return emptyCache()
41
+ return {symbolPrecisionCacheV: SYMBOL_PRECISION_CACHE_V, entries: value.entries.slice(0, MAX_CACHE_ENTRIES)}
42
+ } catch {
43
+ return emptyCache()
44
+ }
45
+ }
46
+
47
+ function writeBoundedCache(path, entries) {
48
+ const kept = entries
49
+ .filter((entry) => entry && typeof entry === 'object')
50
+ .sort((left, right) => Number(right.usedAt) - Number(left.usedAt))
51
+ .slice(0, MAX_CACHE_ENTRIES)
52
+ let body
53
+ do {
54
+ body = JSON.stringify({symbolPrecisionCacheV: SYMBOL_PRECISION_CACHE_V, entries: kept})
55
+ if (Buffer.byteLength(body) <= MAX_CACHE_BYTES || !kept.length) break
56
+ kept.pop()
57
+ } while (true)
58
+ atomicWriteFileSync(path, body, 'utf8')
59
+ }
60
+
61
+ function cacheMatch(entry, {targetId, graph, repoRoot, request}) {
62
+ if (String(entry?.targetId || '') !== targetId || !entry.overlay) return false
63
+ if (!precisionOverlayMatches(entry.overlay, graph, {request})) return false
64
+ return precisionSemanticInputsMatch(entry.overlay, repoRoot, graph)
65
+ }
66
+
67
+ async function storeEntry(path, entry) {
68
+ await withFileLock(`${path}.lock`, async () => {
69
+ const current = readCache(path)
70
+ const entries = current.entries.filter((item) => String(item?.targetId || '') !== entry.targetId)
71
+ entries.unshift(entry)
72
+ writeBoundedCache(path, entries)
73
+ })
74
+ }
75
+
76
+ function cacheable(overlay) {
77
+ if (overlay?.state === 'COMPLETE') return true
78
+ return overlay?.state === 'PARTIAL'
79
+ && overlay?.reason === 'semantic precision stopped at a configured safety limit'
80
+ }
81
+
82
+ export async function querySymbolPrecision({
83
+ repoRoot,
84
+ graphPath,
85
+ targetId,
86
+ maxReferences = 1_000,
87
+ timeoutMs = 30_000,
88
+ clientFactory,
89
+ } = {}) {
90
+ if (!repoRoot || !graphPath || !targetId) throw new Error('symbol precision requires repoRoot, graphPath, and targetId')
91
+ const boundedReferences = boundedInteger(maxReferences, 1_000, 1, 5_000)
92
+ const boundedTimeout = boundedInteger(timeoutMs, 30_000, 1_000, 60_000)
93
+ const request = {maxSymbols: 1, maxReferences: boundedReferences, maxLinks: boundedReferences}
94
+ const rawGraph = loadRawGraph(graphPath)
95
+ const graph = rawGraph.graphPrecisionMode === 'off' ? {...rawGraph, graphPrecisionMode: 'lsp'} : rawGraph
96
+ const target = graph.nodes.find((node) => String(node?.id || '') === String(targetId))
97
+ if (!target) throw new Error('the selected symbol is not present in the active raw graph')
98
+ if (!target.selection_start || !/\.(?:[cm]?[jt]sx?)$/i.test(String(target.source_file || ''))) {
99
+ throw new Error('exact symbol precision currently supports JavaScript and TypeScript symbols with source selections')
100
+ }
101
+
102
+ const id = String(targetId)
103
+ const cachePath = symbolPrecisionCachePath(graphPath)
104
+ const startedAt = Date.now()
105
+ const cached = readCache(cachePath).entries.find((entry) => cacheMatch(entry, {targetId: id, graph, repoRoot, request}))
106
+ if (cached) return {overlay: cached.overlay, cached: true, elapsedMs: Date.now() - startedAt, cachePath}
107
+
108
+ const flightKey = `${graphPath}\0${graph.graphRevision || ''}\0${id}\0${boundedReferences}`
109
+ if (inFlight.has(flightKey)) return inFlight.get(flightKey)
110
+ const operation = (async () => {
111
+ const overlay = await buildLspPrecisionOverlay({
112
+ repoRoot,
113
+ graph,
114
+ mode: 'lsp',
115
+ maxSymbols: 1,
116
+ maxReferences: boundedReferences,
117
+ maxLinks: boundedReferences,
118
+ timeoutMs: boundedTimeout,
119
+ targetIds: [id],
120
+ clientFactory,
121
+ })
122
+ if (cacheable(overlay)) {
123
+ await storeEntry(cachePath, {targetId: id, usedAt: Date.now(), overlay})
124
+ }
125
+ return {overlay, cached: false, elapsedMs: Date.now() - startedAt, cachePath}
126
+ })()
127
+ inFlight.set(flightKey, operation)
128
+ try {
129
+ return await operation
130
+ } finally {
131
+ inFlight.delete(flightKey)
132
+ }
133
+ }