weavatrix 0.2.12 → 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.
- package/README.md +90 -21
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +108 -43
- package/src/analysis/allowed-test-runner.js +20 -9
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.run.js +113 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +22 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +100 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -330
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-verified-change.mjs +12 -4
- package/src/mcp-server.mjs +49 -146
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/version.js +7 -0
- package/docs/releases/v0.2.12.md +0 -45
|
@@ -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
|
+
}
|