weavatrix 0.2.13 → 0.2.14

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.
Files changed (130) hide show
  1. package/README.md +83 -39
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +57 -38
  6. package/src/analysis/architecture/contract-graph.js +119 -0
  7. package/src/analysis/architecture/contract-schema.js +110 -0
  8. package/src/analysis/architecture/contract-storage.js +35 -0
  9. package/src/analysis/architecture/contract-verification.js +168 -0
  10. package/src/analysis/architecture-contract.js +16 -343
  11. package/src/analysis/change-classification/diff-parser.js +103 -0
  12. package/src/analysis/change-classification/options.js +40 -0
  13. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  14. package/src/analysis/change-classification.js +120 -519
  15. package/src/analysis/dead-code-review/policy.js +23 -0
  16. package/src/analysis/dead-code-review.js +9 -19
  17. package/src/analysis/dep-check.js +10 -57
  18. package/src/analysis/dep-rules.js +10 -303
  19. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  20. package/src/analysis/endpoints/common.js +62 -0
  21. package/src/analysis/endpoints/extract.js +107 -0
  22. package/src/analysis/endpoints/inventory.js +84 -0
  23. package/src/analysis/endpoints/mounts.js +129 -0
  24. package/src/analysis/endpoints-java.js +80 -3
  25. package/src/analysis/endpoints.js +3 -448
  26. package/src/analysis/git-history/analytics.js +177 -0
  27. package/src/analysis/git-history/collector.js +109 -0
  28. package/src/analysis/git-history/options.js +68 -0
  29. package/src/analysis/git-history.js +128 -577
  30. package/src/analysis/health-capabilities.js +82 -0
  31. package/src/analysis/http-contracts/analysis.js +169 -0
  32. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  33. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  34. package/src/analysis/http-contracts/graph-context.js +143 -0
  35. package/src/analysis/http-contracts/matching.js +61 -0
  36. package/src/analysis/http-contracts/shared.js +86 -0
  37. package/src/analysis/http-contracts.js +6 -822
  38. package/src/analysis/internal-audit/dependency-health.js +111 -0
  39. package/src/analysis/internal-audit/python-manifests.js +65 -0
  40. package/src/analysis/internal-audit/repo-files.js +55 -0
  41. package/src/analysis/internal-audit/supply-chain.js +132 -0
  42. package/src/analysis/internal-audit.collect.js +5 -106
  43. package/src/analysis/internal-audit.run.js +113 -200
  44. package/src/analysis/jvm-dependency-evidence.js +69 -0
  45. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  46. package/src/analysis/source-correctness.js +225 -0
  47. package/src/analysis/structure/dependency-graph.js +156 -0
  48. package/src/analysis/structure/findings.js +142 -0
  49. package/src/graph/builder/go-receiver-resolution.js +112 -0
  50. package/src/graph/builder/js/queries.js +48 -0
  51. package/src/graph/builder/lang-go.js +89 -4
  52. package/src/graph/builder/lang-js.js +4 -44
  53. package/src/graph/builder/pass2-resolution.js +68 -0
  54. package/src/graph/freshness-probe.js +2 -1
  55. package/src/graph/incremental-refresh.js +3 -2
  56. package/src/graph/internal-builder.build.js +22 -183
  57. package/src/graph/internal-builder.pass2.js +161 -0
  58. package/src/graph/internal-builder.resolvers.js +2 -105
  59. package/src/graph/resolvers/rust.js +117 -0
  60. package/src/mcp/actions/advisories.mjs +18 -0
  61. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  62. package/src/mcp/actions/graph-sync.mjs +195 -0
  63. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  64. package/src/mcp/architecture-bootstrap.mjs +168 -0
  65. package/src/mcp/architecture-starter.mjs +234 -0
  66. package/src/mcp/catalog.mjs +20 -6
  67. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  68. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  69. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  70. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  71. package/src/mcp/graph/context-core.mjs +111 -0
  72. package/src/mcp/graph/context-seeds.mjs +208 -0
  73. package/src/mcp/graph/context-state.mjs +86 -0
  74. package/src/mcp/graph/tools-core.mjs +143 -0
  75. package/src/mcp/graph/tools-query.mjs +223 -0
  76. package/src/mcp/graph-context.mjs +4 -496
  77. package/src/mcp/health/audit-format.mjs +172 -0
  78. package/src/mcp/health/audit.mjs +171 -0
  79. package/src/mcp/health/dead-code.mjs +110 -0
  80. package/src/mcp/health/duplicates.mjs +83 -0
  81. package/src/mcp/health/endpoints.mjs +42 -0
  82. package/src/mcp/health/structure.mjs +219 -0
  83. package/src/mcp/runtime-version.mjs +32 -0
  84. package/src/mcp/server/auto-refresh.mjs +66 -0
  85. package/src/mcp/server/runtime-config.mjs +43 -0
  86. package/src/mcp/server/shutdown.mjs +40 -0
  87. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  88. package/src/mcp/sync/evidence-common.mjs +88 -0
  89. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  90. package/src/mcp/sync/evidence-health.mjs +56 -0
  91. package/src/mcp/sync/evidence-packages.mjs +95 -0
  92. package/src/mcp/sync/payload-common.mjs +97 -0
  93. package/src/mcp/sync/payload-v2.mjs +53 -0
  94. package/src/mcp/sync/payload-v3.mjs +79 -0
  95. package/src/mcp/sync-evidence.mjs +7 -402
  96. package/src/mcp/sync-payload.mjs +10 -244
  97. package/src/mcp/tools-actions.mjs +18 -414
  98. package/src/mcp/tools-architecture.mjs +18 -70
  99. package/src/mcp/tools-context.mjs +56 -15
  100. package/src/mcp/tools-endpoints.mjs +4 -1
  101. package/src/mcp/tools-graph.mjs +17 -331
  102. package/src/mcp/tools-health.mjs +24 -705
  103. package/src/mcp-server.mjs +35 -146
  104. package/src/precision/lsp-client/constants.js +12 -0
  105. package/src/precision/lsp-client/environment.js +20 -0
  106. package/src/precision/lsp-client/errors.js +15 -0
  107. package/src/precision/lsp-client/lifecycle.js +127 -0
  108. package/src/precision/lsp-client/message-parser.js +81 -0
  109. package/src/precision/lsp-client/protocol.js +212 -0
  110. package/src/precision/lsp-client/registry.js +58 -0
  111. package/src/precision/lsp-client/repo-uri.js +60 -0
  112. package/src/precision/lsp-client/stdio-client.js +151 -0
  113. package/src/precision/lsp-client.js +10 -682
  114. package/src/precision/lsp-overlay/build.js +238 -0
  115. package/src/precision/lsp-overlay/contract.js +61 -0
  116. package/src/precision/lsp-overlay/reference-results.js +108 -0
  117. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  118. package/src/precision/lsp-overlay/source-session.js +134 -0
  119. package/src/precision/lsp-overlay/store.js +150 -0
  120. package/src/precision/lsp-overlay/target-index.js +147 -0
  121. package/src/precision/lsp-overlay/target-query.js +55 -0
  122. package/src/precision/lsp-overlay.js +11 -868
  123. package/src/precision/typescript-lsp-provider.js +12 -682
  124. package/src/precision/typescript-provider/client.js +69 -0
  125. package/src/precision/typescript-provider/discovery.js +124 -0
  126. package/src/precision/typescript-provider/project-host.js +249 -0
  127. package/src/precision/typescript-provider/project-safety.js +161 -0
  128. package/src/precision/typescript-provider/reference-usage.js +53 -0
  129. package/src/version.js +7 -0
  130. package/docs/releases/v0.2.13.md +0 -48
