weavatrix 0.3.2 → 0.3.4
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 +31 -0
- package/SECURITY.md +18 -2
- package/docs/releases/v0.3.3.md +25 -0
- package/docs/releases/v0.3.4.md +40 -0
- package/package.json +3 -1
- package/src/analysis/cycle-route.js +1 -1
- package/src/analysis/dead-check.js +29 -23
- package/src/analysis/dead-code-review/policy.js +1 -1
- package/src/analysis/findings.js +1 -2
- package/src/analysis/package-reachability.js +1 -1
- package/src/graph/builder/js/dynamic-import.js +26 -0
- package/src/graph/builder/lang-js.js +3 -1
- package/src/graph/builder/spec-pkg.js +2 -2
- package/src/graph/freshness-probe.js +2 -2
- package/src/graph/internal-builder.langs.js +12 -4
- package/src/graph/parser-artifact-boundary.js +54 -0
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/evidence-snapshot.duplicates.mjs +1 -1
- package/src/mcp/health/audit-format.mjs +24 -4
- package/src/mcp/runtime-version.mjs +1 -2
- package/src/mcp/sync/evidence-common.mjs +1 -1
- package/src/mcp/tool-result.mjs +1 -1
- package/src/precision/lsp-client/lifecycle.js +26 -39
- package/src/precision/lsp-client/registry.js +4 -3
- package/src/precision/lsp-client/stdio-client.js +1 -12
- package/src/precision/symbol-query.js +2 -2
- package/src/precision/typescript-provider/discovery.js +1 -1
- package/src/security/advisory-store.js +1 -1
package/README.md
CHANGED
|
@@ -413,6 +413,37 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
|
|
|
413
413
|
while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
|
|
414
414
|
`STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
|
|
415
415
|
|
|
416
|
+
### 0.3.4 self-audit precision and bounded dynamic imports
|
|
417
|
+
|
|
418
|
+
- JS/TS graph construction now resolves the existing local-file prefix in
|
|
419
|
+
``import(new URL(`./tool.mjs${runtimeValue}`, import.meta.url).href)`` while
|
|
420
|
+
preserving the edge as dynamic. Variable-first paths, remote URLs, alternate
|
|
421
|
+
bases and other runtime-only targets remain explicit unknowns.
|
|
422
|
+
- Health dependency headlines now follow the same production-first path scope as
|
|
423
|
+
the displayed findings. Classified fixture results are counted separately
|
|
424
|
+
instead of making a clean application headline look unhealthy.
|
|
425
|
+
- Dead-code liveness no longer treats comment-only symbol mentions as callers;
|
|
426
|
+
string-addressed runtime registries remain conservatively live. Dogfood removed
|
|
427
|
+
obsolete internal exports/wrappers while preserving every supported Online
|
|
428
|
+
extension point and TypeScript host callback.
|
|
429
|
+
|
|
430
|
+
Full patch notes: [docs/releases/v0.3.4.md](docs/releases/v0.3.4.md).
|
|
431
|
+
|
|
432
|
+
### 0.3.3 parser supply-chain boundary
|
|
433
|
+
|
|
434
|
+
- Before compiling parser WASM, Core now requires the package-pinned `web-tree-sitter` runtime and
|
|
435
|
+
each selected `tree-sitter-wasms` grammar to resolve inside its dependency directory and match a
|
|
436
|
+
release-pinned SHA-256 allowlist. Repository paths, URLs, symlinks and custom parser locations are
|
|
437
|
+
rejected; a mismatch fails the graph build.
|
|
438
|
+
- This bounds the upstream Emscripten dynamic-module loader even though its generated code contains
|
|
439
|
+
`eval` glue for EM_ASM/EM_JS side modules. Weavatrix loads only its known local runtime and grammar
|
|
440
|
+
artifacts; it does not load a parser or grammar from the analyzed repository.
|
|
441
|
+
- Supply-chain documentation now distinguishes the Apache-2.0 license declared by
|
|
442
|
+
`typescript@5.9.3` from the W3C agreement reproduced for one specification in its third-party
|
|
443
|
+
notices. Conservative dependency scanners may still surface that component notice for review.
|
|
444
|
+
|
|
445
|
+
Full patch notes: [docs/releases/v0.3.3.md](docs/releases/v0.3.3.md).
|
|
446
|
+
|
|
416
447
|
### 0.3.2 exact impact over oversized diffs
|
|
417
448
|
|
|
418
449
|
- `change_impact` now recovers a bounded per-file unified diff when one large asset exceeds the
|
package/SECURITY.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
|---|---|
|
|
7
|
-
| `0.
|
|
8
|
-
| `0.2.
|
|
7
|
+
| `0.3.x` | Yes |
|
|
8
|
+
| `0.2.x` and older | Upgrade to the latest release |
|
|
9
9
|
|
|
10
10
|
Security fixes are provided for the latest published version of Weavatrix. Upgrade before reporting
|
|
11
11
|
an issue that may already be resolved.
|
|
@@ -56,6 +56,22 @@ bounded graph-work drain, and closes or tree-terminates TLS/tsserver before exit
|
|
|
56
56
|
Set `WEAVATRIX_PRECISION=off` before server startup (or select `off` in the MCPB semantic-precision
|
|
57
57
|
setting) to keep new graphs parser-only from their first build.
|
|
58
58
|
|
|
59
|
+
The parser runtime and language grammars are also exact package-pinned dependencies. Before compiling
|
|
60
|
+
WASM, Weavatrix requires the runtime and every selected grammar to resolve inside their dependency
|
|
61
|
+
directories and match a release-pinned SHA-256 allowlist. Repository paths, URLs, symlinks and custom
|
|
62
|
+
`runtimeWasm`/`wasmDir` overrides cannot enter this path. This specifically bounds the generated
|
|
63
|
+
Emscripten dynamic-module loader in `web-tree-sitter`: although upstream contains `eval` glue for
|
|
64
|
+
EM_ASM/EM_JS side-module exports, Weavatrix supplies only its known local runtime and grammar artifacts.
|
|
65
|
+
An integrity mismatch fails the graph build instead of silently falling back to an unverified parser.
|
|
66
|
+
|
|
67
|
+
Dependency scanners should distinguish a package's declared license from component notices.
|
|
68
|
+
`typescript@5.9.3` declares `Apache-2.0`; its `ThirdPartyNoticeText.txt` also reproduces the W3C Community
|
|
69
|
+
Final Specification Agreement for the Web Background Synchronization specification. That attribution
|
|
70
|
+
does not change the npm package's declared license, but conservative scanners may report the embedded
|
|
71
|
+
notice separately. `web-tree-sitter@0.25.10` declares MIT; its generated loader is covered by the
|
|
72
|
+
artifact boundary above. These observations are security evidence, not a request to suppress third-party
|
|
73
|
+
scanner warnings or legal advice.
|
|
74
|
+
|
|
59
75
|
`verified_change` is read-only by default. Its optional targeted-test step can execute only an
|
|
60
76
|
existing `package.json` script whose name matches the bounded test/check/verify allowlist. The call
|
|
61
77
|
must set `run_tests:true` and the server operator must separately set
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Weavatrix 0.3.3
|
|
2
|
+
|
|
3
|
+
0.3.3 hardens the portable parser boundary after reviewing two conservative
|
|
4
|
+
third-party supply-chain alerts.
|
|
5
|
+
|
|
6
|
+
`web-tree-sitter@0.25.10` is MIT-licensed, but its generated Emscripten loader
|
|
7
|
+
contains `eval` support for EM_ASM/EM_JS exports in dynamic side modules. The
|
|
8
|
+
same generated path remains in `web-tree-sitter@0.26.11`; changing the version
|
|
9
|
+
would not remove the signal. Weavatrix now verifies the exact SHA-256 of the
|
|
10
|
+
package-owned runtime and every selected `tree-sitter-wasms@0.1.13` grammar
|
|
11
|
+
before compilation. Parser artifacts must resolve inside their pinned package
|
|
12
|
+
directories. Repository paths, URLs, symlink escapes and custom runtime or
|
|
13
|
+
grammar locations are rejected, and an integrity mismatch fails closed.
|
|
14
|
+
|
|
15
|
+
The release also documents a separate license-classification alert.
|
|
16
|
+
`typescript@5.9.3` declares `Apache-2.0`. Its third-party notice reproduces the
|
|
17
|
+
W3C Community Final Specification Agreement for the Web Background
|
|
18
|
+
Synchronization specification; conservative scanners can identify that
|
|
19
|
+
component notice as non-permissive even though it is not the package's declared
|
|
20
|
+
license. Weavatrix keeps the notice visible and does not suppress the external
|
|
21
|
+
scanner finding.
|
|
22
|
+
|
|
23
|
+
The public site now presents the product boundary directly: the 34-tool MIT,
|
|
24
|
+
network-free Core and the separately licensed 39-tool Online overlay, including
|
|
25
|
+
managed Cloud and customer-hosted Enterprise availability.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Weavatrix 0.3.4
|
|
2
|
+
|
|
3
|
+
0.3.4 is a focused self-audit precision release. It removes false Health and
|
|
4
|
+
dead-code noise found while dogfooding Core against its own current source,
|
|
5
|
+
without widening the 34-tool offline boundary.
|
|
6
|
+
|
|
7
|
+
## Bounded dynamic-import evidence
|
|
8
|
+
|
|
9
|
+
JS/TS graph construction now recognizes the statically named local module in
|
|
10
|
+
URL-based imports such as
|
|
11
|
+
``import(new URL(`./tools-health.mjs${cacheBust}`, import.meta.url).href)``.
|
|
12
|
+
The resolved local candidate participates in reachability, while the edge keeps
|
|
13
|
+
its `dynamic` marker. Variable-first templates, remote URLs, non-`import.meta.url`
|
|
14
|
+
bases and other runtime-only expressions remain unresolved rather than being
|
|
15
|
+
guessed.
|
|
16
|
+
|
|
17
|
+
## Health and dead-code precision
|
|
18
|
+
|
|
19
|
+
- The dependency summary uses the same production-first path scope as the
|
|
20
|
+
findings shown below it. Findings isolated to fixtures/tests are reported as a
|
|
21
|
+
separate suppressed count instead of leaking into the production headline.
|
|
22
|
+
- Comment-only lexical mentions no longer prove symbol liveness. String-based
|
|
23
|
+
registries and reflective dispatch stay conservative because they may be real
|
|
24
|
+
runtime entry points.
|
|
25
|
+
- Dogfood removed unnecessary internal exports, obsolete declarations and
|
|
26
|
+
uncalled LSP wrapper methods. Supported Online extension exports and
|
|
27
|
+
TypeScript host callbacks remain intact.
|
|
28
|
+
|
|
29
|
+
Current-source dogfood reports zero Health findings in both full and no-tests
|
|
30
|
+
graphs. The remaining low-confidence dead-code queue contains only the five
|
|
31
|
+
supported Online extension exports and two TypeScript host callbacks. Classified
|
|
32
|
+
benchmark dependency findings remain available through `include_classified`.
|
|
33
|
+
|
|
34
|
+
The release remains gated by the full Node suite, release metadata validation,
|
|
35
|
+
portable MCPB/npm runtime smoke, dependency audit and self-dogfood.
|
|
36
|
+
|
|
37
|
+
The strict source-free real-repository benchmark was refreshed from clean,
|
|
38
|
+
revision-bound checkouts and passes all six language repositories (TypeScript,
|
|
39
|
+
JavaScript, Python, Go, Java and Rust) with complete provenance, expected-signal
|
|
40
|
+
and relation-regression gates.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local repository intelligence MCP for AI coding agents: reusable architecture graph for fast application understanding, Health, dead code, clones, history, blast radius and architecture safeguards.",
|
|
6
6
|
"author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"docs/releases/v0.3.0.md",
|
|
37
37
|
"docs/releases/v0.3.1.md",
|
|
38
38
|
"docs/releases/v0.3.2.md",
|
|
39
|
+
"docs/releases/v0.3.3.md",
|
|
40
|
+
"docs/releases/v0.3.4.md",
|
|
39
41
|
"docs/releases/v0.2.19.md",
|
|
40
42
|
"README.md",
|
|
41
43
|
"SECURITY.md",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const MAX_REPRESENTATIVE_CYCLE_FILES = 64;
|
|
2
2
|
|
|
3
3
|
// Keep the complete closed route for normal cycles. Very large SCCs still get a closed, deterministic
|
|
4
4
|
// head/tail route without letting one finding consume the whole audit response.
|
|
@@ -2,17 +2,37 @@
|
|
|
2
2
|
// calls/imports/references/inherits it) AND its bare name appears NOWHERE in the repo outside its own file.
|
|
3
3
|
// A file is DEAD when nothing imports it, it isn't an entry point, and all its symbols are dead.
|
|
4
4
|
//
|
|
5
|
-
// Pure core `computeDead(graph, sources)` (sources = Map<fileRel, text>)
|
|
6
|
-
//
|
|
7
|
-
// graph-builder) — it only needs {nodes, links} + the source text. See [[graph-builder-internalization]].
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
// Pure core `computeDead(graph, sources)` (sources = Map<fileRel, text>) works on any graph-builder-schema graph
|
|
6
|
+
// and is fully testable with no filesystem. It only needs {nodes, links} + source text. See [[graph-builder-internalization]].
|
|
9
7
|
import { posix } from "node:path";
|
|
10
|
-
import { createRepoBoundary } from "../repo-path.js";
|
|
11
8
|
import { isStructuralRelation } from "../graph/relations.js";
|
|
12
9
|
import { createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
13
10
|
|
|
14
11
|
const IDENT_RE = /[A-Za-z_$][\w$]*/g;
|
|
15
12
|
const bareName = (label) => String(label || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
|
|
13
|
+
// Ignore comment-only lexical mentions, which are documentation rather than callers. Keep strings on
|
|
14
|
+
// purpose: registries/reflection often address a live symbol by name, and static liveness must stay
|
|
15
|
+
// conservative there. This only strips comments that start a logical line, avoiding a language parser
|
|
16
|
+
// guess around inline comment markers, regex literals, or URLs.
|
|
17
|
+
const lexicalEvidenceText = (value) => {
|
|
18
|
+
let inBlockComment = false;
|
|
19
|
+
return String(value || "").split(/\r?\n/).map((line) => {
|
|
20
|
+
let rest = line;
|
|
21
|
+
while (true) {
|
|
22
|
+
if (inBlockComment) {
|
|
23
|
+
const end = rest.indexOf("*/");
|
|
24
|
+
if (end < 0) return "";
|
|
25
|
+
inBlockComment = false;
|
|
26
|
+
rest = rest.slice(end + 2);
|
|
27
|
+
}
|
|
28
|
+
const trimmed = rest.trimStart();
|
|
29
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#")) return "";
|
|
30
|
+
if (!trimmed.startsWith("/*")) return rest;
|
|
31
|
+
inBlockComment = true;
|
|
32
|
+
rest = trimmed.slice(2);
|
|
33
|
+
}
|
|
34
|
+
}).join("\n");
|
|
35
|
+
};
|
|
16
36
|
// entry surfaces are never dead even with no inbound edge (framework/CLI/HTTP enter them externally).
|
|
17
37
|
// Exported for internal-audit.js (reachability entry set) — keep the two in lockstep.
|
|
18
38
|
export const ENTRY_FILE = /(^|[\\/])(index|main|app|server|cli|cmd|bootstrap|entry|run|__main__|manage|wsgi|asgi|setup|conftest)\.[a-z0-9]+$|(^|[\\/])(bin|cmd)[\\/]|(^|[\\/])main\.go$/i;
|
|
@@ -21,8 +41,8 @@ const isTestFile = (file) => hasPathClass(defaultPathClassifier.explain(file), "
|
|
|
21
41
|
|
|
22
42
|
// Framework-owned entry modules are invoked by convention rather than a source import. Keep this narrow:
|
|
23
43
|
// these are Next.js App/Pages Router surfaces and framework metadata files, not every file under `app/`.
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
const NEXT_ENTRY_FILE = /(^|\/)(?:src\/)?app\/(?:.*\/)?(?:page|layout|template|loading|error|global-error|not-found|default|route|robots|sitemap|manifest|opengraph-image|twitter-image|icon|apple-icon)\.[cm]?[jt]sx?$|(^|\/)(?:src\/)?pages\/(?!.*\/(?:components?|lib|utils?)\/).+\.[cm]?[jt]sx?$|(^|\/)(?:middleware|instrumentation)\.[cm]?[jt]s$/i;
|
|
45
|
+
const RUST_ENTRY_FILE = /(^|\/)(?:build\.rs|src\/(?:lib|main)\.rs)$/i;
|
|
26
46
|
export const isFrameworkEntryFile = (file) => {
|
|
27
47
|
const normalized = String(file || "").replace(/\\/g, "/");
|
|
28
48
|
return NEXT_ENTRY_FILE.test(normalized) || RUST_ENTRY_FILE.test(normalized);
|
|
@@ -114,7 +134,7 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
|
114
134
|
// whole-repo identifier frequency: a symbol whose name appears MORE than once total (its definition + at least
|
|
115
135
|
// one use, same-file OR cross-file) is referenced. Errs toward "alive" (common-named symbols never flagged).
|
|
116
136
|
const globalFreq = new Map();
|
|
117
|
-
for (const [, text] of sources) for (const m of
|
|
137
|
+
for (const [, text] of sources) for (const m of lexicalEvidenceText(text).matchAll(IDENT_RE)) { const n = m[0]; globalFreq.set(n, (globalFreq.get(n) || 0) + 1); }
|
|
118
138
|
|
|
119
139
|
const symById = new Map();
|
|
120
140
|
const symsByFile = new Map();
|
|
@@ -127,7 +147,7 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
|
127
147
|
const symbolNames = new Set([...symById.values()].map((node) => bareName(node.label)).filter(Boolean));
|
|
128
148
|
const occurrenceFiles = new Map();
|
|
129
149
|
const occurrenceCounts = new Map();
|
|
130
|
-
for (const [file, text] of sources) for (const match of
|
|
150
|
+
for (const [file, text] of sources) for (const match of lexicalEvidenceText(text).matchAll(IDENT_RE)) {
|
|
131
151
|
const name = match[0];
|
|
132
152
|
if (!symbolNames.has(name)) continue;
|
|
133
153
|
const files = occurrenceFiles.get(name) || new Set();
|
|
@@ -276,17 +296,3 @@ export function computeUnusedExports(graph, sources, { dynamicTargets = new Set(
|
|
|
276
296
|
}
|
|
277
297
|
return out;
|
|
278
298
|
}
|
|
279
|
-
|
|
280
|
-
// fs wrapper: reads each graph file's source once, then runs the pure checker.
|
|
281
|
-
export function analyzeDeadCode(graph, repoRoot) {
|
|
282
|
-
const sources = new Map();
|
|
283
|
-
const files = new Set();
|
|
284
|
-
const boundary = createRepoBoundary(repoRoot);
|
|
285
|
-
for (const n of graph.nodes || []) if (n.source_file) files.add(n.source_file);
|
|
286
|
-
for (const f of files) {
|
|
287
|
-
const resolved = boundary.resolve(f);
|
|
288
|
-
if (!resolved.ok) continue;
|
|
289
|
-
try { sources.set(f, readFileSync(resolved.path, "utf8")); } catch { /* file gone */ }
|
|
290
|
-
}
|
|
291
|
-
return computeDead(graph, sources);
|
|
292
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {hasPathClass} from '../../path-classification.js'
|
|
2
2
|
|
|
3
3
|
export const DEAD_CODE_CONFIDENCE_RANK = Object.freeze({high: 0, medium: 1, low: 2})
|
|
4
|
-
|
|
4
|
+
const DYNAMIC_CODE_RE = /(?:\brequire\s*\(\s*(?!["'])|\bcreateRequire\s*\(|\b__import__\s*\(|\bimportlib\.|(?:^|[^\w.$])(?:eval|exec)\s*\()/m
|
|
5
5
|
const JS_DYNAMIC_IMPORT_RE = /\bimport\s*\(/
|
|
6
6
|
const JS_LIKE_PATH_RE = /\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/i
|
|
7
7
|
export const REFLECTION_CODE_RE = /(?:\b(?:Class\.forName|get(?:Declared)?Method|getattr|setattr|hasattr|Method\.Invoke|GetMethod|GetProcAddress|dlsym)\s*\(|\b(?:globals|locals)\s*\(\s*\)\s*\[|\breflect\.[A-Za-z_$][\w$]*\s*\()/i
|
package/src/analysis/findings.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// so the renderer and the AI summarizer consume one contract regardless of engine.
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
export const FINDING_CATEGORIES = ["unused", "structure", "vulnerability", "malware"];
|
|
6
|
+
const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
8
7
|
|
|
9
8
|
export function dependencyVerification(manifest, imports, decision, mapping) {
|
|
10
9
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Package-level reachability: correlate a dependency finding with the graph's external imports.
|
|
2
2
|
// This is intentionally not function-level exploitability analysis. It only states whether product
|
|
3
3
|
// code imports the package and preserves unknown as unknown.
|
|
4
|
-
|
|
4
|
+
const PACKAGE_REACHABILITY_V = 1
|
|
5
5
|
|
|
6
6
|
export function packageReachability(externalImports, packageName, {isNonProductPath = () => false} = {}) {
|
|
7
7
|
const imports = (Array.isArray(externalImports) ? externalImports : [])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Resolve only the bounded local-file portion of URL-based dynamic imports.
|
|
2
|
+
// Runtime substitutions remain uncertain; this candidate exists solely so a
|
|
3
|
+
// statically named module is not mislabeled as an orphan.
|
|
4
|
+
export function boundedUrlImportTarget(arg, {field, fileRel, resolveJsImport}) {
|
|
5
|
+
if (!arg || arg.type !== "member_expression" || field(arg, "property")?.text !== "href") return null;
|
|
6
|
+
const created = field(arg, "object");
|
|
7
|
+
if (!created || created.type !== "new_expression" || field(created, "constructor")?.text !== "URL") return null;
|
|
8
|
+
const urlArgs = field(created, "arguments")?.namedChildren || [];
|
|
9
|
+
if (urlArgs.length !== 2 || urlArgs[1].text !== "import.meta.url") return null;
|
|
10
|
+
|
|
11
|
+
const source = urlArgs[0];
|
|
12
|
+
let rawSpec = "";
|
|
13
|
+
if (source.type === "string") {
|
|
14
|
+
rawSpec = source.text.replace(/^['"`]|['"`]$/g, "");
|
|
15
|
+
} else if (source.type === "template_string") {
|
|
16
|
+
const parts = source.namedChildren || [];
|
|
17
|
+
if (parts.length !== 2 || parts[0].type !== "string_fragment" || parts[1].type !== "template_substitution") return null;
|
|
18
|
+
rawSpec = parts[0].text;
|
|
19
|
+
} else return null;
|
|
20
|
+
|
|
21
|
+
if (!/^\.\.?\//.test(rawSpec)) return null;
|
|
22
|
+
const staticPath = rawSpec.split(/[?#]/)[0];
|
|
23
|
+
if (!/\.[cm]?[jt]sx?$/i.test(staticPath)) return null;
|
|
24
|
+
const target = resolveJsImport(fileRel, staticPath);
|
|
25
|
+
return target ? {rawSpec: staticPath, target} : null;
|
|
26
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CALLABLE, FUNCS, TOPVARS, TYPES, REQUIRE, parseExportSpecifiers,
|
|
7
7
|
importTypeOnly, reexportTypeOnly,
|
|
8
8
|
} from './js/queries.js';
|
|
9
|
+
import {boundedUrlImportTarget} from './js/dynamic-import.js';
|
|
9
10
|
|
|
10
11
|
export default {
|
|
11
12
|
family: "js",
|
|
@@ -201,7 +202,8 @@ export default {
|
|
|
201
202
|
else if (isBareSpec(rawSpec)) addExternalImport({ spec: rawSpec, kind: "dynamic", line });
|
|
202
203
|
else if (rawSpec) recordUnresolved(rawSpec, "dynamic", line);
|
|
203
204
|
} else {
|
|
204
|
-
|
|
205
|
+
const bounded = boundedUrlImportTarget(arg, {field, fileRel, resolveJsImport});
|
|
206
|
+
addExternalImport({ dynamic: true, kind: "dynamic", line, ...(bounded ? {spec: bounded.rawSpec, target: bounded.target} : {}) });
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
|
|
@@ -62,7 +62,7 @@ export const PY_STDLIB = new Set(("__future__ __main__ _thread abc aifc argparse
|
|
|
62
62
|
|
|
63
63
|
// import name → PyPI dist where they differ. Generic python-X / X-python / X[2]-binary equivalence is
|
|
64
64
|
// handled by the matcher in dep-check; this map covers the truly irregular names.
|
|
65
|
-
|
|
65
|
+
const PY_IMPORT_TO_DIST = {
|
|
66
66
|
yaml: "PyYAML", cv2: "opencv-python", PIL: "Pillow", sklearn: "scikit-learn", skimage: "scikit-image",
|
|
67
67
|
bs4: "beautifulsoup4", dateutil: "python-dateutil", dotenv: "python-dotenv", jose: "python-jose",
|
|
68
68
|
magic: "python-magic", multipart: "python-multipart", docx: "python-docx", pptx: "python-pptx",
|
|
@@ -78,7 +78,7 @@ export const PY_IMPORT_TO_DIST = {
|
|
|
78
78
|
};
|
|
79
79
|
// namespace roots shared by many dists (google.protobuf/google.cloud.* …) — too ambiguous to name one
|
|
80
80
|
// package; "src" is a repo-layout artifact (stale sys.path import), never a PyPI dist.
|
|
81
|
-
|
|
81
|
+
const PY_AMBIGUOUS_TOP = new Set(["google", "src"]);
|
|
82
82
|
export function pySpecToPkg(topModule) {
|
|
83
83
|
const t = String(topModule || "").trim();
|
|
84
84
|
if (!t) return null;
|
|
@@ -8,8 +8,8 @@ import {createRequire} from 'node:module'
|
|
|
8
8
|
import {childProcessEnv} from '../child-env.js'
|
|
9
9
|
import {createRepoBoundary} from '../repo-path.js'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const REPOSITORY_FRESHNESS_PROBE_V = 1
|
|
12
|
+
const GRAPH_BUILDER_SCHEMA_V = 6
|
|
13
13
|
export const GRAPH_BUILDER_VERSION = (() => {
|
|
14
14
|
try { return String(createRequire(import.meta.url)('../../package.json').version) }
|
|
15
15
|
catch { return '0.0.0' }
|
|
@@ -10,6 +10,7 @@ import { createRequire } from "node:module";
|
|
|
10
10
|
import { isPathInside } from "../repo-path.js";
|
|
11
11
|
import { childProcessEnv } from "../child-env.js";
|
|
12
12
|
import { filterWeavatrixIgnored } from "../path-ignore.js";
|
|
13
|
+
import {trustedGrammarWasm, trustedRuntimeWasm} from './parser-artifact-boundary.js'
|
|
13
14
|
import LANG_JS from "./builder/lang-js.js";
|
|
14
15
|
import LANG_PY from "./builder/lang-python.js";
|
|
15
16
|
import LANG_GO from "./builder/lang-go.js";
|
|
@@ -24,7 +25,6 @@ const { Parser, Language, Query } = require("web-tree-sitter");
|
|
|
24
25
|
|
|
25
26
|
const WTS_DIR = dirname(require.resolve("web-tree-sitter"));
|
|
26
27
|
const NODE_MODULES = dirname(WTS_DIR);
|
|
27
|
-
const DEFAULT_RUNTIME_WASM = join(WTS_DIR, "tree-sitter.wasm");
|
|
28
28
|
const DEFAULT_WASM_DIR = join(NODE_MODULES, "tree-sitter-wasms", "out");
|
|
29
29
|
|
|
30
30
|
// ---- language registry (derived from the per-language modules) ----
|
|
@@ -61,11 +61,19 @@ const _langs = {};
|
|
|
61
61
|
// in the repo get compiled. The heavy WASMs (rust ~3MB, c_sharp ~4MB) cost seconds to compile — a tax
|
|
62
62
|
// a JS-only repo must never pay. Omit `wanted` to load everything (old behavior).
|
|
63
63
|
async function ensureParser(opts = {}, wanted = null) {
|
|
64
|
-
if (
|
|
64
|
+
if (opts.runtimeWasm != null || opts.wasmDir != null) {
|
|
65
|
+
throw new Error('custom parser artifacts are not supported; Weavatrix loads only pinned package-owned WASM')
|
|
66
|
+
}
|
|
67
|
+
if (!_ready) {
|
|
68
|
+
const runtimeWasm = trustedRuntimeWasm(WTS_DIR)
|
|
69
|
+
_ready = Parser.init({ locateFile: () => runtimeWasm });
|
|
70
|
+
}
|
|
65
71
|
await _ready;
|
|
66
|
-
const wasmDir = opts.wasmDir || DEFAULT_WASM_DIR;
|
|
67
72
|
const list = wanted ? GRAMMARS.filter((g) => wanted.has(g)) : GRAMMARS;
|
|
68
|
-
for (const g of list) if (!_langs[g]) {
|
|
73
|
+
for (const g of list) if (!_langs[g]) {
|
|
74
|
+
const grammarWasm = trustedGrammarWasm(DEFAULT_WASM_DIR, g)
|
|
75
|
+
try { _langs[g] = await Language.load(grammarWasm); } catch { _langs[g] = null; }
|
|
76
|
+
}
|
|
69
77
|
return _langs;
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto'
|
|
2
|
+
import {readFileSync, realpathSync, statSync} from 'node:fs'
|
|
3
|
+
import {join} from 'node:path'
|
|
4
|
+
import {isPathInside} from '../repo-path.js'
|
|
5
|
+
|
|
6
|
+
// Exact SHA-256 allowlist for the parser artifacts shipped by the package-pinned
|
|
7
|
+
// web-tree-sitter@0.25.10 and tree-sitter-wasms@0.1.13 dependencies. npm integrity
|
|
8
|
+
// protects installation; this second boundary detects replacement or redirection
|
|
9
|
+
// before Emscripten compiles a runtime or dynamic language module.
|
|
10
|
+
const RUNTIME_SHA256 = 'f38dcc4b43b818f9a0785bc1c6d5611a75ac4cdd428ff3f02757c34ca4e46d7f'
|
|
11
|
+
const GRAMMAR_SHA256 = Object.freeze({
|
|
12
|
+
javascript: '63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5',
|
|
13
|
+
typescript: '8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f',
|
|
14
|
+
tsx: '6aa3b2c70e76f5d48eafef1093e9c4de383e13f2fdde2f4e9b98a378f6a8f1b6',
|
|
15
|
+
python: '9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b',
|
|
16
|
+
go: '9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6',
|
|
17
|
+
java: '637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f',
|
|
18
|
+
c_sharp: '6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991',
|
|
19
|
+
rust: '4409921a70d0aa5bec7d1d7ce809a557a8ee1cf6ace901e3ac6a76e62cfea903',
|
|
20
|
+
html: '11b3405c1543fb012f5ed7f8ee73125076dce8b168301e1e787e4c717da6b456',
|
|
21
|
+
css: '5fc615467b1b98420ed7517e5bf9e1f88468132dd903d842dfb13714f6a1cb0c',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export function verifyParserArtifact({file, root, sha256, label = 'parser artifact'}) {
|
|
25
|
+
let rootReal, fileReal
|
|
26
|
+
try {
|
|
27
|
+
rootReal = realpathSync.native(root)
|
|
28
|
+
fileReal = realpathSync.native(file)
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(`${label} is missing or cannot be resolved`)
|
|
31
|
+
}
|
|
32
|
+
if (!isPathInside(rootReal, fileReal) || !statSync(fileReal).isFile()) {
|
|
33
|
+
throw new Error(`${label} resolves outside its pinned package directory`)
|
|
34
|
+
}
|
|
35
|
+
const actual = createHash('sha256').update(readFileSync(fileReal)).digest('hex')
|
|
36
|
+
if (actual !== sha256) throw new Error(`${label} failed the pinned SHA-256 integrity check`)
|
|
37
|
+
return fileReal
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function trustedRuntimeWasm(wtsDir) {
|
|
41
|
+
return verifyParserArtifact({
|
|
42
|
+
file: join(wtsDir, 'tree-sitter.wasm'), root: wtsDir,
|
|
43
|
+
sha256: RUNTIME_SHA256, label: 'web-tree-sitter runtime',
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function trustedGrammarWasm(wasmDir, grammar) {
|
|
48
|
+
const sha256 = GRAMMAR_SHA256[grammar]
|
|
49
|
+
if (!sha256) throw new Error(`unsupported tree-sitter grammar: ${grammar}`)
|
|
50
|
+
return verifyParserArtifact({
|
|
51
|
+
file: join(wasmDir, `tree-sitter-${grammar}.wasm`), root: wasmDir,
|
|
52
|
+
sha256, label: `tree-sitter ${grammar} grammar`,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
@@ -13,7 +13,7 @@ export const PROVISIONAL_BUDGETS = Object.freeze({
|
|
|
13
13
|
maxModuleBoundaryRatio: .65,
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const STARTER_BUDGETS = Object.freeze({
|
|
17
17
|
runtimeCycles: PROVISIONAL_BUDGETS.runtimeCycles,
|
|
18
18
|
maxFileLoc: PROVISIONAL_BUDGETS.maxFileLoc,
|
|
19
19
|
})
|
|
@@ -11,7 +11,7 @@ const MIN_TOKENS = 50
|
|
|
11
11
|
const FILTERED_CLASSES = new Set(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
12
12
|
const SAFE_SYMBOL = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const DUPLICATE_EVIDENCE_THRESHOLDS = Object.freeze({
|
|
15
15
|
clones: Object.freeze({mode: 'renamed', minSimilarityPercent: CLONE_MIN_SIMILARITY, minTokens: MIN_TOKENS}),
|
|
16
16
|
divergence: Object.freeze({
|
|
17
17
|
sameName: true,
|
|
@@ -96,10 +96,29 @@ const auditDependencyCoverageLines = (deps) => {
|
|
|
96
96
|
]
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const scopedDependencyFindingCounts = (allFindings, scopedFindings) => {
|
|
100
|
+
const dependencyFindings = (items) => (items || []).filter(isDependencyAuditFinding)
|
|
101
|
+
const all = dependencyFindings(allFindings)
|
|
102
|
+
const scoped = dependencyFindings(scopedFindings)
|
|
103
|
+
const count = (rule) => scoped.filter((finding) => finding.rule === rule).length
|
|
104
|
+
return {
|
|
105
|
+
unused: count('unused-dep'),
|
|
106
|
+
missing: count('missing-dep'),
|
|
107
|
+
duplicateDeclarations: count('duplicate-dep'),
|
|
108
|
+
suppressed: Math.max(0, all.length - scoped.length),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const auditDependencySummaryLine = (audit, deps, scopedCounts = null) => {
|
|
100
113
|
const ecosystems = Object.values(deps.ecosystems || {}).filter((item) => item?.present)
|
|
101
114
|
const checked = ecosystems.filter((item) => item.status === 'CHECKED')
|
|
102
115
|
const unsupported = ecosystems.filter((item) => item.status === 'NOT_SUPPORTED')
|
|
116
|
+
const unused = scopedCounts?.unused ?? deps.unused ?? 'unknown'
|
|
117
|
+
const missing = scopedCounts?.missing ?? deps.missing ?? 'unknown'
|
|
118
|
+
const duplicateDeclarations = scopedCounts?.duplicateDeclarations ?? deps.duplicateDeclarations ?? 'unknown'
|
|
119
|
+
const scopedSuffix = scopedCounts?.suppressed
|
|
120
|
+
? ` Production-first path policy suppressed ${scopedCounts.suppressed} classified dependency finding(s).`
|
|
121
|
+
: ''
|
|
103
122
|
if (!ecosystems.length || deps.status === 'NOT_CHECKED') {
|
|
104
123
|
return 'Dependency manifests: NOT_CHECKED — no dependency manifest was discovered; manifest-to-import verification did not run and no dependency verdict was produced.'
|
|
105
124
|
}
|
|
@@ -116,9 +135,9 @@ const auditDependencySummaryLine = (audit, deps) => {
|
|
|
116
135
|
const unsupportedDeclared = unsupported.reduce((total, item) => total + (item.declared || 0), 0)
|
|
117
136
|
const checkedNames = checked.map((item) => item.ecosystem).join(', ')
|
|
118
137
|
const unsupportedNames = unsupported.map((item) => item.ecosystem).join(', ')
|
|
119
|
-
return `Dependency manifests: ${deps.status || 'PARTIAL'} — checked ${checkedDeclared} declaration(s) across ${checkedManifests} supported manifest(s) (${checkedNames}); inventoried ${unsupportedDeclared} declaration(s) across ${unsupportedManifests} unsupported manifest(s) (${unsupportedNames}), where package-to-artifact verification is NOT_SUPPORTED. Supported-ecosystem findings: unused ${
|
|
138
|
+
return `Dependency manifests: ${deps.status || 'PARTIAL'} — checked ${checkedDeclared} declaration(s) across ${checkedManifests} supported manifest(s) (${checkedNames}); inventoried ${unsupportedDeclared} declaration(s) across ${unsupportedManifests} unsupported manifest(s) (${unsupportedNames}), where package-to-artifact verification is NOT_SUPPORTED. Supported-ecosystem findings: unused ${unused}, missing ${missing}, duplicate declarations ${duplicateDeclarations}.${scopedSuffix}`
|
|
120
139
|
}
|
|
121
|
-
return `Dependency manifests: ${deps.status || 'UNKNOWN'} — checked ${deps.declared ?? audit.scanned.manifestDeps ?? 0} declared package(s) against ${deps.importRecords ?? audit.scanned.externalImports ?? 0} external import record(s); unused ${
|
|
140
|
+
return `Dependency manifests: ${deps.status || 'UNKNOWN'} — checked ${deps.declared ?? audit.scanned.manifestDeps ?? 0} declared package(s) against ${deps.importRecords ?? audit.scanned.externalImports ?? 0} external import record(s); unused ${unused}, missing ${missing}, duplicate declarations ${duplicateDeclarations}.${scopedSuffix}`
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
const auditConventionLines = (audit) => {
|
|
@@ -140,11 +159,12 @@ export const formatOrdinaryAudit = (audit, args, findings = audit.findings, head
|
|
|
140
159
|
const sev = summary.bySeverity
|
|
141
160
|
const bycat = summary.byCategory
|
|
142
161
|
const deps = audit.dependencyReport || {}
|
|
162
|
+
const dependencyCounts = scopedDependencyFindingCounts(findings, pathScope.findings)
|
|
143
163
|
return [
|
|
144
164
|
heading,
|
|
145
165
|
`Internal audit of ${audit.repo} (${audit.scanned.files} files, ${audit.scanned.symbols} symbols, ${audit.scanned.externalImports} external imports; malware scan: ${audit.scanned.malwareScanMode}).`,
|
|
146
166
|
...auditConventionLines(audit),
|
|
147
|
-
auditDependencySummaryLine(audit, deps),
|
|
167
|
+
auditDependencySummaryLine(audit, deps, dependencyCounts),
|
|
148
168
|
...auditDependencyCoverageLines(deps),
|
|
149
169
|
...auditCapabilityLines(audit),
|
|
150
170
|
...auditExtensionLines(audit),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {readFileSync} from 'node:fs'
|
|
2
2
|
import process from 'node:process'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const STALE_RUNTIME_OVERRIDE_ENV = 'WEAVATRIX_ALLOW_STALE_RUNTIME'
|
|
5
5
|
|
|
6
6
|
export function runtimeVersionStatus({runningVersion, packageJsonPath, allowStale} = {}) {
|
|
7
7
|
let diskVersion = null, packageVersionError = null
|
|
@@ -29,4 +29,3 @@ export function staleRuntimeMessage(status) {
|
|
|
29
29
|
const disk = status.diskVersion || 'unavailable'
|
|
30
30
|
return `STALE_RUNTIME: running Weavatrix ${status.version || 'unknown'} but package.json on disk is ${disk}. Restart/reconnect the MCP server before using tools. For deliberate source-development only, set ${STALE_RUNTIME_OVERRIDE_ENV}=1.`
|
|
31
31
|
}
|
|
32
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'info'])
|
|
2
2
|
export const CONFIDENCE = new Set(['high', 'medium', 'low'])
|
|
3
3
|
export const CATEGORIES = new Set(['unused', 'structure', 'vulnerability', 'malware'])
|
|
4
|
-
|
|
4
|
+
const CHECK_KEYS = ['osv', 'malware']
|
|
5
5
|
export const PACKAGE_DEPENDENCY_KINDS = new Set(['runtime', 'dev', 'optional', 'peer', 'optional-peer'])
|
|
6
6
|
export const CAPS = Object.freeze({
|
|
7
7
|
modules: 500, dependencies: 2000, findings: 500, hotspots: 250, badges: 100,
|
package/src/mcp/tool-result.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// JSON mode adds the stable machine-readable structuredContent envelope and mirrors that envelope
|
|
3
3
|
// into TextContent for workflow runners that cannot consume structured results directly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const TOOL_RESULT_SCHEMA = 'weavatrix.tool.v1'
|
|
6
6
|
|
|
7
7
|
export function toolResult(text, result, extra = {}) {
|
|
8
8
|
return {
|
|
@@ -25,40 +25,19 @@ export function killClient(client, reason = new Error('LSP client was killed'))
|
|
|
25
25
|
}
|
|
26
26
|
if (client.child.exitCode != null || client.child.signalCode != null) return
|
|
27
27
|
if (process.platform === 'win32' && client.child.pid) {
|
|
28
|
-
|
|
29
|
-
const killer = spawnChild('taskkill', ['/pid', String(client.child.pid), '/T', '/F'], {
|
|
30
|
-
shell: false,
|
|
31
|
-
windowsHide: true,
|
|
32
|
-
env: lspChildProcessEnv(),
|
|
33
|
-
stdio: 'ignore',
|
|
34
|
-
})
|
|
35
|
-
const fallback = () => {
|
|
36
|
-
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
37
|
-
}
|
|
38
|
-
killer.once('error', fallback)
|
|
39
|
-
killer.once('exit', (code) => { if (code !== 0) fallback() })
|
|
40
|
-
} catch {
|
|
41
|
-
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
42
|
-
}
|
|
28
|
+
startWindowsTreeKill(client)
|
|
43
29
|
} else {
|
|
44
30
|
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
45
31
|
}
|
|
46
32
|
}
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
clearTimeout(timer)
|
|
56
|
-
resolveKill()
|
|
57
|
-
}
|
|
58
|
-
const timer = setTimeout(() => {
|
|
59
|
-
try { client.child.kill() } catch { /* already exited */ }
|
|
60
|
-
done()
|
|
61
|
-
}, Math.max(250, Math.min(5_000, Number(timeoutMs) || 3_000)))
|
|
34
|
+
function directChildKill(client) {
|
|
35
|
+
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function startWindowsTreeKill(client) {
|
|
39
|
+
if (client.windowsTreeKillPromise) return client.windowsTreeKillPromise
|
|
40
|
+
client.windowsTreeKillPromise = new Promise((resolveKill) => {
|
|
62
41
|
try {
|
|
63
42
|
const killer = spawnChild('taskkill', ['/pid', String(client.child.pid), '/T', '/F'], {
|
|
64
43
|
shell: false,
|
|
@@ -66,19 +45,27 @@ export async function killWindowsTreeAndWait(client, timeoutMs = 3_000) {
|
|
|
66
45
|
env: lspChildProcessEnv(),
|
|
67
46
|
stdio: 'ignore',
|
|
68
47
|
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
killer.once('error', () => { fallback(); done() })
|
|
73
|
-
killer.once('exit', (code) => {
|
|
74
|
-
if (code !== 0) fallback()
|
|
75
|
-
done()
|
|
76
|
-
})
|
|
48
|
+
killer.once('error', () => { directChildKill(client); resolveKill() })
|
|
49
|
+
killer.once('exit', (code) => { if (code !== 0) directChildKill(client); resolveKill() })
|
|
77
50
|
} catch {
|
|
78
|
-
|
|
79
|
-
|
|
51
|
+
directChildKill(client)
|
|
52
|
+
resolveKill()
|
|
80
53
|
}
|
|
81
54
|
})
|
|
55
|
+
return client.windowsTreeKillPromise
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function killWindowsTreeAndWait(client, timeoutMs = 3_000) {
|
|
59
|
+
if (process.platform !== 'win32' || !client.child.pid || client.child.exitCode != null) return
|
|
60
|
+
const boundedTimeout = Math.max(250, Math.min(5_000, Number(timeoutMs) || 3_000))
|
|
61
|
+
let timer
|
|
62
|
+
await Promise.race([
|
|
63
|
+
startWindowsTreeKill(client),
|
|
64
|
+
new Promise((resolveKill) => {
|
|
65
|
+
timer = setTimeout(() => { directChildKill(client); resolveKill() }, boundedTimeout)
|
|
66
|
+
}),
|
|
67
|
+
])
|
|
68
|
+
if (timer) clearTimeout(timer)
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
export async function waitForClientExit(client, timeoutMs = client.requestTimeoutMs) {
|
|
@@ -45,10 +45,11 @@ export async function shutdownActiveLspClients({timeoutMs = 3_000} = {}) {
|
|
|
45
45
|
await Promise.allSettled(survivors.map(async (client) => {
|
|
46
46
|
if (typeof client.killWindowsTreeAndWait === 'function') await client.killWindowsTreeAndWait(forceBudget)
|
|
47
47
|
client.kill(reason)
|
|
48
|
-
if (typeof client.waitForExit === 'function') {
|
|
49
|
-
await client.waitForExit(Math.max(100, boundedTimeout - (Date.now() - startedAt)))
|
|
50
|
-
}
|
|
51
48
|
}))
|
|
49
|
+
const reapBudget = Math.max(250, Math.min(1_000, Math.floor(boundedTimeout * 0.4)))
|
|
50
|
+
await Promise.allSettled(survivors.map((client) => (
|
|
51
|
+
typeof client.waitForExit === 'function' ? client.waitForExit(reapBudget) : undefined
|
|
52
|
+
)))
|
|
52
53
|
}
|
|
53
54
|
return {
|
|
54
55
|
requested: clients.length,
|
|
@@ -15,14 +15,9 @@ import {
|
|
|
15
15
|
unregisterLspClient,
|
|
16
16
|
} from './registry.js'
|
|
17
17
|
import {
|
|
18
|
-
assertClientWritable,
|
|
19
18
|
closeClientDocument,
|
|
20
|
-
defaultClientServerRequest,
|
|
21
|
-
handleClientServerRequest,
|
|
22
19
|
handleClientMessage,
|
|
23
20
|
initializeClient,
|
|
24
|
-
normalizeClientLocations,
|
|
25
|
-
notifyClient,
|
|
26
21
|
openClientDocument,
|
|
27
22
|
queryClientLocations,
|
|
28
23
|
rejectPendingRequests,
|
|
@@ -34,7 +29,6 @@ import {
|
|
|
34
29
|
killClient,
|
|
35
30
|
killWindowsTreeAndWait,
|
|
36
31
|
shutdownClient,
|
|
37
|
-
shutdownClientOnce,
|
|
38
32
|
waitForClientExit,
|
|
39
33
|
} from './lifecycle.js'
|
|
40
34
|
|
|
@@ -70,6 +64,7 @@ export class StdioLspClient {
|
|
|
70
64
|
this.state = 'starting'
|
|
71
65
|
this.stderrTail = ''
|
|
72
66
|
this.shutdownPromise = null
|
|
67
|
+
this.windowsTreeKillPromise = null
|
|
73
68
|
this.child = spawn(executablePath, args, {
|
|
74
69
|
cwd: this.normalizer.rootPath,
|
|
75
70
|
env: lspChildProcessEnv(env),
|
|
@@ -123,18 +118,13 @@ export class StdioLspClient {
|
|
|
123
118
|
}
|
|
124
119
|
|
|
125
120
|
async start() { await this.spawned; return this }
|
|
126
|
-
assertWritable() { return assertClientWritable(this) }
|
|
127
121
|
writeMessage(message) { return writeClientMessage(this, message) }
|
|
128
122
|
request(method, params, options) { return requestFromClient(this, method, params, options) }
|
|
129
|
-
notify(method, params) { return notifyClient(this, method, params) }
|
|
130
123
|
initialize(options) { return initializeClient(this, options) }
|
|
131
124
|
openDocument(options) { return openClientDocument(this, options) }
|
|
132
125
|
closeDocument(filePath) { return closeClientDocument(this, filePath) }
|
|
133
|
-
normalizeLocations(result) { return normalizeClientLocations(this, result) }
|
|
134
126
|
definition(options) { return queryClientLocations(this, 'definition', options) }
|
|
135
127
|
references(options) { return queryClientLocations(this, 'references', options) }
|
|
136
|
-
defaultServerRequest(method, params) { return defaultClientServerRequest(this, method, params) }
|
|
137
|
-
handleServerRequest(message) { return handleClientServerRequest(this, message) }
|
|
138
128
|
handleMessage(message) { return handleClientMessage(this, message) }
|
|
139
129
|
rejectPending(error) { return rejectPendingRequests(this, error) }
|
|
140
130
|
fail(error) { return failClient(this, error) }
|
|
@@ -142,7 +132,6 @@ export class StdioLspClient {
|
|
|
142
132
|
killWindowsTreeAndWait(timeoutMs) { return killWindowsTreeAndWait(this, timeoutMs) }
|
|
143
133
|
waitForExit(timeoutMs) { return waitForClientExit(this, timeoutMs) }
|
|
144
134
|
shutdown(options) { return shutdownClient(this, options) }
|
|
145
|
-
shutdownOnce(options) { return shutdownClientOnce(this, options) }
|
|
146
135
|
}
|
|
147
136
|
|
|
148
137
|
export async function startStdioLspClient(options) {
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
} from './lsp-overlay.js'
|
|
10
10
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const SYMBOL_PRECISION_CACHE_V = 1
|
|
13
|
+
const SYMBOL_PRECISION_CACHE_FILE = 'precision-symbols.json'
|
|
14
14
|
|
|
15
15
|
const MAX_CACHE_ENTRIES = 32
|
|
16
16
|
const MAX_CACHE_BYTES = 8 * 1024 * 1024
|
|
@@ -3,7 +3,7 @@ import {dirname, extname, isAbsolute, join} from 'node:path'
|
|
|
3
3
|
import {createRequire} from 'node:module'
|
|
4
4
|
|
|
5
5
|
const requireFromWeavatrix = createRequire(import.meta.url)
|
|
6
|
-
|
|
6
|
+
const PROVIDER = 'typescript-language-server'
|
|
7
7
|
export const TYPESCRIPT_LSP_CAPABILITY_CONTRACT = 'typescript-references-v4-plugin-suppression'
|
|
8
8
|
let discoveredProvider = null
|
|
9
9
|
|
|
@@ -8,7 +8,7 @@ import {createHash} from 'node:crypto'
|
|
|
8
8
|
import {uniqueBy} from '../util.js'
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_STORE = join(homedir(), '.weavatrix', 'advisories.json')
|
|
11
|
-
|
|
11
|
+
const OSV_SUPPORTED_ECOSYSTEMS = new Set(['npm', 'PyPI', 'Go', 'Maven', 'crates.io'])
|
|
12
12
|
|
|
13
13
|
const keyOf = (ecosystem, name) => `${ecosystem}|${ecosystem === 'PyPI' ? String(name).toLowerCase().replace(/[-_.]+/g, '-') : name}`
|
|
14
14
|
const uniquePackages = (packages) => uniqueBy(packages, (item) => `${item.ecosystem}|${item.name}|${item.version}`)
|