weavatrix 0.2.3 → 0.2.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
@@ -111,10 +111,19 @@ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — instal
111
111
  compile-only edges (Rust module/use, Java imports) are reported separately where that distinction
112
112
  changes the result.
113
113
 
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.
114
+ Every current edge carries versioned provenance. The parser emits `EXTRACTED`, `RESOLVED`, and
115
+ `INFERRED`; the built-in bounded TypeScript/JavaScript precision overlay upgrades only references
116
+ confirmed by its bundled `typescript-language-server` + TypeScript runtime to `EXACT_LSP`.
117
+ `CONFLICT` means evidence disagrees. `graph_stats` reports the provenance breakdown and the semantic
118
+ provider's `COMPLETE`, `PARTIAL`, `UNAVAILABLE`, or `OFF` state; `OFF` means precision was explicitly
119
+ disabled and only static evidence is active. Java and Rust language-server providers are not bundled
120
+ in 0.2.4: their edges never become `EXACT_LSP`, even when a mixed repository reports a complete
121
+ TypeScript/JavaScript overlay.
122
+
123
+ The bounded JS/TS provider is enabled by default for new graphs. Set `WEAVATRIX_PRECISION=off`
124
+ before starting the MCP server for parser-only operation from the first build, or pass
125
+ `precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
126
+ choice as **TypeScript/JavaScript semantic precision**.
118
127
 
119
128
  **search / source** — `search_code` (ripgrep-backed, pure-Node fallback), `read_source` (a
120
129
  symbol's actual code in one hop), `list_endpoints` (HTTP route inventory:
@@ -159,8 +168,10 @@ in the per-user cache and never need to be committed to Git.
159
168
 
160
169
  `query_graph` accepts optional `seed_files` when an architectural question must start from exact
161
170
  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.
171
+ question-derived seeds are also wanted. Broad bootstrap/tool-execution/routing questions now rank
172
+ conventional executables and graph-declared production entry points ahead of site, documentation, benchmark
173
+ and fixture matches. Broad ranking remains orientation evidence; use `seed_files` when the intended
174
+ entry point is already known.
164
175
 
165
176
  **advisories** *(network, explicit opt-in)* — `refresh_advisories`
166
177
 
@@ -173,6 +184,40 @@ disclosed instead of silently guessed; and the server **hot-reloads its watched
173
184
  modules and catalog** when those files change — other MCP helpers and analysis engines require a
174
185
  reconnect.
175
186
 
187
+ ### 0.2.4 graph-mode correctness and semantic precision patch
188
+
189
+ - `graph_diff base_ref=...` builds the immutable baseline in the current graph mode, so a
190
+ `no-tests` graph is never compared with a hidden `full` universe. Previous-rebuild snapshots with
191
+ mismatched modes are rejected instead of reporting false removals or cycle drift.
192
+ - `trace_api_contract` preserves each registered repository's build mode during refresh and exposes
193
+ that mode in `graphReconciliation`.
194
+ - `graph_stats` and `open_repo` expose the canonical graph path and build mode; an explicit mode
195
+ mismatch with `build:false` fails closed instead of silently retargeting.
196
+ - `module_map` is production-only by default and can opt back into classified tests, fixtures,
197
+ benchmarks, generated output and docs with `include_non_product:true`.
198
+ - A bundled, read-only TypeScript/JavaScript language server validates a bounded set of semantic
199
+ references after graph reconciliation. Confirmed edges become `EXACT_LSP`; zero-reference evidence
200
+ can strengthen an internal dead-code candidate only after a successful exact query. Partial,
201
+ unavailable and revision-mismatched overlays stay visible and never become cosmetic exactness.
202
+ The provider is Weavatrix's pinned `typescript-language-server` + TypeScript runtime, never a
203
+ repository binary or `npx` download. Automatic type acquisition is disabled. Its child environment
204
+ is reduced to OS/temp/locale basics with a Node/System path; registry, proxy, cloud, token and
205
+ `NODE_OPTIONS` values are not inherited. TypeScript may still read locally declared project
206
+ configuration, dependencies and type declarations; Weavatrix accepts returned evidence only after
207
+ repository realpath containment. Applicable config chains are audited before provider startup;
208
+ configured language-service plugins and unresolved/outside config are refused, while semantic
209
+ inputs are fingerprinted before cache reuse and rechecked after each run. No source or evidence is
210
+ transmitted, and the provider performs no Weavatrix HTTP request. MCP EOF/SIGTERM drains or
211
+ tree-terminates TLS and tsserver before the stdio process exits.
212
+ - Broad `query_graph` bootstrap and tool-execution ranking now prefers production executables and
213
+ graph-declared entry points over site, docs, benchmark and fixture surfaces. Exact `seed_files` remain
214
+ the deterministic option when the caller already knows the entry point.
215
+ - The bundled skill routes orientation, diff review, cross-repository API tracing and exact-symbol
216
+ work through Weavatrix's own evidence states. Java/Rust exact providers remain `UNAVAILABLE` in
217
+ this release instead of being presented as compiler-confirmed.
218
+
219
+ Full patch notes: [docs/releases/v0.2.4.md](docs/releases/v0.2.4.md).
220
+
176
221
  ### 0.2.3 real-wrapper patch
177
222
 
178
223
  - Auto-discovery recognizes wrappers that pass a fixed HTTP client method and argument array to a
@@ -379,9 +424,9 @@ vulnerability findings. This is where each capability comes from and how it is c
379
424
  | Capability alert | Why it exists | Activation and boundary |
380
425
  |---|---|---|
381
426
  | 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 |
427
+ | 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
428
  | 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` |
