weavatrix 0.2.14 → 0.2.16

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 (65) hide show
  1. package/README.md +69 -13
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.16.md +53 -0
  4. package/package.json +6 -2
  5. package/src/analysis/cargo-dependency-evidence.js +111 -0
  6. package/src/analysis/cargo-manifests.js +74 -0
  7. package/src/analysis/dep-check-ecosystems.js +3 -0
  8. package/src/analysis/dep-check.js +8 -18
  9. package/src/analysis/dependency/conventions.js +16 -0
  10. package/src/analysis/dependency/scoped-dependencies.js +7 -2
  11. package/src/analysis/dependency/source-references.js +21 -0
  12. package/src/analysis/duplicates.tokenize.js +12 -2
  13. package/src/analysis/endpoints-java.js +2 -5
  14. package/src/analysis/findings.js +12 -0
  15. package/src/analysis/go-dependency-evidence.js +68 -0
  16. package/src/analysis/health-capabilities.js +1 -1
  17. package/src/analysis/http-contracts/analysis.js +2 -0
  18. package/src/analysis/http-contracts/client-call-detection.js +1 -0
  19. package/src/analysis/http-contracts/client-call-parser.js +18 -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/supply-chain.js +2 -0
  23. package/src/analysis/internal-audit.reach.js +20 -0
  24. package/src/analysis/internal-audit.run.js +13 -7
  25. package/src/analysis/jvm-dependency-evidence.js +102 -56
  26. package/src/analysis/jvm-manifests.js +114 -0
  27. package/src/analysis/source-correctness.js +2 -5
  28. package/src/analysis/task-retrieval.js +3 -14
  29. package/src/analysis/transport-contracts.js +157 -0
  30. package/src/graph/builder/lang-go.js +4 -2
  31. package/src/graph/builder/lang-java.js +12 -3
  32. package/src/graph/builder/lang-rust.js +22 -3
  33. package/src/graph/freshness-probe.js +1 -1
  34. package/src/graph/internal-builder.build.js +2 -2
  35. package/src/graph/internal-builder.resolvers.js +18 -8
  36. package/src/mcp/actions/advisories.mjs +2 -3
  37. package/src/mcp/catalog.mjs +7 -4
  38. package/src/mcp/company-contract-verdict.mjs +42 -0
  39. package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
  40. package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
  41. package/src/mcp/git-output.mjs +10 -0
  42. package/src/mcp/graph/context-seeds.mjs +3 -14
  43. package/src/mcp/graph/reverse-reach.mjs +42 -0
  44. package/src/mcp/sync/evidence-duplicates.mjs +1 -4
  45. package/src/mcp/tools-company.mjs +28 -53
  46. package/src/mcp/tools-impact-change.mjs +2 -38
  47. package/src/mcp/tools-impact-precision.mjs +89 -0
  48. package/src/mcp/tools-impact.mjs +53 -136
  49. package/src/path-classification.js +14 -0
  50. package/src/precision/lsp-overlay/build.js +10 -0
  51. package/src/precision/lsp-overlay/contract.js +1 -1
  52. package/src/precision/symbol-query.js +39 -0
  53. package/src/precision/typescript-provider/client.js +16 -3
  54. package/src/precision/typescript-provider/discovery.js +1 -1
  55. package/src/precision/typescript-provider/isolated-runtime.js +39 -0
  56. package/src/precision/typescript-provider/project-host.js +11 -6
  57. package/src/precision/typescript-provider/project-safety.js +5 -0
  58. package/src/security/advisory-store.js +2 -2
  59. package/src/security/installed-jvm-rust.js +46 -0
  60. package/src/security/installed.js +21 -1
  61. package/src/security/malware-heuristics.sweep.js +1 -1
  62. package/src/security/registry-sig.classify.js +2 -1
  63. package/src/security/registry-sig.rules.js +3 -3
  64. package/src/util.js +8 -0
  65. package/docs/releases/v0.2.14.md +0 -93
package/README.md CHANGED
@@ -117,10 +117,20 @@ Every dead-code, orphan, dependency and duplicate item remains review evidence.
117
117
  conventions and source use before editing; Weavatrix does not auto-delete or merge findings.
118
118
 
