weavatrix 0.2.3 → 0.2.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.
Files changed (36) hide show
  1. package/README.md +91 -20
  2. package/SECURITY.md +18 -0
  3. package/package.json +3 -1
  4. package/skill/SKILL.md +49 -13
  5. package/src/analysis/dead-check.js +48 -2
  6. package/src/analysis/dead-code-review.js +22 -8
  7. package/src/analysis/duplicates.compute.js +11 -6
  8. package/src/analysis/git-ref-graph.js +10 -2
  9. package/src/analysis/hot-path-review.js +228 -0
  10. package/src/analysis/internal-audit.run.js +36 -0
  11. package/src/analysis/source-complexity.report.js +5 -0
  12. package/src/analysis/source-complexity.walk.js +64 -10
  13. package/src/build-graph.js +49 -10
  14. package/src/graph/build-worker.js +21 -1
  15. package/src/graph/builder/lang-java.js +1 -0
  16. package/src/graph/builder/lang-js.js +24 -0
  17. package/src/graph/builder/lang-python.js +161 -4
  18. package/src/graph/freshness-probe.js +3 -3
  19. package/src/graph/incremental-refresh.js +1 -1
  20. package/src/graph/internal-builder.barrels.js +33 -4
  21. package/src/graph/internal-builder.build.js +52 -8
  22. package/src/mcp/catalog.mjs +14 -11
  23. package/src/mcp/graph-context.mjs +143 -14
  24. package/src/mcp/sync-payload.mjs +2 -1
  25. package/src/mcp/tools-actions.mjs +59 -20
  26. package/src/mcp/tools-company.mjs +11 -3
  27. package/src/mcp/tools-graph.mjs +12 -6
  28. package/src/mcp/tools-health.mjs +112 -11
  29. package/src/mcp/tools-impact.mjs +24 -6
  30. package/src/mcp/tools-source.mjs +237 -0
  31. package/src/mcp-server.mjs +99 -11
  32. package/src/path-classification.js +2 -2
  33. package/src/precision/lsp-client.js +682 -0
  34. package/src/precision/lsp-overlay.js +872 -0
  35. package/src/precision/symbol-query.js +137 -0
  36. package/src/precision/typescript-lsp-provider.js +682 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Grep sees text. Weavatrix sees structure. It builds a dependency graph of any local repository —
6
6
  files, symbols, and the imports/calls/inheritance connecting them — and serves it to Claude Code,
7
7
  Codex, or any MCP client: change impact, transitive dependents, health audit, clone detection,
8
- coverage mapping. **32 tools available; 29 enabled by the default offline profile. Local-first: with
8
+ coverage mapping. **34 tools available; 31 enabled by the default offline profile. Local-first: with
9
9
  the defaults, no repository data leaves your machine.**
10
10
 
11
11
  - Website: [weavatrix.com](https://weavatrix.com)
@@ -21,8 +21,9 @@ answers grep can't produce:
21
21
  untracked included), maps the changed files and symbols onto the graph, and lists everything that
22
22
  depends on them — with test coverage attached, so the **untested part of the blast radius** stands
23
23
  out before you ship.
24
- - *"Who calls this function?"* → `get_dependents` walks reverse edges transitively: every caller,
25
- importer and subclass that can feel the refactor, ranked by proximity × connectivity.
24
+ - *"Who calls this function?"* → `inspect_symbol` returns exact bounded TS/JS occurrences plus
25
+ source context; `get_dependents` walks the symbol-level reverse graph transitively. Opt into
26
+ `include_container_importers:true` only when a broader module-import radius is intended.
26
27
  - *"Did my refactor actually decouple anything?"* → `graph_diff base_ref=HEAD~1` builds an immutable
27
28
  baseline graph without checking it out, then reports the structural delta: new module
28
29
  dependencies, broken or introduced import cycles, and symbols that lost their last caller.
@@ -111,25 +112,42 @@ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — instal
111
112
  compile-only edges (Rust module/use, Java imports) are reported separately where that distinction
112
113
  changes the result.
113
114
 
