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 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.2.15.x` | Yes |
8
- | `0.2.14` and older | Upgrade to the latest release |
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.2",
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
- export const MAX_REPRESENTATIVE_CYCLE_FILES = 64;
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>) is fully testable with no filesystem.
6
- // `analyzeDeadCode(graph, repoRoot)` is the thin fs wrapper. Works on ANY graph-builder-schema graph (built-in OR
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
- export 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;
25
- export const RUST_ENTRY_FILE = /(^|\/)(?:build\.rs|src\/(?:lib|main)\.rs)$/i;
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 String(text || "").matchAll(IDENT_RE)) { const n = m[0]; globalFreq.set(n, (globalFreq.get(n) || 0) + 1); }
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 String(text || "").matchAll(IDENT_RE)) {
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
- export const DYNAMIC_CODE_RE = /(?:\brequire\s*\(\s*(?!["'])|\bcreateRequire\s*\(|\b__import__\s*\(|\bimportlib\.|(?:^|[^\w.$])(?:eval|exec)\s*\()/m
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
@@ -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
- export const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
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
- export const PACKAGE_REACHABILITY_V = 1
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
- addExternalImport({ dynamic: true, kind: "dynamic", line });
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
- export const PY_IMPORT_TO_DIST = {
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
- export const PY_AMBIGUOUS_TOP = new Set(["google", "src"]);
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
- export const REPOSITORY_FRESHNESS_PROBE_V = 1
12
- export const GRAPH_BUILDER_SCHEMA_V = 6
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 (!_ready) _ready = Parser.init({ locateFile: () => opts.runtimeWasm || DEFAULT_RUNTIME_WASM });
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]) { try { _langs[g] = await Language.load(join(wasmDir, `tree-sitter-${g}.wasm`)); } catch { _langs[g] = null; } }
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
- export const STARTER_BUDGETS = Object.freeze({
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
- export const DUPLICATE_EVIDENCE_THRESHOLDS = Object.freeze({
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 auditDependencySummaryLine = (audit, deps) => {
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 ${deps.unused ?? 'unknown'}, missing ${deps.missing ?? 'unknown'}, duplicate declarations ${deps.duplicateDeclarations ?? 'unknown'}.`
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 ${deps.unused ?? 'unknown'}, missing ${deps.missing ?? 'unknown'}, duplicate declarations ${deps.duplicateDeclarations ?? 'unknown'}.`
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
- export const STALE_RUNTIME_OVERRIDE_ENV = 'WEAVATRIX_ALLOW_STALE_RUNTIME'
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
- export const CHECK_KEYS = ['osv', 'malware']
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,
@@ -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
- export const TOOL_RESULT_SCHEMA = 'weavatrix.tool.v1'
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
- try {
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
- export async function killWindowsTreeAndWait(client, timeoutMs = 3_000) {
49
- if (process.platform !== 'win32' || !client.child.pid || client.child.exitCode != null) return
50
- await new Promise((resolveKill) => {
51
- let settled = false
52
- const done = () => {
53
- if (settled) return
54
- settled = true
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
- const fallback = () => {
70
- try { client.child.kill('SIGKILL') } catch { /* already exited */ }
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
- try { client.child.kill('SIGKILL') } catch { /* already exited */ }
79
- done()
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
- export const SYMBOL_PRECISION_CACHE_V = 1
13
- export const SYMBOL_PRECISION_CACHE_FILE = 'precision-symbols.json'
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
- export const PROVIDER = 'typescript-language-server'
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
- export const OSV_SUPPORTED_ECOSYSTEMS = new Set(['npm', 'PyPI', 'Go', 'Maven', 'crates.io'])
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}`)