119
119
  `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.
120
+ ecosystem do not imply `RUNTIME_CORRECTNESS` or `CONCURRENCY` is complete. npm, nested Python and Go
121
+ modules, Maven properties, common Gradle declarations/version catalogs, and Cargo workspace/renamed
122
+ packages are compared with indexed imports. JVM reflection/generated/runtime-only use, Cargo
123
+ features/proc macros and unresolved build logic remain review evidence; bounded Go/Java checks are
124
+ neither compiler proof nor a race detector.
125
+
126
+ ### Where this saves agent context — and where it does not
127
+
128
+ The largest saving comes from graph operations that replace repeated discovery: one
129
+ `change_impact`, `get_dependents`, `context_bundle`, contract trace or dependency audit can collapse
130
+ many search/read hops into a bounded evidence set. `read_source` is mainly a convenient exact window
131
+ and is not inherently cheaper than a client's native offset read. Database queries, runtime state,
132
+ disk forensics and background workflow investigation remain outside a static repository graph.
133
+ Typecheck, tests and runtime checks remain the release authority.
124
134
 
125
135
  ### Trace an event, queue, worker, cron or CLI flow
126
136
 
@@ -259,8 +269,9 @@ and builds a missing graph automatically. A normal
259
269
  probes without building and refuses a legacy graph. Retargeting is offline but intentionally changes
260
270
  the filesystem boundary for subsequent tools; select `pinned` when that boundary must not move.
261
271
 
262
- An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — install as
263
- `~/.claude/skills/weavatrix/SKILL.md`.
272
+ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — install it as
273
+ `~/.claude/skills/weavatrix/SKILL.md` for Claude Code or
274
+ `~/.codex/skills/weavatrix/SKILL.md` for Codex.
264
275
 
265
276
  ## Tools
266
277
 
@@ -281,6 +292,13 @@ disabled and only static evidence is active. Java and Rust language-server provi
281
292
  their edges never become `EXACT_LSP`, even when a mixed repository reports a complete
282
293
  TypeScript/JavaScript overlay.
283
294
 
295
+ Configured TypeScript plugins (including Next.js) are recorded but suppressed; repository-local
296
+ plugin code is never loaded. The broad overlay is deliberately budgeted, so `PARTIAL` can mean the
297
+ candidate cap was reached. `get_dependents` then spends a bounded point query on the requested JS/TS
298
+ symbol and replaces direct heuristic references only when exact absence/presence is proven.
299
+ `change_impact` batches the same exact query for changed symbols. Further transitive hops remain
300
+ graph-backed and are labelled as such.
301
+
284
302
  The bounded JS/TS provider is enabled by default for new graphs. Set `WEAVATRIX_PRECISION=off`
285
303
  before starting the MCP server for parser-only operation from the first build, or pass
286
304
  `precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
@@ -302,8 +320,8 @@ functions, methods, and symbols, with evidence tiers, completed/remaining verifi
302
320
  public/framework/dynamic caveats),