114
- Every current edge also carries versioned provenance: `EXTRACTED`, `RESOLVED`, or `INFERRED` today,
115
- with `EXACT_LSP` and `CONFLICT` reserved for the optional precision overlay. `graph_stats` reports
116
- the complete breakdown; provenance describes how the relationship was established, while the older
117
- `confidence` field remains for compatibility.
115
+ Every current edge carries versioned provenance. The parser emits `EXTRACTED`, `RESOLVED`, and
116
+ `INFERRED`; the built-in bounded TypeScript/JavaScript precision overlay upgrades only references
117
+ confirmed by its bundled `typescript-language-server` + TypeScript runtime to `EXACT_LSP`.
118
+ `CONFLICT` means evidence disagrees. `graph_stats` reports the provenance breakdown and the semantic
119
+ provider's `COMPLETE`, `PARTIAL`, `UNAVAILABLE`, or `OFF` state; `OFF` means precision was explicitly
120
+ disabled and only static evidence is active. Java and Rust language-server providers are not bundled
121
+ in 0.2.5: their edges never become `EXACT_LSP`, even when a mixed repository reports a complete
122
+ TypeScript/JavaScript overlay.
123
+
124
+ The bounded JS/TS provider is enabled by default for new graphs. Set `WEAVATRIX_PRECISION=off`
125
+ before starting the MCP server for parser-only operation from the first build, or pass
126
+ `precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
127
+ choice as **TypeScript/JavaScript semantic precision**.
118
128
 
119
129
  **search / source** — `search_code` (ripgrep-backed, pure-Node fallback), `read_source` (a
120
- symbol's actual code in one hop), `list_endpoints` (HTTP route inventory:
130
+ symbol's actual code in one hop), `inspect_symbol` (one exact bounded TS/JS reference query,
131
+ logical containers, impact and source excerpts), `list_endpoints` (HTTP route inventory:
121
132
  Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web …)
122
133
 
123
- **health** — `find_dead_code` (bounded review queue for statically unreferenced files, functions,
124
- methods, and symbols, with confidence/evidence and explicit public/framework/dynamic caveats),
125
- `run_audit` (unused files/exports/dependencies, missing npm/Go/Python deps, runtime
134
+ **health** — `find_dead_code` (bounded review queue for statically unreferenced and test-only files,
135
+ functions, methods, and symbols, with confidence/evidence and explicit public/framework/dynamic caveats),
136
+ `run_audit` (unused files/exports/dependencies with an explicit manifest-check summary,
137
+ missing npm/Go/Python deps, runtime
126
138
  cycles, type-only/compile-only coupling, orphans, boundary rules, offline OSV vulnerabilities + typosquat +
127
139
  lockfile drift; accepts an immutable `base_ref`, `changed_files`, and `debt: new|existing|all` for
128
140
  review-scoped results), `find_duplicates` (MOSS winnowing over method bodies — catches copy-paste even
129
- after renames), `coverage_map` (existing coverage reports mapped onto the graph; untested hotspots
130
- ranked by connectivity — tests are never executed), `verify_architecture`,
141
+ after renames and can inspect strict small clones down to 12 tokens), `coverage_map` (existing coverage reports mapped onto the graph; untested hotspots
142
+ ranked by connectivity — tests are never executed), `hot_path_review` (bounded local-cost evidence
143
+ with separate graph/test risk), `verify_architecture`,
131
144
  `explain_architecture_violation`, `propose_architecture_exception`
132
145
 
146
+ `hot_path_review` ranks production symbols using parser-derived local time, memory, cyclomatic and
147
+ call-site facts plus exact inside-loop allocation, copy, scan, sort and recursion evidence. Graph
148
+ fan-in/fan-out and measured coverage (or explicitly labelled static test reachability) remain
149
+ separate from local syntax cost. The ranking is not profiler data or interprocedural Big-O.
150
+
133
151
  **build** — `rebuild_graph` (reports the structural delta, keeps the prior state as
134
152
  `graph.prev.json`)
135
153
 
@@ -159,8 +177,10 @@ in the per-user cache and never need to be committed to Git.
159
177
 
160
178
  `query_graph` accepts optional `seed_files` when an architectural question must start from exact
161
179
  entry points. Resolved explicit seeds are exclusive by default; set `augment_seeds:true` only when
162
- fuzzy question-derived seeds are also wanted. Broad bootstrap/routing/authentication questions
163
- without explicit seeds rank conventional entry points and high-level modules ahead of incidental matches.
180
+ question-derived seeds are also wanted. Broad bootstrap/tool-execution/routing questions now rank
181
+ conventional executables and graph-declared production entry points ahead of site, documentation, benchmark
182
+ and fixture matches. Broad ranking remains orientation evidence; use `seed_files` when the intended
183
+ entry point is already known.
164
184
 
165
185
  **advisories** *(network, explicit opt-in)* — `refresh_advisories`
166
186
 
@@ -173,6 +193,57 @@ disclosed instead of silently guessed; and the server **hot-reloads its watched
173
193
  modules and catalog** when those files change — other MCP helpers and analysis engines require a
174
194
  reconnect.
175
195
 
196
+ ### 0.2.5 exact-symbol and graph-fidelity patch
197
+
198
+ - New `inspect_symbol` spends a bounded LSP query on one requested TS/JS declaration, groups exact
199
+ occurrences by logical container, and returns definition/caller source context. Its separate LRU
200
+ cache is revision/config fingerprinted and never replaces the broad precision overlay.
201
+ - Default-object service facades resolve to their underlying helpers, while `get_dependents` no
202
+ longer silently expands every symbol query to all importers of its file.
203
+ - Python receiver-aware method dispatch and unambiguous wildcard imports distinguish same-named
204
+ methods across classes without promoting ambiguous dynamic evidence.
205
+ - `run_audit` makes unused-dependency checking visible even when clean; dead-code review identifies
206
+ production symbols referenced only by tests; small-clone scanning can be lowered to 12 tokens with
207
+ stricter evidence rules.
208
+ - `hot_path_review` adds a bounded offline performance-review queue with line-addressable local cost
209
+ evidence, separate graph risk, and honest measured/unavailable coverage states.
210
+
211
+ Full patch notes: [docs/releases/v0.2.5.md](docs/releases/v0.2.5.md).
212
+
213
+ ### 0.2.4 graph-mode correctness and semantic precision patch
214
+
215
+ - `graph_diff base_ref=...` builds the immutable baseline in the current graph mode, so a
216
+ `no-tests` graph is never compared with a hidden `full` universe. Previous-rebuild snapshots with
217
+ mismatched modes are rejected instead of reporting false removals or cycle drift.
218
+ - `trace_api_contract` preserves each registered repository's build mode during refresh and exposes
219
+ that mode in `graphReconciliation`.
220
+ - `graph_stats` and `open_repo` expose the canonical graph path and build mode; an explicit mode
221
+ mismatch with `build:false` fails closed instead of silently retargeting.
222
+ - `module_map` is production-only by default and can opt back into classified tests, fixtures,
223
+ benchmarks, generated output and docs with `include_non_product:true`.
224
+ - A bundled, read-only TypeScript/JavaScript language server validates a bounded set of semantic
225
+ references after graph reconciliation. Confirmed edges become `EXACT_LSP`; zero-reference evidence
226
+ can strengthen an internal dead-code candidate only after a successful exact query. Partial,
227
+ unavailable and revision-mismatched overlays stay visible and never become cosmetic exactness.
228
+ The provider is Weavatrix's pinned `typescript-language-server` + TypeScript runtime, never a
229
+ repository binary or `npx` download. Automatic type acquisition is disabled. Its child environment
230
+ is reduced to OS/temp/locale basics with a Node/System path; registry, proxy, cloud, token and
231
+ `NODE_OPTIONS` values are not inherited. TypeScript may still read locally declared project
232
+ configuration, dependencies and type declarations; Weavatrix accepts returned evidence only after
233
+ repository realpath containment. Applicable config chains are audited before provider startup;
234
+ configured language-service plugins and unresolved/outside config are refused, while semantic
235
+ inputs are fingerprinted before cache reuse and rechecked after each run. No source or evidence is
236
+ transmitted, and the provider performs no Weavatrix HTTP request. MCP EOF/SIGTERM drains or
237
+ tree-terminates TLS and tsserver before the stdio process exits.
238
+ - Broad `query_graph` bootstrap and tool-execution ranking now prefers production executables and
239
+ graph-declared entry points over site, docs, benchmark and fixture surfaces. Exact `seed_files` remain
240
+ the deterministic option when the caller already knows the entry point.
241
+ - The bundled skill routes orientation, diff review, cross-repository API tracing and exact-symbol
242
+ work through Weavatrix's own evidence states. Java/Rust exact providers remain `UNAVAILABLE` in
243
+ this release instead of being presented as compiler-confirmed.
244
+
245
+ Full patch notes: [docs/releases/v0.2.4.md](docs/releases/v0.2.4.md).
246
+
176
247
  ### 0.2.3 real-wrapper patch
177
248
 
178
249
  - Auto-discovery recognizes wrappers that pass a fixed HTTP client method and argument array to a
@@ -379,9 +450,9 @@ vulnerability findings. This is where each capability comes from and how it is c
379
450
  | Capability alert | Why it exists | Activation and boundary |
380
451
  |---|---|---|
381
452
  | Network access | `refresh_advisories` sends pinned package names and versions to OSV; `pull_architecture_contract` sends an opaque repository UUID and receives an owner-approved contract; `sync_graph` sends a normalized repository label plus an allowlisted graph/evidence payload. Evidence is derived locally from source and manifests, but source bodies, snippets, absolute paths, environment values, credentials and Git remotes are excluded | `offline` and `pinned` expose no network tools. `osv` exposes only advisory refresh. `hosted`/`full` expose all three; every request still requires an explicit tool call, and hosted calls require `WEAVATRIX_SYNC_URL` |
382
- | Shell access | Local `git` powers staleness/change impact; `rg` accelerates search; timed-out Windows child processes may be terminated | Used only by the corresponding local operation; it does not imply network access |
453
+ | Shell access | Local `git` powers staleness/change impact; `rg` accelerates search; the bundled TLS/tsserver process supplies bounded JS/TS semantic evidence; timed-out Windows child trees may be terminated | Used only by the corresponding local operation. The semantic provider is package-pinned, disables automatic type acquisition, and never invokes a repository binary, script, installer or `npx` |
383
454
  | Debug / dynamic loading | Cache-busted `import()` hot-reloads watched MCP tool entry modules; `createRequire` loads package metadata and parser dependencies | Loads files from the installed package; no `eval` |
384
- | Environment access | Reads `WEAVATRIX_*` configuration; local child processes inherit the normal host environment | `WEAVATRIX_SYNC_TOKEN` is removed from every child-process and worker environment and read only by `sync_graph` / `pull_architecture_contract` |
455
+ | Environment access | Reads `WEAVATRIX_*` configuration; ordinary local helpers inherit a credential-stripped environment, while TLS/tsserver receives only allowlisted OS/temp/locale values and a constrained executable path | `WEAVATRIX_SYNC_TOKEN` is removed from every child-process and worker environment. TLS/tsserver also receives no registry/proxy/cloud credentials or `NODE_OPTIONS` |
385
456
  | Filesystem access | Reads the active repository, graph, lockfiles and coverage reports; writes derived graphs and advisory/architecture caches | Realpath containment blocks traversal and symlink/junction escapes. The `pinned` profile removes `open_repo`; the default `offline` profile permits only explicit local switching. The optional malware dependency scan may inspect installed dependency caches such as GOPATH |
386
457
  | URL strings | Fixed OSV/documentation URLs plus a user-configured sync URL | A URL string causes no request by itself; only the three network-profile tools perform requests |
387
458
 
@@ -435,9 +506,9 @@ loader) behind the thin stdio entry `src/mcp-server.mjs`.
435
506
  [app.weavatrix.com](https://app.weavatrix.com): explicit source-free sync,
436
507
  immutable history, Flow/DSM/Map, target architecture and bounded review
437
508
  evidence. Hosted use remains optional and owner-authenticated.
438
- - **Semantic precision bridge** optional local language-server validation for
439
- ambiguous impact, reference and dead-code evidence; the current parser graph
440
- remains the offline fallback.
509
+ - **Semantic precision bridge** shipped for TypeScript/JavaScript in 0.2.4: a bounded, revision-bound
510
+ local overlay validates references with the bundled language server while the parser graph remains
511
+ the fallback. Java and Rust providers are not bundled yet and stay explicitly `UNAVAILABLE`.
441
512
  - **Git-native architecture history** — bounded tag/ref timelines and branch
442
513
  reports built outside the worktree; graph artifacts stay out of Git.
443
514
  - **Cross-repository company evidence** — endpoints, events and internal
package/SECURITY.md CHANGED
@@ -28,6 +28,24 @@ dependency caches such as GOPATH. `offline` permits repository switching only th
28
28
  local `open_repo` call; select `pinned` to remove that tool, the global repository listing, and
29
29
  cross-repository API tracing, holding a hard startup-repository boundary.
30
30
 
31
+ The JS/TS precision overlay is enabled by default for new graphs and runs the package-pinned `typescript-language-server` and
32
+ TypeScript runtime as local child processes. It never resolves a repository executable, invokes
33
+ `npx`, runs package scripts, or installs dependencies. Automatic type acquisition is disabled, and
34
+ the provider receives an allowlisted OS/temp/locale environment with a constrained executable path;
35
+ registry, proxy, cloud, token and `NODE_OPTIONS` values are excluded. The provider makes no
36
+ Weavatrix HTTP request and Weavatrix transmits no source or evidence. TypeScript may still read
37
+ locally declared project configuration, dependencies and type declarations; returned evidence is
38
+ accepted only after repository realpath containment. Before spawning the provider, Weavatrix parses
39
+ the applicable repo-contained TypeScript/JavaScript config chains with its bundled TypeScript API and
40
+ refuses semantic mode when a configured language-service plugin, unresolved/outside config, or input
41
+ safety limit is encountered. Config and configured-project inputs are fingerprinted and rechecked
42
+ before cached evidence is used and after each provider run.
43
+ This is an application/process boundary rather than an operating-system
44
+ network sandbox. On stdio EOF, SIGTERM or SIGINT, the MCP server stops new providers, performs a
45
+ bounded graph-work drain, and closes or tree-terminates TLS/tsserver before exiting.
46
+ Set `WEAVATRIX_PRECISION=off` before server startup (or select `off` in the MCPB semantic-precision
47
+ setting) to keep new graphs parser-only from their first build.
48
+
31
49
  Network capabilities are split by purpose. `osv` adds only `refresh_advisories`, which sends pinned
32
50
  package names and versions to OSV.dev when called. `hosted` / `full` also expose `sync_graph` and
33
51
  `pull_architecture_contract`; both require a user-configured endpoint, and contract pull requires
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "Weavatrix — code graph & blast-radius MCP server for AI coding agents: change impact, transitive dependents, health audit, duplicate detection, and coverage mapping over any local repository.",
6
6
  "author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
@@ -55,6 +55,8 @@
55
55
  ],
56
56
  "dependencies": {
57
57
  "tree-sitter-wasms": "0.1.13",
58
+ "typescript": "5.9.3",
59
+ "typescript-language-server": "4.4.1",
58
60
  "web-tree-sitter": "0.25.10"
59
61
  },
60
62
  "devDependencies": {
package/skill/SKILL.md CHANGED
@@ -29,6 +29,29 @@ Profiles use the same npm package and binary:
29
29
  The legacy `online` capability remains a compatibility alias for `advisories,hosted`; prefer the
30
30
  named profiles for new registrations.
31
31
 
32
+ ## Intent router
33
+
34
+ Start from the task, not from the complete tool list:
35
+
36
+ - **Orient in a repository**: `graph_stats` -> verify `Repo`, `Graph`, and `Build mode`; if the target
37
+ is wrong, call `open_repo`, then `graph_stats` again -> `module_map` (production-only by default).
38
+ - **Review a branch/diff**: `change_impact` for changed-file blast radius; add
39
+ `graph_diff base_ref=<merge-base>` for structural drift, then drill into exact candidates with
40
+ `get_dependents` and `read_source`.
41
+ - **Trace an API across repositories**: `list_known_repos` -> `trace_api_contract` with an explicit
42
+ backend and client list; inspect each `graphReconciliation.buildMode` before using the verdict.
43
+ - **Inspect exact symbol references**: start with `graph_stats`, then call `inspect_symbol` with an
44
+ exact node ID (or an unambiguous label). It spends a bounded point query beyond the broad overlay
45
+ cap and returns occurrence containers plus source context. Use `get_dependents` for transitive
46
+ graph impact and `find_dead_code` for bounded zero-reference
47
+ candidates. Treat `PARTIAL`, `UNAVAILABLE`, `OFF`, or zero exact edges as incomplete evidence, not
48
+ a compiler-exact result; `OFF` means the caller explicitly selected static-only mode. Java and Rust
49
+ providers are not bundled in 0.2.5, so their edges never become `EXACT_LSP` even when a mixed
50
+ repository has a complete TypeScript/JavaScript overlay.
51
+ - **Explore an architectural question**: use broad `query_graph` when entry points are unknown; its
52
+ bootstrap/tool-execution ranking prefers production executables over docs, sites and fixtures.
53
+ Pass exact `seed_files` when the intended entry points are already known.
54
+
32
55
  ## Ground rules
33
56
 
34
57
  - **Evidence, not verdicts**: treat audit, hub, orphan and duplicate output as hypotheses. Confirm a
@@ -47,11 +70,12 @@ named profiles for new registrations.
47
70
  `change_impact` and `graph_diff` label the distinction; `god_nodes` ranks unique connectivity and
48
71
  reports repeated references separately. Do not schedule a runtime-cycle refactor from
49
72
  compile-time-only edges.
50
- - **Edge provenance**: distinguish how an edge was established from legacy confidence. Current
51
- graphs use `EXTRACTED`, `RESOLVED`, and `INFERRED`; `EXACT_LSP` is reserved for a bounded local
52
- precision overlay and `CONFLICT` means evidence disagrees. Treat `INFERRED` and `CONFLICT` as
53
- review signals rather than compiler-exact facts; an `UNKNOWN` count in `graph_stats` requires a
54
- rebuild before precision-sensitive work.
73
+ - **Edge provenance**: distinguish how an edge was established from legacy confidence. The parser
74
+ emits `EXTRACTED`, `RESOLVED`, and `INFERRED`; the bundled bounded TypeScript/JavaScript language
75
+ server emits `EXACT_LSP` only for references it confirms. `CONFLICT` means evidence disagrees.
76
+ Treat `INFERRED`, `CONFLICT`, `PARTIAL`, `UNAVAILABLE`, and `OFF` as review signals rather than
77
+ compiler-exact facts; an `UNKNOWN` count or revision-mismatched overlay in `graph_stats` requires
78
+ a rebuild before precision-sensitive work.
55
79
  - **Repository universe**: in Git repositories, graph and duplicate scans include tracked plus
56
80
  non-ignored untracked files. If an old graph still contains packaged/generated output, rebuild it
57
81
  before interpreting the result. Repository-root `.weavatrixignore` applies the same tracked-file
@@ -60,19 +84,28 @@ named profiles for new registrations.
60
84
  roots. Dead-code/clone/audit review also suppresses `benchmarks/**` and `**/__temp/**` as classified
61
85
  non-production surfaces. A verified production benchmark can opt back in narrowly through
62
86
  `.weavatrix.json` `classify.product`, for example `{"classify":{"product":["benchmarks/core/**"]}}`.
63
- - **Architectural queries**: for bootstrap/routing/authentication questions, inspect the returned
64
- seeds before trusting the traversal. Pass exact repository-relative `seed_files` to `query_graph`
65
- when the intended entry points are already known.
87
+ - **Architectural queries**: broad bootstrap/tool-execution/routing questions rank conventional
88
+ production and graph-declared entry points ahead of classified docs, sites, benchmarks and
89
+ fixtures. Inspect the returned seeds before trusting the traversal. Pass exact repository-relative
90
+ `seed_files` to `query_graph` when the intended entry points are already known.
66
91
  - **Coverage**: `coverage_map` reads an existing report. `unavailable` means no supported report was
67
92
  found, not 0% coverage; do not rank testing risk from that state.
68
- - **Audit completeness**: read `checks.osv.status` and `checks.malware.status`. For OSV, `OK` is
93
+ - **Local hot paths**: `hot_path_review` ranks parser-derived local syntax cost and reports graph
94
+ fan-in/fan-out plus test evidence separately. `actualCoverage: NOT_AVAILABLE` is not zero coverage,
95
+ and the score is not profiler data or an interprocedural Big-O proof. Inspect its line evidence and
96
+ measure runtime before scheduling a performance rewrite.
97
+ - **Audit completeness**: read `dependencyReport.status` plus its unused/missing counts, then
98
+ `checks.osv.status` and `checks.malware.status`. A dependency result from a partial graph is not a
99
+ repository-wide clean zero. For OSV, `OK` is
69
100
  complete for the recorded dependency fingerprint; `PARTIAL` is incomplete or stale,
70
101
  `NOT_CHECKED` has no repository-specific result, and `ERROR` means the local check failed. Treat
71
102
  the same non-`OK` states as incomplete for malware scanning. Refresh OSV only when the user has
72
103
  authorized selecting the optional `osv` profile (or `advisories` capability) and then
73
104
  invoking `refresh_advisories`; enabling the group alone sends nothing.
74
- - **Offline by design**: scans and graph queries run in-process against local files; coverage tools
75
- read existing reports and never run tests. The ONLY network-touching tools live in the optional
105
+ - **Offline by design**: scans and graph queries use local files; the semantic overlay may launch
106
+ Weavatrix's bundled read-only TypeScript language-server child process, but it never runs repository
107
+ scripts or downloads a provider. Coverage tools read existing reports and never run tests. The
108
+ ONLY network-touching tools live in the optional
76
109
  `advisories` / `hosted` capabilities and run solely when explicitly called:
77
110
  `refresh_advisories` (queries OSV.dev with
78
111
  package names + versions so `run_audit` has fresh vulnerability data) and `sync_graph` (derives a
@@ -92,8 +125,10 @@ named profiles for new registrations.
92
125
  - **Orient in the configured repo**: `module_map` → `list_communities` → `god_nodes`. Hub ranking
93
126
  is production-only by default; use `include_classified:true` only when tests/generated/build
94
127
  surfaces are deliberately part of the question.
95
- - **Refactor safety for one symbol**: `get_dependents` → `coverage_map` (low coverage × many
128
+ - **Refactor safety for one symbol**: `inspect_symbol` → `get_dependents` → `coverage_map` (low coverage × many
96
129
  dependents ⇒ write tests first) → `read_source`.
130
+ - **Performance review**: `hot_path_review` → inspect its local evidence with `read_source` → use
131
+ `get_dependents` for change risk → confirm with the repository's profiler/benchmark before editing.
97
132
  - **Pre-PR review of your current changes**: `change_impact` (auto merge-base; includes uncommitted
98
133
  and untracked work, coverage attached, untested hotspots called out) → drill with `get_dependents`.
99
134
  - **Impact of a PR that is NOT checked out**: pass its changed-file list explicitly —
@@ -105,7 +140,8 @@ named profiles for new registrations.
105
140
  orphan drift rather than raw edge counts.
106
141
  - **Health sweep**: for a branch/PR review prefer
107
142
  `run_audit base_ref=<merge-base> debt=new changed_files=[…]`; for repository maintenance use
108
- `run_audit debt=all`. Then call `find_dead_code`, `find_duplicates` and `coverage_map`. A changed-file-only audit is
143
+ `run_audit debt=all`. Then call `find_dead_code`, `find_duplicates`, `coverage_map` and
144
+ `hot_path_review`. A changed-file-only audit is
109
145
  scope, not proof of new debt. Inspect the source behind shortlisted findings before proposing edits.
110
146
  - **Dead-code and dead-method review**: call `find_dead_code` for a bounded production-code queue of
111
147
  files, functions, methods and symbols. Narrow with `path` or `kinds=["method"]`; keep the default
@@ -100,7 +100,16 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
100
100
  const nodes = graph.nodes || [], links = graph.links || [];
101
101
  const ep = (v) => (v && typeof v === "object" ? v.id : v);
102
102
  const inbound = new Set();
103
- for (const l of links) if (!isStructuralRelation(l.relation)) inbound.add(ep(l.target));
103
+ const inboundSources = new Map();
104
+ const nodesById = new Map(nodes.map((node) => [String(node.id), node]));
105
+ for (const l of links) if (!isStructuralRelation(l.relation)) {
106
+ const target = String(ep(l.target));
107
+ const source = String(ep(l.source));
108
+ inbound.add(target);
109
+ const values = inboundSources.get(target) || [];
110
+ values.push(source);
111
+ inboundSources.set(target, values);
112
+ }
104
113
 
105
114
  // whole-repo identifier frequency: a symbol whose name appears MORE than once total (its definition + at least
106
115
  // one use, same-file OR cross-file) is referenced. Errs toward "alive" (common-named symbols never flagged).
@@ -115,6 +124,16 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
115
124
  (symsByFile.get(n.source_file) || symsByFile.set(n.source_file, []).get(n.source_file)).push(n);
116
125
  }
117
126
 
127
+ const symbolNames = new Set([...symById.values()].map((node) => bareName(node.label)).filter(Boolean));
128
+ const occurrenceFiles = new Map();
129
+ for (const [file, text] of sources) for (const match of String(text || "").matchAll(IDENT_RE)) {
130
+ const name = match[0];
131
+ if (!symbolNames.has(name)) continue;
132
+ const files = occurrenceFiles.get(name) || new Set();
133
+ files.add(file);
134
+ occurrenceFiles.set(name, files);
135
+ }
136
+
118
137
  // decorated defs (@app.route/@app.event/@pytest.fixture…) are entered by the framework: trust the
119
138
  // builder's flag when present, else walk the source line(s) above the definition (graph-builder graphs).
120
139
  const lineCache = new Map();
@@ -151,6 +170,32 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
151
170
  }
152
171
  const deadSet = new Set(deadSymbols.map((s) => s.id));
153
172
 
173
+ // A production declaration consumed only by tests is live to the raw graph but dead to production.
174
+ // Keep it in a separate review class: this is useful evidence, never an automatic delete verdict.
175
+ const testOnlySymbols = [];
176
+ for (const n of symById.values()) {
177
+ if (deadSet.has(n.id) || isTestFile(n.source_file) || isDecorated(n)) continue;
178
+ const sourcesForSymbol = inboundSources.get(String(n.id)) || [];
179
+ const sourceFiles = sourcesForSymbol.map((id) => nodesById.get(id)?.source_file || (id.includes("#") ? id.split("#")[0] : id));
180
+ const hasTestInbound = sourceFiles.some((file) => isTestFile(file));
181
+ const hasProductionInbound = sourceFiles.some((file) => file && !isTestFile(file));
182
+ const name = bareName(n.label);
183
+ const occurrenceSet = occurrenceFiles.get(name) || new Set();
184
+ const externalOccurrences = [...occurrenceSet].filter((file) => file !== n.source_file);
185
+ const lexicalTestOnly = externalOccurrences.length > 0 && externalOccurrences.every((file) => isTestFile(file));
186
+ if (hasProductionInbound || (!hasTestInbound && !lexicalTestOnly)) continue;
187
+ testOnlySymbols.push({
188
+ id: n.id,
189
+ file: n.source_file,
190
+ label: n.label,
191
+ test: false,
192
+ reason: "referenced only from test/e2e code; no production consumer was found",
193
+ testConsumerFiles: [...new Set(sourceFiles.filter((file) => file && isTestFile(file)))].sort(),
194
+ evidence: hasTestInbound ? "graph" : "lexical",
195
+ publicApi: n.exported === true || ["public", "protected"].includes(String(n.visibility || "").toLowerCase()),
196
+ });
197
+ }
198
+
154
199
  const deadFiles = [];
155
200
  for (const n of nodes) {
156
201
  if (String(n.id).includes("#")) continue; // file nodes only
@@ -161,9 +206,10 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
161
206
 
162
207
  return {
163
208
  deadSymbols,
209
+ testOnlySymbols,
164
210
  deadFiles,
165
211
  referencedIds: [...symById.values()].filter(isReferenced).map((n) => n.id),
166
- stats: { symbols: symById.size, deadSymbols: deadSymbols.length, files: nodes.filter((n) => !String(n.id).includes("#")).length, deadFiles: deadFiles.length },
212
+ stats: { symbols: symById.size, deadSymbols: deadSymbols.length, testOnlySymbols: testOnlySymbols.length, files: nodes.filter((n) => !String(n.id).includes("#")).length, deadFiles: deadFiles.length },
167
213
  };
168
214
  }
169
215
 
@@ -45,15 +45,24 @@ function symbolCandidate(item, node, context) {
45
45
  const source = String(context.sources.get(file) || "");
46
46
  const pathInfo = context.classify(file, source);
47
47
  const publicSurface = isPublicSurface(node);
48
+ const testOnly = item.testOnly === true;
48
49
  const externalEntry = context.entrySet.has(file) || isFrameworkEntryFile(file);
49
50
  const framework = context.frameworkByFile.get(file) || null;
50
51
  const dynamicFile = context.dynamicTargets.has(file) || DYNAMIC_RE.test(source);
51
52
  const reflectionFile = REFLECTION_RE.test(source);
52
53
  const kind = kindOf(node);
53
- let confidence = (String(node?.visibility || "").toLowerCase() === "private" || (!publicSurface && ["method", "function"].includes(kind)))
54
- ? "high" : "medium";
54
+ const exactNoReference = context.exactNoReferenceIds.has(String(item.id));
55
+ const internallyScoped = String(node?.visibility || "").toLowerCase() === "private"
56
+ || (!publicSurface && ["method", "function"].includes(kind));
57
+ // Static absence alone is never high-confidence dead code. High is reserved for a successfully
58
+ // queried semantic declaration whose language server also returned no in-workspace references.
59
+ let confidence = internallyScoped && exactNoReference ? "high" : "medium";
55
60
  const caveats = [];
56
61
 
62
+ if (internallyScoped && !exactNoReference) {
63
+ caveats.push("No complete exact semantic no-reference result is available for this declaration; static absence remains medium confidence.");
64
+ }
65
+
57
66
  if (publicSurface) {
58
67
  confidence = "low";
59
68
  caveats.push("Public/exported APIs can be consumed by downstream packages, interfaces, reflection, dependency injection, templates, or configuration outside this repository.");
@@ -78,7 +87,7 @@ function symbolCandidate(item, node, context) {
78
87
  return {
79
88
  id: String(item.id),
80
89
  kind,
81
- classification: publicSurface ? `public-${kind}` : kind === "method" ? "internal-method" : kind === "function" ? "internal-function" : "unreferenced-symbol",
90
+ classification: testOnly ? `test-only-${kind}` : publicSurface ? `public-${kind}` : kind === "method" ? "internal-method" : kind === "function" ? "internal-function" : "unreferenced-symbol",
82
91
  file,
83
92
  line: lineOf(node),
84
93
  symbol: bareLabel(node?.label || item.label),
@@ -88,15 +97,18 @@ function symbolCandidate(item, node, context) {
88
97
  confidence,
89
98
  reason: item.reason,
90
99
  evidence: [
91
- { kind: "graph", fact: "No inbound non-structural graph edge targets this symbol." },
92
- { kind: "source-index", fact: "Its identifier has no second indexed occurrence that establishes a caller." },
100
+ ...(testOnly ? [{kind: item.evidence || "graph", fact: `Only test/e2e consumers were found${item.testConsumerFiles?.length ? `: ${item.testConsumerFiles.join(", ")}` : "."}`}]
101
+ : [{ kind: "graph", fact: "No inbound non-structural graph edge targets this symbol." }, { kind: "source-index", fact: "Its identifier has no second indexed occurrence that establishes a caller." }]),
102
+ ...(exactNoReference ? [{kind: "exact-lsp", fact: "The active language server returned no in-workspace references for this exact declaration."}] : []),
93
103
  ],
94
104
  caveats,
95
105
  publicApi: publicSurface,
96
106
  externallyEnteredFile: externalEntry,
97
107
  pathClasses: pathInfo.classes || [],
98
108
  matchedPathRule: pathInfo.matchedRule || null,
99
- reviewAction: "Confirm with read_source, get_dependents, exact search, framework/config inspection and tests; never auto-delete.",
109
+ reviewAction: testOnly
110
+ ? "Confirm that no production/config/framework consumer exists; decide whether the declaration is intentional test support or removable with its tests. Never auto-delete."
111
+ : "Confirm with read_source, get_dependents, exact search, framework/config inspection and tests; never auto-delete.",
100
112
  };
101
113
  }
102
114
 
@@ -169,10 +181,11 @@ export function computeDeadCodeReview(graph, sources, options = {}) {
169
181
  const minConfidence = Object.hasOwn(CONFIDENCE_RANK, options.minConfidence) ? options.minConfidence : "medium";
170
182
  const pathPrefix = normalizedPath(options.path || "").replace(/\/+$/, "");
171
183
  const requestedKinds = new Set(Array.isArray(options.kinds) && options.kinds.length ? options.kinds : ["file", "function", "method", "symbol"]);
172
- const context = { sources, entrySet, dynamicTargets, frameworkByFile, classify, repoSignals };
184
+ const exactNoReferenceIds = new Set(graph.precisionNoReferenceSymbols || options.exactNoReferenceIds || []);
185
+ const context = { sources, entrySet, dynamicTargets, frameworkByFile, classify, repoSignals, exactNoReferenceIds };
173
186
  const dead = computeDead(graph, sources, { entrySet });
174
187
  const nodesById = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
175
- const rawSymbols = dead.deadSymbols
188
+ const rawSymbols = [...dead.deadSymbols, ...(dead.testOnlySymbols || []).map((item) => ({...item, testOnly: true}))]
176
189
  .map((item) => ({ item, node: nodesById.get(String(item.id)) }))
177
190
  .filter((entry) => entry.node)
178
191
  .map(({ item, node }) => symbolCandidate(item, node, context));
@@ -226,6 +239,7 @@ export function computeDeadCodeReview(graph, sources, options = {}) {
226
239
  indexedSymbols: dead.stats.symbols,
227
240
  indexedFiles: dead.stats.files,
228
241
  rawDeadSymbols: rawSymbols.length,
242
+ rawTestOnlySymbols: (dead.testOnlySymbols || []).length,
229
243
  rawDeadFiles: rawFiles.length,
230
244
  reviewCandidates: candidates.length,
231
245
  byConfidence: {
@@ -6,7 +6,7 @@ import { createPathClassifier, hasPathClass } from "../path-classification.js";
6
6
  import { listRepoFiles } from "./internal-audit.collect.js";
7
7
  import { stripNonCode, bodyEndLineCount, tokenize, fingerprints, extractLargeStrings } from "./duplicates.tokenize.js";
8
8
 
9
- const FLOOR_TOKENS = 30; // fragments below this never enter the index (UI slider min)
9
+ const FLOOR_TOKENS = 12; // absolute safety floor; normal scans still default to 30+ tokens
10
10
  const FLOOR_SIM = 0.5; // pairs below this are not reported (UI slider min)
11
11
  const MAX_BODY_LINES = 400;
12
12
  // File types with no code SYMBOLS to fragment (stylesheets, markup, docs, single-file components). The
@@ -93,7 +93,11 @@ function pairsForMode(frags, mode) {
93
93
  const i = Math.floor(key / 1000000), j = key % 1000000;
94
94
  const union = eff[i] + eff[j] - count;
95
95
  const sim = union > 0 ? count / union : 0;
96
- if (sim >= FLOOR_SIM && count >= MIN_SHARED_FP) pairs.push([i, j, Math.round(sim * 100)]);
96
+ const smallFragment = Math.min(frags[i].n, frags[j].n) < 30;
97
+ const enoughEvidence = smallFragment
98
+ ? sim >= 0.95 && count >= 2
99
+ : count >= MIN_SHARED_FP;
100
+ if (sim >= FLOOR_SIM && enoughEvidence) pairs.push([i, j, Math.round(sim * 100)]);
97
101
  }
98
102
  return pairs;
99
103
  }
@@ -125,6 +129,7 @@ function loadGraph(graphInput) {
125
129
 
126
130
  export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
127
131
  const includeStrings = !!opts.includeStrings;
132
+ const scanTokenFloor = Math.max(FLOOR_TOKENS, Math.min(400, Number(opts.minTokens) || 30));
128
133
  const graph = loadGraph(graphJsonPath);
129
134
  const byFile = symbolRanges(graph);
130
135
  // total symbol nodes in the graph — 0 means a file-only graph (built by an older builder before
@@ -169,7 +174,7 @@ export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
169
174
  const end = start + body.length - 1;
170
175
  if (end - start < 2) continue;
171
176
  const toks = tokenize(stripNonCode(body.join("\n"), py));
172
- if (toks.strict.length < FLOOR_TOKENS) continue;
177
+ if (toks.strict.length < scanTokenFloor) continue;
173
178
  frags.push({
174
179
  id: syms[i].id, label: syms[i].label, file, start, end,
175
180
  n: toks.strict.length,
@@ -184,7 +189,7 @@ export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
184
189
  const base = file.split("/").pop();
185
190
  const pushStr = (startLine, endLine, content) => {
186
191
  const toks = tokenize(content);
187
- if (toks.strict.length < FLOOR_TOKENS) return;
192
+ if (toks.strict.length < scanTokenFloor) return;
188
193
  frags.push({
189
194
  id: `${file}#str@${startLine}`, label: `${base}:${startLine} ~${endLine - startLine + 1}-line string`,
190
195
  file, start: startLine, end: endLine, n: toks.strict.length, ...classificationFields(pathInfo), kind: "string",
@@ -224,7 +229,7 @@ export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
224
229
  const end = Math.min(start + WINDOW_LINES - 1, lines.length);
225
230
  if (end - start < 2) continue;
226
231
  const toks = tokenize(stripNonCode(lines.slice(start - 1, end).join("\n"), false));
227
- if (toks.strict.length < FLOOR_TOKENS) continue;
232
+ if (toks.strict.length < scanTokenFloor) continue;
228
233
  frags.push({
229
234
  id: `${file}#win@${start}`, label: `${file.split("/").pop()}:${start}-${end}`, file, start, end,
230
235
  n: toks.strict.length, ...classificationFields(pathInfo),
@@ -250,6 +255,6 @@ export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
250
255
  },
251
256
  nameTwinsTruncated: !!nameTwins && nameTwins.length >= 200,
252
257
  },
253
- floors: { tokens: FLOOR_TOKENS, sim: FLOOR_SIM * 100 },
258
+ floors: { tokens: scanTokenFloor, absoluteTokens: FLOOR_TOKENS, sim: FLOOR_SIM * 100 },
254
259
  };
255
260
  }
@@ -7,6 +7,7 @@ import {tmpdir} from 'node:os'
7
7
  import {spawnSync} from 'node:child_process'
8
8
  import {childProcessEnv} from '../child-env.js'
9
9
  import {buildInternalGraph} from '../graph/internal-builder.js'
10
+ import {filterGraphForMode} from '../graph/graph-filter.js'
10
11
 
11
12
  const SAFE_REF = /^(?!-)[A-Za-z0-9][A-Za-z0-9._\/@{}+~^-]{0,199}$/
12
13
 
@@ -68,7 +69,14 @@ export async function withGitRefCheckout(repoRoot, requestedRef, operation) {
68
69
  }
69
70
  }
70
71
 
71
- export async function buildGraphAtGitRef(repoRoot, requestedRef) {
72
- const result = await withGitRefCheckout(repoRoot, requestedRef, async (checkout) => buildInternalGraph(checkout))
72
+ export async function buildGraphAtGitRef(repoRoot, requestedRef, {mode = 'full'} = {}) {
73
+ const buildMode = ['full', 'no-tests', 'tests-only'].includes(mode) ? mode : 'full'
74
+ const result = await withGitRefCheckout(repoRoot, requestedRef, async (checkout) => {
75
+ const fullGraph = await buildInternalGraph(checkout)
76
+ return {
77
+ ...filterGraphForMode(fullGraph, buildMode, {repoRoot: checkout}),
78
+ graphBuildMode: buildMode,
79
+ }
80
+ })
73
81
  return result.ok ? {...result, graph: result.value} : result
74
82
  }