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.
- package/README.md +69 -13
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.16.md +53 -0
- package/package.json +6 -2
- package/src/analysis/cargo-dependency-evidence.js +111 -0
- package/src/analysis/cargo-manifests.js +74 -0
- package/src/analysis/dep-check-ecosystems.js +3 -0
- package/src/analysis/dep-check.js +8 -18
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +7 -2
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/endpoints-java.js +2 -5
- package/src/analysis/findings.js +12 -0
- package/src/analysis/go-dependency-evidence.js +68 -0
- package/src/analysis/health-capabilities.js +1 -1
- package/src/analysis/http-contracts/analysis.js +2 -0
- package/src/analysis/http-contracts/client-call-detection.js +1 -0
- package/src/analysis/http-contracts/client-call-parser.js +18 -4
- package/src/analysis/internal-audit/dependency-health.js +15 -13
- package/src/analysis/internal-audit/python-manifests.js +18 -5
- package/src/analysis/internal-audit/supply-chain.js +2 -0
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +13 -7
- package/src/analysis/jvm-dependency-evidence.js +102 -56
- package/src/analysis/jvm-manifests.js +114 -0
- package/src/analysis/source-correctness.js +2 -5
- package/src/analysis/task-retrieval.js +3 -14
- package/src/analysis/transport-contracts.js +157 -0
- package/src/graph/builder/lang-go.js +4 -2
- package/src/graph/builder/lang-java.js +12 -3
- package/src/graph/builder/lang-rust.js +22 -3
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/internal-builder.build.js +2 -2
- package/src/graph/internal-builder.resolvers.js +18 -8
- package/src/mcp/actions/advisories.mjs +2 -3
- package/src/mcp/catalog.mjs +7 -4
- package/src/mcp/company-contract-verdict.mjs +42 -0
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-seeds.mjs +3 -14
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/sync/evidence-duplicates.mjs +1 -4
- package/src/mcp/tools-company.mjs +28 -53
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact-precision.mjs +89 -0
- package/src/mcp/tools-impact.mjs +53 -136
- package/src/path-classification.js +14 -0
- package/src/precision/lsp-overlay/build.js +10 -0
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/symbol-query.js +39 -0
- package/src/precision/typescript-provider/client.js +16 -3
- package/src/precision/typescript-provider/discovery.js +1 -1
- package/src/precision/typescript-provider/isolated-runtime.js +39 -0
- package/src/precision/typescript-provider/project-host.js +11 -6
- package/src/precision/typescript-provider/project-safety.js +5 -0
- package/src/security/advisory-store.js +2 -2
- package/src/security/installed-jvm-rust.js +46 -0
- package/src/security/installed.js +21 -1
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/util.js +8 -0
- 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.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
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
|
-
###
|
|
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
|
-
- **
|
|
814
|
-
|
|
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.
|
|
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.
|
|
8
|
-
| `0.2.
|
|
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.
|
|
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.
|
|
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$]
|
|
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)) {
|
|
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 =
|
|
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;
|