303
321
  `run_audit` (an explicit capability matrix plus unused files/exports/dependencies with per-finding manifest/indexed-source/script/config verification;
304
322
  `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
323
+ missing npm/Go/Python/Maven/Gradle/Cargo deps, bounded import-to-artifact/crate verification, concrete
324
+ npm/PyPI/Go/Maven/crates.io advisory pins, Go/Java correctness candidates, runtime
307
325
  cycles, type-only/compile-only coupling, orphans, boundary rules, offline OSV vulnerabilities + typosquat +
308
326
  lockfile drift; accepts an immutable `base_ref`, `changed_files`, and `debt: new|existing|all` for
309
327
  review-scoped results; production paths are the default and `include_classified:true` opts into
@@ -392,6 +410,37 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
392
410
  while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
393
411
  `STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
394
412
 
413
+ ### 0.2.16 exact dependents, multi-ecosystem dependencies and transport contracts
414
+
415
+ - Safe TypeScript project plugins no longer disable semantic precision. Plugin loading remains
416
+ suppressed, while `get_dependents` and `change_impact` can obtain exact direct JS/TS references on
417
+ demand and identify graph-backed transitive hops separately.
418
+ - Dependency evidence covers nested npm/Python/Go scopes, Maven properties, Gradle catalogs/locks,
419
+ and Cargo workspace inheritance/renames/locks. Explicit OSV refresh accepts npm, PyPI, Go, Maven
420
+ and crates.io pins.
421
+ - Cross-repository tracing adds static GraphQL, gRPC and event-topic joins alongside HTTP and keeps
422
+ runtime configuration, reflection and other dynamic targets explicitly `UNKNOWN`.
423
+ - npm/MCPB metadata now calls out Codex/OpenAI Codex workflows, and Hosted distinguishes aggregate
424
+ folder-boundary feedback from proven file-level runtime cycles.
425
+
426
+ Full patch notes: [docs/releases/v0.2.16.md](docs/releases/v0.2.16.md).
427
+
428
+ ### 0.2.15 self-audit precision patch
429
+
430
+ - Dependency review now scopes nested manifests correctly, recognizes framework peer packages and
431
+ package references assembled from bounded dynamic path fragments, and avoids misclassifying
432
+ dependency-owned source as an unused root declaration.
433
+ - Reachability follows source-owned HTML asset references, so maintained browser entry scripts and
434
+ styles remain part of the production surface instead of appearing as dead files.
435
+ - Malware heuristics distinguish inert placeholders, comments, documentation URLs, and ordinary
436
+ Unicode text from executable registry or download behavior while retaining explicit review
437
+ evidence for actionable patterns.
438
+ - Duplicate review ignores policy-like numeric tables and stable ordered-member declarations where
439
+ repetition is intentional, and the remaining reverse-reach, Git-output, member-order, path-term,
440
+ and retrieval logic now uses shared owners instead of drifting copies.
441
+
442
+ Full patch notes: [docs/releases/v0.2.15.md](docs/releases/v0.2.15.md).
443
+
395
444
  ### 0.2.14 typed flows, honest Health, and architecture bootstrap
396
445
 
397
446
  - Go call resolution now follows receiver types through parameters, locals, constructor returns,
@@ -642,13 +691,19 @@ Java graphs:
642
691
  agent review queue. Public/exported methods, framework entries, decorators, dynamic loading and
643
692
  reflection lower confidence; the result always says `REVIEW_REQUIRED` and `autoDelete:false`.
644
693
 
645
- ### HTTP clients and wrappers
694
+ ### Cross-repository transports
646
695
 
647
696
  `trace_api_contract` recognizes built-in object clients such as `axios.get(...)`, explicit bare or
648
697
  object/member wrappers, and simple auto-discovered functions that forward a URL parameter directly
649
698
  to a known HTTP client. Auto-discovered wrappers are restricted to their bounded reverse-import
650
699
  scope; ambiguous same-name definitions are skipped and reported as incomplete evidence.
651
700
 
701
+ The same tool can select GraphQL, gRPC or event evidence. It joins static GraphQL schema fields and
702
+ operations, proto service methods and typed stub calls, and static Kafka/event-bus topic producers
703
+ and consumers across registered repositories. Computed URLs, runtime configuration, dynamic
704
+ GraphQL documents/topics, reflection and dynamic stubs remain `UNKNOWN`; they are not converted into
705
+ an absence claim.
706
+
652
707
  Persistent per-client-repository configuration lives in `.weavatrix.json`:
653
708
 
654
709
  ```json
@@ -810,16 +865,17 @@ loader) behind the thin stdio entry `src/mcp-server.mjs`.
810
865
  cross-repository wrapper/liveness fixture, framework/convention fixture, full MCP lifecycle gate
811
866
  and a portable real-repository runner. Six source-free 0.2.1 real-repository baselines are
812
867
  recorded; edge provenance is gated end-to-end and the strict six-repository release command passes.
813
- - **Wrapper-aware API contracts** shipped in 0.2.2: persistent/ad-hoc configuration,
814
- conservative discovery, cross-repository handler liveness and explicit unknown states. The next
815
- hosted increment joins privacy-safe contract identities across separately synced services.
868
+ - **Cross-transport contracts** cover bounded HTTP wrappers plus static GraphQL, gRPC and event-bus
869
+ identities across registered local repositories, with explicit runtime/dynamic unknown states.
816
870
  - **Hosted architecture workbench** at
817
871
  [app.weavatrix.com](https://app.weavatrix.com) is an access-controlled preview for
818
872
  owner-authenticated source-free evidence and revision history. Its UI and backend evolve
819
873
  independently from the public MCP release; local use remains fully optional.
820
874
  - **Semantic precision bridge** shipped for TypeScript/JavaScript in 0.2.4: a bounded, revision-bound
821
875
  local overlay validates references with the bundled language server while the parser graph remains
822
- the fallback. Java and Rust providers are not bundled yet and stay explicitly `UNAVAILABLE`.
876
+ the fallback. 0.2.16 adds safe configured-plugin suppression plus exact on-demand point and changed-
877
+ symbol batch queries. Java and Rust language-server providers are not bundled and stay explicitly
878
+ unavailable as semantic providers; their parser/dependency evidence is still active.
823
879
  - **Git-native architecture history** — bounded tag/ref timelines and branch
824
880
  reports built outside the worktree; graph artifacts stay out of Git.
825
881
  - **Cross-repository company evidence** — endpoints, events and internal
package/SECURITY.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |---|---|
7
- | `0.2.14.x` | Yes |
8
- | `0.2.13` and older | Upgrade to the latest release |
7
+ | `0.2.15.x` | Yes |
8
+ | `0.2.14` and older | Upgrade to the latest release |
9
9
 
10
10
  Security fixes are provided for the latest published version of Weavatrix. Upgrade before reporting
11
11
  an issue that may already be resolved.
@@ -0,0 +1,53 @@
1
+ # Weavatrix 0.2.16
2
+
3
+ 0.2.16 is a semantic-precision, dependency-coverage, and cross-transport contract release. It keeps
4
+ the 38-tool MCP surface and local-first defaults while fixing the critical case where a safe
5
+ TypeScript project plugin could disable every exact reference edge.
6
+
7
+ ## Exact dependents for real TypeScript projects
8
+
9
+ - TypeScript project plugins such as Next.js are recorded and suppressed instead of making the
10
+ semantic overlay unavailable. Repository-local plugin code is never loaded.
11
+ - `get_dependents` performs a bounded exact point query for JS/TS symbols by default. A complete LSP
12
+ result replaces heuristic direct references; an incomplete result retains graph evidence and says
13
+ why.
14
+ - `change_impact` batches exact direct-reference queries for changed JS/TS symbols. Transitive hops
15
+ remain explicitly graph-backed instead of being presented as LSP proof.
16
+ - Real-repository validation moved an affected Next.js repository from `UNAVAILABLE / 0 EXACT_LSP`
17
+ to a bounded partial overlay with 154 exact edges; a point query outside the initial budget found
18
+ 16 exact direct references and then served the same result from cache.
19
+
20
+ ## Multi-ecosystem dependency and advisory evidence
21
+
22
+ - Python checks nested requirements, pyproject/Poetry/Pipfile scopes; Go resolves nested modules,
23
+ direct/indirect requirements and replacements; both compare declarations with indexed imports.
24
+ - Maven resolves properties and dependency declarations; Gradle resolves common Groovy/Kotlin
25
+ declarations, version catalogs and lock evidence. Import-to-artifact mapping uses bounded group
26
+ prefixes and artifact tokens and keeps reflection/generated/runtime-only cases review-only.
27
+ - Cargo resolves workspace inheritance, renamed packages and lockfile pins, compares crate paths to
28
+ the nearest manifest, and reports feature/proc-macro uncertainty rather than claiming compiler
29
+ proof.
30
+ - Explicit advisory refresh now includes concrete Maven and crates.io pins in addition to npm,
31
+ PyPI and Go. It remains opt-in and sends only package identities and versions to OSV.
32
+
33
+ ## Contracts beyond static HTTP
34
+
35
+ - `trace_api_contract` can select HTTP, GraphQL, gRPC or event contracts. Static GraphQL
36
+ operations/schema fields, proto service methods/stub calls and Kafka/event topics join across
37
+ registered local repositories.
38
+ - Runtime-computed URLs, operations/topics, reflection and dynamic stubs remain `UNKNOWN` with
39
+ bounded evidence. Runtime configuration is never converted into a false clean result.
40
+
41
+ ## Agent and Hosted fixes
42
+
43
+ - Package metadata now names Codex/OpenAI Codex alongside other MCP and coding-agent workflows.
44
+ - The Hosted Flow view distinguishes an aggregate folder-boundary feedback loop from a proven
45
+ file-level runtime import cycle. Current Weavatrix evidence has zero file-level runtime cycles.
46
+ - The release checklist includes a fresh preview-confirmed private graph sync, so public demo history
47
+ and authenticated repository maps cannot silently drift apart.
48
+
49
+ ## Verification
50
+
51
+ The release is gated by the full Node suite, six-language fixture benchmark, real-repository
52
+ semantic checks, exact LSP dogfooding, release-manifest verification, packed npm and MCPB stdio
53
+ checks, npm audit, Hosted tests/build/lint, explicit private sync, and live browser verification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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,7 @@
26
26
  "skill",
27
27
  "scripts/run-agent-task-benchmark.mjs",
28
28
  "docs/agent-task-benchmark.md",
29
- "docs/releases/v0.2.14.md",
29
+ "docs/releases/v0.2.16.md",
30
30
  "README.md",
31
31
  "SECURITY.md",
32
32
  "LICENSE"
@@ -39,6 +39,7 @@
39
39
  "benchmark:real:release": "node scripts/run-real-benchmark.mjs --require-all",
40
40
  "benchmark:agent": "node scripts/run-agent-task-benchmark.mjs",
41
41
  "benchmark:agent:release": "node scripts/run-agent-task-benchmark.mjs --require-independent",
42
+ "sync:private": "node scripts/sync-private.mjs",
42
43
  "build:mcpb": "node scripts/build-mcpb.mjs",
43
44
  "verify:release": "node scripts/verify-release.mjs"
44
45
  },
@@ -56,6 +57,9 @@
56
57
  "dependency-graph",
57
58
  "tree-sitter",
58
59
  "claude",
60
+ "codex",
61
+ "openai-codex",
62
+ "coding-agents",
59
63
  "ai-agents"
60
64
  ],
61
65
  "dependencies": {
@@ -0,0 +1,111 @@
1
+ import { dirname } from "node:path";
2
+ import { createRepoBoundary } from "../repo-path.js";
3
+ import { cargoName, parseCargoToml } from "./cargo-manifests.js";
4
+ import { dependencyVerification, makeFinding } from "./findings.js";
5
+ import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
6
+
7
+ const rootOf = (file) => {
8
+ const root = dirname(String(file || "").replace(/\\/g, "/"));
9
+ return root === "." ? "" : root;
10
+ };
11
+ const owns = (root, file) => !root || file === root || String(file || "").replace(/\\/g, "/").startsWith(`${root}/`);
12
+
13
+ export function collectCargoDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [] } = {}) {
14
+ const boundary = createRepoBoundary(repoRoot);
15
+ const manifests = files.filter((file) => /(^|\/)Cargo\.toml$/i.test(file));
16
+ const scopes = manifests.map((file) => ({ file, root: rootOf(file), ...parseCargoToml(readRepoText(boundary, file)) }))
17
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
18
+ const workspaceDeps = new Map();
19
+ for (const scope of scopes) for (const dependency of scope.workspaceDependencies) workspaceDeps.set(cargoName(dependency.alias), dependency);
20
+ const issues = [];
21
+ for (const scope of scopes) {
22
+ scope.dependencies = scope.dependencies.map((dependency) => {
23
+ if (!dependency.inherited) return dependency;
24
+ const inherited = workspaceDeps.get(cargoName(dependency.alias));
25
+ if (!inherited) { issues.push(`${scope.file}: workspace dependency ${dependency.alias} is unresolved`); return dependency; }
26
+ return { ...dependency, name: inherited.name, version: inherited.version, inherited: true };
27
+ });
28
+ }
29
+ const importsByScope = new Map(scopes.map((scope) => [scope, []]));
30
+ for (const entry of externalImports) {
31
+ if (entry.ecosystem !== "crates.io" || !entry.pkg || entry.builtin || entry.unresolved) continue;
32
+ const owner = scopes.find((scope) => owns(scope.root, entry.file));
33
+ if (owner) importsByScope.get(owner).push(entry);
34
+ }
35
+ const findings = [], declared = new Set();
36
+ let mappedImports = 0, unmappedImports = 0;
37
+ if (!scopes.length) {
38
+ const missing = new Map();
39
+ for (const entry of externalImports.filter((item) => item.ecosystem === "crates.io" && item.pkg && !item.builtin && !item.unresolved)) {
40
+ if (!missing.has(cargoName(entry.pkg))) missing.set(cargoName(entry.pkg), entry);
41
+ }
42
+ for (const [name, entry] of missing) findings.push(makeFinding({
43
+ category: "unused", rule: "missing-dep", severity: "low", confidence: "high",
44
+ title: `Rust crate path without Cargo.toml: ${name}`,
45
+ reason: "An indexed external Rust crate path exists, but no Cargo.toml was discovered.",
46
+ detail: `"${entry.spec || name}" is used by ${entry.file}, but no Cargo manifest owns the file.`,
47
+ package: name, file: entry.file, line: entry.line || 0, source: "internal",
48
+ 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" },
49
+ fixHint: "initialize or restore the owning Cargo manifest",
50
+ }));
51
+ }
52
+ for (const scope of scopes) {
53
+ const imports = importsByScope.get(scope);
54
+ const used = new Map(), missingSeen = new Set();
55
+ for (const entry of imports) {
56
+ const imported = cargoName(entry.pkg);
57
+ const dependency = scope.dependencies.find((item) => cargoName(item.alias) === imported || cargoName(item.name) === imported);
58
+ if (dependency) {
59
+ const evidence = used.get(dependency.alias) || [];
60
+ evidence.push(entry); used.set(dependency.alias, evidence); mappedImports++;
61
+ continue;
62
+ }
63
+ unmappedImports++;
64
+ if (missingSeen.has(imported)) continue;
65
+ missingSeen.add(imported);
66
+ findings.push(makeFinding({
67
+ category: "unused", rule: "missing-dep", severity: "medium", confidence: "high",
68
+ title: `Missing Cargo dependency: ${entry.pkg}`,
69
+ reason: "An indexed external Rust crate path has no matching dependency alias or package name in the nearest Cargo.toml.",
70
+ 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).`,
71
+ package: entry.pkg, file: entry.file, line: entry.line || 0, manifest: scope.file,
72
+ evidence: [{ file: entry.file, line: entry.line || 0, snippet: entry.spec || "" }], source: "internal",
73
+ 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" },
74
+ fixHint: `cargo add ${entry.pkg}`,
75
+ }));
76
+ }
77
+ for (const dependency of scope.dependencies) {
78
+ const identity = cargoName(dependency.name);
79
+ if (!identity) continue;
80
+ declared.add(`${scope.root || "."}:${identity}`);
81
+ const evidence = used.get(dependency.alias) || [];
82
+ if (evidence.length || dependency.optional || dependency.build) continue;
83
+ findings.push(makeFinding({
84
+ category: "unused", rule: "unused-dep", severity: dependency.dev ? "info" : "low", confidence: "low",
85
+ title: `Unused Cargo dependency: ${dependency.name}`,
86
+ reason: "No indexed Rust use/path/extern-crate evidence maps to this dependency; feature-only, proc-macro, generated and reflective registration remain possible.",
87
+ 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.`,
88
+ package: dependency.name, version: dependency.version, manifest: scope.file, source: "internal",
89
+ verification: dependencyVerification(scope.file, [], "REVIEW_REQUIRED", "Cargo alias/package normalization"),
90
+ fixHint: `cargo remove ${dependency.alias} after checking all features and targets`,
91
+ }));
92
+ }
93
+ }
94
+ const present = manifests.length > 0;
95
+ return {
96
+ present,
97
+ status: present ? "CHECKED" : "NOT_PRESENT",
98
+ completeness: present ? (issues.length ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
99
+ manifests,
100
+ declared,
101
+ mappedImports,
102
+ unmappedImports,
103
+ findings,
104
+ reasons: issues,
105
+ reason: !present
106
+ ? "No Cargo.toml was discovered."
107
+ : issues.length
108
+ ? `Cargo declarations and indexed crate paths were checked, but ${issues.length} workspace inheritance reference(s) remain unresolved.`
109
+ : `Cargo declarations from ${manifests.length} scope(s), workspace inheritance, renamed packages and every indexed external crate path were compared.`,
110
+ };
111
+ }
@@ -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);
@@ -7,7 +7,9 @@
7
7
  // script/config-text mention scanning + a config-ecosystem prefix rule, and we NEVER say "safe to
8
8
  // auto-remove", only "review".
9
9
  import { makeFinding } from "./findings.js";
10
+ import {FRAMEWORK_RUNTIME_PEERS, IMPLICIT_STYLE_COMPILERS} from './dependency/conventions.js'
10
11
  import {createScopedDepFindings} from './dependency/scoped-dependencies.js'
12
+ import {createPackageSourceMatcher} from './dependency/source-references.js'
11
13
 
12
14
  // Packages referenced by config CONVENTION, not imports (eslint extends "airbnb" → eslint-config-airbnb).
13
15
  // Flagged only at low confidence when nothing mentions them anywhere.
@@ -21,22 +23,6 @@ const BIN_PKG = {
21
23
  "electron-rebuild": "@electron/rebuild", playwright: "@playwright/test",
22
24
  };
23
25
 
24
- // Required peers consumed inside a framework/build tool rather than imported by application source.
25
- // These contracts are package-scope local and deliberately narrow; declaring the provider is required
26
- // for suppression, so an unrelated app still gets the normal unused-dependency finding.
27
- const FRAMEWORK_RUNTIME_PEERS = new Map([
28
- ["next", ["react-dom"]], ["vinext", ["@vitejs/plugin-react", "@vitejs/plugin-rsc", "react-server-dom-webpack", "vite"]],
29
- ["electron-vite", ["vite"]], ["@cloudflare/vite-plugin", ["vite", "wrangler"]],
30
- ]);
31
-
32
- // Style preprocessors are compiler inputs rather than JavaScript imports. Their presence is proven by
33
- // source extensions in the same package scope, so do not report them as unused merely because Vite,
34
- // webpack, etc. load them internally. Keep this list to exact compiler/package contracts.
35
- const IMPLICIT_STYLE_COMPILERS = new Map([
36
- ["sass", /\.(?:scss|sass)$/i], ["sass-embedded", /\.(?:scss|sass)$/i],
37
- ["less", /\.less$/i], ["stylus", /\.styl(?:us)?$/i],
38
- ]);
39
-
40
26
  const isStylesheetSpecifier = (spec) => /\.(?:css|scss|sass|less|styl(?:us)?)(?:[?#].*)?$/i.test(String(spec || ""));
41
27
  const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
28
  // word-ish mention: the name not embedded inside a longer identifier/path segment
@@ -47,9 +33,10 @@ const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\
47
33
  // pkg — parsed package.json ({} for non-JS repos → no dep findings)
48
34
  // workspacePkgNames — Set of monorepo-local package names (never "missing")
49
35
  // configTexts — Map<fileName, text> of root config files + CI workflows (mention scanning)
36
+ // sourceTexts — Map<fileName, text> for narrow dynamic/package-path literal evidence
50
37
  export function computeDepFindings({
51
38
  externalImports = [], pkg = {}, workspacePkgNames = new Set(), configTexts = new Map(),
52
- aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
39
+ sourceTexts = new Map(), aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
53
40
  } = {}) {
54
41
  const findings = [];
55
42
  const meta = { scope: scope || ".", manifest };
@@ -137,12 +124,13 @@ export function computeDepFindings({
137
124
  return false;
138
125
  };
139
126
  const typesBase = (name) => (name.startsWith("@types/") ? name.slice(7).replace(/^(.+?)__(.+)$/, "@$1/$2") : null); // @types/babel__core → @babel/core
127
+ const sourceReferenced = createPackageSourceMatcher(sourceTexts);
140
128
 
141
129
  // ---- unused dependencies (per section; prod vs dev differ in severity/confidence) ----
142
130
  for (const [section, deps] of Object.entries(sections)) {
143
131
  if (section === "peerDependencies") continue; // peers are consumer-facing contracts, not usage
144
132
  for (const name of Object.keys(deps)) {
145
- if (name === selfName || usedPackages.has(name) || frameworkRuntime.has(name) || implicitCompilerUsage.has(name)) continue;
133
+ if (name === selfName || usedPackages.has(name) || frameworkRuntime.has(name) || implicitCompilerUsage.has(name) || sourceReferenced(name)) continue;
146
134
  const tb = typesBase(name);
147
135
  if (tb) { // @types/x is used iff x is used (or it types the Node builtins)
148
136
  if (tb === "node" ? builtinUsed : usedPackages.has(tb) || frameworkRuntime.has(tb) || mentioned(configBlob, tb)) continue;
@@ -159,7 +147,9 @@ export function computeDepFindings({
159
147
  title: `Unused ${section === "dependencies" ? "dependency" : section.replace(/ies$/, "y")}: ${name}`,
160
148
  reason: "No indexed package import, package-script command, recognized config mention, framework peer contract, or implicit style-compiler input uses this declaration.",
161
149
  detail: `"${name}" is declared in ${section}, but no usage was found in the indexed source scope, package scripts, or ${configTexts.size} known config file(s). Dynamic/plugin/config-convention usage is not proven absent — review before removing.`,
150
+ file: manifest,
162
151
  package: name,
152
+ evidence: [{ file: manifest, line: 0, snippet: `declared in ${section}` }],
163
153
  source: "internal",
164
154
  actionability: "MANIFEST_REVIEW_REQUIRED",
165
155
  autoRemove: false,
@@ -0,0 +1,16 @@
1
+ // Exact package contracts consumed by frameworks/build tools without a source
2
+ // import. Keep them package-scope local; these are not repo-wide allowlists.
3
+ export const FRAMEWORK_RUNTIME_PEERS = new Map([
4
+ ['next', ['react-dom']],
5
+ ['vinext', ['@vitejs/plugin-react', '@vitejs/plugin-rsc', 'react-server-dom-webpack', 'vite']],
6
+ ['electron-vite', ['vite']],
7
+ ['@cloudflare/vite-plugin', ['vite', 'wrangler']],
8
+ ])
9
+
10
+ // Style preprocessors are compiler inputs proven by source extensions.
11
+ export const IMPLICIT_STYLE_COMPILERS = new Map([
12
+ ['sass', /\.(?:scss|sass)$/i],
13
+ ['sass-embedded', /\.(?:scss|sass)$/i],
14
+ ['less', /\.less$/i],
15
+ ['stylus', /\.styl(?:us)?$/i],
16
+ ])
@@ -5,13 +5,14 @@ const ownsFile = (scope, file) => !scope || file === scope || String(file || '')
5
5
  export function createScopedDepFindings(computeDepFindings) {
6
6
  return function computeScopedDepFindings({
7
7
  externalImports = [], packageScopes = [], workspacePkgNames = new Set(), configTexts = new Map(),
8
- nonRuntimeRoots = [], sourceFiles = [],
8
+ sourceTexts = new Map(), nonRuntimeRoots = [], sourceFiles = [],
9
9
  } = {}) {
10
10
  const scopes = packageScopes.length
11
11
  ? packageScopes.map((scope) => ({...scope, root: normalizeScope(scope.root)})).sort((a, b) => b.root.length - a.root.length)
12
12
  : [{root: '', manifest: 'package.json', pkg: {}, aliases: []}]
13
13
  const importsByScope = new Map(scopes.map((scope) => [scope, []]))
14
14
  const sourceFilesByScope = new Map(scopes.map((scope) => [scope, []]))
15
+ const sourceTextsByScope = new Map(scopes.map((scope) => [scope, new Map()]))
15
16
  for (const item of externalImports) {
16
17
  const owner = scopes.find((scope) => ownsFile(scope.root, item.file)) || scopes[scopes.length - 1]
17
18
  importsByScope.get(owner).push(item)
@@ -20,6 +21,10 @@ export function createScopedDepFindings(computeDepFindings) {
20
21
  const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
21
22
  sourceFilesByScope.get(owner).push(file)
22
23
  }
24
+ for (const [file, text] of sourceTexts) {
25
+ const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
26
+ sourceTextsByScope.get(owner).set(file, text)
27
+ }
23
28
  const configOwner = new Map()
24
29
  for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1])
25
30
  const findings = [], usedPackages = new Map(), declared = new Set()
@@ -27,7 +32,7 @@ export function createScopedDepFindings(computeDepFindings) {
27
32
  const scopeConfig = new Map([...configTexts].filter(([file]) => configOwner.get(file) === scope))
28
33
  const result = computeDepFindings({
29
34
  externalImports: importsByScope.get(scope), pkg: scope.pkg || {}, workspacePkgNames,
30
- configTexts: scopeConfig, aliases: scope.aliases || [], scope: scope.root,
35
+ configTexts: scopeConfig, sourceTexts: sourceTextsByScope.get(scope), aliases: scope.aliases || [], scope: scope.root,
31
36
  manifest: scope.manifest || (scope.root ? `${scope.root}/package.json` : 'package.json'),
32
37
  nonRuntimeRoots, sourceFiles: sourceFilesByScope.get(scope),
33
38
  })
@@ -0,0 +1,21 @@
1
+ // Dynamic package loaders often construct a node_modules path instead of using
2
+ // import/require, so they never become graph externalImports. Restrict this
3
+ // fallback to executable npm source: manifests merely declare every dependency
4
+ // and must never count as usage evidence.
5
+ const NPM_SOURCE_RE = /\.(?:[cm]?[jt]sx?|vue|svelte)$/i
6
+ const escRe = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
7
+ const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob)
8
+
9
+ export function createPackageSourceMatcher(sourceTexts = new Map()) {
10
+ const blob = [...sourceTexts]
11
+ .filter(([file]) => NPM_SOURCE_RE.test(String(file || '')))
12
+ .map(([, text]) => String(text || ''))
13
+ .join('\n')
14
+ return (name) => {
15
+ if (mentioned(blob, name)) return true
16
+ if (!name.startsWith('@') || !name.includes('/')) return false
17
+ const [namespace, base] = name.split('/', 2)
18
+ // join(root, "node_modules", "@scope", "package", ...)
19
+ return new RegExp(`["'\x60]${escRe(namespace)}["'\x60][\\s\\S]{0,96}?["'\x60]${escRe(base)}["'\x60]`).test(blob)
20
+ }
21
+ }
@@ -149,11 +149,17 @@ export function extractLargeStrings(text, { py = false, cs = false, minLines = 6
149
149
 
150
150
  // one raw token stream; the two modes differ only in identifier canonicalization
151
151
  export function tokenize(text) {
152
- const raw = text.match(/[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|[^\s\w]/g) || [];
152
+ const raw = text.match(/[A-Za-z_$][\w$]*|(?:0[xX][\dA-Fa-f_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d[\d_]*)?)n?|[^\s\w]/g) || [];
153
153
  const strict = [];
154
154
  const renamed = [];
155
155
  for (const t of raw) {
156
- if (/^\d/.test(t)) { strict.push("N"); renamed.push("N"); continue; }
156
+ if (/^\d/.test(t)) {
157
+ // Numeric constants are behavior, not renameable identifiers. Preserve
158
+ // their value so same-shape limit/config tables with different numbers do
159
+ // not become perfect clones; a small constant edit still leaves the rest
160
+ // of a genuine copied body highly similar.
161
+ strict.push(`N:${t}`); renamed.push(`N:${t}`); continue;
162
+ }
157
163
  if (/^[A-Za-z_$]/.test(t) && !KEYWORDS.has(t)) { strict.push(t); renamed.push("I"); continue; }
158
164
  strict.push(t); renamed.push(t);
159
165
  }
@@ -178,5 +184,9 @@ export function fingerprints(toks) {
178
184
  for (let j = i; j < Math.min(i + W, hashes.length); j++) if (hashes[j] < min) min = hashes[j];
179
185
  fp.add(min);
180
186
  }
187
+ // Winnowing can skip every changed literal in a repetitive policy table.
188
+ // Exact numeric anchors keep behavior-defining limits in the similarity
189
+ // denominator while leaving ordinary copied bodies tolerant of a small edit.
190
+ for (const token of toks) if (token.startsWith("N:")) fp.add(`numeric:${token}`);
181
191
  return fp;
182
192
  }
@@ -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;