429
+ | 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
430
  | 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
431
  | 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
432
 
@@ -435,9 +480,9 @@ loader) behind the thin stdio entry `src/mcp-server.mjs`.
435
480
  [app.weavatrix.com](https://app.weavatrix.com): explicit source-free sync,
436
481
  immutable history, Flow/DSM/Map, target architecture and bounded review
437
482
  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.
483
+ - **Semantic precision bridge** shipped for TypeScript/JavaScript in 0.2.4: a bounded, revision-bound
484
+ local overlay validates references with the bundled language server while the parser graph remains
485
+ the fallback. Java and Rust providers are not bundled yet and stay explicitly `UNAVAILABLE`.
441
486
  - **Git-native architecture history** — bounded tag/ref timelines and branch
442
487
  reports built outside the worktree; graph artifacts stay out of Git.
443
488
  - **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.4",
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,28 @@ 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` and read semantic precision state,
44
+ provider and `EXACT_LSP` count. For TypeScript/JavaScript, use an exact node id with
45
+ `get_dependents` and inspect the edge provenance; use `find_dead_code` for bounded zero-reference
46
+ candidates. Treat `PARTIAL`, `UNAVAILABLE`, `OFF`, or zero exact edges as incomplete evidence, not
47
+ a compiler-exact result; `OFF` means the caller explicitly selected static-only mode. Java and Rust
48
+ providers are not bundled in 0.2.4, so their edges never become `EXACT_LSP` even when a mixed
49
+ repository has a complete TypeScript/JavaScript overlay.
50
+ - **Explore an architectural question**: use broad `query_graph` when entry points are unknown; its
51
+ bootstrap/tool-execution ranking prefers production executables over docs, sites and fixtures.
52
+ Pass exact `seed_files` when the intended entry points are already known.
53
+
32
54
  ## Ground rules
33
55
 
34
56
  - **Evidence, not verdicts**: treat audit, hub, orphan and duplicate output as hypotheses. Confirm a
@@ -47,11 +69,12 @@ named profiles for new registrations.
47
69
  `change_impact` and `graph_diff` label the distinction; `god_nodes` ranks unique connectivity and
48
70
  reports repeated references separately. Do not schedule a runtime-cycle refactor from
49
71
  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.
72
+ - **Edge provenance**: distinguish how an edge was established from legacy confidence. The parser
73
+ emits `EXTRACTED`, `RESOLVED`, and `INFERRED`; the bundled bounded TypeScript/JavaScript language
74
+ server emits `EXACT_LSP` only for references it confirms. `CONFLICT` means evidence disagrees.
75
+ Treat `INFERRED`, `CONFLICT`, `PARTIAL`, `UNAVAILABLE`, and `OFF` as review signals rather than
76
+ compiler-exact facts; an `UNKNOWN` count or revision-mismatched overlay in `graph_stats` requires
77
+ a rebuild before precision-sensitive work.
55
78
  - **Repository universe**: in Git repositories, graph and duplicate scans include tracked plus
56
79
  non-ignored untracked files. If an old graph still contains packaged/generated output, rebuild it
57
80
  before interpreting the result. Repository-root `.weavatrixignore` applies the same tracked-file
@@ -60,9 +83,10 @@ named profiles for new registrations.
60
83
  roots. Dead-code/clone/audit review also suppresses `benchmarks/**` and `**/__temp/**` as classified
61
84
  non-production surfaces. A verified production benchmark can opt back in narrowly through
62
85
  `.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.
86
+ - **Architectural queries**: broad bootstrap/tool-execution/routing questions rank conventional
87
+ production and graph-declared entry points ahead of classified docs, sites, benchmarks and
88
+ fixtures. Inspect the returned seeds before trusting the traversal. Pass exact repository-relative
89
+ `seed_files` to `query_graph` when the intended entry points are already known.
66
90
  - **Coverage**: `coverage_map` reads an existing report. `unavailable` means no supported report was
67
91
  found, not 0% coverage; do not rank testing risk from that state.
68
92
  - **Audit completeness**: read `checks.osv.status` and `checks.malware.status`. For OSV, `OK` is
@@ -71,8 +95,10 @@ named profiles for new registrations.
71
95
  the same non-`OK` states as incomplete for malware scanning. Refresh OSV only when the user has
72
96
  authorized selecting the optional `osv` profile (or `advisories` capability) and then
73
97
  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
98
+ - **Offline by design**: scans and graph queries use local files; the semantic overlay may launch
99
+ Weavatrix's bundled read-only TypeScript language-server child process, but it never runs repository
100
+ scripts or downloads a provider. Coverage tools read existing reports and never run tests. The
101
+ ONLY network-touching tools live in the optional
76
102
  `advisories` / `hosted` capabilities and run solely when explicitly called:
77
103
  `refresh_advisories` (queries OSV.dev with
78
104
  package names + versions so `run_audit` has fresh vulnerability data) and `sync_graph` (derives a
@@ -50,10 +50,18 @@ function symbolCandidate(item, node, context) {
50
50
  const dynamicFile = context.dynamicTargets.has(file) || DYNAMIC_RE.test(source);
51
51
  const reflectionFile = REFLECTION_RE.test(source);
52
52
  const kind = kindOf(node);
53
- let confidence = (String(node?.visibility || "").toLowerCase() === "private" || (!publicSurface && ["method", "function"].includes(kind)))
54
- ? "high" : "medium";
53
+ const exactNoReference = context.exactNoReferenceIds.has(String(item.id));
54
+ const internallyScoped = String(node?.visibility || "").toLowerCase() === "private"
55
+ || (!publicSurface && ["method", "function"].includes(kind));
56
+ // Static absence alone is never high-confidence dead code. High is reserved for a successfully
57
+ // queried semantic declaration whose language server also returned no in-workspace references.
58
+ let confidence = internallyScoped && exactNoReference ? "high" : "medium";
55
59
  const caveats = [];
56
60
 
61
+ if (internallyScoped && !exactNoReference) {
62
+ caveats.push("No complete exact semantic no-reference result is available for this declaration; static absence remains medium confidence.");
63
+ }
64
+
57
65
  if (publicSurface) {
58
66
  confidence = "low";
59
67
  caveats.push("Public/exported APIs can be consumed by downstream packages, interfaces, reflection, dependency injection, templates, or configuration outside this repository.");
@@ -90,6 +98,7 @@ function symbolCandidate(item, node, context) {
90
98
  evidence: [
91
99
  { kind: "graph", fact: "No inbound non-structural graph edge targets this symbol." },
92
100
  { kind: "source-index", fact: "Its identifier has no second indexed occurrence that establishes a caller." },
101
+ ...(exactNoReference ? [{kind: "exact-lsp", fact: "The active language server returned no in-workspace references for this exact declaration."}] : []),
93
102
  ],
94
103
  caveats,
95
104
  publicApi: publicSurface,
@@ -169,7 +178,8 @@ export function computeDeadCodeReview(graph, sources, options = {}) {
169
178
  const minConfidence = Object.hasOwn(CONFIDENCE_RANK, options.minConfidence) ? options.minConfidence : "medium";
170
179
  const pathPrefix = normalizedPath(options.path || "").replace(/\/+$/, "");
171
180
  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 };
181
+ const exactNoReferenceIds = new Set(graph.precisionNoReferenceSymbols || options.exactNoReferenceIds || []);
182
+ const context = { sources, entrySet, dynamicTargets, frameworkByFile, classify, repoSignals, exactNoReferenceIds };
173
183
  const dead = computeDead(graph, sources, { entrySet });
174
184
  const nodesById = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
175
185
  const rawSymbols = dead.deadSymbols
@@ -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
  }
@@ -14,7 +14,8 @@ import { graphHomeDir, graphOutDirForRepo, graphStorageKey, repoTopFolders, summ
14
14
  import { registerRepository } from "./graph/repo-registry.js";
15
15
  import { refreshGraphIncrementally, snapshotRepository } from "./graph/incremental-refresh.js";
16
16
  import { atomicWriteFileSync, withFileLock } from "./graph/file-lock.js";
17
- import { repositoryFreshnessProbe, stampRepositoryFreshness } from "./graph/freshness-probe.js";
17
+ import { graphSchemaIsCurrent, repositoryFreshnessProbe, stampRepositoryFreshness } from "./graph/freshness-probe.js";
18
+ import { buildLspPrecisionOverlay, invalidatePrecisionOverlay, precisionSummary } from "./precision/lsp-overlay.js";
18
19
 
19
20
  // The worker path deadlocks web-tree-sitter's WASM in Electron's worker threads (fine in plain Node) — off
20
21
  // until that's Electron-safe. In-process + event-loop yielding keeps the window responsive without it.
@@ -26,6 +27,10 @@ const USE_BUILD_WORKER = false;
26
27
  // freeze the whole app on the main thread). 4 min is generous for very large repos.
27
28
  const BUILD_WORKER_TIMEOUT_MS = 4 * 60 * 1000;
28
29
 
30
+ export function defaultPrecisionMode(env = process.env) {
31
+ return env?.WEAVATRIX_PRECISION === "off" ? "off" : "lsp";
32
+ }
33
+
29
34
  function buildGraphInWorker(payload) {
30
35
  return new Promise((resolve, reject) => {
31
36
  let worker;
@@ -56,7 +61,7 @@ function buildGraphInWorker(payload) {
56
61
  });
57
62
  }
58
63
 
59
- async function buildAndWriteInProcess(repoPath, { mode, scope, graphJson, central }) {
64
+ async function buildAndWriteInProcess(repoPath, { mode, scope, precision, graphJson, central }) {
60
65
  const { buildInternalGraph } = await import("./graph/internal-builder.js");
61
66
  // Capture the cheap Git state on both sides of the authoritative build. Only an unchanged pair is
62
67
  // safe to persist: if the working tree moves while parsing, the next process must take the slow path.
@@ -66,7 +71,7 @@ async function buildAndWriteInProcess(repoPath, { mode, scope, graphJson, centra
66
71
  if (mode === "full" && !scope && existsSync(graphJson)) {
67
72
  try {
68
73
  const existing = JSON.parse(readFileSync(graphJson, "utf8"));
69
- if (existing.graphBuildMode === "full" && !existing.graphBuildScope) {
74
+ if (existing.graphBuildMode === "full" && !existing.graphBuildScope && graphSchemaIsCurrent(existing)) {
70
75
  refresh = await refreshGraphIncrementally(repoPath, existing, { buildGraph: buildInternalGraph });
71
76
  graph = refresh.graph;
72
77
  }
@@ -75,7 +80,7 @@ async function buildAndWriteInProcess(repoPath, { mode, scope, graphJson, centra
75
80
  if (!graph && mode !== "full" && !scope && existsSync(graphJson)) {
76
81
  try {
77
82
  const existing = JSON.parse(readFileSync(graphJson, "utf8"));
78
- if (existing.graphBuildMode === mode && existing.graphRevision) {
83
+ if (existing.graphBuildMode === mode && existing.graphRevision && graphSchemaIsCurrent(existing)) {
79
84
  const snapshot = snapshotRepository(repoPath);
80
85
  if (snapshot.revision === existing.graphRevision) {
81
86
  graph = existing;
@@ -93,6 +98,9 @@ async function buildAndWriteInProcess(repoPath, { mode, scope, graphJson, centra
93
98
  if (scope) graph = filterGraphByScope(graph, scope);
94
99
  graph.graphBuildMode = mode;
95
100
  graph.graphBuildScope = scope || "";
101
+ const requestedPrecision = precision === "off" ? "off" : "lsp";
102
+ const precisionModeChanged = graph.graphPrecisionMode !== requestedPrecision;
103
+ graph.graphPrecisionMode = requestedPrecision;
96
104
  const probeAfter = scope ? null : repositoryFreshnessProbe(repoPath);
97
105
  const stableProbe = probeBefore && probeAfter === probeBefore ? probeAfter : null;
98
106
  const freshnessMetadataChanged = stampRepositoryFreshness(graph, stableProbe, mode);
@@ -100,20 +108,50 @@ async function buildAndWriteInProcess(repoPath, { mode, scope, graphJson, centra
100
108
  // multi-megabyte graph merely to answer that nothing changed (or manufacture a newer mtime).
101
109
  // A one-time metadata-only write is intentional for legacy graphs so the next MCP process can use
102
110
  // the persisted probe instead of repeating a full repository snapshot.
103
- if (refresh?.kind !== "none" || freshnessMetadataChanged) {
111
+ if (refresh?.kind !== "none" || freshnessMetadataChanged || precisionModeChanged) {
104
112
  mkdirSync(central, { recursive: true });
105
113
  atomicWriteFileSync(graphJson, JSON.stringify(graph), "utf8");
106
114
  }
115
+ mkdirSync(central, { recursive: true });
116
+ let precisionOverlay = await buildLspPrecisionOverlay({
117
+ repoRoot: repoPath,
118
+ graph,
119
+ graphPath: graphJson,
120
+ mode: requestedPrecision,
121
+ });
122
+ if (requestedPrecision === "lsp") {
123
+ try {
124
+ // LSP deliberately runs after graph serialization and can outlive the initial snapshot. A
125
+ // complete second content snapshot catches source/config add-delete races (including Git
126
+ // assume-unchanged paths) that a status-only token cannot prove away.
127
+ if (snapshotRepository(repoPath).revision !== graph.graphRevision) {
128
+ precisionOverlay = invalidatePrecisionOverlay(graphJson, graph);
129
+ }
130
+ } catch {
131
+ precisionOverlay = invalidatePrecisionOverlay(
132
+ graphJson,
133
+ graph,
134
+ "repository freshness could not be verified after semantic precision",
135
+ );
136
+ }
137
+ }
107
138
  return {
108
139
  nodes: graph.nodes.length,
109
140
  links: graph.links.length,
110
141
  communities: summarizeCommunities(graphJson),
111
142
  hotspots: summarizeHotspots(graphJson),
112
143
  refresh,
144
+ precision: precisionSummary(precisionOverlay),
113
145
  };
114
146
  }
115
147
 
116
- export async function buildGraphForRepo(repoPath, { mode = "full", scope = "", outDir, graphHome } = {}) {
148
+ export async function buildGraphForRepo(repoPath, {
149
+ mode = "full",
150
+ scope = "",
151
+ precision = defaultPrecisionMode(),
152
+ outDir,
153
+ graphHome,
154
+ } = {}) {
117
155
  if (!existsSync(repoPath)) return { ok: false, error: "Repo path not found", builder: "internal" };
118
156
  const registryHome = graphHome || graphHomeDir();
119
157
  const canonicalDir = graphHome ? join(registryHome, graphStorageKey(repoPath)) : graphOutDirForRepo(repoPath);
@@ -126,11 +164,11 @@ export async function buildGraphForRepo(repoPath, { mode = "full", scope = "", o
126
164
  // between file chunks, so the main-thread parse no longer freezes the window — the reason the worker was
127
165
  // introduced. buildGraphInWorker/USE_BUILD_WORKER are kept for a future Electron-safe re-enable.
128
166
  const build = () => USE_BUILD_WORKER
129
- ? buildGraphInWorker({ repoPath, mode, scope, graphJson, central }).catch((e) => {
130
- if (e && e.workerStartFailed) return buildAndWriteInProcess(repoPath, { mode, scope, graphJson, central });
167
+ ? buildGraphInWorker({ repoPath, mode, scope, precision, graphJson, central }).catch((e) => {
168
+ if (e && e.workerStartFailed) return buildAndWriteInProcess(repoPath, { mode, scope, precision, graphJson, central });
131
169
  throw e;
132
170
  })
133
- : buildAndWriteInProcess(repoPath, { mode, scope, graphJson, central });
171
+ : buildAndWriteInProcess(repoPath, { mode, scope, precision, graphJson, central });
134
172
  // Canonical graphs are shared by all local MCP clients. Serialize the complete read/refresh/write
135
173
  // transaction so an older process cannot overwrite a newer incremental result.
136
174
  const canonical = !scope && resolve(central) === resolve(canonicalDir);
@@ -154,7 +192,8 @@ export async function buildGraphForRepo(repoPath, { mode = "full", scope = "", o
154
192
  communities: built.communities,
155
193
  hotspots: built.hotspots,
156
194
  refresh: built.refresh,
157
- log: `built-in builder: ${built.nodes} nodes, ${built.links} links (${built.refresh?.kind || "full"}: ${built.refresh?.reason || "build"})`
195
+ precision: built.precision,
196
+ log: `built-in builder: ${built.nodes} nodes, ${built.links} static links (${built.refresh?.kind || "full"}: ${built.refresh?.reason || "build"}); semantic precision ${built.precision?.state || "UNAVAILABLE"}, ${built.precision?.verifiedEdges || 0} EXACT_LSP edge(s)`
158
197
  };
159
198
  } catch (error) {
160
199
  return { ok: false, error: `graph build failed: ${error.message}`, builder: "internal" };
@@ -8,20 +8,40 @@ import { parentPort, workerData } from "node:worker_threads";
8
8
  import { buildInternalGraph } from "./internal-builder.js";
9
9
  import { filterGraphForMode, filterGraphByScope, summarizeCommunities, summarizeHotspots } from "./layout.js";
10
10
  import { atomicWriteFileSync } from "./file-lock.js";
11
+ import { snapshotRepository } from "./incremental-refresh.js";
12
+ import { buildLspPrecisionOverlay, invalidatePrecisionOverlay, precisionSummary } from "../precision/lsp-overlay.js";
11
13
 
12
14
  (async () => {
13
- const { repoPath, mode, scope, graphJson, central } = workerData || {};
15
+ const { repoPath, mode, scope, precision, graphJson, central } = workerData || {};
14
16
  try {
15
17
  let graph = await buildInternalGraph(repoPath);
16
18
  if (mode === "no-tests" || mode === "tests-only") graph = filterGraphForMode(graph, mode, { repoRoot: repoPath });
17
19
  if (scope) graph = filterGraphByScope(graph, scope);
20
+ graph.graphBuildMode = mode;
21
+ graph.graphBuildScope = scope || "";
22
+ graph.graphPrecisionMode = precision === "off" ? "off" : "lsp";
18
23
  atomicWriteFileSync(graphJson, JSON.stringify(graph), "utf8");
24
+ let overlay = await buildLspPrecisionOverlay({repoRoot: repoPath, graph, graphPath: graphJson, mode: graph.graphPrecisionMode});
25
+ if (graph.graphPrecisionMode === "lsp") {
26
+ try {
27
+ if (snapshotRepository(repoPath).revision !== graph.graphRevision) {
28
+ overlay = invalidatePrecisionOverlay(graphJson, graph);
29
+ }
30
+ } catch {
31
+ overlay = invalidatePrecisionOverlay(
32
+ graphJson,
33
+ graph,
34
+ "repository freshness could not be verified after semantic precision",
35
+ );
36
+ }
37
+ }
19
38
  parentPort.postMessage({
20
39
  ok: true,
21
40
  nodes: graph.nodes.length,
22
41
  links: graph.links.length,
23
42
  communities: summarizeCommunities(graphJson),
24
43
  hotspots: summarizeHotspots(graphJson),
44
+ precision: precisionSummary(overlay),
25
45
  });
26
46
  } catch (e) {
27
47
  parentPort.postMessage({ ok: false, error: (e && e.message) || String(e) });
@@ -83,6 +83,7 @@ export default {
83
83
  const id = nodeIds.has(base) ? `${base}:c${nameNode.startPosition.column + 1}` : base;
84
84
  addSym(nameNode.text, lineOf(nameNode), callable, {
85
85
  ...extra,
86
+ selectionNode: nameNode,
86
87
  ...(id === base ? {} : { idSuffix: id.slice(base.length) }),
87
88
  });
88
89
  return nodeIds.has(id) ? id : null;
@@ -93,6 +93,7 @@ export default {
93
93
  const exportStatement = !isMethod && exportStatementOf(cap.node);
94
94
  addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name !== "class", {
95
95
  sourceNode: cap.node.parent,
96
+ selectionNode: cap.node,
96
97
  ...(exportStatement ? { exported: true } : {}),
97
98
  ...(isMethod ? methodMetadata(cap.node) : { symbolKind: cap.name === "class" ? "class" : "function", moduleDeclaration: isModuleDeclaration(cap.node) })
98
99
  });
@@ -111,6 +112,7 @@ export default {
111
112
  const exported = isExportedDecl(cap.node);
112
113
  addSym(nameNode.text, nameNode.startPosition.row + 1, !!(val && CALLABLE.test(val.type)), {
113
114
  sourceNode: val || cap.node,
115
+ selectionNode: nameNode,
114
116
  ...(exported ? { exported: true } : {}),
115
117
  symbolKind: "variable",
116
118
  moduleDeclaration: true
@@ -9,7 +9,7 @@ import {childProcessEnv} from '../child-env.js'
9
9
  import {createRepoBoundary} from '../repo-path.js'
10
10
 
11
11
  export const REPOSITORY_FRESHNESS_PROBE_V = 1
12
- export const GRAPH_BUILDER_SCHEMA_V = 2
12
+ export const GRAPH_BUILDER_SCHEMA_V = 3
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' }
@@ -25,7 +25,7 @@ const CURRENT_GRAPH_SCHEMA = Object.freeze({
25
25
  complexityV: 1,
26
26
  repoBoundaryV: 1,
27
27
  barrelResolutionV: 1,
28
- extractorSchemaV: 1,
28
+ extractorSchemaV: 3,
29
29
  })
30
30
  const CONTROL_FILES = ['.gitignore', '.weavatrixignore', '.weavatrix.json']
31
31
  const MAX_CONTROL_BYTES = 1_000_000
@@ -183,7 +183,7 @@ export async function refreshGraphIncrementally(repoDir, existingGraph, {
183
183
 
184
184
  if (!existingGraph || !existingGraph.fileHashes || !existingGraph.fileExportSignatures
185
185
  || !existingGraph.controlHashes || !existingGraph.jsExportRecords || Number(existingGraph.barrelResolutionV) < 1
186
- || Number(existingGraph.extractorSchemaV) < 1 || Number(existingGraph.edgeProvenanceV) < 1) {
186
+ || Number(existingGraph.extractorSchemaV) < 3 || Number(existingGraph.edgeProvenanceV) < 1) {
187
187
  return full("incremental-baseline-unavailable");
188
188
  }
189
189
  if (!sameRecord(existingGraph.controlHashes, snapshot.controlHashes)) return full("ignore-or-control-config-changed");
@@ -89,6 +89,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
89
89
  const suffix = /^:[A-Za-z0-9_-]+$/.test(extra?.idSuffix || "") ? extra.idSuffix : "";
90
90
  const id = `${fileRel}#${name}@${line}${suffix}`; if (nodeIds.has(id)) return;
91
91
  const sourceNode = extra && extra.sourceNode;
92
+ const selectionNode = extra && extra.selectionNode;
92
93
  const endLine = sourceNode?.endPosition ? sourceNode.endPosition.row + 1 : 0;
93
94
  let complexity = null;
94
95
  if (callable && sourceNode) {
@@ -98,10 +99,38 @@ export async function buildInternalGraph(repoDir, opts = {}) {
98
99
  addNode({
99
100
  id,
100
101
  label: callable ? `${name}()` : name,
102
+ ...(callable ? { callable: true } : {}),
101
103
  file_type: "code",
102
104
  source_file: fileRel,
103
105
  source_location: `L${line}`,
104
106
  ...(endLine >= line ? { source_end: `L${endLine}` } : {}),
107
+ ...(sourceNode?.startPosition && sourceNode?.endPosition ? {
108
+ // web-tree-sitter Point columns are zero-based UTF-16 code-unit offsets. Keep the
109
+ // declaration body range as well as the identifier selection so LSP reference
110
+ // locations on a boundary line cannot be attributed to the wrong symbol.
111
+ source_range: {
112
+ start: {
113
+ line: sourceNode.startPosition.row,
114
+ character: sourceNode.startPosition.column,
115
+ },
116
+ end: {
117
+ line: sourceNode.endPosition.row,
118
+ character: sourceNode.endPosition.column,
119
+ },
120
+ },
121
+ } : {}),
122
+ ...(selectionNode?.startPosition && selectionNode?.endPosition ? {
123
+ // web-tree-sitter's JavaScript Point columns are already zero-based UTF-16 code-unit
124
+ // offsets, matching LSP positions exactly (including text before non-ASCII identifiers).
125
+ selection_start: {
126
+ line: selectionNode.startPosition.row,
127
+ character: selectionNode.startPosition.column,
128
+ },
129
+ selection_end: {
130
+ line: selectionNode.endPosition.row,
131
+ character: selectionNode.endPosition.column,
132
+ },
133
+ } : {}),
105
134
  ...(complexity ? { complexity } : {}),
106
135
  ...(extra && extra.exported ? { exported: true } : {}),
107
136
  ...(extra && extra.decorated ? { decorated: true } : {}),
@@ -217,7 +246,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
217
246
  for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
218
247
  const caller = enclosing(fileRel, cap.node.startPosition.row + 1); if (!caller) continue;
219
248
  const target = resolveCall(cap.node.text, fileRel); if (!target || target === caller.id) continue;
220
- links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
249
+ links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
221
250
  }
222
251
  // qualified/selector calls (Go): `pkg.Func()` → the imported package's dir; else `receiver.Method()` → the
223
252
  // SAME package (heuristic by method name — connects lifecycle methods like peer.Enable() that need type info).
@@ -229,7 +258,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
229
258
  const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
230
259
  const dm = goDirSymbols.get(imp && imp.targetDir ? imp.targetDir : dir);
231
260
  const target = dm && dm.get(fld.text);
232
- if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
261
+ if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
233
262
  }
234
263
  for (const heritageSpec of lang.heritage || []) {
235
264
  const query = typeof heritageSpec === "string" ? heritageSpec : heritageSpec.query;
@@ -253,7 +282,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
253
282
  if (origin.status !== "resolved") continue;
254
283
  const target = symByFileName.get(origin.origin.file)?.get(origin.origin.name);
255
284
  const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
256
- if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
285
+ if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
257
286
  }
258
287
  for (const cap of caps(grammar, `[
259
288
  (jsx_opening_element name: (_) @jsx)
@@ -335,7 +364,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
335
364
  complexityV: 1,
336
365
  repoBoundaryV: 1,
337
366
  barrelResolutionV: 1,
338
- extractorSchemaV: 1,
367
+ extractorSchemaV: 3,
339
368
  jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
340
369
  fileHashes: snapshot.fileHashes,
341
370
  fileExportSignatures: snapshot.fileExportSignatures,