@@ -0,0 +1,238 @@
1
+ import {boundedInteger} from '../../bounds.js'
2
+ import {createTypeScriptLspClient} from '../typescript-lsp-provider.js'
3
+ import {baseOverlay} from './contract.js'
4
+ import {precisionOverlayMatches} from './contract.js'
5
+ import {collectReferenceResults} from './reference-results.js'
6
+ import {
7
+ PrecisionBudgetError,
8
+ PrecisionLimitError,
9
+ PrecisionStaleGraphError,
10
+ PrecisionStaleSemanticInputsError,
11
+ graphJavaScriptUniverse,
12
+ precisionSemanticInputs,
13
+ publicSemanticSafetyReason,
14
+ } from './semantic-inputs.js'
15
+ import {
16
+ awaitWithBudget,
17
+ ensureBudget,
18
+ initializeSourceSession,
19
+ remaining,
20
+ } from './source-session.js'
21
+ import {readPrecisionOverlay, writePrecisionOverlay} from './store.js'
22
+ import {eligibleTargets, symbolIndex} from './target-index.js'
23
+ import {queryPrecisionTarget} from './target-query.js'
24
+
25
+ function persist(session, overlay) {
26
+ return session.graphPath ? writePrecisionOverlay(session.graphPath, overlay) : overlay
27
+ }
28
+
29
+ function coverage(session, verifiedEdges = session.links.length) {
30
+ return {
31
+ candidates: session.eligible.total,
32
+ selected: session.targets.length,
33
+ queried: session.queried,
34
+ references: session.references,
35
+ unclassifiedReferences: session.unclassifiedReferences,
36
+ verifiedEdges,
37
+ truncated: session.truncated,
38
+ }
39
+ }
40
+
41
+ function completedOverlay(session) {
42
+ const state = session.errors || session.truncated || session.unclassifiedReferences
43
+ ? 'PARTIAL' : 'COMPLETE'
44
+ return baseOverlay(session.graph, state, {
45
+ request: session.request,
46
+ engines: [{
47
+ provider: session.client.provider || 'typescript-language-server',
48
+ version: session.client.version || null,
49
+ typescriptVersion: session.client.typescriptVersion || null,
50
+ typescriptSource: session.client.typescriptSource || null,
51
+ language: 'typescript/javascript',
52
+ capability: 'textDocument/references',
53
+ status: state,
54
+ }],
55
+ semanticInputFingerprint: session.semanticInputs.fingerprint,
56
+ coverage: coverage(session),
57
+ links: session.links,
58
+ referenceEvidence: session.referenceEvidence,
59
+ ...(session.collectLocations ? {locations: session.exactLocations} : {}),
60
+ noReferenceSymbols: session.noReferenceSymbols,
61
+ ...(session.errors ? {reason: `${session.errors} semantic request(s) failed or were refused`}
62
+ : session.truncated ? {reason: 'semantic precision stopped at a configured safety limit'}
63
+ : session.unclassifiedReferences
64
+ ? {reason: 'some exact references could not be classified as runtime or type-only'} : {}),
65
+ })
66
+ }
67
+
68
+ function failedOverlay(session, error) {
69
+ const stale = error instanceof PrecisionStaleGraphError
70
+ const semanticInputsChanged = error instanceof PrecisionStaleSemanticInputsError
71
+ const deadlineReached = error instanceof PrecisionBudgetError || remaining(session) <= 0
72
+ const state = stale || semanticInputsChanged || deadlineReached ? 'PARTIAL' : 'UNAVAILABLE'
73
+ return baseOverlay(session.graph, state, {
74
+ request: session.request,
75
+ reason: stale
76
+ ? 'repository content no longer matched the graph snapshot'
77
+ : semanticInputsChanged ? 'TypeScript project inputs changed while semantic precision was running'
78
+ : deadlineReached ? 'semantic precision stopped at its global deadline'
79
+ : error?.name === 'LspTimeoutError' ? 'bundled TypeScript language server timed out'
80
+ : 'bundled TypeScript language server was unavailable',
81
+ engines: [{
82
+ provider: 'typescript-language-server',
83
+ version: null,
84
+ language: 'typescript/javascript',
85
+ capability: 'textDocument/references',
86
+ status: state,
87
+ }],
88
+ coverage: {
89
+ candidates: session.eligible.total,
90
+ selected: session.targets.length,
91
+ queried: stale || semanticInputsChanged ? 0 : session.queried,
92
+ references: stale || semanticInputsChanged ? 0 : session.references,
93
+ unclassifiedReferences: stale || semanticInputsChanged ? 0 : session.unclassifiedReferences,
94
+ verifiedEdges: 0,
95
+ truncated: session.truncated || stale || semanticInputsChanged || deadlineReached,
96
+ },
97
+ links: [],
98
+ noReferenceSymbols: [],
99
+ })
100
+ }
101
+
102
+ function createSession(options) {
103
+ const boundedMax = boundedInteger(options.maxSymbols, 32, 1, 64)
104
+ const boundedReferences = boundedInteger(options.maxReferences, 2_048, 1, 16_384)
105
+ const boundedLinks = boundedInteger(options.maxLinks, 2_048, 1, 16_384)
106
+ const boundedTimeout = boundedInteger(options.timeoutMs, 45_000, 100, 60_000)
107
+ return {
108
+ ...options,
109
+ boundedMax,
110
+ boundedReferences,
111
+ boundedLinks,
112
+ deadline: Date.now() + boundedTimeout,
113
+ request: {maxSymbols: boundedMax, maxReferences: boundedReferences, maxLinks: boundedLinks},
114
+ links: [],
115
+ seen: new Set(),
116
+ evidenceSeen: new Set(),
117
+ queried: 0,
118
+ references: 0,
119
+ unclassifiedReferences: 0,
120
+ errors: 0,
121
+ truncated: false,
122
+ noReferenceSymbols: [],
123
+ referenceEvidence: [],
124
+ exactLocations: [],
125
+ collectLocations: Array.isArray(options.targetIds) && options.targetIds.length > 0,
126
+ stop: false,
127
+ }
128
+ }
129
+
130
+ export async function buildLspPrecisionOverlay({
131
+ repoRoot,
132
+ graph,
133
+ graphPath,
134
+ mode = 'lsp',
135
+ maxSymbols = Number(process.env.WEAVATRIX_PRECISION_MAX_SYMBOLS) || 32,
136
+ maxReferences = Number(process.env.WEAVATRIX_PRECISION_MAX_REFERENCES) || 2_048,
137
+ maxLinks = Number(process.env.WEAVATRIX_PRECISION_MAX_LINKS) || 2_048,
138
+ timeoutMs = Number(process.env.WEAVATRIX_PRECISION_TIMEOUT_MS) || 45_000,
139
+ targetIds,
140
+ clientFactory,
141
+ } = {}) {
142
+ if (!graph || !repoRoot) throw new Error('precision overlay requires repoRoot and graph')
143
+ const session = createSession({
144
+ repoRoot, graph, graphPath, mode, maxSymbols, maxReferences, maxLinks,
145
+ timeoutMs, targetIds, clientFactory,
146
+ })
147
+ if (mode === 'off') {
148
+ return persist(session, baseOverlay(graph, 'OFF', {
149
+ request: session.request,
150
+ reason: 'precision disabled by request',
151
+ }))
152
+ }
153
+ session.universe = graphJavaScriptUniverse(graph)
154
+ if (!session.universe.files.length) {
155
+ return persist(session, baseOverlay(graph, 'UNAVAILABLE', {
156
+ request: session.request,
157
+ reason: 'semantic precision currently supports JavaScript and TypeScript repositories',
158
+ }))
159
+ }
160
+ session.semanticInputs = precisionSemanticInputs(repoRoot, graph, {deadline: session.deadline})
161
+ if (!session.semanticInputs.safe) {
162
+ return persist(session, baseOverlay(graph, 'UNAVAILABLE', {
163
+ request: session.request,
164
+ reason: publicSemanticSafetyReason(session.semanticInputs.reason),
165
+ engines: [{
166
+ provider: 'typescript-language-server', version: null,
167
+ language: 'typescript/javascript', capability: 'textDocument/references',
168
+ status: 'UNAVAILABLE',
169
+ }],
170
+ }))
171
+ }
172
+ if (graphPath) {
173
+ const cached = readPrecisionOverlay(graphPath, graph)
174
+ if (cached?.state === 'COMPLETE'
175
+ && precisionOverlayMatches(cached, graph, {request: session.request})
176
+ && cached.semanticInputFingerprint === session.semanticInputs.fingerprint) return cached
177
+ }
178
+ session.eligible = eligibleTargets(graph, session.boundedMax, targetIds)
179
+ session.targets = session.eligible.targets
180
+ if (!session.targets.length) {
181
+ return persist(session, baseOverlay(graph, 'COMPLETE', {
182
+ request: session.request,
183
+ semanticInputFingerprint: session.semanticInputs.fingerprint,
184
+ reason: 'no eligible JavaScript/TypeScript semantic targets',
185
+ }))
186
+ }
187
+ session.truncated = session.eligible.total > session.boundedMax
188
+ session.index = symbolIndex(graph)
189
+ session.nodesById = new Map((graph.nodes || []).map((node) => [String(node.id), node]))
190
+ initializeSourceSession(session)
191
+ try {
192
+ const makeClient = clientFactory || createTypeScriptLspClient
193
+ session.client = await awaitWithBudget(
194
+ session,
195
+ () => makeClient({repoRoot, timeoutMs: Math.max(100, remaining(session))}),
196
+ )
197
+ for (const target of session.targets) {
198
+ let locations
199
+ try {
200
+ locations = await queryPrecisionTarget(session, target)
201
+ } catch (error) {
202
+ if (error instanceof PrecisionStaleGraphError) throw error
203
+ if (error instanceof PrecisionBudgetError || error instanceof PrecisionLimitError
204
+ || remaining(session) <= 0) {
205
+ session.truncated = true
206
+ session.stop = true
207
+ break
208
+ }
209
+ session.errors++
210
+ continue
211
+ }
212
+ collectReferenceResults(session, target, locations)
213
+ if (remaining(session) <= 0) {
214
+ session.truncated = true
215
+ session.stop = true
216
+ }
217
+ if (session.stop) break
218
+ }
219
+ ensureBudget(session)
220
+ const semanticInputsAfter = precisionSemanticInputs(repoRoot, graph, {deadline: session.deadline})
221
+ ensureBudget(session)
222
+ if (!semanticInputsAfter.safe
223
+ || semanticInputsAfter.fingerprint !== session.semanticInputs.fingerprint) {
224
+ throw new PrecisionStaleSemanticInputsError()
225
+ }
226
+ return persist(session, completedOverlay(session))
227
+ } catch (error) {
228
+ return persist(session, failedOverlay(session, error))
229
+ } finally {
230
+ if (session.client) {
231
+ const closeBudget = Math.min(2_000, Math.max(0, remaining(session)))
232
+ if (closeBudget > 0 && session.client.close) {
233
+ try { await awaitWithBudget(session, () => session.client.close(closeBudget)) }
234
+ catch { session.client.kill?.() }
235
+ } else session.client.kill?.()
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,61 @@
1
+ import {typeScriptLspContract} from '../typescript-lsp-provider.js'
2
+
3
+ export const PRECISION_OVERLAY_V = 3
4
+ export const PRECISION_FILE = 'precision.json'
5
+ export const JS_TS_FILE = /\.(?:[cm]?[jt]sx?)$/i
6
+ export const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value)
7
+ export const norm = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
8
+ export const graphMode = (graph) => ['full', 'no-tests', 'tests-only'].includes(graph?.graphBuildMode)
9
+ ? graph.graphBuildMode : 'full'
10
+ export const graphScope = (graph) => String(graph?.graphBuildScope || '')
11
+ export const precisionMode = (graph) => graph?.graphPrecisionMode === 'off' ? 'off' : 'lsp'
12
+ export const providerContractFor = (graph) => precisionMode(graph) === 'off' ? 'off' : typeScriptLspContract()
13
+ export const graphContractFor = (graph) => ({
14
+ extractorSchemaV: Number(graph?.extractorSchemaV) || 0,
15
+ ...(graph?.repositoryFreshnessBuilderVersion != null
16
+ ? {repositoryFreshnessBuilderVersion: String(graph.repositoryFreshnessBuilderVersion)} : {}),
17
+ ...(graph?.graphBuilderVersion != null ? {graphBuilderVersion: String(graph.graphBuilderVersion)} : {}),
18
+ ...(graph?.internalBuilderVersion != null ? {internalBuilderVersion: String(graph.internalBuilderVersion)} : {}),
19
+ })
20
+
21
+ function sameRequest(actual, expected) {
22
+ if (!expected) return true
23
+ return Number(actual?.maxSymbols) === Number(expected.maxSymbols)
24
+ && Number(actual?.maxReferences) === Number(expected.maxReferences)
25
+ && Number(actual?.maxLinks) === Number(expected.maxLinks)
26
+ }
27
+
28
+ export function precisionOverlayMatches(overlay, graph, {request} = {}) {
29
+ const actualContract = overlay?.graphContract
30
+ return Number(overlay?.precisionOverlayV) === PRECISION_OVERLAY_V
31
+ && String(overlay?.baseGraphRevision || '') === String(graph?.graphRevision || '')
32
+ && String(overlay?.graphBuildMode || 'full') === graphMode(graph)
33
+ && String(overlay?.graphBuildScope || '') === graphScope(graph)
34
+ && String(overlay?.precisionMode || '') === precisionMode(graph)
35
+ && String(overlay?.providerContract || '') === providerContractFor(graph)
36
+ && actualContract != null && typeof actualContract === 'object'
37
+ && JSON.stringify(actualContract) === JSON.stringify(graphContractFor(graph))
38
+ && sameRequest(overlay?.request, request)
39
+ }
40
+
41
+ export function baseOverlay(graph, state, extra = {}) {
42
+ return {
43
+ precisionOverlayV: PRECISION_OVERLAY_V,
44
+ baseGraphRevision: String(graph.graphRevision || ''),
45
+ graphBuildMode: graphMode(graph),
46
+ graphBuildScope: graphScope(graph),
47
+ precisionMode: precisionMode(graph),
48
+ providerContract: providerContractFor(graph),
49
+ graphContract: graphContractFor(graph),
50
+ state,
51
+ engines: [],
52
+ coverage: {
53
+ candidates: 0, selected: 0, queried: 0, references: 0,
54
+ unclassifiedReferences: 0, verifiedEdges: 0, truncated: false,
55
+ },
56
+ links: [],
57
+ referenceEvidence: [],
58
+ noReferenceSymbols: [],
59
+ ...extra,
60
+ }
61
+ }
@@ -0,0 +1,108 @@
1
+ import {classifyTypeScriptReferenceUsage} from '../typescript-lsp-provider.js'
2
+ import {endpoint, norm} from './contract.js'
3
+ import {locationStart, repoFileFromLocation, sourceAt} from './target-index.js'
4
+ import {remaining, sourceForClassification} from './source-session.js'
5
+
6
+ function moduleDependencyAt(session, source, file, targetFile, exactLine) {
7
+ if (source !== file || !targetFile) return null
8
+ return (session.graph.links || []).find((link) => endpoint(link.source) === file
9
+ && endpoint(link.target) === targetFile
10
+ && ['imports', 're_exports'].includes(String(link.relation || ''))
11
+ && Number.isInteger(link.line) && link.line === exactLine)
12
+ }
13
+
14
+ function addUnknownEvidence(session, source, targetId, line, character) {
15
+ const key = `${source}\0${targetId}\0${line}\0${character}`
16
+ if (session.evidenceSeen.has(key)) return true
17
+ if (session.referenceEvidence.length >= session.boundedLinks) {
18
+ session.truncated = true
19
+ session.stop = true
20
+ return false
21
+ }
22
+ session.evidenceSeen.add(key)
23
+ session.unclassifiedReferences++
24
+ session.referenceEvidence.push({
25
+ source,
26
+ target: targetId,
27
+ line,
28
+ character,
29
+ classification: 'unknown',
30
+ provider: 'typescript-language-server',
31
+ })
32
+ return true
33
+ }
34
+
35
+ function addExactLink(session, targetId, source, location, line, character, usage, moduleDependency) {
36
+ const relation = 'references'
37
+ const key = `${source}\0${relation}\0${targetId}\0${line}\0${character}`
38
+ if (session.seen.has(key)) return true
39
+ if (session.links.length >= session.boundedLinks) {
40
+ session.truncated = true
41
+ session.stop = true
42
+ return false
43
+ }
44
+ session.seen.add(key)
45
+ session.links.push({
46
+ source,
47
+ target: targetId,
48
+ relation,
49
+ line,
50
+ character,
51
+ ...(Number.isInteger(location?.range?.end?.line) ? {endLine: location.range.end.line + 1} : {}),
52
+ ...(Number.isInteger(location?.range?.end?.character) ? {endCharacter: location.range.end.character} : {}),
53
+ provenance: 'EXACT_LSP',
54
+ provider: 'typescript-language-server',
55
+ ...(usage === 'type' || moduleDependency?.typeOnly === true ? {typeOnly: true} : {}),
56
+ ...(usage === 'compile' || moduleDependency?.compileOnly === true ? {compileOnly: true} : {}),
57
+ })
58
+ return true
59
+ }
60
+
61
+ export function collectReferenceResults(session, target, locations) {
62
+ for (const location of locations) {
63
+ if (remaining(session) <= 0 || session.references >= session.boundedReferences) {
64
+ session.truncated = true
65
+ session.stop = true
66
+ break
67
+ }
68
+ session.references++
69
+ const file = repoFileFromLocation(session.repoRoot, location)
70
+ const start = locationStart(location)
71
+ if (!file || !start || !Number.isInteger(start.line) || !Number.isInteger(start.character)) continue
72
+ const source = sourceAt(session.index, file, start)
73
+ const targetId = String(target.id)
74
+ const exactLine = start.line + 1
75
+ const character = start.character
76
+ const targetFile = norm(target.source_file)
77
+ const moduleDependency = moduleDependencyAt(
78
+ session, source, file, targetFile, exactLine,
79
+ )
80
+ const sourceText = sourceForClassification(session, file)
81
+ let usage = sourceText == null
82
+ ? 'unknown'
83
+ : classifyTypeScriptReferenceUsage(file, sourceText, start)
84
+ if (usage === 'unknown' && moduleDependency?.typeOnly === true) usage = 'type'
85
+ if (usage === 'unknown' && moduleDependency?.compileOnly === true) usage = 'compile'
86
+ if (session.collectLocations) session.exactLocations.push({
87
+ file,
88
+ source: source || file,
89
+ target: targetId,
90
+ line: exactLine,
91
+ character,
92
+ ...(Number.isInteger(location?.range?.end?.line) ? {endLine: location.range.end.line + 1} : {}),
93
+ ...(Number.isInteger(location?.range?.end?.character) ? {endCharacter: location.range.end.character} : {}),
94
+ classification: usage,
95
+ })
96
+ if (!source || source === targetId) {
97
+ if (usage === 'unknown') session.unclassifiedReferences++
98
+ continue
99
+ }
100
+ if (usage === 'unknown') {
101
+ if (!addUnknownEvidence(session, source, targetId, exactLine, character)) break
102
+ continue
103
+ }
104
+ if (!addExactLink(
105
+ session, targetId, source, location, exactLine, character, usage, moduleDependency,
106
+ )) break
107
+ }
108
+ }
@@ -0,0 +1,62 @@
1
+ import {typeScriptProjectSafety} from '../typescript-lsp-provider.js'
2
+ import {JS_TS_FILE, graphMode, graphScope, norm} from './contract.js'
3
+
4
+ export class PrecisionBudgetError extends Error {
5
+ constructor(message = 'semantic precision deadline reached') {
6
+ super(message)
7
+ this.name = 'PrecisionBudgetError'
8
+ }
9
+ }
10
+
11
+ export class PrecisionLimitError extends Error {
12
+ constructor(message) { super(message); this.name = 'PrecisionLimitError' }
13
+ }
14
+
15
+ export class PrecisionStaleGraphError extends Error {
16
+ constructor() {
17
+ super('repository content did not match the graph snapshot')
18
+ this.name = 'PrecisionStaleGraphError'
19
+ }
20
+ }
21
+
22
+ export class PrecisionStaleSemanticInputsError extends Error {
23
+ constructor() {
24
+ super('TypeScript project inputs changed while semantic precision was running')
25
+ this.name = 'PrecisionStaleSemanticInputsError'
26
+ }
27
+ }
28
+
29
+ export function graphJavaScriptUniverse(graph) {
30
+ const files = [...new Set((graph.nodes || []).filter((node) => {
31
+ const id = String(node?.id || '')
32
+ const file = norm(node?.source_file || id)
33
+ return id === file && JS_TS_FILE.test(file)
34
+ }).map((node) => norm(node.source_file || node.id)))].sort()
35
+ const hashed = Object.keys(graph.fileHashes || {}).map(norm).filter((file) => JS_TS_FILE.test(file)).sort()
36
+ const fileSet = new Set(files)
37
+ const complete = graphMode(graph) === 'full' && !graphScope(graph)
38
+ && files.length === hashed.length && hashed.every((file) => fileSet.has(file))
39
+ return {files, complete}
40
+ }
41
+
42
+ export function precisionSemanticInputs(repoRoot, graph, options = {}) {
43
+ const universe = graphJavaScriptUniverse(graph)
44
+ if (!universe.files.length) {
45
+ return {safe: false, reason: 'NO_JAVASCRIPT_TYPESCRIPT_INPUTS', fingerprint: null, universe}
46
+ }
47
+ return {...typeScriptProjectSafety(repoRoot, universe.files, options), universe}
48
+ }
49
+
50
+ export function precisionSemanticInputsMatch(overlay, repoRoot, graph) {
51
+ const current = precisionSemanticInputs(repoRoot, graph)
52
+ return current.safe === true
53
+ && typeof current.fingerprint === 'string'
54
+ && current.fingerprint.length > 0
55
+ && String(overlay?.semanticInputFingerprint || '') === current.fingerprint
56
+ }
57
+
58
+ export function publicSemanticSafetyReason(reason) {
59
+ return reason === 'CONFIGURED_TSSERVER_PLUGINS'
60
+ ? 'configured TypeScript language-service plugins are not allowed'
61
+ : 'TypeScript project configuration could not be verified safely'
62
+ }
@@ -0,0 +1,134 @@
1
+ import {createHash} from 'node:crypto'
2
+ import {readFileSync, statSync} from 'node:fs'
3
+ import {createRepoBoundary} from '../../repo-path.js'
4
+ import {norm} from './contract.js'
5
+ import {
6
+ PrecisionBudgetError,
7
+ PrecisionLimitError,
8
+ PrecisionStaleGraphError,
9
+ } from './semantic-inputs.js'
10
+
11
+ export function initializeSourceSession(session) {
12
+ session.boundary = createRepoBoundary(session.repoRoot)
13
+ session.opened = new Set()
14
+ session.openedTexts = new Map()
15
+ session.classificationTexts = new Map()
16
+ session.classificationBytes = 0
17
+ session.openedBytes = 0
18
+ session.fullUniverseOpened = false
19
+ }
20
+
21
+ export const remaining = (session) => session.deadline - Date.now()
22
+
23
+ export function ensureBudget(session) {
24
+ if (remaining(session) <= 0) throw new PrecisionBudgetError()
25
+ }
26
+
27
+ export function awaitWithBudget(session, operation) {
28
+ ensureBudget(session)
29
+ const wait = remaining(session)
30
+ return new Promise((resolvePromise, rejectPromise) => {
31
+ const timer = setTimeout(() => rejectPromise(new PrecisionBudgetError()), wait)
32
+ Promise.resolve().then(operation).then(
33
+ (value) => { clearTimeout(timer); resolvePromise(value) },
34
+ (error) => { clearTimeout(timer); rejectPromise(error) },
35
+ )
36
+ })
37
+ }
38
+
39
+ function verifiedSource(session, relPath, maxBytes = 4 * 1024 * 1024) {
40
+ const file = norm(relPath)
41
+ const expectedHash = session.graph.fileHashes?.[file]
42
+ if (!file || !/^[a-f0-9]{64}$/i.test(String(expectedHash || ''))) {
43
+ throw new PrecisionStaleGraphError()
44
+ }
45
+ const resolvedFile = session.boundary.resolve(file)
46
+ if (!resolvedFile.ok) throw new PrecisionStaleGraphError()
47
+ let size
48
+ try { size = statSync(resolvedFile.path).size }
49
+ catch { throw new PrecisionStaleGraphError() }
50
+ if (size > maxBytes) throw new PrecisionLimitError('precision source-read budget reached')
51
+ let body
52
+ try { body = readFileSync(resolvedFile.path) }
53
+ catch { throw new PrecisionStaleGraphError() }
54
+ if (body.byteLength > maxBytes) throw new PrecisionLimitError('precision source-read budget reached')
55
+ if (createHash('sha256').update(body).digest('hex') !== expectedHash) {
56
+ throw new PrecisionStaleGraphError()
57
+ }
58
+ return {file, body, bytes: body.byteLength, text: body.toString('utf8')}
59
+ }
60
+
61
+ export async function ensureOpen(session, relPath) {
62
+ const file = norm(relPath)
63
+ if (!file || session.opened.has(file)) return
64
+ ensureBudget(session)
65
+ if (session.opened.size >= 96) throw new PrecisionLimitError('precision open-document limit reached')
66
+ const maxBytes = Math.min(4 * 1024 * 1024, 32 * 1024 * 1024 - session.openedBytes)
67
+ const {bytes, text} = verifiedSource(session, file, maxBytes)
68
+ if (bytes > 4 * 1024 * 1024) throw new PrecisionLimitError('precision document exceeds 4 MiB limit')
69
+ if (session.openedBytes + bytes > 32 * 1024 * 1024) {
70
+ throw new PrecisionLimitError('precision source-transfer budget reached')
71
+ }
72
+ await awaitWithBudget(session, () => session.client.openDocument(file, text))
73
+ session.opened.add(file)
74
+ session.openedTexts.set(file, text)
75
+ session.openedBytes += bytes
76
+ }
77
+
78
+ export function sourceForClassification(session, relPath) {
79
+ const file = norm(relPath)
80
+ if (session.openedTexts.has(file)) return session.openedTexts.get(file)
81
+ if (session.classificationTexts.has(file)) return session.classificationTexts.get(file)
82
+ if (session.classificationTexts.size >= 96) return null
83
+ let source
84
+ try {
85
+ source = verifiedSource(
86
+ session,
87
+ file,
88
+ Math.min(4 * 1024 * 1024, 32 * 1024 * 1024 - session.classificationBytes),
89
+ )
90
+ } catch (error) {
91
+ if (error instanceof PrecisionLimitError) return null
92
+ throw error
93
+ }
94
+ session.classificationTexts.set(file, source.text)
95
+ session.classificationBytes += source.bytes
96
+ return source.text
97
+ }
98
+
99
+ export async function ensureFullUniverse(session) {
100
+ if (session.fullUniverseOpened) return true
101
+ if (!session.universe.complete) return false
102
+ const additional = session.universe.files.filter((file) => !session.opened.has(file))
103
+ if (session.opened.size + additional.length > 96) {
104
+ session.truncated = true
105
+ return false
106
+ }
107
+ let projectedBytes = session.openedBytes
108
+ for (const file of additional) {
109
+ ensureBudget(session)
110
+ if (!/^[a-f0-9]{64}$/i.test(String(session.graph.fileHashes?.[file] || ''))) {
111
+ throw new PrecisionStaleGraphError()
112
+ }
113
+ const resolvedFile = session.boundary.resolve(file)
114
+ if (!resolvedFile.ok) throw new PrecisionStaleGraphError()
115
+ let bytes
116
+ try { bytes = statSync(resolvedFile.path).size }
117
+ catch { throw new PrecisionStaleGraphError() }
118
+ if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes > 32 * 1024 * 1024) {
119
+ session.truncated = true
120
+ return false
121
+ }
122
+ projectedBytes += bytes
123
+ }
124
+ for (const file of additional) await ensureOpen(session, file)
125
+ session.fullUniverseOpened = session.universe.files.every((file) => session.opened.has(file))
126
+ return session.fullUniverseOpened
127
+ }
128
+
129
+ export function requestReferences(session, relPath, position) {
130
+ return awaitWithBudget(
131
+ session,
132
+ () => session.client.references(relPath, position, false, Math.max(1, remaining(session))),
133
+ )
134
+ }