weavatrix 0.2.14 → 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 +69 -13
- package/SECURITY.md +2 -2
- 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/dep-check.js +8 -18
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +7 -2
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- 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.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +13 -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/task-retrieval.js +3 -14
- 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/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-seeds.mjs +3 -14
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/sync/evidence-duplicates.mjs +1 -4
- package/src/mcp/tools-company.mjs +28 -53
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact-precision.mjs +89 -0
- package/src/mcp/tools-impact.mjs +53 -136
- package/src/path-classification.js +14 -0
- 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/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/util.js +8 -0
- package/docs/releases/v0.2.14.md +0 -93
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -2,53 +2,16 @@
|
|
|
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 {
|
|
12
|
-
import {
|
|
9
|
+
import {querySymbolPrecision} from '../precision/symbol-query.js'
|
|
10
|
+
import {graphWithExactDirectReferences, refineChangeImpact} from './tools-impact-precision.mjs'
|
|
11
|
+
import {reverseReach} from './graph/reverse-reach.mjs'
|
|
13
12
|
import {tChangeImpactV2} from './tools-impact-change.mjs'
|
|
14
13
|
import {buildGraphAtGitRef} from '../analysis/git-ref-graph.js'
|
|
15
14
|
|
|
16
|
-
function reverseReach(g, seeds, maxDepth) {
|
|
17
|
-
const states = new Map([...seeds].map((id) => [String(id), {
|
|
18
|
-
runtimeDepth: 0, runtimeRelation: null, compileDepth: null, compileRelation: null,
|
|
19
|
-
}]))
|
|
20
|
-
const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
|
|
21
|
-
for (let cursor = 0; cursor < frontier.length; cursor++) {
|
|
22
|
-
const current = frontier[cursor]
|
|
23
|
-
if (current.depth >= maxDepth) continue
|
|
24
|
-
for (const e of g.inn.get(current.id) || []) {
|
|
25
|
-
if (isStructuralRelation(e.relation) || e.barrelProxy === true) continue
|
|
26
|
-
const id = String(e.id)
|
|
27
|
-
const compileOnly = current.compileOnly || e.typeOnly === true || e.compileOnly === true
|
|
28
|
-
const depth = current.depth + 1
|
|
29
|
-
const entry = states.get(id) || {
|
|
30
|
-
runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null,
|
|
31
|
-
}
|
|
32
|
-
const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
|
|
33
|
-
const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
|
|
34
|
-
const provenanceKey = compileOnly ? 'compileProvenance' : 'runtimeProvenance'
|
|
35
|
-
if (entry[depthKey] != null && entry[depthKey] <= depth) continue
|
|
36
|
-
entry[depthKey] = depth
|
|
37
|
-
entry[relationKey] = e.relation || 'rel'
|
|
38
|
-
entry[provenanceKey] = e.provenance || 'UNKNOWN'
|
|
39
|
-
states.set(id, entry)
|
|
40
|
-
frontier.push({id, depth, compileOnly})
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return new Map([...states].map(([id, entry]) => [id, {
|
|
44
|
-
...entry,
|
|
45
|
-
depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
|
|
46
|
-
compileOnly: entry.runtimeDepth == null,
|
|
47
|
-
relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
|
|
48
|
-
provenance: entry.runtimeDepth != null ? entry.runtimeProvenance : entry.compileProvenance,
|
|
49
|
-
}]))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
15
|
const impactKind = (entry) => {
|
|
53
16
|
if (entry?.runtimeDepth != null) {
|
|
54
17
|
return entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime'
|
|
@@ -59,7 +22,7 @@ const impactKind = (entry) => {
|
|
|
59
22
|
// Transitive blast-radius: who is affected if this node changes. Walks REVERSE dependency edges
|
|
60
23
|
// (calls/imports/inherits — not structural `contains`) out to `depth`. For a symbol, also seeds its
|
|
61
24
|
// containing file, because importers depend on the file rather than the individual symbol.
|
|
62
|
-
|
|
25
|
+
function getDependentsFromGraph(g, {label, depth = 3, max_nodes = 40, include_container_importers = false} = {}, precisionNote = null) {
|
|
63
26
|
const info = resolveNodeInfo(g, label)
|
|
64
27
|
const n = info.node
|
|
65
28
|
if (!n) return `No node found matching "${label}".`
|
|
@@ -81,18 +44,64 @@ export function tGetDependents(g, {label, depth = 3, max_nodes = 40, include_con
|
|
|
81
44
|
.filter(([nid]) => !seeds.has(nid))
|
|
82
45
|
.map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid)}))
|
|
83
46
|
.sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
|
|
84
|
-
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')
|
|
85
48
|
const shown = ranked.slice(0, cap)
|
|
86
49
|
const runtimeCount = ranked.filter((entry) => !entry.entry.compileOnly).length
|
|
87
50
|
const compileCount = ranked.length - runtimeCount
|
|
88
51
|
return [
|
|
89
52
|
note,
|
|
53
|
+
precisionNote,
|
|
90
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.`,
|
|
91
55
|
containingFile ? `Includes importers of its containing file ${labelOf(g, containingFile)} by explicit request.` : null,
|
|
92
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}]`),
|
|
93
57
|
].filter(Boolean).join('\n')
|
|
94
58
|
}
|
|
95
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
|
+
|
|
96
105
|
// Re-query the last rebuild's before/after pair (graph.prev.json vs graph.json), optionally scoped.
|
|
97
106
|
export async function tGraphDiff(g, args, ctx) {
|
|
98
107
|
const current = rawGraph(ctx)
|
|
@@ -141,104 +150,12 @@ export async function tGraphDiff(g, args, ctx) {
|
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
// ---- change impact -------------------------------------------------------------------------------
|
|
144
|
-
function gitLines(repoRoot, args) {
|
|
145
|
-
const res = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
|
|
146
|
-
if (res.status !== 0) return null
|
|
147
|
-
return String(res.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function resolveImpactBase(repoRoot, requested) {
|
|
151
|
-
const candidates = requested ? [requested] : ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
|
|
152
|
-
for (const ref of candidates) {
|
|
153
|
-
const ok = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
|
|
154
|
-
if (ok.status === 0) return ref
|
|
155
|
-
}
|
|
156
|
-
return null
|
|
157
|
-
}
|
|
158
|
-
|
|
159
153
|
// Blast radius of a change, without any GitHub API: diff the CURRENT change (branch
|
|
160
154
|
// commits since the merge-base + staged/unstaged + untracked) against a base ref, map the changed
|
|
161
155
|
// files and their symbols onto the graph, and walk REVERSE dependency edges — everything the change
|
|
162
156
|
// can break, ranked by proximity + connectivity, with file-level test coverage attached so the
|
|
163
157
|
// untested part of the blast radius stands out. The pre-PR review, in one call.
|
|
164
158
|
export function tChangeImpact(g, args, ctx) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Kept private during the v2 rollout so the surrounding get_dependents/graph_diff implementation is
|
|
169
|
-
// untouched; focused tests exercise the exported symbol-aware path above.
|
|
170
|
-
function tChangeImpactLegacy(g, args, ctx) {
|
|
171
|
-
if (!ctx.repoRoot) return 'change_impact needs the repo root (not provided to this server).'
|
|
172
|
-
// Explicit file list (e.g. a PR's changed files) skips the local git diff entirely — this is how
|
|
173
|
-
// a NOT-checked-out PR gets its impact assessed.
|
|
174
|
-
const explicit = Array.isArray(args.files)
|
|
175
|
-
? [...new Set(args.files.map((f) => String(f).replace(/\\/g, '/').trim()).filter(Boolean))]
|
|
176
|
-
: null
|
|
177
|
-
let changed
|
|
178
|
-
let sourceLabel
|
|
179
|
-
if (explicit) {
|
|
180
|
-
if (!explicit.length) return 'files was provided but empty — pass repo-relative paths, or omit it to diff the local change.'
|
|
181
|
-
changed = explicit
|
|
182
|
-
sourceLabel = `for ${changed.length} provided file(s)`
|
|
183
|
-
} else {
|
|
184
|
-
const base = resolveImpactBase(ctx.repoRoot, args.base ? String(args.base).trim() : '')
|
|
185
|
-
if (!base) return `Could not resolve a base ref${args.base ? ` ("${args.base}")` : ''} — pass base explicitly (e.g. origin/main or HEAD~1).`
|
|
186
|
-
const committed = gitLines(ctx.repoRoot, ['diff', '--name-only', `${base}...HEAD`])
|
|
187
|
-
if (committed === null) return `git diff against ${base} failed — is ${ctx.repoRoot} a git repository?`
|
|
188
|
-
const uncommitted = gitLines(ctx.repoRoot, ['diff', '--name-only', 'HEAD']) || []
|
|
189
|
-
const untracked = gitLines(ctx.repoRoot, ['ls-files', '--others', '--exclude-standard']) || []
|
|
190
|
-
changed = [...new Set([...committed, ...uncommitted, ...untracked])]
|
|
191
|
-
sourceLabel = `vs ${base}`
|
|
192
|
-
if (!changed.length) return `No changes vs ${base} — working tree clean and no branch commits.`
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const changedSet = new Set(changed)
|
|
196
|
-
const seeds = new Set()
|
|
197
|
-
const unmapped = []
|
|
198
|
-
for (const file of changed) {
|
|
199
|
-
if (!g.byId.has(file)) { unmapped.push(file); continue }
|
|
200
|
-
seeds.add(file)
|
|
201
|
-
for (const e of g.out.get(file) || []) if (e.relation === 'contains') seeds.add(String(e.id))
|
|
202
|
-
}
|
|
203
|
-
if (!seeds.size) {
|
|
204
|
-
return [
|
|
205
|
-
`${changed.length} changed file(s) ${sourceLabel}, but none are in the graph — new files or non-code.`,
|
|
206
|
-
`Run rebuild_graph and retry for the full picture.`,
|
|
207
|
-
`Changed: ${changed.slice(0, 20).join(', ')}${changed.length > 20 ? ', …' : ''}`,
|
|
208
|
-
].join('\n')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const maxDepth = Math.max(1, Math.min(4, Number(args.depth) || 2))
|
|
212
|
-
const cap = Math.max(5, Math.min(120, Number(args.max_nodes) || 40))
|
|
213
|
-
const reached = reverseReach(g, seeds, maxDepth)
|
|
214
|
-
const fileOfNode = (id) => { const s = String(id); const h = s.indexOf('#'); return h < 0 ? s : s.slice(0, h) }
|
|
215
|
-
const impacted = [...reached.entries()]
|
|
216
|
-
.filter(([nid]) => !seeds.has(nid) && !changedSet.has(fileOfNode(nid)))
|
|
217
|
-
.map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid), file: fileOfNode(nid)}))
|
|
218
|
-
.sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
|
|
219
|
-
|
|
220
|
-
// coverage overlay from EXISTING reports (fractions 0..1) — see coverage_map for details
|
|
221
|
-
const knownFiles = (rawGraph(ctx).nodes || []).filter((n) => !String(n.id).includes('#') && n.source_file).map((n) => n.source_file)
|
|
222
|
-
const coverage = readCoverageForRepo(ctx.repoRoot, knownFiles)
|
|
223
|
-
const covOf = (file) => coverage.get(String(file).replace(/\\/g, '/'))?.pct ?? null
|
|
224
|
-
const pctStr = (v) => (v == null ? 'cov n/a' : `cov ${Math.round(v * 100)}%`)
|
|
225
|
-
const hasCoverage = coverage.size > 0
|
|
226
|
-
|
|
227
|
-
const shown = impacted.slice(0, cap)
|
|
228
|
-
const runtimeImpacted = impacted.filter((entry) => !entry.entry.compileOnly).length
|
|
229
|
-
const compileImpacted = impacted.length - runtimeImpacted
|
|
230
|
-
const untestedHotspots = shown.filter((n) => { const c = covOf(n.file); return c != null && c < 0.5 && n.deg >= 5 })
|
|
231
|
-
return [
|
|
232
|
-
`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)` : ''}.`,
|
|
233
|
-
impacted.length
|
|
234
|
-
? `${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s) (${runtimeImpacted} runtime, ${compileImpacted} compile-time-only), showing ${shown.length}:`
|
|
235
|
-
: `Nothing else in the graph depends on the changed code within ${maxDepth} hop(s).`,
|
|
236
|
-
hasCoverage ? `Coverage: existing report mapped where available.` : `Coverage: unavailable (no supported report found); per-node coverage labels omitted.`,
|
|
237
|
-
...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}]`),
|
|
238
|
-
untestedHotspots.length ? `` : null,
|
|
239
|
-
untestedHotspots.length ? `Untested hotspots in the blast radius (<50% coverage, deg ≥5) — cover these before shipping:` : null,
|
|
240
|
-
...untestedHotspots.slice(0, 10).map((n) => ` ${labelOf(g, n.id)} (${pctStr(covOf(n.file))}, deg ${n.deg}) ${n.file}`),
|
|
241
|
-
``,
|
|
242
|
-
`Drill into any node with get_dependents / read_source; per-symbol coverage via coverage_map.`,
|
|
243
|
-
].filter((x) => x != null).join('\n')
|
|
159
|
+
const baseline = tChangeImpactV2(g, args, ctx)
|
|
160
|
+
return refineChangeImpact(g, args, ctx, baseline, tChangeImpactV2)
|
|
244
161
|
}
|
|
@@ -5,6 +5,20 @@ import { openSync, closeSync, readSync, readFileSync, statSync } from "node:fs";
|
|
|
5
5
|
import { createRepoBoundary } from "./repo-path.js";
|
|
6
6
|
|
|
7
7
|
export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp"]);
|
|
8
|
+
export const PATH_CLASS_QUERY_TERMS = Object.freeze({
|
|
9
|
+
test: Object.freeze(["test", "tests", "testing", "unit"]),
|
|
10
|
+
e2e: Object.freeze(["e2e", "playwright", "cypress"]),
|
|
11
|
+
generated: Object.freeze(["generated", "autogenerated", "dist"]),
|
|
12
|
+
mock: Object.freeze(["mock", "mocks", "fixture", "fixtures", "fake"]),
|
|
13
|
+
story: Object.freeze(["story", "stories", "storybook"]),
|
|
14
|
+
docs: Object.freeze(["doc", "docs", "documentation", "readme", "guide"]),
|
|
15
|
+
benchmark: Object.freeze(["benchmark", "benchmarks", "bench"]),
|
|
16
|
+
temp: Object.freeze(["temp", "temporary", "tmp"]),
|
|
17
|
+
});
|
|
18
|
+
export const PATH_CLASS_TASK_QUERY_TERMS = Object.freeze({
|
|
19
|
+
...PATH_CLASS_QUERY_TERMS,
|
|
20
|
+
test: Object.freeze([...PATH_CLASS_QUERY_TERMS.test, "spec", "coverage", "verify"]),
|
|
21
|
+
});
|
|
8
22
|
const PATH_CLASS_SET = new Set(PATH_CLASS_NAMES);
|
|
9
23
|
const MAX_CONFIG_BYTES = 64 * 1024;
|
|
10
24
|
const MAX_RULES_PER_CLASS = 64;
|
|
@@ -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
|
}
|
|
@@ -57,6 +57,7 @@ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
|
|
|
57
57
|
|
|
58
58
|
const configRecords = new Map()
|
|
59
59
|
const projectFiles = new Set()
|
|
60
|
+
const configuredPlugins = new Set()
|
|
60
61
|
const projects = {}
|
|
61
62
|
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
62
63
|
if (Date.now() >= budget.deadline) return {safe: false, reason: 'SAFETY_DEADLINE', fingerprint: null}
|
|
@@ -68,7 +69,9 @@ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
|
|
|
68
69
|
projects[configRel] = {
|
|
69
70
|
projectFiles: parsed.projectFiles,
|
|
70
71
|
configFiles: [...parsed.configRecords.keys()].sort(),
|
|
72
|
+
configuredPlugins: parsed.plugins,
|
|
71
73
|
}
|
|
74
|
+
for (const plugin of parsed.plugins || []) configuredPlugins.add(plugin)
|
|
72
75
|
for (const [file, digest] of parsed.configRecords) {
|
|
73
76
|
configRecords.set(file, digest)
|
|
74
77
|
if (configRecords.size > MAX_CONFIG_FILES) {
|
|
@@ -133,6 +136,8 @@ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
|
|
|
133
136
|
projectFiles: [...projectFiles].sort(),
|
|
134
137
|
fileConfigs,
|
|
135
138
|
projects,
|
|
139
|
+
configuredPlugins: [...configuredPlugins].sort(),
|
|
140
|
+
pluginsSuppressed: configuredPlugins.size,
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -17,7 +17,7 @@ export const DEFAULT_STORE = join(homedir(), ".weavatrix", "advisories.json");
|
|
|
17
17
|
const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
18
18
|
const OSV_VULN_URL = "https://api.osv.dev/v1/vulns/";
|
|
19
19
|
const DEFAULT_FETCH_TIMEOUT_MS = Number(process.env.WEAVATRIX_OSV_TIMEOUT_MS || 20000);
|
|
20
|
-
export const OSV_SUPPORTED_ECOSYSTEMS = new Set(["npm", "PyPI", "Go"]);
|
|
20
|
+
export const OSV_SUPPORTED_ECOSYSTEMS = new Set(["npm", "PyPI", "Go", "Maven", "crates.io"]);
|
|
21
21
|
|
|
22
22
|
const keyOf = (ecosystem, name) => `${ecosystem}|${ecosystem === "PyPI" ? String(name).toLowerCase().replace(/[-_.]+/g, "-") : name}`;
|
|
23
23
|
|
|
@@ -121,7 +121,7 @@ export async function refreshAdvisories({ installed = [], storePath = DEFAULT_ST
|
|
|
121
121
|
ok: false,
|
|
122
122
|
queried: 0,
|
|
123
123
|
unsupported,
|
|
124
|
-
error: "No OSV-supported pinned package versions found to check.
|
|
124
|
+
error: "No OSV-supported pinned package versions found to check. Weavatrix queries OSV for npm, PyPI, Go, Maven/Gradle, and crates.io packages with concrete versions.",
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
const store = loadStore(storePath);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRepoBoundary } from "../repo-path.js";
|
|
2
|
+
import { listRepoFiles, readRepoText } from "../analysis/internal-audit/repo-files.js";
|
|
3
|
+
import { parseCargoLockPackages } from "../analysis/cargo-manifests.js";
|
|
4
|
+
import { parseGradleDependencies, parseGradleLockPackages, parseGradleVersionCatalog, parseMavenPom } from "../analysis/jvm-manifests.js";
|
|
5
|
+
|
|
6
|
+
const dedupe = (items) => {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
return items.filter((item) => {
|
|
9
|
+
const key = `${item.ecosystem}|${item.name}|${item.version}`;
|
|
10
|
+
if (!item.name || !item.version || seen.has(key)) return false;
|
|
11
|
+
seen.add(key); return true;
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
const concreteVersion = (value) => !!value && !/[\[\](),${}*+]/.test(String(value));
|
|
15
|
+
|
|
16
|
+
export function collectJvmRustInstalled(repoPath) {
|
|
17
|
+
const boundary = createRepoBoundary(repoPath);
|
|
18
|
+
if (!boundary.root) return [];
|
|
19
|
+
const files = listRepoFiles(repoPath);
|
|
20
|
+
const installed = [];
|
|
21
|
+
for (const file of files.filter((name) => /(^|\/)Cargo\.lock$/i.test(name))) {
|
|
22
|
+
installed.push(...parseCargoLockPackages(readRepoText(boundary, file)));
|
|
23
|
+
}
|
|
24
|
+
for (const file of files.filter((name) => /(^|\/)pom\.xml$/i.test(name))) {
|
|
25
|
+
const parsed = parseMavenPom(readRepoText(boundary, file));
|
|
26
|
+
installed.push(...parsed.dependencies.filter((item) => concreteVersion(item.version)).map((item) => ({
|
|
27
|
+
ecosystem: "Maven", name: item.name, version: item.version, dev: item.scope === "test",
|
|
28
|
+
integrity: "", source: "pom",
|
|
29
|
+
})));
|
|
30
|
+
}
|
|
31
|
+
const catalog = new Map();
|
|
32
|
+
for (const file of files.filter((name) => /(^|\/)libs\.versions\.toml$/i.test(name))) {
|
|
33
|
+
for (const [alias, entry] of parseGradleVersionCatalog(readRepoText(boundary, file))) catalog.set(alias, entry);
|
|
34
|
+
}
|
|
35
|
+
for (const file of files.filter((name) => /(^|\/)(?:build|settings|[^/]+)\.gradle(?:\.kts)?$/i.test(name))) {
|
|
36
|
+
const dependencies = parseGradleDependencies(readRepoText(boundary, file), catalog);
|
|
37
|
+
installed.push(...dependencies.filter((item) => concreteVersion(item.version)).map((item) => ({
|
|
38
|
+
ecosystem: "Maven", name: item.name, version: item.version,
|
|
39
|
+
dev: /test/i.test(item.scope || ""), integrity: "", source: "gradle-manifest",
|
|
40
|
+
})));
|
|
41
|
+
}
|
|
42
|
+
for (const file of files.filter((name) => /(^|\/)(?:gradle\.lockfile|dependency-locks\/[^/]+\.lockfile)$/i.test(name))) {
|
|
43
|
+
installed.push(...parseGradleLockPackages(readRepoText(boundary, file)));
|
|
44
|
+
}
|
|
45
|
+
return dedupe(installed);
|
|
46
|
+
}
|
|
@@ -7,6 +7,8 @@ import { join, relative } from "node:path";
|
|
|
7
7
|
import { parseGoMod } from "../analysis/manifests.js";
|
|
8
8
|
import { uniqueBy } from "../util.js";
|
|
9
9
|
import { createRepoBoundary } from "../repo-path.js";
|
|
10
|
+
import { collectJvmRustInstalled } from "./installed-jvm-rust.js";
|
|
11
|
+
import { listRepoFiles, readRepoText } from "../analysis/internal-audit/repo-files.js";
|
|
10
12
|
|
|
11
13
|
const pep503 = (name) => String(name).toLowerCase().replace(/[-_.]+/g, "-"); // PyPI canonical name
|
|
12
14
|
|
|
@@ -256,5 +258,23 @@ export function collectInstalled(repoPath) {
|
|
|
256
258
|
if (!vers) merged.push(d);
|
|
257
259
|
else if (!vers.has(d.version)) drift.push({ name: d.name, locked: locked.get(d.name).version, installed: d.version });
|
|
258
260
|
}
|
|
259
|
-
|
|
261
|
+
const jvmRust = collectJvmRustInstalled(repoPath);
|
|
262
|
+
// The earlier fast path covers roots and immediate services. Complete the tracked repository
|
|
263
|
+
// universe so deeply nested Python/Go services are not silently omitted from advisory coverage.
|
|
264
|
+
const repositoryFiles = listRepoFiles(repoPath);
|
|
265
|
+
const allPython = repositoryFiles.flatMap((file) => {
|
|
266
|
+
const text = readRepoText(boundary, file);
|
|
267
|
+
if (/(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(file) || /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(file)) return parseRequirements(text || "");
|
|
268
|
+
if (/(^|\/)poetry\.lock$/i.test(file)) return parseTomlLockPackages(text || "", "poetry-lock");
|
|
269
|
+
if (/(^|\/)uv\.lock$/i.test(file)) return parseTomlLockPackages(text || "", "uv-lock");
|
|
270
|
+
if (/(^|\/)Pipfile\.lock$/i.test(file)) { try { return parsePipfileLock(JSON.parse(text)); } catch { return []; } }
|
|
271
|
+
return [];
|
|
272
|
+
});
|
|
273
|
+
const allGo = repositoryFiles.flatMap((file) => {
|
|
274
|
+
const text = readRepoText(boundary, file) || "";
|
|
275
|
+
if (/(^|\/)go\.sum$/i.test(file)) return parseGoSum(text);
|
|
276
|
+
if (/(^|\/)go\.mod$/i.test(file)) return parseGoModPackages(text);
|
|
277
|
+
return [];
|
|
278
|
+
});
|
|
279
|
+
return { installed: dedupe([...merged, ...py, ...go, ...allPython, ...allGo, ...jvmRust]), drift };
|
|
260
280
|
}
|
|
@@ -74,7 +74,7 @@ export async function rgSweep(rg, contentRoots, timeoutMs) {
|
|
|
74
74
|
const lineText = String(obj.data?.lines?.text || "");
|
|
75
75
|
const subText = String(obj.data?.submatches?.[0]?.match?.text || "");
|
|
76
76
|
for (const at of matchRuleOffsets(rule, lineText, subText)) {
|
|
77
|
-
const text = (lineText ? lineText.slice(Math.max(0, at -
|
|
77
|
+
const text = (lineText ? lineText.slice(Math.max(0, at - 160), at + 180) : subText).trim().slice(0, 340);
|
|
78
78
|
hits.push({ pkg, file, line: obj.data?.line_number || 0, text, rule });
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -88,6 +88,7 @@ const BENIGN_URL_HOSTS = [
|
|
|
88
88
|
"apache.org", "opensource.org", "gnu.org", "creativecommons.org", "choosealicense.com",
|
|
89
89
|
"mozilla.org", "nodejs.org", "npmjs.com", "npmjs.org", "yarnpkg.com", "python.org", "pypi.org",
|
|
90
90
|
"golang.org", "go.dev", "godoc.org", "readthedocs.io", "readthedocs.org", "editorconfig.org",
|
|
91
|
+
"aka.ms", "example.com", "example.org", "example.net",
|
|
91
92
|
];
|
|
92
93
|
const DOC_URL_PATH_RE = /\/(docs?|documentation|wiki|issues?|pull|blob|tree|releases|licenses?|rfc\d*|help|manual|guide|spec|schemas?)([/#?.]|$)/i;
|
|
93
94
|
const URL_TOKEN_RE = /https?:\/\/[^\s'"`)\\<>]+/gi;
|
|
@@ -109,7 +110,7 @@ export function isBenignUrlContext(text) {
|
|
|
109
110
|
const s = String(text || "");
|
|
110
111
|
const urls = [...s.matchAll(URL_TOKEN_RE)];
|
|
111
112
|
if (!urls.length) return false;
|
|
112
|
-
if (COMMENT_MARKER_RE.test(s.slice(
|
|
113
|
+
if (COMMENT_MARKER_RE.test(s.slice(0, urls[0].index))) return true;
|
|
113
114
|
return urls.every((m) => {
|
|
114
115
|
const pre = s.slice(Math.max(0, m.index - 80), m.index);
|
|
115
116
|
const post = s.slice(m.index + m[0].length, m.index + m[0].length + 48);
|