weavatrix 0.2.15 → 0.2.18

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 (66) hide show
  1. package/README.md +99 -13
  2. package/docs/adr/0001-v0.3-offline-online-split.md +58 -0
  3. package/docs/releases/v0.2.18.md +41 -0
  4. package/package.json +7 -2
  5. package/src/analysis/cargo-dependency-evidence.js +105 -0
  6. package/src/analysis/cargo-manifests.js +74 -0
  7. package/src/analysis/dep-check-ecosystems.js +3 -0
  8. package/src/analysis/dependency/scoped-dependencies.js +13 -7
  9. package/src/analysis/duplicates.tokenize.js +13 -6
  10. package/src/analysis/endpoints-java.js +2 -5
  11. package/src/analysis/findings.js +12 -0
  12. package/src/analysis/git-history/options.js +2 -4
  13. package/src/analysis/go-dependency-evidence.js +62 -0
  14. package/src/analysis/health-capabilities.js +1 -1
  15. package/src/analysis/hot-path-review.js +2 -4
  16. package/src/analysis/http-contracts/analysis.js +2 -0
  17. package/src/analysis/http-contracts/client-call-detection.js +1 -0
  18. package/src/analysis/http-contracts/client-call-parser.js +18 -4
  19. package/src/analysis/http-contracts/shared.js +3 -4
  20. package/src/analysis/internal-audit/dependency-health.js +15 -13
  21. package/src/analysis/internal-audit/python-manifests.js +18 -5
  22. package/src/analysis/internal-audit/repo-files.js +5 -6
  23. package/src/analysis/internal-audit/supply-chain.js +2 -0
  24. package/src/analysis/internal-audit.reach.js +14 -0
  25. package/src/analysis/internal-audit.run.js +15 -10
  26. package/src/analysis/jvm-dependency-evidence.js +102 -56
  27. package/src/analysis/jvm-manifests.js +114 -0
  28. package/src/analysis/manifests.js +0 -49
  29. package/src/analysis/source-correctness.js +2 -5
  30. package/src/analysis/structure/dependency-graph.js +3 -8
  31. package/src/analysis/transport-contracts.js +157 -0
  32. package/src/graph/builder/lang-go.js +4 -2
  33. package/src/graph/builder/lang-java.js +12 -3
  34. package/src/graph/builder/lang-rust.js +22 -3
  35. package/src/graph/freshness-probe.js +1 -1
  36. package/src/graph/internal-builder.build.js +2 -2
  37. package/src/graph/internal-builder.langs.js +19 -0
  38. package/src/graph/internal-builder.resolvers.js +18 -8
  39. package/src/graph/node-id.js +7 -0
  40. package/src/mcp/actions/advisories.mjs +2 -3
  41. package/src/mcp/architecture-bootstrap.mjs +3 -3
  42. package/src/mcp/catalog.mjs +7 -4
  43. package/src/mcp/company-contract-verdict.mjs +42 -0
  44. package/src/mcp/evidence-snapshot.common.mjs +1 -3
  45. package/src/mcp/evidence-snapshot.health.mjs +11 -4
  46. package/src/mcp/evidence-snapshot.mjs +1 -1
  47. package/src/mcp/graph-diff.mjs +2 -1
  48. package/src/mcp/tools-architecture.mjs +2 -6
  49. package/src/mcp/tools-company.mjs +28 -53
  50. package/src/mcp/tools-impact-change.mjs +3 -8
  51. package/src/mcp/tools-impact-precision.mjs +89 -0
  52. package/src/mcp/tools-impact.mjs +52 -94
  53. package/src/precision/lsp-overlay/build.js +10 -0
  54. package/src/precision/lsp-overlay/contract.js +1 -1
  55. package/src/precision/symbol-query.js +39 -0
  56. package/src/precision/typescript-provider/client.js +16 -3
  57. package/src/precision/typescript-provider/discovery.js +1 -1
  58. package/src/precision/typescript-provider/isolated-runtime.js +39 -0
  59. package/src/precision/typescript-provider/project-host.js +11 -6
  60. package/src/precision/typescript-provider/project-safety.js +5 -0
  61. package/src/security/advisory-store.js +2 -2
  62. package/src/security/installed-jvm-rust.js +46 -0
  63. package/src/security/installed.js +21 -1
  64. package/src/security/typosquat.js +15 -6
  65. package/src/util.js +13 -0
  66. package/docs/releases/v0.2.15.md +0 -37
