weavatrix 0.3.3 → 0.3.5
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 +40 -0
- package/docs/releases/v0.3.4.md +40 -0
- package/docs/releases/v0.3.5.md +50 -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/internal-audit.run.js +5 -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/parser-artifact-boundary.js +0 -2
- 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/lsp-overlay/budget-policy.js +78 -0
- package/src/precision/lsp-overlay/build.js +23 -15
- package/src/precision/lsp-overlay/source-session.js +15 -7
- 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
|
@@ -311,6 +311,17 @@ before starting the MCP server for parser-only operation from the first build, o
|
|
|
311
311
|
`precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
|
|
312
312
|
choice as **TypeScript/JavaScript semantic precision**.
|
|
313
313
|
|
|
314
|
+
The broad prewarm stays conservative by default: it queries 32 ranked symbols and never more than
|
|
315
|
+
64. A deliberate high-budget run can set `WEAVATRIX_PRECISION_MAX_SYMBOLS` above 64 (for example,
|
|
316
|
+
`1000`), or set `WEAVATRIX_PRECISION_PREWARM=full` to select every eligible target up to the hard
|
|
317
|
+
10,000-symbol safety ceiling. Expanded runs receive proportionate reference/link and deadline
|
|
318
|
+
budgets, still capped at 131,072 references/links, a 30-minute deadline, 1,024 open documents and
|
|
319
|
+
128 MiB of verified source. `WEAVATRIX_PRECISION_MAX_REFERENCES`,
|
|
320
|
+
`WEAVATRIX_PRECISION_MAX_LINKS`, and `WEAVATRIX_PRECISION_TIMEOUT_MS` can lower or explicitly tune
|
|
321
|
+
those budgets. Repositories that exceed any hard ceiling remain honestly `PARTIAL`; the default
|
|
322
|
+
memory/time profile is unchanged. On-demand `get_dependents`, `inspect_symbol`, and
|
|
323
|
+
`change_impact` queries remain revision-bound separate caches and do not require a full prewarm.
|
|
324
|
+
|
|
314
325
|
**search / source** — `search_code` (ripgrep-backed, pure-Node fallback, with repository-relative
|
|
315
326
|
path globs on Windows/macOS/Linux), `read_source` (a
|
|
316
327
|
symbol's actual code in one hop), `context_bundle` (definition plus grouped inbound/outbound
|
|
@@ -413,6 +424,35 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
|
|
|
413
424
|
while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
|
|
414
425
|
`STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
|
|
415
426
|
|
|
427
|
+
### 0.3.5 explicit full semantic prewarm
|
|
428
|
+
|
|
429
|
+
- Normal startup remains bounded to 32 ranked semantic targets. An explicit
|
|
430
|
+
`WEAVATRIX_PRECISION_PREWARM=full` run, or a requested symbol budget above
|
|
431
|
+
64, can now cover every eligible JS/TS target within hard time, source and
|
|
432
|
+
memory ceilings instead of being silently clamped to 64.
|
|
433
|
+
- Real Hosted dogfood queried all 800 eligible targets in about 49 seconds and
|
|
434
|
+
produced 4,127 `EXACT_LSP` edges with `COMPLETE` semantic precision.
|
|
435
|
+
- Unreferenced public exports are now reported as low-confidence module-surface
|
|
436
|
+
evidence, not as proof that their implementations are dead.
|
|
437
|
+
|
|
438
|
+
Full patch notes: [docs/releases/v0.3.5.md](docs/releases/v0.3.5.md).
|
|
439
|
+
|
|
440
|
+
### 0.3.4 self-audit precision and bounded dynamic imports
|
|
441
|
+
|
|
442
|
+
- JS/TS graph construction now resolves the existing local-file prefix in
|
|
443
|
+
``import(new URL(`./tool.mjs${runtimeValue}`, import.meta.url).href)`` while
|
|
444
|
+
preserving the edge as dynamic. Variable-first paths, remote URLs, alternate
|
|
445
|
+
bases and other runtime-only targets remain explicit unknowns.
|
|
446
|
+
- Health dependency headlines now follow the same production-first path scope as
|
|
447
|
+
the displayed findings. Classified fixture results are counted separately
|
|
448
|
+
instead of making a clean application headline look unhealthy.
|
|
449
|
+
- Dead-code liveness no longer treats comment-only symbol mentions as callers;
|
|
450
|
+
string-addressed runtime registries remain conservatively live. Dogfood removed
|
|
451
|
+
obsolete internal exports/wrappers while preserving every supported Online
|
|
452
|
+
extension point and TypeScript host callback.
|
|
453
|
+
|
|
454
|
+
Full patch notes: [docs/releases/v0.3.4.md](docs/releases/v0.3.4.md).
|
|
455
|
+
|
|
416
456
|
### 0.3.3 parser supply-chain boundary
|
|
417
457
|
|
|
418
458
|
- Before compiling parser WASM, Core now requires the package-pinned `web-tree-sitter` runtime and
|
|
@@ -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.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Weavatrix 0.3.5
|
|
2
|
+
|
|
3
|
+
0.3.5 removes the hidden 64-symbol ceiling from deliberately high-budget
|
|
4
|
+
TypeScript/JavaScript semantic prewarms while keeping the ordinary offline
|
|
5
|
+
startup profile unchanged.
|
|
6
|
+
|
|
7
|
+
## Explicit full semantic prewarm
|
|
8
|
+
|
|
9
|
+
- The default remains a 32-symbol ranked prewarm with the previous 64-symbol,
|
|
10
|
+
16,384-reference/link and 60-second hard ceilings.
|
|
11
|
+
- Setting `WEAVATRIX_PRECISION_MAX_SYMBOLS` above 64 now explicitly opts into
|
|
12
|
+
the expanded policy instead of being silently clamped back to 64.
|
|
13
|
+
- `WEAVATRIX_PRECISION_PREWARM=full` selects all eligible targets, bounded by a
|
|
14
|
+
10,000-symbol hard ceiling. It is intended for CI, release evidence and
|
|
15
|
+
manually requested deep repository audits, not implicit startup work.
|
|
16
|
+
- Expanded runs scale their default reference/link budget to 16 per requested
|
|
17
|
+
symbol and their deadline to one second per requested symbol. Hard limits
|
|
18
|
+
remain 131,072 references/links and 30 minutes.
|
|
19
|
+
- Verified source remains bounded to 4 MiB per file, 1,024 simultaneously open
|
|
20
|
+
documents and 128 MiB total. Crossing a ceiling remains `PARTIAL`; it never
|
|
21
|
+
becomes a false complete result or an unbounded allocation.
|
|
22
|
+
|
|
23
|
+
The point-query cache used by `get_dependents` and `inspect_symbol`, and the
|
|
24
|
+
bounded changed-symbol batch used by `change_impact`, remain separate from this
|
|
25
|
+
broad overlay. Existing default and on-demand behavior is unchanged.
|
|
26
|
+
|
|
27
|
+
The regression suite proves both sides of the contract: an ordinary build still
|
|
28
|
+
selects 32 of 80 candidates and reports `PARTIAL`, while an explicit 1,000-symbol
|
|
29
|
+
budget and `full` mode each query all 80 targets and report `COMPLETE` with a
|
|
30
|
+
cooperative provider.
|
|
31
|
+
|
|
32
|
+
## Dead-code verdict consistency
|
|
33
|
+
|
|
34
|
+
- `run_audit` now labels an unreferenced public export as low-confidence
|
|
35
|
+
`unused-export-surface` evidence and sets `deadCodeCandidate:false`.
|
|
36
|
+
- The audit explains that an unused module surface is not proof of a dead
|
|
37
|
+
implementation; downstream packages and runtime registries may be invisible.
|
|
38
|
+
- This matches `find_dead_code`, which keeps the same uncertain public surface
|
|
39
|
+
out of its default medium-confidence review queue while reporting the
|
|
40
|
+
suppressed low-confidence count.
|
|
41
|
+
|
|
42
|
+
## Consistent unused-export confidence
|
|
43
|
+
|
|
44
|
+
`run_audit` now labels an unused export as low-confidence export-surface
|
|
45
|
+
evidence, not a medium-confidence dead implementation. The finding explicitly
|
|
46
|
+
distinguishes removing an unnecessary export keyword from deleting the symbol,
|
|
47
|
+
which still requires the separate exact/reference-aware `find_dead_code`
|
|
48
|
+
workflow. This aligns Health with the default dead-code queue, where public and
|
|
49
|
+
framework-sensitive candidates are intentionally suppressed until
|
|
50
|
+
`min_confidence=low` is requested.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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>",
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"docs/releases/v0.3.1.md",
|
|
38
38
|
"docs/releases/v0.3.2.md",
|
|
39
39
|
"docs/releases/v0.3.3.md",
|
|
40
|
+
"docs/releases/v0.3.4.md",
|
|
41
|
+
"docs/releases/v0.3.5.md",
|
|
40
42
|
"docs/releases/v0.2.19.md",
|
|
41
43
|
"README.md",
|
|
42
44
|
"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 {
|
|
@@ -148,13 +148,16 @@ export async function runInternalAudit(repoPath, {
|
|
|
148
148
|
category: "unused",
|
|
149
149
|
rule: "unused-export",
|
|
150
150
|
severity: "info",
|
|
151
|
-
confidence: "
|
|
151
|
+
confidence: "low",
|
|
152
152
|
title: `Unused export: ${symbol.label.replace(/\(\)$/, "")} — ${symbol.file}`,
|
|
153
|
-
detail: `${symbol.reason}.
|
|
153
|
+
detail: `${symbol.reason}. This is unused module-surface evidence, not proof that the implementation is dead: downstream packages and runtime registration can be invisible to the repository graph. Review external consumers first; remove only the export keyword when the symbol is still used internally, and delete the implementation only after separate dead-code verification.`,
|
|
154
154
|
file: symbol.file,
|
|
155
155
|
symbol: symbol.label,
|
|
156
156
|
graphNodeId: symbol.id,
|
|
157
157
|
source: "internal",
|
|
158
|
+
classification: "unused-export-surface",
|
|
159
|
+
deadCodeCandidate: false,
|
|
160
|
+
fixHint: "check downstream/runtime consumers, then remove only the export surface or run a separate exact dead-code review before deleting the implementation",
|
|
158
161
|
}));
|
|
159
162
|
unusedExportCount++;
|
|
160
163
|
}
|
|
@@ -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' }
|
|
@@ -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) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {boundedInteger} from '../../bounds.js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_SYMBOLS = 32
|
|
4
|
+
const BOUNDED_MAX_SYMBOLS = 64
|
|
5
|
+
const EXPANDED_MAX_SYMBOLS = 10_000
|
|
6
|
+
const DEFAULT_MAX_REFERENCES = 2_048
|
|
7
|
+
const BOUNDED_MAX_REFERENCES = 16_384
|
|
8
|
+
const EXPANDED_MAX_REFERENCES = 131_072
|
|
9
|
+
const DEFAULT_MAX_LINKS = 2_048
|
|
10
|
+
const BOUNDED_MAX_LINKS = 16_384
|
|
11
|
+
const EXPANDED_MAX_LINKS = 131_072
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 45_000
|
|
13
|
+
const BOUNDED_MAX_TIMEOUT_MS = 60_000
|
|
14
|
+
const EXPANDED_MAX_TIMEOUT_MS = 30 * 60_000
|
|
15
|
+
|
|
16
|
+
function configured(value, environmentName) {
|
|
17
|
+
if (value !== undefined && value !== null && String(value).trim() !== '') return value
|
|
18
|
+
const environmentValue = process.env[environmentName]
|
|
19
|
+
return environmentValue != null && String(environmentValue).trim() !== ''
|
|
20
|
+
? environmentValue : undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function precisionPrewarmBudget(options = {}) {
|
|
24
|
+
const targetBatch = Array.isArray(options.targetIds) && options.targetIds.length > 0
|
|
25
|
+
const configuredMode = String(configured(
|
|
26
|
+
options.prewarmMode,
|
|
27
|
+
'WEAVATRIX_PRECISION_PREWARM',
|
|
28
|
+
) || 'bounded').trim().toLowerCase()
|
|
29
|
+
const full = configuredMode === 'full' && !targetBatch
|
|
30
|
+
const requestedSymbols = configured(options.maxSymbols, 'WEAVATRIX_PRECISION_MAX_SYMBOLS')
|
|
31
|
+
const numericSymbols = Number(requestedSymbols)
|
|
32
|
+
const expanded = full || (Number.isFinite(numericSymbols) && numericSymbols > BOUNDED_MAX_SYMBOLS)
|
|
33
|
+
const maxSymbols = full ? EXPANDED_MAX_SYMBOLS : boundedInteger(
|
|
34
|
+
requestedSymbols,
|
|
35
|
+
DEFAULT_MAX_SYMBOLS,
|
|
36
|
+
1,
|
|
37
|
+
expanded ? EXPANDED_MAX_SYMBOLS : BOUNDED_MAX_SYMBOLS,
|
|
38
|
+
)
|
|
39
|
+
const referenceFallback = expanded
|
|
40
|
+
? Math.min(EXPANDED_MAX_REFERENCES, Math.max(DEFAULT_MAX_REFERENCES, maxSymbols * 16))
|
|
41
|
+
: DEFAULT_MAX_REFERENCES
|
|
42
|
+
const linkFallback = expanded
|
|
43
|
+
? Math.min(EXPANDED_MAX_LINKS, Math.max(DEFAULT_MAX_LINKS, maxSymbols * 16))
|
|
44
|
+
: DEFAULT_MAX_LINKS
|
|
45
|
+
const timeoutFallback = expanded
|
|
46
|
+
? Math.min(15 * 60_000, Math.max(DEFAULT_TIMEOUT_MS, maxSymbols * 1_000))
|
|
47
|
+
: DEFAULT_TIMEOUT_MS
|
|
48
|
+
const maxReferences = boundedInteger(
|
|
49
|
+
configured(options.maxReferences, 'WEAVATRIX_PRECISION_MAX_REFERENCES'),
|
|
50
|
+
referenceFallback,
|
|
51
|
+
1,
|
|
52
|
+
expanded ? EXPANDED_MAX_REFERENCES : BOUNDED_MAX_REFERENCES,
|
|
53
|
+
)
|
|
54
|
+
const maxLinks = boundedInteger(
|
|
55
|
+
configured(options.maxLinks, 'WEAVATRIX_PRECISION_MAX_LINKS'),
|
|
56
|
+
linkFallback,
|
|
57
|
+
1,
|
|
58
|
+
expanded ? EXPANDED_MAX_LINKS : BOUNDED_MAX_LINKS,
|
|
59
|
+
)
|
|
60
|
+
const timeoutMs = boundedInteger(
|
|
61
|
+
configured(options.timeoutMs, 'WEAVATRIX_PRECISION_TIMEOUT_MS'),
|
|
62
|
+
timeoutFallback,
|
|
63
|
+
100,
|
|
64
|
+
expanded ? EXPANDED_MAX_TIMEOUT_MS : BOUNDED_MAX_TIMEOUT_MS,
|
|
65
|
+
)
|
|
66
|
+
return {
|
|
67
|
+
expanded,
|
|
68
|
+
full,
|
|
69
|
+
maxSymbols,
|
|
70
|
+
maxReferences,
|
|
71
|
+
maxLinks,
|
|
72
|
+
timeoutMs,
|
|
73
|
+
maxOpenDocuments: expanded ? 1_024 : 96,
|
|
74
|
+
maxOpenBytes: expanded ? 128 * 1024 * 1024 : 32 * 1024 * 1024,
|
|
75
|
+
maxClassificationDocuments: expanded ? 1_024 : 96,
|
|
76
|
+
maxClassificationBytes: expanded ? 128 * 1024 * 1024 : 32 * 1024 * 1024,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {boundedInteger} from '../../bounds.js'
|
|
2
1
|
import {createTypeScriptLspClient} from '../typescript-lsp-provider.js'
|
|
2
|
+
import {precisionPrewarmBudget} from './budget-policy.js'
|
|
3
3
|
import {baseOverlay} from './contract.js'
|
|
4
4
|
import {precisionOverlayMatches} from './contract.js'
|
|
5
5
|
import {collectReferenceResults} from './reference-results.js'
|
|
@@ -106,17 +106,24 @@ function failedOverlay(session, error) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function createSession(options) {
|
|
109
|
-
const
|
|
110
|
-
const boundedReferences = boundedInteger(options.maxReferences, 2_048, 1, 16_384)
|
|
111
|
-
const boundedLinks = boundedInteger(options.maxLinks, 2_048, 1, 16_384)
|
|
112
|
-
const boundedTimeout = boundedInteger(options.timeoutMs, 45_000, 100, 60_000)
|
|
109
|
+
const budget = precisionPrewarmBudget(options)
|
|
113
110
|
return {
|
|
114
111
|
...options,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
expandedPrewarm: budget.expanded,
|
|
113
|
+
fullPrewarm: budget.full,
|
|
114
|
+
boundedMax: budget.maxSymbols,
|
|
115
|
+
boundedReferences: budget.maxReferences,
|
|
116
|
+
boundedLinks: budget.maxLinks,
|
|
117
|
+
maxOpenDocuments: budget.maxOpenDocuments,
|
|
118
|
+
maxOpenBytes: budget.maxOpenBytes,
|
|
119
|
+
maxClassificationDocuments: budget.maxClassificationDocuments,
|
|
120
|
+
maxClassificationBytes: budget.maxClassificationBytes,
|
|
121
|
+
deadline: Date.now() + budget.timeoutMs,
|
|
122
|
+
request: {
|
|
123
|
+
maxSymbols: budget.maxSymbols,
|
|
124
|
+
maxReferences: budget.maxReferences,
|
|
125
|
+
maxLinks: budget.maxLinks,
|
|
126
|
+
},
|
|
120
127
|
links: [],
|
|
121
128
|
seen: new Set(),
|
|
122
129
|
evidenceSeen: new Set(),
|
|
@@ -138,17 +145,18 @@ export async function buildLspPrecisionOverlay({
|
|
|
138
145
|
graph,
|
|
139
146
|
graphPath,
|
|
140
147
|
mode = 'lsp',
|
|
141
|
-
maxSymbols
|
|
142
|
-
maxReferences
|
|
143
|
-
maxLinks
|
|
144
|
-
timeoutMs
|
|
148
|
+
maxSymbols,
|
|
149
|
+
maxReferences,
|
|
150
|
+
maxLinks,
|
|
151
|
+
timeoutMs,
|
|
152
|
+
prewarmMode,
|
|
145
153
|
targetIds,
|
|
146
154
|
clientFactory,
|
|
147
155
|
} = {}) {
|
|
148
156
|
if (!graph || !repoRoot) throw new Error('precision overlay requires repoRoot and graph')
|
|
149
157
|
const session = createSession({
|
|
150
158
|
repoRoot, graph, graphPath, mode, maxSymbols, maxReferences, maxLinks,
|
|
151
|
-
timeoutMs, targetIds, clientFactory,
|
|
159
|
+
timeoutMs, prewarmMode, targetIds, clientFactory,
|
|
152
160
|
})
|
|
153
161
|
if (mode === 'off') {
|
|
154
162
|
return persist(session, baseOverlay(graph, 'OFF', {
|
|
@@ -62,11 +62,15 @@ export async function ensureOpen(session, relPath) {
|
|
|
62
62
|
const file = norm(relPath)
|
|
63
63
|
if (!file || session.opened.has(file)) return
|
|
64
64
|
ensureBudget(session)
|
|
65
|
-
|
|
66
|
-
const
|
|
65
|
+
const maxDocuments = Number(session.maxOpenDocuments) || 96
|
|
66
|
+
const maxOpenBytes = Number(session.maxOpenBytes) || 32 * 1024 * 1024
|
|
67
|
+
if (session.opened.size >= maxDocuments) {
|
|
68
|
+
throw new PrecisionLimitError('precision open-document limit reached')
|
|
69
|
+
}
|
|
70
|
+
const maxBytes = Math.min(4 * 1024 * 1024, maxOpenBytes - session.openedBytes)
|
|
67
71
|
const {bytes, text} = verifiedSource(session, file, maxBytes)
|
|
68
72
|
if (bytes > 4 * 1024 * 1024) throw new PrecisionLimitError('precision document exceeds 4 MiB limit')
|
|
69
|
-
if (session.openedBytes + bytes >
|
|
73
|
+
if (session.openedBytes + bytes > maxOpenBytes) {
|
|
70
74
|
throw new PrecisionLimitError('precision source-transfer budget reached')
|
|
71
75
|
}
|
|
72
76
|
await awaitWithBudget(session, () => session.client.openDocument(file, text))
|
|
@@ -79,13 +83,15 @@ export function sourceForClassification(session, relPath) {
|
|
|
79
83
|
const file = norm(relPath)
|
|
80
84
|
if (session.openedTexts.has(file)) return session.openedTexts.get(file)
|
|
81
85
|
if (session.classificationTexts.has(file)) return session.classificationTexts.get(file)
|
|
82
|
-
|
|
86
|
+
const maxDocuments = Number(session.maxClassificationDocuments) || 96
|
|
87
|
+
const maxClassificationBytes = Number(session.maxClassificationBytes) || 32 * 1024 * 1024
|
|
88
|
+
if (session.classificationTexts.size >= maxDocuments) return null
|
|
83
89
|
let source
|
|
84
90
|
try {
|
|
85
91
|
source = verifiedSource(
|
|
86
92
|
session,
|
|
87
93
|
file,
|
|
88
|
-
Math.min(4 * 1024 * 1024,
|
|
94
|
+
Math.min(4 * 1024 * 1024, maxClassificationBytes - session.classificationBytes),
|
|
89
95
|
)
|
|
90
96
|
} catch (error) {
|
|
91
97
|
if (error instanceof PrecisionLimitError) return null
|
|
@@ -100,7 +106,9 @@ export async function ensureFullUniverse(session) {
|
|
|
100
106
|
if (session.fullUniverseOpened) return true
|
|
101
107
|
if (!session.universe.complete) return false
|
|
102
108
|
const additional = session.universe.files.filter((file) => !session.opened.has(file))
|
|
103
|
-
|
|
109
|
+
const maxDocuments = Number(session.maxOpenDocuments) || 96
|
|
110
|
+
const maxOpenBytes = Number(session.maxOpenBytes) || 32 * 1024 * 1024
|
|
111
|
+
if (session.opened.size + additional.length > maxDocuments) {
|
|
104
112
|
session.truncated = true
|
|
105
113
|
return false
|
|
106
114
|
}
|
|
@@ -115,7 +123,7 @@ export async function ensureFullUniverse(session) {
|
|
|
115
123
|
let bytes
|
|
116
124
|
try { bytes = statSync(resolvedFile.path).size }
|
|
117
125
|
catch { throw new PrecisionStaleGraphError() }
|
|
118
|
-
if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes >
|
|
126
|
+
if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes > maxOpenBytes) {
|
|
119
127
|
session.truncated = true
|
|
120
128
|
return false
|
|
121
129
|
}
|
|
@@ -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}`)
|