weavatrix 0.2.15 → 0.2.16
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 +53 -13
- package/docs/releases/v0.2.16.md +53 -0
- package/package.json +6 -2
- package/src/analysis/cargo-dependency-evidence.js +111 -0
- package/src/analysis/cargo-manifests.js +74 -0
- package/src/analysis/dep-check-ecosystems.js +3 -0
- package/src/analysis/endpoints-java.js +2 -5
- package/src/analysis/findings.js +12 -0
- package/src/analysis/go-dependency-evidence.js +68 -0
- package/src/analysis/health-capabilities.js +1 -1
- package/src/analysis/http-contracts/analysis.js +2 -0
- package/src/analysis/http-contracts/client-call-detection.js +1 -0
- package/src/analysis/http-contracts/client-call-parser.js +18 -4
- package/src/analysis/internal-audit/dependency-health.js +15 -13
- package/src/analysis/internal-audit/python-manifests.js +18 -5
- package/src/analysis/internal-audit/supply-chain.js +2 -0
- package/src/analysis/internal-audit.run.js +12 -7
- package/src/analysis/jvm-dependency-evidence.js +102 -56
- package/src/analysis/jvm-manifests.js +114 -0
- package/src/analysis/source-correctness.js +2 -5
- package/src/analysis/transport-contracts.js +157 -0
- package/src/graph/builder/lang-go.js +4 -2
- package/src/graph/builder/lang-java.js +12 -3
- package/src/graph/builder/lang-rust.js +22 -3
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/internal-builder.build.js +2 -2
- package/src/graph/internal-builder.resolvers.js +18 -8
- package/src/mcp/actions/advisories.mjs +2 -3
- package/src/mcp/catalog.mjs +7 -4
- package/src/mcp/company-contract-verdict.mjs +42 -0
- package/src/mcp/tools-company.mjs +28 -53
- package/src/mcp/tools-impact-precision.mjs +89 -0
- package/src/mcp/tools-impact.mjs +52 -94
- package/src/precision/lsp-overlay/build.js +10 -0
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/symbol-query.js +39 -0
- package/src/precision/typescript-provider/client.js +16 -3
- package/src/precision/typescript-provider/discovery.js +1 -1
- package/src/precision/typescript-provider/isolated-runtime.js +39 -0
- package/src/precision/typescript-provider/project-host.js +11 -6
- package/src/precision/typescript-provider/project-safety.js +5 -0
- package/src/security/advisory-store.js +2 -2
- package/src/security/installed-jvm-rust.js +46 -0
- package/src/security/installed.js +21 -1
- package/src/util.js +8 -0
- package/docs/releases/v0.2.15.md +0 -37
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
// can never pass an arbitrary filesystem path through this tool.
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {analyzeHttpContracts} from '../analysis/http-contracts.js'
|
|
6
|
+
import {analyzeTransportContracts} from '../analysis/transport-contracts.js'
|
|
6
7
|
import {buildGraphForRepo} from '../build-graph.js'
|
|
7
8
|
import {graphHomeDir} from '../graph/layout.js'
|
|
8
9
|
import {liveRepositoryRecords} from '../graph/repo-registry.js'
|
|
9
10
|
import {loadGraph} from './graph-context.mjs'
|
|
10
11
|
import {toolResult} from './tool-result.mjs'
|
|
12
|
+
import {contractVerdict, contractVerdictLine} from './company-contract-verdict.mjs'
|
|
11
13
|
|
|
12
|
-
const CROSS_REPO_HTTP_CONTRACT_V =
|
|
14
|
+
const CROSS_REPO_HTTP_CONTRACT_V = 3
|
|
13
15
|
const selectorText = (value) => String(value ?? '').trim()
|
|
14
16
|
|
|
15
17
|
function selectRecord(records, selector) {
|
|
@@ -103,52 +105,6 @@ async function reconcileGraph(record, alias, role, graphHome) {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
function verdictFor(analysis) {
|
|
107
|
-
const endpointsWithCallers = analysis.endpoints.filter((endpoint) => endpoint.callsites.length > 0)
|
|
108
|
-
const affectedFiles = new Set()
|
|
109
|
-
const affectedScreens = new Set()
|
|
110
|
-
for (const endpoint of endpointsWithCallers) {
|
|
111
|
-
for (const item of endpoint.affected.files || []) affectedFiles.add(`${item.client}\0${item.file}`)
|
|
112
|
-
for (const item of endpoint.affected.screens || []) affectedScreens.add(`${item.client}\0${item.file}`)
|
|
113
|
-
}
|
|
114
|
-
let code = 'NO_ENDPOINTS_MATCHED', risk = 'unknown'
|
|
115
|
-
if (analysis.totals.endpoints > 0 && analysis.totals.matches === 0) {
|
|
116
|
-
code = analysis.totals.methodMismatches > 0 ? 'HTTP_METHOD_MISMATCH' : 'NO_STATIC_CLIENT_CALLERS'
|
|
117
|
-
risk = analysis.totals.methodMismatches > 0 ? 'high' : 'unknown'
|
|
118
|
-
} else if (analysis.totals.matches > 0 && analysis.totals.methodMismatches > 0) {
|
|
119
|
-
code = 'CLIENTS_AT_RISK_WITH_METHOD_MISMATCHES'; risk = 'high'
|
|
120
|
-
} else if (analysis.totals.matches > 0) {
|
|
121
|
-
code = 'CLIENTS_AT_RISK'; risk = 'medium'
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
code,
|
|
125
|
-
risk,
|
|
126
|
-
endpointsWithCallers: endpointsWithCallers.length,
|
|
127
|
-
callsites: analysis.totals.matches,
|
|
128
|
-
affectedFiles: affectedFiles.size,
|
|
129
|
-
affectedScreens: affectedScreens.size,
|
|
130
|
-
methodMismatches: analysis.totals.methodMismatches,
|
|
131
|
-
uncertainCalls: analysis.totals.uncertainCalls,
|
|
132
|
-
notDeadExternalUse: analysis.totals.notDeadExternalUse,
|
|
133
|
-
notDeadExternalHandlers: analysis.totals.notDeadExternalHandlers,
|
|
134
|
-
possibleExternalUse: analysis.totals.possibleExternalUse,
|
|
135
|
-
unknownLiveness: analysis.totals.unknownLiveness,
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function verdictLine(verdict, endpoints) {
|
|
140
|
-
if (verdict.code === 'NO_ENDPOINTS_MATCHED') {
|
|
141
|
-
return 'VERDICT NO_ENDPOINTS_MATCHED — no backend endpoint satisfied the requested method/path/change filter.'
|
|
142
|
-
}
|
|
143
|
-
if (verdict.code === 'NO_STATIC_CLIENT_CALLERS') {
|
|
144
|
-
return `VERDICT NO_STATIC_CLIENT_CALLERS — ${endpoints} backend endpoint(s) matched, but no bounded literal/template client call was proven; this is unknown, not proof of no consumers.`
|
|
145
|
-
}
|
|
146
|
-
if (verdict.code === 'HTTP_METHOD_MISMATCH') {
|
|
147
|
-
return `VERDICT HTTP_METHOD_MISMATCH — ${verdict.methodMismatches} client call(s) match the route shape with a different method.`
|
|
148
|
-
}
|
|
149
|
-
return `VERDICT ${verdict.code} — ${verdict.callsites} callsite(s) reach ${verdict.endpointsWithCallers} endpoint(s); ${verdict.affectedScreens} screen(s) and ${verdict.affectedFiles} file(s) are in the bounded blast radius.`
|
|
150
|
-
}
|
|
151
|
-
|
|
152
108
|
export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
153
109
|
const graphHome = ctx.graphHome || graphHomeDir()
|
|
154
110
|
const records = liveRepositoryRecords(graphHome)
|
|
@@ -215,7 +171,7 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
215
171
|
}
|
|
216
172
|
|
|
217
173
|
try {
|
|
218
|
-
|
|
174
|
+
let analysis = analyzeHttpContracts({
|
|
219
175
|
backend: {id: backendAlias, repoRoot: backend.repoPath, graph: backendGraph.graph},
|
|
220
176
|
clients: clientGraphs.map((item) => ({id: item.alias, repoRoot: item.record.repoPath, graph: item.graph})),
|
|
221
177
|
method: args.method,
|
|
@@ -229,9 +185,25 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
229
185
|
maxEndpoints: args.max_endpoints,
|
|
230
186
|
maxMatches: args.max_matches,
|
|
231
187
|
maxAffectedFiles: args.max_affected_files,
|
|
188
|
+
runtimeValues: args.runtime_config,
|
|
232
189
|
})
|
|
233
|
-
const
|
|
234
|
-
|
|
190
|
+
const selectedTransport = ['all', 'http', 'graphql', 'grpc', 'event'].includes(args.transport) ? args.transport : 'all'
|
|
191
|
+
if (!['all', 'http'].includes(selectedTransport)) analysis = {
|
|
192
|
+
...analysis, status: 'complete', completeness: {complete: true, reasons: []}, endpoints: [], uncertain: [],
|
|
193
|
+
totals: {...analysis.totals, endpoints: 0, clientCalls: 0, matches: 0, methodMismatches: 0, uncertainCalls: 0, notDeadExternalUse: 0, notDeadExternalHandlers: 0, possibleExternalUse: 0, unknownLiveness: 0},
|
|
194
|
+
}
|
|
195
|
+
const transportAnalysis = selectedTransport === 'http'
|
|
196
|
+
? {transportContractsV: 1, transport: 'http', status: 'COMPLETE', completeness: {complete: true, reasons: []}, totals: {contracts: 0, matches: 0, uncertain: 0, filesScanned: 0}, contracts: [], uncertain: []}
|
|
197
|
+
: analyzeTransportContracts({
|
|
198
|
+
backend: {id: backendAlias, repoRoot: backend.repoPath, graph: backendGraph.graph},
|
|
199
|
+
clients: clientGraphs.map((item) => ({id: item.alias, repoRoot: item.record.repoPath, graph: item.graph})),
|
|
200
|
+
transport: selectedTransport === 'all' ? 'all' : selectedTransport,
|
|
201
|
+
includeTests: args.include_tests === true,
|
|
202
|
+
maxImpactDepth: args.max_impact_depth,
|
|
203
|
+
maxAffectedFiles: args.max_affected_files,
|
|
204
|
+
})
|
|
205
|
+
const verdict = contractVerdict(analysis, transportAnalysis)
|
|
206
|
+
const reasons = [...new Set([...reconciliationReasons, ...(analysis.completeness?.reasons || []), ...(transportAnalysis.completeness?.reasons || [])])]
|
|
235
207
|
const completeness = {
|
|
236
208
|
complete: reasons.length === 0 && analysis.completeness?.complete === true,
|
|
237
209
|
status: reasons.length === 0 && analysis.completeness?.complete === true ? 'COMPLETE' : 'PARTIAL',
|
|
@@ -243,8 +215,8 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
243
215
|
right.affected.files.length - left.affected.files.length ||
|
|
244
216
|
left.path.localeCompare(right.path))
|
|
245
217
|
const lines = [
|
|
246
|
-
|
|
247
|
-
`Scope: ${backend.label} → ${clients.map(({record}) => record.label).join(', ')}; ${analysis.totals.endpoints} endpoint(s), ${analysis.totals.clientCalls} inspected
|
|
218
|
+
contractVerdictLine(verdict, analysis.totals.endpoints + transportAnalysis.totals.contracts),
|
|
219
|
+
`Scope: ${backend.label} → ${clients.map(({record}) => record.label).join(', ')}; ${analysis.totals.endpoints} HTTP endpoint(s), ${transportAnalysis.totals.contracts} GraphQL/gRPC/event contract(s), ${analysis.totals.clientCalls} inspected HTTP call(s), ${verdict.uncertainCalls} uncertain.`,
|
|
248
220
|
...ranked.slice(0, topN).flatMap((endpoint) => {
|
|
249
221
|
const location = endpoint.file ? ` (${endpoint.backend}:${endpoint.file}${endpoint.line ? `:${endpoint.line}` : ''})` : ''
|
|
250
222
|
const callsites = endpoint.callsites.slice(0, 3)
|
|
@@ -257,8 +229,10 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
257
229
|
...screens,
|
|
258
230
|
]
|
|
259
231
|
}),
|
|
232
|
+
...transportAnalysis.contracts.filter((contract) => contract.callsites.length).slice(0, topN).map((contract) =>
|
|
233
|
+
` ${contract.transport.toUpperCase()} ${contract.service ? `${contract.service}.` : contract.operation ? `${contract.operation} ` : ''}${contract.name} (${contract.file}:${contract.line}) [${contract.liveness}] → ${contract.callsites.length} callsite(s), ${contract.affected.files.length} affected file(s)`),
|
|
260
234
|
completeness.complete
|
|
261
|
-
? 'Completeness: complete within the declared repository graphs and static HTTP
|
|
235
|
+
? 'Completeness: complete within the declared repository graphs and supported static HTTP/GraphQL/gRPC/event models.'
|
|
262
236
|
: `Completeness: partial — ${completeness.reasons.join('; ')}.`,
|
|
263
237
|
]
|
|
264
238
|
const result = {
|
|
@@ -269,6 +243,7 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
269
243
|
clients: clients.map(({record, alias}) => publicRecord(record, alias)),
|
|
270
244
|
},
|
|
271
245
|
...analysis,
|
|
246
|
+
transportContracts: transportAnalysis,
|
|
272
247
|
status: completeness.status,
|
|
273
248
|
graphReconciliation,
|
|
274
249
|
completeness,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {isStructuralRelation} from '../graph/relations.js'
|
|
2
|
+
import {querySymbolsPrecision} from '../precision/symbol-query.js'
|
|
3
|
+
|
|
4
|
+
export function graphWithExactDirectReferences(g, targetId, overlay) {
|
|
5
|
+
const exact = (overlay?.links || []).filter((link) => String(link?.target || '') === targetId
|
|
6
|
+
&& link.provenance === 'EXACT_LSP')
|
|
7
|
+
const provenEmpty = overlay?.state === 'COMPLETE'
|
|
8
|
+
&& (overlay?.noReferenceSymbols || []).some((id) => String(id) === targetId)
|
|
9
|
+
if (overlay?.state !== 'COMPLETE' || (!exact.length && !provenEmpty)) return null
|
|
10
|
+
const inn = new Map(g.inn)
|
|
11
|
+
const structural = (g.inn.get(targetId) || []).filter((edge) => isStructuralRelation(edge.relation))
|
|
12
|
+
inn.set(targetId, [...structural, ...exact.map((link) => ({
|
|
13
|
+
id: String(link.source),
|
|
14
|
+
relation: link.relation || 'references',
|
|
15
|
+
provenance: 'EXACT_LSP',
|
|
16
|
+
...(link.typeOnly === true ? {typeOnly: true} : {}),
|
|
17
|
+
...(link.compileOnly === true ? {compileOnly: true} : {}),
|
|
18
|
+
}))])
|
|
19
|
+
return {...g, inn}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function refineChangeImpact(g, args, ctx, baseline, computeImpact) {
|
|
23
|
+
const requested = ['auto', 'graph', 'lsp'].includes(args?.precision) ? args.precision : 'auto'
|
|
24
|
+
if (requested === 'graph' || !baseline?.result?.seeds?.ids || !ctx?.repoRoot || !ctx?.graphPath
|
|
25
|
+
|| (requested === 'auto' && g.graphPrecisionMode === 'off')) return baseline
|
|
26
|
+
const targets = baseline.result.seeds.ids.filter((id) => {
|
|
27
|
+
const node = g.byId.get(String(id))
|
|
28
|
+
return node?.selection_start && /\.(?:[cm]?[jt]sx?)$/i.test(String(node.source_file || ''))
|
|
29
|
+
}).slice(0, 16)
|
|
30
|
+
if (!targets.length) return baseline
|
|
31
|
+
return (async () => {
|
|
32
|
+
try {
|
|
33
|
+
const precision = await querySymbolsPrecision({
|
|
34
|
+
repoRoot: ctx.repoRoot,
|
|
35
|
+
graphPath: ctx.graphPath,
|
|
36
|
+
targetIds: targets,
|
|
37
|
+
maxReferences: Math.max(100, Math.min(16_384, Number(args.max_references) || 5_000)),
|
|
38
|
+
timeoutMs: Math.max(1_000, Math.min(60_000, Number(args.timeout_ms) || 45_000)),
|
|
39
|
+
clientFactory: ctx.precisionClientFactory,
|
|
40
|
+
})
|
|
41
|
+
let exactGraph = g
|
|
42
|
+
const verified = []
|
|
43
|
+
for (const target of targets) {
|
|
44
|
+
const next = graphWithExactDirectReferences(exactGraph, String(target), precision.overlay)
|
|
45
|
+
if (!next) continue
|
|
46
|
+
exactGraph = next
|
|
47
|
+
verified.push(String(target))
|
|
48
|
+
}
|
|
49
|
+
const value = verified.length ? computeImpact(exactGraph, args, ctx) : baseline
|
|
50
|
+
const allVerified = verified.length === targets.length
|
|
51
|
+
const status = allVerified ? 'DIRECT_EXACT_TRANSITIVE_GRAPH' : 'PARTIAL'
|
|
52
|
+
value.text = [
|
|
53
|
+
value.text.split('\n')[0],
|
|
54
|
+
`Semantic precision: ${status}; EXACT_LSP verified direct references for ${verified.length}/${targets.length} changed JavaScript/TypeScript symbol(s). Transitive hops remain graph-backed.`,
|
|
55
|
+
...value.text.split('\n').slice(1),
|
|
56
|
+
].join('\n')
|
|
57
|
+
value.result.semanticPrecision = {
|
|
58
|
+
status,
|
|
59
|
+
requestedTargets: targets,
|
|
60
|
+
verifiedTargets: verified,
|
|
61
|
+
exactDirectEdges: (precision.overlay?.links || []).filter((link) => verified.includes(String(link?.target || ''))).length,
|
|
62
|
+
transitiveEvidence: 'GRAPH',
|
|
63
|
+
provider: precision.overlay?.engines?.[0]?.provider || null,
|
|
64
|
+
elapsedMs: precision.elapsedMs,
|
|
65
|
+
}
|
|
66
|
+
if (!allVerified) value.warnings.push({
|
|
67
|
+
code: 'CHANGE_IMPACT_PRECISION_PARTIAL',
|
|
68
|
+
message: precision.overlay?.reason || 'Not every changed symbol had complete exact direct-reference evidence.',
|
|
69
|
+
})
|
|
70
|
+
return value
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const reason = error?.message || 'batch point query failed'
|
|
73
|
+
baseline.result.semanticPrecision = {
|
|
74
|
+
status: 'UNAVAILABLE', requestedTargets: targets, verifiedTargets: [],
|
|
75
|
+
exactDirectEdges: 0, transitiveEvidence: 'GRAPH', reason,
|
|
76
|
+
}
|
|
77
|
+
baseline.warnings.push({
|
|
78
|
+
code: 'CHANGE_IMPACT_PRECISION_UNAVAILABLE',
|
|
79
|
+
message: `Exact changed-symbol references were unavailable; graph evidence was retained (${reason}).`,
|
|
80
|
+
})
|
|
81
|
+
baseline.text = [
|
|
82
|
+
baseline.text.split('\n')[0],
|
|
83
|
+
`Semantic precision: UNAVAILABLE; graph evidence retained (${reason}).`,
|
|
84
|
+
...baseline.text.split('\n').slice(1),
|
|
85
|
+
].join('\n')
|
|
86
|
+
return baseline
|
|
87
|
+
}
|
|
88
|
+
})()
|
|
89
|
+
}
|
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
// last rebuild (graph_diff), and the blast radius of the current change set (change_impact).
|
|
3
3
|
// Hot-reloadable (re-imported by catalog.mjs on change).
|
|
4
4
|
import {readFileSync} from 'node:fs'
|
|
5
|
-
import {spawnSync} from 'node:child_process'
|
|
6
|
-
import {childProcessEnv} from '../child-env.js'
|
|
7
5
|
import {
|
|
8
6
|
isSymbol, degreeOf, labelOf, resolveNodeInfo, ambiguityNote,
|
|
9
7
|
rawGraph, prevGraphPathFor, edgeEndpoint, diffGraphs, formatGraphDiff,
|
|
10
8
|
} from './graph-context.mjs'
|
|
11
|
-
import {
|
|
9
|
+
import {querySymbolPrecision} from '../precision/symbol-query.js'
|
|
10
|
+
import {graphWithExactDirectReferences, refineChangeImpact} from './tools-impact-precision.mjs'
|
|
12
11
|
import {reverseReach} from './graph/reverse-reach.mjs'
|
|
13
|
-
import {gitLines} from './git-output.mjs'
|
|
14
12
|
import {tChangeImpactV2} from './tools-impact-change.mjs'
|
|
15
13
|
import {buildGraphAtGitRef} from '../analysis/git-ref-graph.js'
|
|
16
14
|
|
|
@@ -24,7 +22,7 @@ const impactKind = (entry) => {
|
|
|
24
22
|
// Transitive blast-radius: who is affected if this node changes. Walks REVERSE dependency edges
|
|
25
23
|
// (calls/imports/inherits — not structural `contains`) out to `depth`. For a symbol, also seeds its
|
|
26
24
|
// containing file, because importers depend on the file rather than the individual symbol.
|
|
27
|
-
|
|
25
|
+
function getDependentsFromGraph(g, {label, depth = 3, max_nodes = 40, include_container_importers = false} = {}, precisionNote = null) {
|
|
28
26
|
const info = resolveNodeInfo(g, label)
|
|
29
27
|
const n = info.node
|
|
30
28
|
if (!n) return `No node found matching "${label}".`
|
|
@@ -46,18 +44,64 @@ export function tGetDependents(g, {label, depth = 3, max_nodes = 40, include_con
|
|
|
46
44
|
.filter(([nid]) => !seeds.has(nid))
|
|
47
45
|
.map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid)}))
|
|
48
46
|
.sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
|
|
49
|
-
if (!ranked.length) return [note, `No dependents found for ${n.label ?? id} within depth ${maxDepth} — nothing in the graph calls, imports, or inherits it.`].filter(Boolean).join('\n')
|
|
47
|
+
if (!ranked.length) return [note, precisionNote, `No dependents found for ${n.label ?? id} within depth ${maxDepth} — nothing in the graph calls, imports, or inherits it.`].filter(Boolean).join('\n')
|
|
50
48
|
const shown = ranked.slice(0, cap)
|
|
51
49
|
const runtimeCount = ranked.filter((entry) => !entry.entry.compileOnly).length
|
|
52
50
|
const compileCount = ranked.length - runtimeCount
|
|
53
51
|
return [
|
|
54
52
|
note,
|
|
53
|
+
precisionNote,
|
|
55
54
|
`Dependents of ${n.label ?? id} (reverse calls/imports/inherits, depth ≤${maxDepth}): ${ranked.length} found (${runtimeCount} runtime, ${compileCount} compile-time-only), showing ${shown.length} by proximity + connectivity.`,
|
|
56
55
|
containingFile ? `Includes importers of its containing file ${labelOf(g, containingFile)} by explicit request.` : null,
|
|
57
56
|
...shown.map((r) => ` [d${r.d} ${impactKind(r.entry)}] ${r.entry.relation || 'rel'} [${r.entry.provenance || 'UNKNOWN'}] ${labelOf(g, r.id)} (deg ${r.deg}) [${r.id}]`),
|
|
58
57
|
].filter(Boolean).join('\n')
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
export function tGetDependents(g, args = {}, ctx = {}) {
|
|
61
|
+
const info = resolveNodeInfo(g, args.label)
|
|
62
|
+
const id = String(info.node?.id || '')
|
|
63
|
+
const requested = ['auto', 'graph', 'lsp'].includes(args.precision) ? args.precision : 'auto'
|
|
64
|
+
const canQuery = isSymbol(id) && ctx.repoRoot && ctx.graphPath
|
|
65
|
+
&& (requested === 'lsp' || (requested === 'auto' && g.graphPrecisionMode !== 'off'))
|
|
66
|
+
if (!canQuery) {
|
|
67
|
+
const note = isSymbol(id) && requested !== 'graph'
|
|
68
|
+
? 'Semantic precision: graph-only; an exact point query was unavailable or disabled.' : null
|
|
69
|
+
return getDependentsFromGraph(g, args, note)
|
|
70
|
+
}
|
|
71
|
+
return (async () => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await querySymbolPrecision({
|
|
74
|
+
repoRoot: ctx.repoRoot,
|
|
75
|
+
graphPath: ctx.graphPath,
|
|
76
|
+
targetId: id,
|
|
77
|
+
maxReferences: Math.max(100, Math.min(5_000, Number(args.max_references) || 1_000)),
|
|
78
|
+
timeoutMs: Math.max(1_000, Math.min(60_000, Number(args.timeout_ms) || 30_000)),
|
|
79
|
+
clientFactory: ctx.precisionClientFactory,
|
|
80
|
+
})
|
|
81
|
+
const exactGraph = graphWithExactDirectReferences(g, id, result.overlay)
|
|
82
|
+
if (exactGraph) {
|
|
83
|
+
const count = (result.overlay.links || []).filter((link) => String(link?.target || '') === id).length
|
|
84
|
+
return getDependentsFromGraph(
|
|
85
|
+
exactGraph,
|
|
86
|
+
args,
|
|
87
|
+
`Semantic precision: EXACT_LSP point query${result.cached ? ' (cache hit)' : ''}; ${count} classified direct reference edge(s).`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return getDependentsFromGraph(
|
|
91
|
+
g,
|
|
92
|
+
args,
|
|
93
|
+
`Semantic precision: ${result.overlay?.state || 'UNAVAILABLE'}; exact absence was not proven, so graph edges are retained (${result.overlay?.reason || 'incomplete project coverage'}).`,
|
|
94
|
+
)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return getDependentsFromGraph(
|
|
97
|
+
g,
|
|
98
|
+
args,
|
|
99
|
+
`Semantic precision: UNAVAILABLE; graph evidence retained (${error?.message || 'point query failed'}).`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
})()
|
|
103
|
+
}
|
|
104
|
+
|
|
61
105
|
// Re-query the last rebuild's before/after pair (graph.prev.json vs graph.json), optionally scoped.
|
|
62
106
|
export async function tGraphDiff(g, args, ctx) {
|
|
63
107
|
const current = rawGraph(ctx)
|
|
@@ -106,98 +150,12 @@ export async function tGraphDiff(g, args, ctx) {
|
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
// ---- change impact -------------------------------------------------------------------------------
|
|
109
|
-
function resolveImpactBase(repoRoot, requested) {
|
|
110
|
-
const candidates = requested ? [requested] : ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
|
|
111
|
-
for (const ref of candidates) {
|
|
112
|
-
const ok = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
|
|
113
|
-
if (ok.status === 0) return ref
|
|
114
|
-
}
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
153
|
// Blast radius of a change, without any GitHub API: diff the CURRENT change (branch
|
|
119
154
|
// commits since the merge-base + staged/unstaged + untracked) against a base ref, map the changed
|
|
120
155
|
// files and their symbols onto the graph, and walk REVERSE dependency edges — everything the change
|
|
121
156
|
// can break, ranked by proximity + connectivity, with file-level test coverage attached so the
|
|
122
157
|
// untested part of the blast radius stands out. The pre-PR review, in one call.
|
|
123
158
|
export function tChangeImpact(g, args, ctx) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Kept private during the v2 rollout so the surrounding get_dependents/graph_diff implementation is
|
|
128
|
-
// untouched; focused tests exercise the exported symbol-aware path above.
|
|
129
|
-
function tChangeImpactLegacy(g, args, ctx) {
|
|
130
|
-
if (!ctx.repoRoot) return 'change_impact needs the repo root (not provided to this server).'
|
|
131
|
-
// Explicit file list (e.g. a PR's changed files) skips the local git diff entirely — this is how
|
|
132
|
-
// a NOT-checked-out PR gets its impact assessed.
|
|
133
|
-
const explicit = Array.isArray(args.files)
|
|
134
|
-
? [...new Set(args.files.map((f) => String(f).replace(/\\/g, '/').trim()).filter(Boolean))]
|
|
135
|
-
: null
|
|
136
|
-
let changed
|
|
137
|
-
let sourceLabel
|
|
138
|
-
if (explicit) {
|
|
139
|
-
if (!explicit.length) return 'files was provided but empty — pass repo-relative paths, or omit it to diff the local change.'
|
|
140
|
-
changed = explicit
|
|
141
|
-
sourceLabel = `for ${changed.length} provided file(s)`
|
|
142
|
-
} else {
|
|
143
|
-
const base = resolveImpactBase(ctx.repoRoot, args.base ? String(args.base).trim() : '')
|
|
144
|
-
if (!base) return `Could not resolve a base ref${args.base ? ` ("${args.base}")` : ''} — pass base explicitly (e.g. origin/main or HEAD~1).`
|
|
145
|
-
const committed = gitLines(ctx.repoRoot, ['diff', '--name-only', `${base}...HEAD`])
|
|
146
|
-
if (committed === null) return `git diff against ${base} failed — is ${ctx.repoRoot} a git repository?`
|
|
147
|
-
const uncommitted = gitLines(ctx.repoRoot, ['diff', '--name-only', 'HEAD']) || []
|
|
148
|
-
const untracked = gitLines(ctx.repoRoot, ['ls-files', '--others', '--exclude-standard']) || []
|
|
149
|
-
changed = [...new Set([...committed, ...uncommitted, ...untracked])]
|
|
150
|
-
sourceLabel = `vs ${base}`
|
|
151
|
-
if (!changed.length) return `No changes vs ${base} — working tree clean and no branch commits.`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const changedSet = new Set(changed)
|
|
155
|
-
const seeds = new Set()
|
|
156
|
-
const unmapped = []
|
|
157
|
-
for (const file of changed) {
|
|
158
|
-
if (!g.byId.has(file)) { unmapped.push(file); continue }
|
|
159
|
-
seeds.add(file)
|
|
160
|
-
for (const e of g.out.get(file) || []) if (e.relation === 'contains') seeds.add(String(e.id))
|
|
161
|
-
}
|
|
162
|
-
if (!seeds.size) {
|
|
163
|
-
return [
|
|
164
|
-
`${changed.length} changed file(s) ${sourceLabel}, but none are in the graph — new files or non-code.`,
|
|
165
|
-
`Run rebuild_graph and retry for the full picture.`,
|
|
166
|
-
`Changed: ${changed.slice(0, 20).join(', ')}${changed.length > 20 ? ', …' : ''}`,
|
|
167
|
-
].join('\n')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const maxDepth = Math.max(1, Math.min(4, Number(args.depth) || 2))
|
|
171
|
-
const cap = Math.max(5, Math.min(120, Number(args.max_nodes) || 40))
|
|
172
|
-
const reached = reverseReach(g, seeds, maxDepth)
|
|
173
|
-
const fileOfNode = (id) => { const s = String(id); const h = s.indexOf('#'); return h < 0 ? s : s.slice(0, h) }
|
|
174
|
-
const impacted = [...reached.entries()]
|
|
175
|
-
.filter(([nid]) => !seeds.has(nid) && !changedSet.has(fileOfNode(nid)))
|
|
176
|
-
.map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid), file: fileOfNode(nid)}))
|
|
177
|
-
.sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
|
|
178
|
-
|
|
179
|
-
// coverage overlay from EXISTING reports (fractions 0..1) — see coverage_map for details
|
|
180
|
-
const knownFiles = (rawGraph(ctx).nodes || []).filter((n) => !String(n.id).includes('#') && n.source_file).map((n) => n.source_file)
|
|
181
|
-
const coverage = readCoverageForRepo(ctx.repoRoot, knownFiles)
|
|
182
|
-
const covOf = (file) => coverage.get(String(file).replace(/\\/g, '/'))?.pct ?? null
|
|
183
|
-
const pctStr = (v) => (v == null ? 'cov n/a' : `cov ${Math.round(v * 100)}%`)
|
|
184
|
-
const hasCoverage = coverage.size > 0
|
|
185
|
-
|
|
186
|
-
const shown = impacted.slice(0, cap)
|
|
187
|
-
const runtimeImpacted = impacted.filter((entry) => !entry.entry.compileOnly).length
|
|
188
|
-
const compileImpacted = impacted.length - runtimeImpacted
|
|
189
|
-
const untestedHotspots = shown.filter((n) => { const c = covOf(n.file); return c != null && c < 0.5 && n.deg >= 5 })
|
|
190
|
-
return [
|
|
191
|
-
`Change impact ${sourceLabel}: ${changed.length} changed file(s) → ${seeds.size} graph seed(s) incl. their symbols${unmapped.length ? `; ${unmapped.length} file(s) not in the graph (new/non-code — rebuild_graph refreshes)` : ''}.`,
|
|
192
|
-
impacted.length
|
|
193
|
-
? `${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s) (${runtimeImpacted} runtime, ${compileImpacted} compile-time-only), showing ${shown.length}:`
|
|
194
|
-
: `Nothing else in the graph depends on the changed code within ${maxDepth} hop(s).`,
|
|
195
|
-
hasCoverage ? `Coverage: existing report mapped where available.` : `Coverage: unavailable (no supported report found); per-node coverage labels omitted.`,
|
|
196
|
-
...shown.map((n) => ` [d${n.d} ${impactKind(n.entry)}] ${n.entry.relation || 'rel'} ${labelOf(g, n.id)} (deg ${n.deg}${hasCoverage ? `, ${pctStr(covOf(n.file))}` : ''}) [${n.id}]`),
|
|
197
|
-
untestedHotspots.length ? `` : null,
|
|
198
|
-
untestedHotspots.length ? `Untested hotspots in the blast radius (<50% coverage, deg ≥5) — cover these before shipping:` : null,
|
|
199
|
-
...untestedHotspots.slice(0, 10).map((n) => ` ${labelOf(g, n.id)} (${pctStr(covOf(n.file))}, deg ${n.deg}) ${n.file}`),
|
|
200
|
-
``,
|
|
201
|
-
`Drill into any node with get_dependents / read_source; per-symbol coverage via coverage_map.`,
|
|
202
|
-
].filter((x) => x != null).join('\n')
|
|
159
|
+
const baseline = tChangeImpactV2(g, args, ctx)
|
|
160
|
+
return refineChangeImpact(g, args, ctx, baseline, tChangeImpactV2)
|
|
203
161
|
}
|
|
@@ -51,8 +51,14 @@ function completedOverlay(session) {
|
|
|
51
51
|
language: 'typescript/javascript',
|
|
52
52
|
capability: 'textDocument/references',
|
|
53
53
|
status: state,
|
|
54
|
+
configuredPluginsSuppressed: session.semanticInputs.pluginsSuppressed || 0,
|
|
55
|
+
repoLocalPluginLoads: false,
|
|
54
56
|
}],
|
|
55
57
|
semanticInputFingerprint: session.semanticInputs.fingerprint,
|
|
58
|
+
pluginPolicy: {
|
|
59
|
+
configuredPluginsSuppressed: session.semanticInputs.pluginsSuppressed || 0,
|
|
60
|
+
repoLocalPluginLoads: false,
|
|
61
|
+
},
|
|
56
62
|
coverage: coverage(session),
|
|
57
63
|
links: session.links,
|
|
58
64
|
referenceEvidence: session.referenceEvidence,
|
|
@@ -181,6 +187,10 @@ export async function buildLspPrecisionOverlay({
|
|
|
181
187
|
return persist(session, baseOverlay(graph, 'COMPLETE', {
|
|
182
188
|
request: session.request,
|
|
183
189
|
semanticInputFingerprint: session.semanticInputs.fingerprint,
|
|
190
|
+
pluginPolicy: {
|
|
191
|
+
configuredPluginsSuppressed: session.semanticInputs.pluginsSuppressed || 0,
|
|
192
|
+
repoLocalPluginLoads: false,
|
|
193
|
+
},
|
|
184
194
|
reason: 'no eligible JavaScript/TypeScript semantic targets',
|
|
185
195
|
}))
|
|
186
196
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {typeScriptLspContract} from '../typescript-lsp-provider.js'
|
|
2
2
|
|
|
3
|
-
export const PRECISION_OVERLAY_V =
|
|
3
|
+
export const PRECISION_OVERLAY_V = 4
|
|
4
4
|
export const PRECISION_FILE = 'precision.json'
|
|
5
5
|
export const JS_TS_FILE = /\.(?:[cm]?[jt]sx?)$/i
|
|
6
6
|
export const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value)
|
|
@@ -182,3 +182,42 @@ export async function querySymbolPrecision({
|
|
|
182
182
|
inFlight.delete(flightKey)
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
export async function querySymbolsPrecision({
|
|
187
|
+
repoRoot,
|
|
188
|
+
graphPath,
|
|
189
|
+
targetIds,
|
|
190
|
+
maxReferences = 5_000,
|
|
191
|
+
timeoutMs = 45_000,
|
|
192
|
+
clientFactory,
|
|
193
|
+
} = {}) {
|
|
194
|
+
if (!repoRoot || !graphPath || !Array.isArray(targetIds) || !targetIds.length) {
|
|
195
|
+
throw new Error('batch symbol precision requires repoRoot, graphPath, and targetIds')
|
|
196
|
+
}
|
|
197
|
+
const ids = [...new Set(targetIds.map(String).filter(Boolean))].slice(0, 16)
|
|
198
|
+
const boundedReferences = boundedInteger(maxReferences, 5_000, 1, 16_384)
|
|
199
|
+
const boundedTimeout = boundedInteger(timeoutMs, 45_000, 1_000, 60_000)
|
|
200
|
+
const rawGraph = loadRawGraph(graphPath)
|
|
201
|
+
const graph = rawGraph.graphPrecisionMode === 'off' ? {...rawGraph, graphPrecisionMode: 'lsp'} : rawGraph
|
|
202
|
+
const nodes = new Map((graph.nodes || []).map((node) => [String(node?.id || ''), node]))
|
|
203
|
+
for (const id of ids) {
|
|
204
|
+
const target = nodes.get(id)
|
|
205
|
+
if (!target) throw new Error(`precision target is absent from the active graph: ${id}`)
|
|
206
|
+
if (!target.selection_start || !/\.(?:[cm]?[jt]sx?)$/i.test(String(target.source_file || ''))) {
|
|
207
|
+
throw new Error(`exact batch precision does not support target: ${id}`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const startedAt = Date.now()
|
|
211
|
+
const overlay = await buildLspPrecisionOverlay({
|
|
212
|
+
repoRoot,
|
|
213
|
+
graph,
|
|
214
|
+
mode: 'lsp',
|
|
215
|
+
maxSymbols: ids.length,
|
|
216
|
+
maxReferences: boundedReferences,
|
|
217
|
+
maxLinks: boundedReferences,
|
|
218
|
+
timeoutMs: boundedTimeout,
|
|
219
|
+
targetIds: ids,
|
|
220
|
+
clientFactory,
|
|
221
|
+
})
|
|
222
|
+
return {overlay, targetIds: ids, elapsedMs: Date.now() - startedAt}
|
|
223
|
+
}
|
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
typeScriptLanguageId,
|
|
7
7
|
typeScriptLspContract,
|
|
8
8
|
} from './discovery.js'
|
|
9
|
+
import {isolateTypeScriptRuntime} from './isolated-runtime.js'
|
|
9
10
|
|
|
10
11
|
/** Starts Weavatrix's bundled TypeScript server without executing repository configuration. */
|
|
11
12
|
export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} = {}) {
|
|
12
13
|
const discovered = discoverTypeScriptProvider()
|
|
13
14
|
const absoluteRepoRoot = resolve(repoRoot)
|
|
15
|
+
const isolated = isolateTypeScriptRuntime(discovered.tsserverPath)
|
|
14
16
|
let client
|
|
15
17
|
let reportedTypeScript = null
|
|
16
18
|
try {
|
|
@@ -41,11 +43,16 @@ export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} =
|
|
|
41
43
|
initializationOptions: {
|
|
42
44
|
hostInfo: 'weavatrix',
|
|
43
45
|
disableAutomaticTypingAcquisition: true,
|
|
44
|
-
tsserver: {path:
|
|
46
|
+
tsserver: {path: isolated.tsserverPath},
|
|
47
|
+
// An explicit empty list keeps the language-server plugin manager
|
|
48
|
+
// from adding --globalPlugins or --pluginProbeLocations. tsserver's
|
|
49
|
+
// local-plugin loading remains disabled by default as well.
|
|
50
|
+
plugins: [],
|
|
45
51
|
},
|
|
46
52
|
})
|
|
47
53
|
} catch (error) {
|
|
48
54
|
client?.kill(error)
|
|
55
|
+
isolated.cleanup()
|
|
49
56
|
throw error
|
|
50
57
|
}
|
|
51
58
|
return Object.freeze({
|
|
@@ -63,7 +70,13 @@ export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} =
|
|
|
63
70
|
},
|
|
64
71
|
definition(relPath, position) { return client.definition({filePath: relPath, position}) },
|
|
65
72
|
closeDocument(relPath) { return client.closeDocument(relPath) },
|
|
66
|
-
close(shutdownTimeoutMs = timeoutMs) {
|
|
67
|
-
|
|
73
|
+
async close(shutdownTimeoutMs = timeoutMs) {
|
|
74
|
+
try { return await client.shutdown({timeoutMs: shutdownTimeoutMs}) }
|
|
75
|
+
finally { isolated.cleanup() }
|
|
76
|
+
},
|
|
77
|
+
kill() {
|
|
78
|
+
try { client.kill() }
|
|
79
|
+
finally { isolated.cleanup() }
|
|
80
|
+
},
|
|
68
81
|
})
|
|
69
82
|
}
|
|
@@ -4,7 +4,7 @@ import {createRequire} from 'node:module'
|
|
|
4
4
|
|
|
5
5
|
const requireFromWeavatrix = createRequire(import.meta.url)
|
|
6
6
|
export const PROVIDER = 'typescript-language-server'
|
|
7
|
-
export const TYPESCRIPT_LSP_CAPABILITY_CONTRACT = 'typescript-references-
|
|
7
|
+
export const TYPESCRIPT_LSP_CAPABILITY_CONTRACT = 'typescript-references-v4-plugin-suppression'
|
|
8
8
|
let discoveredProvider = null
|
|
9
9
|
|
|
10
10
|
function resolveOwn(specifier) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {copyFileSync, linkSync, mkdirSync, mkdtempSync, readdirSync, rmSync} from 'node:fs'
|
|
2
|
+
import {tmpdir} from 'node:os'
|
|
3
|
+
import {basename, dirname, join} from 'node:path'
|
|
4
|
+
|
|
5
|
+
const RUNTIME_FILES = new Set([
|
|
6
|
+
'tsserver.js', '_tsserver.js', 'typescript.js',
|
|
7
|
+
'typingsInstaller.js', '_typingsInstaller.js', 'typesMap.json', 'watchGuard.js',
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
function materialize(source, target) {
|
|
11
|
+
try { linkSync(source, target) }
|
|
12
|
+
catch { copyFileSync(source, target) }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isolateTypeScriptRuntime(tsserverPath) {
|
|
16
|
+
const sourceLib = dirname(tsserverPath)
|
|
17
|
+
const sourceRoot = dirname(sourceLib)
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), 'weavatrix-tsserver-'))
|
|
19
|
+
const lib = join(root, 'lib')
|
|
20
|
+
mkdirSync(lib, {recursive: true, mode: 0o700})
|
|
21
|
+
try {
|
|
22
|
+
materialize(join(sourceRoot, 'package.json'), join(root, 'package.json'))
|
|
23
|
+
const files = readdirSync(sourceLib).filter((name) =>
|
|
24
|
+
RUNTIME_FILES.has(name) || /^lib(?:\..+)?\.d\.ts$/i.test(name))
|
|
25
|
+
for (const name of files) materialize(join(sourceLib, name), join(lib, basename(name)))
|
|
26
|
+
let cleaned = false
|
|
27
|
+
return {
|
|
28
|
+
tsserverPath: join(lib, 'tsserver.js'),
|
|
29
|
+
cleanup() {
|
|
30
|
+
if (cleaned) return
|
|
31
|
+
cleaned = true
|
|
32
|
+
try { rmSync(root, {recursive: true, force: true}) } catch { /* process cleanup fallback */ }
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
try { rmSync(root, {recursive: true, force: true}) } catch { /* best effort */ }
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -154,7 +154,7 @@ export function parseRepoConfig(context, configPath, budget) {
|
|
|
154
154
|
const state = {
|
|
155
155
|
outsideAccess: false,
|
|
156
156
|
limitExceeded: false,
|
|
157
|
-
configuredPlugins:
|
|
157
|
+
configuredPlugins: new Set(),
|
|
158
158
|
bytes: 0,
|
|
159
159
|
records: new Map(),
|
|
160
160
|
}
|
|
@@ -197,8 +197,9 @@ export function parseRepoConfig(context, configPath, budget) {
|
|
|
197
197
|
}
|
|
198
198
|
try {
|
|
199
199
|
const raw = ts.parseConfigFileTextToJson(path, body.toString('utf8')).config
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
for (const plugin of raw?.compilerOptions?.plugins || []) {
|
|
201
|
+
const name = typeof plugin?.name === 'string' ? plugin.name.trim() : ''
|
|
202
|
+
if (name) state.configuredPlugins.add(name.slice(0, 256))
|
|
202
203
|
}
|
|
203
204
|
} catch { /* parser diagnostics below decide safety */ }
|
|
204
205
|
return body.toString('utf8')
|
|
@@ -213,8 +214,9 @@ export function parseRepoConfig(context, configPath, budget) {
|
|
|
213
214
|
try { parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host) }
|
|
214
215
|
catch { return {complete: false, reason: 'CONFIG_PARSE_FAILED'} }
|
|
215
216
|
const errors = [...diagnostics, ...(parsed?.errors || [])]
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
for (const plugin of parsed?.options?.plugins || []) {
|
|
218
|
+
const name = typeof plugin?.name === 'string' ? plugin.name.trim() : ''
|
|
219
|
+
if (name) state.configuredPlugins.add(name.slice(0, 256))
|
|
218
220
|
}
|
|
219
221
|
if (budget.reason) return {complete: false, reason: budget.reason}
|
|
220
222
|
if (!parsed || state.outsideAccess || state.limitExceeded
|
|
@@ -244,6 +246,9 @@ export function parseRepoConfig(context, configPath, budget) {
|
|
|
244
246
|
projectFiles,
|
|
245
247
|
projectKeys,
|
|
246
248
|
configRecords: state.records,
|
|
247
|
-
|
|
249
|
+
// The bundled provider runs an isolated tsserver copy whose plugin search
|
|
250
|
+
// root contains only TypeScript runtime files. Keep configured plugin names
|
|
251
|
+
// as evidence, but never execute them from the inspected repository/bundle.
|
|
252
|
+
plugins: [...state.configuredPlugins].sort(),
|
|
248
253
|
}
|
|
249
254
|
}
|