package/README.md CHANGED
@@ -14,6 +14,26 @@ the defaults, no repository data leaves your machine.**
14
14
  - Source: [github.com/sergii-ziborov/weavatrix](https://github.com/sergii-ziborov/weavatrix)
15
15
  - npm: [`weavatrix`](https://www.npmjs.com/package/weavatrix) — `npx -y weavatrix <repoRoot>`
16
16
 
17
+ ## 0.3 boundary: offline stays MIT
18
+
19
+ The next major boundary makes this package the complete offline engine: graph,
20
+ local semantic precision, Health, dependencies, duplicates, impact, history and
21
+ architecture safeguards remain here under the existing MIT license. The MIT
22
+ license is not changing.
23
+
24
+ Every outbound HTTP capability moves to a separately versioned public package,
25
+ `weavatrix-online`, beginning at 0.1.0. That connector targets either the
26
+ managed Weavatrix Cloud or a commercially licensed self-hosted Weavatrix
27
+ Enterprise deployment through one source-free wire contract. It is a thin
28
+ extension of this package, not a fork of the parser/graph/Health engine, so core
29
+ updates arrive as normal dependency updates.
30
+
31
+ Version 0.2.18 still contains the explicit opt-in compatibility profiles
32
+ documented below. The paired 0.3/0.1 release removes their network
33
+ implementations from this package; until then the new connector is not
34
+ publishable. The decision and release gates are in
35
+ [docs/adr/0001-v0.3-offline-online-split.md](docs/adr/0001-v0.3-offline-online-split.md).
36
+
17
37
  ## One graph, many views
18
38
 
19
39
  The 38 MCP methods are not one linear workflow. They project the same reusable graph into the
@@ -117,10 +137,20 @@ Every dead-code, orphan, dependency and duplicate item remains review evidence.
117
137
  conventions and source use before editing; Weavatrix does not auto-delete or merge findings.
118
138
 
119
139
  `run_audit` also returns an explicit capability matrix. `STRUCTURE CHECKED` and a supported package
120
- ecosystem do not imply `RUNTIME_CORRECTNESS` or `CONCURRENCY` is complete. Maven/Gradle manifests are
121
- reported `NOT_SUPPORTED`/`PARTIAL` for import-to-artifact verification rather than as a false clean
122
- `0 declared / 0 external`; bounded Go/Java correctness checks disclose that they are neither compiler
123
- proof nor a race detector.
140
+ ecosystem do not imply `RUNTIME_CORRECTNESS` or `CONCURRENCY` is complete. npm, nested Python and Go
141
+ modules, Maven properties, common Gradle declarations/version catalogs, and Cargo workspace/renamed
142
+ packages are compared with indexed imports. JVM reflection/generated/runtime-only use, Cargo
143
+ features/proc macros and unresolved build logic remain review evidence; bounded Go/Java checks are
144
+ neither compiler proof nor a race detector.
145
+
146
+ ### Where this saves agent context — and where it does not
147
+
148
+ The largest saving comes from graph operations that replace repeated discovery: one
149
+ `change_impact`, `get_dependents`, `context_bundle`, contract trace or dependency audit can collapse
150
+ many search/read hops into a bounded evidence set. `read_source` is mainly a convenient exact window
151
+ and is not inherently cheaper than a client's native offset read. Database queries, runtime state,
152
+ disk forensics and background workflow investigation remain outside a static repository graph.
153
+ Typecheck, tests and runtime checks remain the release authority.
124
154
 
125
155
  ### Trace an event, queue, worker, cron or CLI flow
126
156
 
@@ -259,8 +289,9 @@ and builds a missing graph automatically. A normal
259
289
  probes without building and refuses a legacy graph. Retargeting is offline but intentionally changes
260
290
  the filesystem boundary for subsequent tools; select `pinned` when that boundary must not move.
261
291
 
262
- An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — install as
263
- `~/.claude/skills/weavatrix/SKILL.md`.
292
+ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — install it as
293
+ `~/.claude/skills/weavatrix/SKILL.md` for Claude Code or
294
+ `~/.codex/skills/weavatrix/SKILL.md` for Codex.
264
295
 
265
296
  ## Tools
266
297
 
@@ -281,6 +312,13 @@ disabled and only static evidence is active. Java and Rust language-server provi
281
312
  their edges never become `EXACT_LSP`, even when a mixed repository reports a complete
282
313
  TypeScript/JavaScript overlay.
283
314
 
315
+ Configured TypeScript plugins (including Next.js) are recorded but suppressed; repository-local
316
+ plugin code is never loaded. The broad overlay is deliberately budgeted, so `PARTIAL` can mean the
317
+ candidate cap was reached. `get_dependents` then spends a bounded point query on the requested JS/TS
318
+ symbol and replaces direct heuristic references only when exact absence/presence is proven.
319
+ `change_impact` batches the same exact query for changed symbols. Further transitive hops remain
320
+ graph-backed and are labelled as such.
321
+
284
322
  The bounded JS/TS provider is enabled by default for new graphs. Set `WEAVATRIX_PRECISION=off`
285
323
  before starting the MCP server for parser-only operation from the first build, or pass
286
324
  `precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
@@ -302,8 +340,8 @@ functions, methods, and symbols, with evidence tiers, completed/remaining verifi
302
340
  public/framework/dynamic caveats),
303
341
  `run_audit` (an explicit capability matrix plus unused files/exports/dependencies with per-finding manifest/indexed-source/script/config verification;
304
342
  `category:dependencies` isolates missing/unused/duplicate declarations, unresolved imports and lockfile drift,
305
- missing npm/Go/Python deps, honest Maven/Gradle `NOT_SUPPORTED`/`PARTIAL` import verification, bounded
306
- Go/Java correctness candidates, runtime
343
+ missing npm/Go/Python/Maven/Gradle/Cargo deps, bounded import-to-artifact/crate verification, concrete
344
+ npm/PyPI/Go/Maven/crates.io advisory pins, Go/Java correctness candidates, runtime
307
345
  cycles, type-only/compile-only coupling, orphans, boundary rules, offline OSV vulnerabilities + typosquat +
308
346
  lockfile drift; accepts an immutable `base_ref`, `changed_files`, and `debt: new|existing|all` for
309
347
  review-scoped results; production paths are the default and `include_classified:true` opts into
@@ -392,6 +430,47 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
392
430
  while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
393
431
  `STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
394
432
 
433
+ ### 0.2.18 repository-root and self-audit trust patch
434
+
435
+ - A directory nested under an ignored parent Git repository is no longer mistaken for an empty
436
+ repository. The internal builder now falls back to its boundary-safe walker for that ambiguous
437
+ case while preserving Git ignore semantics at a real repository root.
438
+ - This is the first npm release containing the 0.2.17 self-audit work: Drizzle reachability,
439
+ scoped typosquat precision, regex-aware clone anchors, dead parser removal and shared owners.
440
+ - The 0.3 boundary remains unchanged: this offline package stays MIT, and the separately licensed
441
+ online connector owns outbound HTTP after the major split.
442
+
443
+ Full patch notes: [docs/releases/v0.2.18.md](docs/releases/v0.2.18.md).
444
+
445
+ ### 0.2.17 self-audit trust patch (tagged, not published to npm)
446
+
447
+ - Health no longer treats configured Drizzle schema modules as orphaned/test-only production code,
448
+ and scoped typosquat checks no longer compare legitimate scoped packages with unrelated unscoped
449
+ names.
450
+ - Duplicate detection retains equality anchors for executable regular-expression bodies, preventing
451
+ unrelated validation/normalization pipelines from becoming perfect renamed clones.
452
+ - Dogfooding removed obsolete test-only parsers and consolidated dependency-scope, graph-ID,
453
+ architecture-contract, bounded-option and safe repository-read helpers.
454
+ - The accepted 0.3 product boundary keeps the offline engine MIT and moves all outbound HTTP tools
455
+ into the separately licensed `weavatrix-online` connector.
456
+
457
+ Full patch notes: [docs/releases/v0.2.17.md](docs/releases/v0.2.17.md).
458
+
459
+ ### 0.2.16 exact dependents, multi-ecosystem dependencies and transport contracts
460
+
461
+ - Safe TypeScript project plugins no longer disable semantic precision. Plugin loading remains
462
+ suppressed, while `get_dependents` and `change_impact` can obtain exact direct JS/TS references on
463
+ demand and identify graph-backed transitive hops separately.
464
+ - Dependency evidence covers nested npm/Python/Go scopes, Maven properties, Gradle catalogs/locks,
465
+ and Cargo workspace inheritance/renames/locks. Explicit OSV refresh accepts npm, PyPI, Go, Maven
466
+ and crates.io pins.
467
+ - Cross-repository tracing adds static GraphQL, gRPC and event-topic joins alongside HTTP and keeps
468
+ runtime configuration, reflection and other dynamic targets explicitly `UNKNOWN`.
469
+ - npm/MCPB metadata now calls out Codex/OpenAI Codex workflows, and Hosted distinguishes aggregate
470
+ folder-boundary feedback from proven file-level runtime cycles.
471
+
472
+ Full patch notes: [docs/releases/v0.2.16.md](docs/releases/v0.2.16.md).
473
+
395
474
  ### 0.2.15 self-audit precision patch
396
475
 
397
476
  - Dependency review now scopes nested manifests correctly, recognizes framework peer packages and
@@ -658,13 +737,19 @@ Java graphs:
658
737
  agent review queue. Public/exported methods, framework entries, decorators, dynamic loading and
659
738
  reflection lower confidence; the result always says `REVIEW_REQUIRED` and `autoDelete:false`.
660
739
 
661
- ### HTTP clients and wrappers
740
+ ### Cross-repository transports
662
741
 
663
742
  `trace_api_contract` recognizes built-in object clients such as `axios.get(...)`, explicit bare or
664
743
  object/member wrappers, and simple auto-discovered functions that forward a URL parameter directly
665
744
  to a known HTTP client. Auto-discovered wrappers are restricted to their bounded reverse-import
666
745
  scope; ambiguous same-name definitions are skipped and reported as incomplete evidence.
667
746
 
747
+ The same tool can select GraphQL, gRPC or event evidence. It joins static GraphQL schema fields and
748
+ operations, proto service methods and typed stub calls, and static Kafka/event-bus topic producers
749
+ and consumers across registered repositories. Computed URLs, runtime configuration, dynamic
750
+ GraphQL documents/topics, reflection and dynamic stubs remain `UNKNOWN`; they are not converted into
751
+ an absence claim.
752
+
668
753
  Persistent per-client-repository configuration lives in `.weavatrix.json`:
669
754
 
670
755
  ```json
@@ -826,16 +911,17 @@ loader) behind the thin stdio entry `src/mcp-server.mjs`.
826
911
  cross-repository wrapper/liveness fixture, framework/convention fixture, full MCP lifecycle gate
827
912
  and a portable real-repository runner. Six source-free 0.2.1 real-repository baselines are
828
913
  recorded; edge provenance is gated end-to-end and the strict six-repository release command passes.
829
- - **Wrapper-aware API contracts** shipped in 0.2.2: persistent/ad-hoc configuration,
830
- conservative discovery, cross-repository handler liveness and explicit unknown states. The next
831
- hosted increment joins privacy-safe contract identities across separately synced services.
914
+ - **Cross-transport contracts** cover bounded HTTP wrappers plus static GraphQL, gRPC and event-bus
915
+ identities across registered local repositories, with explicit runtime/dynamic unknown states.
832
916
  - **Hosted architecture workbench** at
833
917
  [app.weavatrix.com](https://app.weavatrix.com) is an access-controlled preview for
834
918
  owner-authenticated source-free evidence and revision history. Its UI and backend evolve
835
919
  independently from the public MCP release; local use remains fully optional.
836
920
  - **Semantic precision bridge** shipped for TypeScript/JavaScript in 0.2.4: a bounded, revision-bound
837
921
  local overlay validates references with the bundled language server while the parser graph remains
838
- the fallback. Java and Rust providers are not bundled yet and stay explicitly `UNAVAILABLE`.
922
+ the fallback. 0.2.16 adds safe configured-plugin suppression plus exact on-demand point and changed-
923
+ symbol batch queries. Java and Rust language-server providers are not bundled and stay explicitly
924
+ unavailable as semantic providers; their parser/dependency evidence is still active.
839
925
  - **Git-native architecture history** — bounded tag/ref timelines and branch
840
926
  reports built outside the worktree; graph artifacts stay out of Git.
841
927
  - **Cross-repository company evidence** — endpoints, events and internal
@@ -0,0 +1,58 @@
1
+ # ADR 0001: Weavatrix 0.3 offline/online split
2
+
3
+ Status: accepted direction; extraction in progress
4
+
5
+ Date: 2026-07-19
6
+
7
+ ## Decision
8
+
9
+ The public `weavatrix` package remains MIT licensed and becomes a strictly
10
+ offline MCP in 0.3. Its analyzers, graph, local LSP provider, cache, repository
11
+ registry, Health, dependency, duplicate, impact, history and architecture tools
12
+ remain available without a network capability.
13
+
14
+ All outbound HTTP tools move to the separate public-source `weavatrix-online`
15
+ package, starting at 0.1.0. Its permission-required source license must be
16
+ approved before publication; the scaffold remains unpublished and
17
+ `UNLICENSED`. It extends the MIT engine rather than copying or relicensing it.
18
+
19
+ The managed Cloud and licensed self-hosted Enterprise products are private
20
+ server implementations of the same source-free wire contract. Neither is
21
+ bundled into the offline package.
22
+
23
+ ## Tool ownership after extraction
24
+
25
+ Offline owns local advisory matching but not remote refresh. Online owns:
26
+
27
+ - `refresh_advisories`;
28
+ - `preview_sync`;
29
+ - `sync_graph`;
30
+ - `pull_architecture_contract`;
31
+ - endpoint capability negotiation and authentication.
32
+
33
+ The exact final catalog is locked by tests before release. There is no legacy
34
+ `online`, `hosted` or `full` alias in the offline artifact that can re-enable an
35
+ HTTP implementation.
36
+
37
+ ## Update model
38
+
39
+ `weavatrix-online` depends on a compatible `weavatrix` release and uses a
40
+ supported tool-extension API. Parser, graph schema, LSP and Health fixes are
41
+ therefore updated once in the MIT core. The online repository contains only
42
+ transport, endpoint/authentication and consent-flow code.
43
+
44
+ ## Release gates
45
+
46
+ The two public packages are released as a compatible pair only after:
47
+
48
+ 1. offline npm, MCPB, skill, server metadata and docs expose no HTTP tools or
49
+ hosted URL/token inputs;
50
+ 2. network action modules and fetch paths are absent from the offline package;
51
+ 3. the online connector passes the same local graph/Health regression suite
52
+ through its core dependency;
53
+ 4. Cloud and Enterprise pass one wire-contract suite;
54
+ 5. license metadata clearly distinguishes MIT core from permission-required,
55
+ source-available connector code;
56
+ 6. stale 0.2 processes fail clearly rather than advertising mixed catalogs.
57
+
58
+ Existing MIT releases are unchanged and remain usable under MIT.
@@ -0,0 +1,41 @@
1
+ # Weavatrix 0.2.18
2
+
3
+ 0.2.18 is the publishable repository-root and self-audit trust patch. The `v0.2.17` source tag was
4
+ created, but its release validation exposed the nested-root discovery regression before an npm
5
+ package was published. The tag remains immutable; 0.2.18 contains that release plus the fix.
6
+
7
+ ## Repository discovery
8
+
9
+ - The internal builder distinguishes an actual Git root from a directory that merely lives below
10
+ another Git repository.
11
+ - When the selected nested directory is ignored by its parent and Git returns an ambiguous empty
12
+ file universe, Weavatrix uses its boundary-safe filesystem walker instead of building a zero-node
13
+ graph.
14
+ - A real Git root with an empty tracked/unignored universe stays empty, so ignored build output and
15
+ secrets are not reintroduced.
16
+ - The live stdio hot-reload regression now exercises the corrected path and passes independently,
17
+ matching the Linux release runner rather than depending on full-suite ordering.
18
+
19
+ ## Included 0.2.17 trust work
20
+
21
+ - Drizzle configuration roots its schema modules in production reachability.
22
+ - Scoped typosquat review compares only compatible scoped targets.
23
+ - Executable regular expressions retain equality anchors for clone fingerprints without exposing
24
+ their source text.
25
+ - Obsolete test-only dependency parsers were removed and repeated dependency, graph and architecture
26
+ helpers were consolidated under shared owners.
27
+ - Hosted evidence applies the same production-first classification and recomputes bounded summaries
28
+ after filtering.
29
+
30
+ ## Product boundary
31
+
32
+ - `weavatrix` remains MIT and offline-first. No license change is made here.
33
+ - The accepted 0.3 split moves outbound HTTP into the separately versioned `weavatrix-online`
34
+ connector, whose permission-required license remains a release gate.
35
+
36
+ ## Verification
37
+
38
+ Release validation covers the full Node test suite on supported Node versions, independent hot
39
+ reload execution, release-manifest checks, npm/MCPB packing and runtime smoke tests. The paired
40
+ Hosted deployment is rebuilt from this graph, then verified through its live Health and History
41
+ views.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.2.15",
3
+ "version": "0.2.18",
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>",
@@ -26,7 +26,8 @@
26
26
  "skill",
27
27
  "scripts/run-agent-task-benchmark.mjs",
28
28
  "docs/agent-task-benchmark.md",
29
- "docs/releases/v0.2.15.md",
29
+ "docs/adr/0001-v0.3-offline-online-split.md",
30
+ "docs/releases/v0.2.18.md",
30
31
  "README.md",
31
32
  "SECURITY.md",
32
33
  "LICENSE"
@@ -39,6 +40,7 @@
39
40
  "benchmark:real:release": "node scripts/run-real-benchmark.mjs --require-all",
40
41
  "benchmark:agent": "node scripts/run-agent-task-benchmark.mjs",
41
42
  "benchmark:agent:release": "node scripts/run-agent-task-benchmark.mjs --require-independent",
43
+ "sync:private": "node scripts/sync-private.mjs",
42
44
  "build:mcpb": "node scripts/build-mcpb.mjs",
43
45
  "verify:release": "node scripts/verify-release.mjs"
44
46
  },
@@ -56,6 +58,9 @@
56
58
  "dependency-graph",
57
59
  "tree-sitter",
58
60
  "claude",
61
+ "codex",
62
+ "openai-codex",
63
+ "coding-agents",
59
64
  "ai-agents"
60
65
  ],
61
66
  "dependencies": {
@@ -0,0 +1,105 @@
1
+ import { createRepoBoundary } from "../repo-path.js";
2
+ import { cargoName, parseCargoToml } from "./cargo-manifests.js";
3
+ import { dependencyVerification, makeFinding } from "./findings.js";
4
+ import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
5
+ import { dependencyScopeOwnsFile, dependencyScopeRoot } from "./dependency/scoped-dependencies.js";
6
+
7
+ export function collectCargoDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [] } = {}) {
8
+ const boundary = createRepoBoundary(repoRoot);
9
+ const manifests = files.filter((file) => /(^|\/)Cargo\.toml$/i.test(file));
10
+ const scopes = manifests.map((file) => ({ file, root: dependencyScopeRoot(file), ...parseCargoToml(readRepoText(boundary, file)) }))
11
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
12
+ const workspaceDeps = new Map();
13
+ for (const scope of scopes) for (const dependency of scope.workspaceDependencies) workspaceDeps.set(cargoName(dependency.alias), dependency);
14
+ const issues = [];
15
+ for (const scope of scopes) {
16
+ scope.dependencies = scope.dependencies.map((dependency) => {
17
+ if (!dependency.inherited) return dependency;
18
+ const inherited = workspaceDeps.get(cargoName(dependency.alias));
19
+ if (!inherited) { issues.push(`${scope.file}: workspace dependency ${dependency.alias} is unresolved`); return dependency; }
20
+ return { ...dependency, name: inherited.name, version: inherited.version, inherited: true };
21
+ });
22
+ }
23
+ const importsByScope = new Map(scopes.map((scope) => [scope, []]));
24
+ for (const entry of externalImports) {
25
+ if (entry.ecosystem !== "crates.io" || !entry.pkg || entry.builtin || entry.unresolved) continue;
26
+ const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, entry.file));
27
+ if (owner) importsByScope.get(owner).push(entry);
28
+ }
29
+ const findings = [], declared = new Set();
30
+ let mappedImports = 0, unmappedImports = 0;
31
+ if (!scopes.length) {
32
+ const missing = new Map();
33
+ for (const entry of externalImports.filter((item) => item.ecosystem === "crates.io" && item.pkg && !item.builtin && !item.unresolved)) {
34
+ if (!missing.has(cargoName(entry.pkg))) missing.set(cargoName(entry.pkg), entry);
35
+ }
36
+ for (const [name, entry] of missing) findings.push(makeFinding({
37
+ category: "unused", rule: "missing-dep", severity: "low", confidence: "high",
38
+ title: `Rust crate path without Cargo.toml: ${name}`,
39
+ reason: "An indexed external Rust crate path exists, but no Cargo.toml was discovered.",
40
+ detail: `"${entry.spec || name}" is used by ${entry.file}, but no Cargo manifest owns the file.`,
41
+ package: name, file: entry.file, line: entry.line || 0, source: "internal",
42
+ verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_PRESENT" }, indexedSourceImports: { status: "FOUND", count: 1, files: [entry.file] }, mapping: "Cargo crate name normalization" },
43
+ fixHint: "initialize or restore the owning Cargo manifest",
44
+ }));
45
+ }
46
+ for (const scope of scopes) {
47
+ const imports = importsByScope.get(scope);
48
+ const used = new Map(), missingSeen = new Set();
49
+ for (const entry of imports) {
50
+ const imported = cargoName(entry.pkg);
51
+ const dependency = scope.dependencies.find((item) => cargoName(item.alias) === imported || cargoName(item.name) === imported);
52
+ if (dependency) {
53
+ const evidence = used.get(dependency.alias) || [];
54
+ evidence.push(entry); used.set(dependency.alias, evidence); mappedImports++;
55
+ continue;
56
+ }
57
+ unmappedImports++;
58
+ if (missingSeen.has(imported)) continue;
59
+ missingSeen.add(imported);
60
+ findings.push(makeFinding({
61
+ category: "unused", rule: "missing-dep", severity: "medium", confidence: "high",
62
+ title: `Missing Cargo dependency: ${entry.pkg}`,
63
+ reason: "An indexed external Rust crate path has no matching dependency alias or package name in the nearest Cargo.toml.",
64
+ detail: `"${entry.spec || entry.pkg}" is used by ${entry.file}, but ${scope.file} does not declare crate "${entry.pkg}" (including workspace inheritance and renamed packages).`,
65
+ package: entry.pkg, file: entry.file, line: entry.line || 0, manifest: scope.file,
66
+ evidence: [{ file: entry.file, line: entry.line || 0, snippet: entry.spec || "" }], source: "internal",
67
+ verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_FOUND", file: scope.file }, indexedSourceImports: { status: "FOUND", count: 1, files: [entry.file] }, mapping: "Cargo alias/package normalization" },
68
+ fixHint: `cargo add ${entry.pkg}`,
69
+ }));
70
+ }
71
+ for (const dependency of scope.dependencies) {
72
+ const identity = cargoName(dependency.name);
73
+ if (!identity) continue;
74
+ declared.add(`${scope.root || "."}:${identity}`);
75
+ const evidence = used.get(dependency.alias) || [];
76
+ if (evidence.length || dependency.optional || dependency.build) continue;
77
+ findings.push(makeFinding({
78
+ category: "unused", rule: "unused-dep", severity: dependency.dev ? "info" : "low", confidence: "low",
79
+ title: `Unused Cargo dependency: ${dependency.name}`,
80
+ reason: "No indexed Rust use/path/extern-crate evidence maps to this dependency; feature-only, proc-macro, generated and reflective registration remain possible.",
81
+ detail: `"${dependency.name}" (${dependency.alias}) is declared in ${scope.file}, but no indexed .rs file in that Cargo scope references its crate path. Confirm with cargo check/test and feature combinations before removal.`,
82
+ package: dependency.name, version: dependency.version, manifest: scope.file, source: "internal",
83
+ verification: dependencyVerification(scope.file, [], "REVIEW_REQUIRED", "Cargo alias/package normalization"),
84
+ fixHint: `cargo remove ${dependency.alias} after checking all features and targets`,
85
+ }));
86
+ }
87
+ }
88
+ const present = manifests.length > 0;
89
+ return {
90
+ present,
91
+ status: present ? "CHECKED" : "NOT_PRESENT",
92
+ completeness: present ? (issues.length ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
93
+ manifests,
94
+ declared,
95
+ mappedImports,
96
+ unmappedImports,
97
+ findings,
98
+ reasons: issues,
99
+ reason: !present
100
+ ? "No Cargo.toml was discovered."
101
+ : issues.length
102
+ ? `Cargo declarations and indexed crate paths were checked, but ${issues.length} workspace inheritance reference(s) remain unresolved.`
103
+ : `Cargo declarations from ${manifests.length} scope(s), workspace inheritance, renamed packages and every indexed external crate path were compared.`,
104
+ };
105
+ }
@@ -0,0 +1,74 @@
1
+ const clean = (value) => String(value || "").trim().replace(/^['"]|['"]$/g, "");
2
+ export const cargoName = (value) => clean(value).toLowerCase().replace(/_/g, "-");
3
+
4
+ function inlineTable(value) {
5
+ const fields = new Map();
6
+ for (const match of String(value || "").matchAll(/([A-Za-z][\w.-]*)\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|true|false)/g)) {
7
+ fields.set(match[1], clean(match[2]));
8
+ }
9
+ return fields;
10
+ }
11
+
12
+ // Bounded Cargo.toml parser for package/workspace dependency tables. It intentionally owns
13
+ // dependency identity, aliases and concrete versions; feature activation is not treated as usage.
14
+ export function parseCargoToml(text) {
15
+ const source = String(text || "");
16
+ const dependencies = [];
17
+ const workspaceDependencies = [];
18
+ let section = "", packageName = "", workspace = false;
19
+ for (const raw of source.split(/\r?\n/)) {
20
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
21
+ if (!line) continue;
22
+ const header = /^\[([^\]]+)]$/.exec(line);
23
+ if (header) { section = header[1].trim(); workspace ||= section === "workspace"; continue; }
24
+ const kv = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
25
+ if (!kv) continue;
26
+ if (section === "package" && kv[1] === "name") { packageName = clean(kv[2]); continue; }
27
+ const dependencyTable = /^(?:target\..+\.)?(dependencies|dev-dependencies|build-dependencies)$/.exec(section);
28
+ const workspaceTable = section === "workspace.dependencies";
29
+ if (!dependencyTable && !workspaceTable) continue;
30
+ const alias = kv[1], value = kv[2].trim();
31
+ const table = value.startsWith("{") ? inlineTable(value) : new Map();
32
+ const version = value.startsWith("{") ? clean(table.get("version")) : clean(value);
33
+ const name = clean(table.get("package")) || alias;
34
+ const record = {
35
+ alias,
36
+ name,
37
+ version: /^\d/.test(version) || /^=?\s*\d/.test(version) ? version.replace(/^=\s*/, "") : "",
38
+ dev: dependencyTable?.[1] === "dev-dependencies",
39
+ build: dependencyTable?.[1] === "build-dependencies",
40
+ optional: table.get("optional") === "true",
41
+ inherited: table.get("workspace") === "true",
42
+ };
43
+ (workspaceTable ? workspaceDependencies : dependencies).push(record);
44
+ }
45
+ return { packageName, workspace, dependencies, workspaceDependencies };
46
+ }
47
+
48
+ // Cargo.lock v3/v4 [[package]] records. Registry packages are exact installed crates; workspace
49
+ // and git packages are excluded because OSV crates.io versions must not be inferred for them.
50
+ export function parseCargoLockPackages(text) {
51
+ const packages = [];
52
+ let current = null;
53
+ const flush = () => {
54
+ if (current?.name && current.version && /^registry\+https:\/\/github\.com\/rust-lang\/crates\.io-index|^registry\+https:\/\/index\.crates\.io\//.test(current.source)) {
55
+ packages.push({ ecosystem: "crates.io", name: current.name, version: current.version, dev: false, integrity: current.checksum || "", source: "cargo-lock" });
56
+ }
57
+ };
58
+ for (const raw of String(text || "").split(/\r?\n/)) {
59
+ const line = raw.trim();
60
+ if (line === "[[package]]") { flush(); current = { name: "", version: "", source: "", checksum: "" }; continue; }
61
+ if (!current) continue;
62
+ if (/^\[/.test(line)) { flush(); current = null; continue; }
63
+ const match = /^(name|version|source|checksum)\s*=\s*"([^"]+)"/.exec(line);
64
+ if (match) current[match[1]] = match[2];
65
+ }
66
+ flush();
67
+ const seen = new Set();
68
+ return packages.filter((pkg) => {
69
+ const key = `${pkg.name}@${pkg.version}`;
70
+ if (seen.has(key)) return false;
71
+ seen.add(key);
72
+ return true;
73
+ });
74
+ }
@@ -194,6 +194,9 @@ export function computePyDepFindings(options = {}) {
194
194
  findings.push(...result.findings.map((finding) => ({
195
195
  ...finding,
196
196
  ...(scope.manifests?.length ? {manifest: scope.manifests[0]} : {}),
197
+ verification: finding.rule === "missing-dep"
198
+ ? { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: scope.present ? "NOT_FOUND" : "NOT_PRESENT", files: scope.manifests || [] }, indexedSourceImports: { status: "FOUND", count: finding.evidence?.length || 1, files: (finding.evidence || []).map((item) => item.file) }, mapping: "PEP 503 plus bounded import-to-distribution aliases" }
199
+ : { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "REVIEW_REQUIRED", manifestDeclaration: { status: "FOUND", files: scope.manifests || [] }, indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] }, mapping: "PEP 503 plus bounded import-to-distribution aliases" },
197
200
  })));
198
201
  for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
199
202
  for (const name of result.managed) managed.add(name);
@@ -1,5 +1,11 @@
1
- const normalizeScope = (root) => String(root || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '')
2
- const ownsFile = (scope, file) => !scope || file === scope || String(file || '').startsWith(`${scope}/`)
1
+ import {posix} from 'node:path'
2
+
3
+ export const normalizeDependencyScope = (root) => String(root || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '')
4
+ export const dependencyScopeRoot = (manifest) => {
5
+ const root = posix.dirname(normalizeDependencyScope(manifest))
6
+ return root === '.' ? '' : root
7
+ }
8
+ export const dependencyScopeOwnsFile = (scope, file) => !scope || file === scope || String(file || '').startsWith(`${scope}/`)
3
9
 
4
10
  // Bind the ecosystem-specific core without introducing a facade/helper import cycle.
5
11
  export function createScopedDepFindings(computeDepFindings) {
@@ -8,25 +14,25 @@ export function createScopedDepFindings(computeDepFindings) {
8
14
  sourceTexts = new Map(), nonRuntimeRoots = [], sourceFiles = [],
9
15
  } = {}) {
10
16
  const scopes = packageScopes.length
11
- ? packageScopes.map((scope) => ({...scope, root: normalizeScope(scope.root)})).sort((a, b) => b.root.length - a.root.length)
17
+ ? packageScopes.map((scope) => ({...scope, root: normalizeDependencyScope(scope.root)})).sort((a, b) => b.root.length - a.root.length)
12
18
  : [{root: '', manifest: 'package.json', pkg: {}, aliases: []}]
13
19
  const importsByScope = new Map(scopes.map((scope) => [scope, []]))
14
20
  const sourceFilesByScope = new Map(scopes.map((scope) => [scope, []]))
15
21
  const sourceTextsByScope = new Map(scopes.map((scope) => [scope, new Map()]))
16
22
  for (const item of externalImports) {
17
- const owner = scopes.find((scope) => ownsFile(scope.root, item.file)) || scopes[scopes.length - 1]
23
+ const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, item.file)) || scopes[scopes.length - 1]
18
24
  importsByScope.get(owner).push(item)
19
25
  }
20
26
  for (const file of sourceFiles) {
21
- const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
27
+ const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, file)) || scopes[scopes.length - 1]
22
28
  sourceFilesByScope.get(owner).push(file)
23
29
  }
24
30
  for (const [file, text] of sourceTexts) {
25
- const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
31
+ const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, file)) || scopes[scopes.length - 1]
26
32
  sourceTextsByScope.get(owner).set(file, text)
27
33
  }
28
34
  const configOwner = new Map()
29
- for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1])
35
+ for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => dependencyScopeOwnsFile(scope.root, file)) || scopes[scopes.length - 1])
30
36
  const findings = [], usedPackages = new Map(), declared = new Set()
31
37
  for (const scope of scopes) {
32
38
  const scopeConfig = new Map([...configTexts].filter(([file]) => configOwner.get(file) === scope))
@@ -3,9 +3,16 @@
3
3
 
4
4
  const K = 8; // k-gram length (tokens)
5
5
  const W = 4; // winnowing window → guaranteed detection of matches ≥ K+W-1 tokens
6
- const STRLIT = String.fromCharCode(0); // opaque-literal sentinel for every string/regex body: a NUL
6
+ const STRLIT = String.fromCharCode(0); // opaque-literal sentinel for string bodies: a NUL
7
7
  // its own class (never an identifier "I" nor a number "N"), so a literal can
8
8
  // never masquerade as code in either mode
9
+ const REGEXLIT = String.fromCharCode(1); // regex bodies are executable behavior, so retain an equality anchor
10
+ const literalHash = (value) => {
11
+ let hash = 2166136261;
12
+ for (const char of String(value || "")) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
13
+ return (hash >>> 0).toString(16);
14
+ };
15
+ const regexMarker = (value) => ` ${REGEXLIT} 0x${literalHash(value)} `;
9
16
 
10
17
  const KEYWORDS = new Set(("if else for while do switch case break continue return function const let var new class extends async await try catch finally throw import from export default typeof instanceof in of delete void yield static get set this super null undefined true false def elif except lambda pass raise with as is not and or None True False func go defer chan map range struct interface type package nil err string int bool byte float64 public private protected final void long double boolean").split(" "));
11
18
 
@@ -16,9 +23,9 @@ const KEYWORDS = new Set(("if else for while do switch case break continue retur
16
23
  // regex and ate the rest of the line.
17
24
  const REGEX_PREV = new Set("(,=:[!&|?{};~*%^<>".split(""));
18
25
 
19
- // Replace comments with nothing and string/regex BODIES with the sentinel, so their contents never
20
- // count as code. Handles Python triple-quoted docstrings (embedded quotes no longer desync the scan)
21
- // and JS regex literals (a quote inside /['"]/ no longer opens a phantom string).
26
+ // Replace comments with nothing and string bodies with an opaque sentinel. Regex bodies retain only
27
+ // a one-way equality anchor: their source is executable behavior, and collapsing every pattern to the
28
+ // same token creates false clone groups for unrelated replace/validate pipelines.
22
29
  export function stripNonCode(text, py) {
23
30
  let out = "", i = 0, prevSig = "";
24
31
  const s = String(text || "");
@@ -34,7 +41,7 @@ export function stripNonCode(text, py) {
34
41
  if (!py && two === "//") { while (i < s.length && s[i] !== "\n") i++; continue; }
35
42
  if (!py && two === "/*") { i += 2; while (i < s.length && s.slice(i, i + 2) !== "*/") i++; i += 2; continue; }
36
43
  if (!py && ch === "/" && REGEX_PREV.has(prevSig)) {
37
- i++; let inClass = false; // '/' inside a [...] char class is literal, not the terminator
44
+ i++; const regexStart = i; let inClass = false; // '/' inside a [...] char class is literal, not the terminator
38
45
  while (i < s.length && s[i] !== "\n") {
39
46
  const c = s[i];
40
47
  if (c === "\\") { i += 2; continue; }
@@ -44,7 +51,7 @@ export function stripNonCode(text, py) {
44
51
  i++;
45
52
  }
46
53
  while (i < s.length && /[a-z]/i.test(s[i])) i++; // regex flags (gimsuy)
47
- push(` ${STRLIT} `); continue;
54
+ push(regexMarker(s.slice(regexStart, i))); continue;
48
55
  }
49
56
  if (ch === '"' || ch === "'" || ch === "`") {
50
57
  const q = ch; i++;
@@ -2,16 +2,13 @@
2
2
  // literal annotation paths: inventing values for constants would make an architecture inventory look
3
3
  // more complete than the evidence permits.
4
4
  import { maskJavaNonCode } from "./java-source.js";
5
+ import { lineNumberAt } from "../util.js";
5
6
 
6
7
  const SPRING_MAPPING = /@(?:org\.springframework\.web\.bind\.annotation\.)?(RequestMapping|GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping)\b/g;
7
8
  const SPRING_CONDITION = /@(?:org\.springframework\.boot\.autoconfigure\.condition\.)?(ConditionalOnExpression|ConditionalOnProperty)\b/g;
8
9
  const REQUEST_METHOD = /\bRequestMethod\s*\.\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b/g;
9
10
 
10
- const lineAt = (text, index) => {
11
- let line = 1;
12
- for (let i = 0; i < index && i < text.length; i++) if (text[i] === "\n") line++;
13
- return line;
14
- };
11
+ const lineAt = lineNumberAt;
15
12
 
16
13
  function skipTrivia(text, start) {
17
14
  let i = start;
@@ -6,6 +6,18 @@ import { createHash } from "node:crypto";
6
6
  export const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
7
7
  export const FINDING_CATEGORIES = ["unused", "structure", "vulnerability", "malware"];
8
8
 
9
+ export function dependencyVerification(manifest, imports, decision, mapping) {
10
+ return {
11
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
12
+ decision,
13
+ manifestDeclaration: { status: "FOUND", file: manifest },
14
+ indexedSourceImports: imports.length
15
+ ? { status: "FOUND", count: imports.length, files: [...new Set(imports.map((item) => item.file))].slice(0, 10) }
16
+ : { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] },
17
+ mapping,
18
+ };
19
+ }
20
+
9
21
  // Stable id: survives re-runs so the UI can persist expand/dismiss state per finding.
10
22
  export function makeFinding(f) {
11
23
  const cycleIdentity = Array.isArray(f.cycleMembers)