weavatrix 0.3.14 → 1.0.0
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/LICENSE +1 -0
- package/README.md +231 -199
- package/bin/native/darwin-arm64/weavatrix +0 -0
- package/bin/native/darwin-x64/weavatrix +0 -0
- package/bin/native/linux-arm64/weavatrix +0 -0
- package/bin/native/linux-x64/weavatrix +0 -0
- package/bin/native/win32-arm64/weavatrix.exe +0 -0
- package/bin/native/win32-x64/weavatrix.exe +0 -0
- package/bin/resolve-binary.mjs +73 -0
- package/bin/run-native.mjs +26 -0
- package/bin/weavatrix-mcp.mjs +7 -6
- package/bin/weavatrix.mjs +5 -0
- package/package.json +8 -66
- package/skill/SKILL.md +62 -374
- package/skill/agents/openai.yaml +4 -0
- package/skill/references/tool-routing.md +45 -0
- package/SECURITY.md +0 -101
- package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
- package/docs/agent-task-benchmark.md +0 -58
- package/docs/releases/v0.2.19.md +0 -33
- package/docs/releases/v0.3.0.md +0 -43
- package/docs/releases/v0.3.1.md +0 -30
- package/docs/releases/v0.3.10.md +0 -76
- package/docs/releases/v0.3.11.md +0 -55
- package/docs/releases/v0.3.12.md +0 -44
- package/docs/releases/v0.3.13.md +0 -34
- package/docs/releases/v0.3.14.md +0 -37
- package/docs/releases/v0.3.2.md +0 -15
- package/docs/releases/v0.3.3.md +0 -25
- package/docs/releases/v0.3.4.md +0 -40
- package/docs/releases/v0.3.5.md +0 -50
- package/docs/releases/v0.3.6.md +0 -44
- package/docs/releases/v0.3.7.md +0 -37
- package/docs/releases/v0.3.8.md +0 -21
- package/docs/releases/v0.3.9.md +0 -36
- package/scripts/run-agent-task-benchmark.mjs +0 -118
- package/src/analysis/allowed-test-runner.js +0 -70
- package/src/analysis/architecture/contract-graph.js +0 -119
- package/src/analysis/architecture/contract-schema.js +0 -110
- package/src/analysis/architecture/contract-storage.js +0 -35
- package/src/analysis/architecture/contract-verification.js +0 -168
- package/src/analysis/architecture-contract.js +0 -16
- package/src/analysis/audit-debt.js +0 -94
- package/src/analysis/audit-extensions.js +0 -60
- package/src/analysis/cargo-dependency-evidence.js +0 -108
- package/src/analysis/cargo-manifests.js +0 -81
- package/src/analysis/change-classification/diff-parser.js +0 -103
- package/src/analysis/change-classification/options.js +0 -40
- package/src/analysis/change-classification/symbol-classifier.js +0 -177
- package/src/analysis/change-classification.js +0 -188
- package/src/analysis/coverage-reports-tarpaulin.js +0 -112
- package/src/analysis/coverage-reports.js +0 -246
- package/src/analysis/cycle-route.js +0 -14
- package/src/analysis/data-flow-evidence.js +0 -114
- package/src/analysis/dead-check.js +0 -300
- package/src/analysis/dead-code-review/candidates.js +0 -194
- package/src/analysis/dead-code-review/policy.js +0 -23
- package/src/analysis/dead-code-review.js +0 -119
- package/src/analysis/dep-check-ecosystems.js +0 -205
- package/src/analysis/dep-check.js +0 -290
- package/src/analysis/dep-rules.js +0 -10
- package/src/analysis/dependency/conventions.js +0 -16
- package/src/analysis/dependency/scoped-dependencies.js +0 -51
- package/src/analysis/dependency/source-references.js +0 -21
- package/src/analysis/duplicate-groups.js +0 -104
- package/src/analysis/duplicates-worker.js +0 -11
- package/src/analysis/duplicates.compute.js +0 -269
- package/src/analysis/duplicates.js +0 -15
- package/src/analysis/duplicates.run.js +0 -52
- package/src/analysis/duplicates.tokenize.js +0 -199
- package/src/analysis/endpoints/common.js +0 -79
- package/src/analysis/endpoints/extract.js +0 -120
- package/src/analysis/endpoints/inventory.js +0 -84
- package/src/analysis/endpoints/mounts.js +0 -129
- package/src/analysis/endpoints-java.js +0 -260
- package/src/analysis/endpoints-rust.js +0 -124
- package/src/analysis/endpoints.js +0 -3
- package/src/analysis/findings.js +0 -70
- package/src/analysis/git-history/analytics.js +0 -177
- package/src/analysis/git-history/collector.js +0 -67
- package/src/analysis/git-history/options.js +0 -66
- package/src/analysis/git-history.js +0 -143
- package/src/analysis/git-ref-graph.js +0 -76
- package/src/analysis/go-dependency-evidence.js +0 -62
- package/src/analysis/graph-analysis.aggregate.js +0 -296
- package/src/analysis/graph-analysis.edges.js +0 -24
- package/src/analysis/graph-analysis.js +0 -7
- package/src/analysis/graph-analysis.refs.js +0 -202
- package/src/analysis/graph-analysis.summaries.js +0 -65
- package/src/analysis/health-capabilities.js +0 -83
- package/src/analysis/hot-path-review.js +0 -266
- package/src/analysis/http-contract-wrappers.js +0 -241
- package/src/analysis/http-contracts/analysis.js +0 -179
- package/src/analysis/http-contracts/client-call-detection.js +0 -82
- package/src/analysis/http-contracts/client-call-parser.js +0 -273
- package/src/analysis/http-contracts/graph-context.js +0 -143
- package/src/analysis/http-contracts/matching.js +0 -61
- package/src/analysis/http-contracts/shared.js +0 -85
- package/src/analysis/http-contracts.js +0 -6
- package/src/analysis/internal-audit/dependency-health.js +0 -113
- package/src/analysis/internal-audit/python-manifests.js +0 -78
- package/src/analysis/internal-audit/repo-files.js +0 -50
- package/src/analysis/internal-audit/supply-chain.js +0 -149
- package/src/analysis/internal-audit.collect.js +0 -213
- package/src/analysis/internal-audit.js +0 -9
- package/src/analysis/internal-audit.reach.js +0 -201
- package/src/analysis/internal-audit.run.js +0 -256
- package/src/analysis/java-source.js +0 -36
- package/src/analysis/jvm-artifact-index.js +0 -87
- package/src/analysis/jvm-dependency-evidence.js +0 -138
- package/src/analysis/jvm-manifests.js +0 -114
- package/src/analysis/manifests.js +0 -123
- package/src/analysis/package-reachability.js +0 -39
- package/src/analysis/source-complexity.ast.js +0 -97
- package/src/analysis/source-complexity.constants.js +0 -55
- package/src/analysis/source-complexity.js +0 -14
- package/src/analysis/source-complexity.report.js +0 -81
- package/src/analysis/source-complexity.walk.js +0 -228
- package/src/analysis/source-correctness/retry-patterns.js +0 -93
- package/src/analysis/source-correctness.js +0 -222
- package/src/analysis/static-test-reachability.js +0 -150
- package/src/analysis/structure/dependency-graph.js +0 -151
- package/src/analysis/structure/findings.js +0 -162
- package/src/analysis/task-retrieval.js +0 -107
- package/src/analysis/transport-contracts.js +0 -224
- package/src/analysis/transport-runtime-evidence.js +0 -155
- package/src/analysis-kit.mjs +0 -17
- package/src/bounds.js +0 -4
- package/src/build-graph.js +0 -209
- package/src/child-env.js +0 -7
- package/src/config.js +0 -5
- package/src/extension/local-services.mjs +0 -75
- package/src/git-exec.js +0 -67
- package/src/graph/build-worker.js +0 -49
- package/src/graph/builder/go-receiver-resolution.js +0 -112
- package/src/graph/builder/js/dynamic-import.js +0 -26
- package/src/graph/builder/js/queries.js +0 -48
- package/src/graph/builder/lang-csharp.js +0 -40
- package/src/graph/builder/lang-css.js +0 -29
- package/src/graph/builder/lang-go.js +0 -140
- package/src/graph/builder/lang-html.js +0 -29
- package/src/graph/builder/lang-java-calls.js +0 -142
- package/src/graph/builder/lang-java.js +0 -215
- package/src/graph/builder/lang-js.js +0 -291
- package/src/graph/builder/lang-python.js +0 -235
- package/src/graph/builder/lang-rust-calls.js +0 -63
- package/src/graph/builder/lang-rust.js +0 -299
- package/src/graph/builder/lang-solidity.js +0 -126
- package/src/graph/builder/lang-sql.js +0 -263
- package/src/graph/builder/pass2-resolution.js +0 -81
- package/src/graph/builder/spec-pkg.js +0 -87
- package/src/graph/community.js +0 -27
- package/src/graph/edge-provenance.js +0 -31
- package/src/graph/file-lock.js +0 -69
- package/src/graph/freshness-probe.js +0 -142
- package/src/graph/graph-filter.js +0 -63
- package/src/graph/incremental-refresh.js +0 -235
- package/src/graph/internal-builder.barrels.js +0 -235
- package/src/graph/internal-builder.build.js +0 -300
- package/src/graph/internal-builder.java.js +0 -33
- package/src/graph/internal-builder.js +0 -12
- package/src/graph/internal-builder.langs.js +0 -157
- package/src/graph/internal-builder.pass2.js +0 -163
- package/src/graph/internal-builder.resolvers.js +0 -286
- package/src/graph/layout.js +0 -51
- package/src/graph/node-id.js +0 -7
- package/src/graph/parser-artifact-boundary.js +0 -55
- package/src/graph/relations.js +0 -4
- package/src/graph/repo-registry.js +0 -124
- package/src/graph/resolvers/rust.js +0 -117
- package/src/infra/infra-items.js +0 -255
- package/src/infra/infra-registry.js +0 -184
- package/src/infra/infra.detect.js +0 -119
- package/src/infra/infra.js +0 -38
- package/src/infra/infra.match.js +0 -102
- package/src/infra/infra.scan.js +0 -93
- package/src/mcp/actions/graph-lifecycle.mjs +0 -148
- package/src/mcp/architecture-bootstrap.mjs +0 -168
- package/src/mcp/architecture-starter.mjs +0 -234
- package/src/mcp/catalog.mjs +0 -233
- package/src/mcp/company-contract-page.mjs +0 -142
- package/src/mcp/company-contract-reconcile.mjs +0 -87
- package/src/mcp/company-contract-verdict.mjs +0 -42
- package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
- package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
- package/src/mcp/evidence/package-graph-common.mjs +0 -115
- package/src/mcp/evidence/package-lock-graph.mjs +0 -92
- package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
- package/src/mcp/evidence-snapshot.common.mjs +0 -158
- package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
- package/src/mcp/evidence-snapshot.health.mjs +0 -102
- package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
- package/src/mcp/evidence-snapshot.mjs +0 -50
- package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
- package/src/mcp/evidence-snapshot.structure.mjs +0 -84
- package/src/mcp/extension-api.mjs +0 -77
- package/src/mcp/git-output.mjs +0 -7
- package/src/mcp/graph/context-core.mjs +0 -111
- package/src/mcp/graph/context-seeds.mjs +0 -207
- package/src/mcp/graph/context-state.mjs +0 -85
- package/src/mcp/graph/reverse-reach.mjs +0 -42
- package/src/mcp/graph/tools-core.mjs +0 -143
- package/src/mcp/graph/tools-query.mjs +0 -276
- package/src/mcp/graph-context.mjs +0 -5
- package/src/mcp/graph-diff.mjs +0 -218
- package/src/mcp/health/audit-format.mjs +0 -199
- package/src/mcp/health/audit.mjs +0 -199
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -121
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -220
- package/src/mcp/runtime-version.mjs +0 -31
- package/src/mcp/server/auto-refresh.mjs +0 -66
- package/src/mcp/server/runtime-config.mjs +0 -43
- package/src/mcp/server/shutdown.mjs +0 -40
- package/src/mcp/staleness-notice.mjs +0 -20
- package/src/mcp/sync/evidence-architecture.mjs +0 -54
- package/src/mcp/sync/evidence-common.mjs +0 -88
- package/src/mcp/sync/evidence-duplicates.mjs +0 -97
- package/src/mcp/sync/evidence-health.mjs +0 -56
- package/src/mcp/sync/evidence-packages.mjs +0 -95
- package/src/mcp/sync/payload-common.mjs +0 -97
- package/src/mcp/sync/payload-v2.mjs +0 -54
- package/src/mcp/sync/payload-v3.mjs +0 -79
- package/src/mcp/sync-evidence.mjs +0 -23
- package/src/mcp/sync-payload.mjs +0 -70
- package/src/mcp/tool-result.mjs +0 -54
- package/src/mcp/tools-actions.mjs +0 -11
- package/src/mcp/tools-architecture.mjs +0 -177
- package/src/mcp/tools-company.mjs +0 -251
- package/src/mcp/tools-context.mjs +0 -222
- package/src/mcp/tools-endpoints.mjs +0 -207
- package/src/mcp/tools-graph-hubs.mjs +0 -78
- package/src/mcp/tools-graph.mjs +0 -17
- package/src/mcp/tools-health.mjs +0 -24
- package/src/mcp/tools-history.mjs +0 -28
- package/src/mcp/tools-impact-change.mjs +0 -220
- package/src/mcp/tools-impact-precision.mjs +0 -89
- package/src/mcp/tools-impact.mjs +0 -169
- package/src/mcp/tools-source.mjs +0 -238
- package/src/mcp/tools-verified-change.mjs +0 -200
- package/src/mcp-rg.mjs +0 -50
- package/src/mcp-runtime.mjs +0 -2
- package/src/mcp-server.mjs +0 -298
- package/src/mcp-source-tools.mjs +0 -163
- package/src/path-classification.js +0 -226
- package/src/path-ignore.js +0 -69
- package/src/precision/lsp-client/constants.js +0 -12
- package/src/precision/lsp-client/environment.js +0 -20
- package/src/precision/lsp-client/errors.js +0 -15
- package/src/precision/lsp-client/lifecycle.js +0 -114
- package/src/precision/lsp-client/message-parser.js +0 -81
- package/src/precision/lsp-client/protocol.js +0 -212
- package/src/precision/lsp-client/registry.js +0 -59
- package/src/precision/lsp-client/repo-uri.js +0 -60
- package/src/precision/lsp-client/stdio-client.js +0 -140
- package/src/precision/lsp-client.js +0 -10
- package/src/precision/lsp-overlay/budget-policy.js +0 -78
- package/src/precision/lsp-overlay/build.js +0 -261
- package/src/precision/lsp-overlay/contract.js +0 -61
- package/src/precision/lsp-overlay/reference-results.js +0 -108
- package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
- package/src/precision/lsp-overlay/source-session.js +0 -142
- package/src/precision/lsp-overlay/store.js +0 -150
- package/src/precision/lsp-overlay/target-index.js +0 -152
- package/src/precision/lsp-overlay/target-query.js +0 -55
- package/src/precision/lsp-overlay.js +0 -11
- package/src/precision/symbol-query.js +0 -224
- package/src/precision/typescript-lsp-provider.js +0 -12
- package/src/precision/typescript-provider/client.js +0 -89
- package/src/precision/typescript-provider/discovery.js +0 -124
- package/src/precision/typescript-provider/isolated-runtime.js +0 -39
- package/src/precision/typescript-provider/project-host.js +0 -254
- package/src/precision/typescript-provider/project-safety.js +0 -166
- package/src/precision/typescript-provider/reference-usage.js +0 -53
- package/src/process.js +0 -95
- package/src/repo-path.js +0 -53
- package/src/scan/discover.inventory.js +0 -78
- package/src/scan/discover.js +0 -5
- package/src/scan/discover.list.js +0 -79
- package/src/scan/discover.stack.js +0 -227
- package/src/scan/search.core.js +0 -24
- package/src/scan/search.git.js +0 -102
- package/src/scan/search.js +0 -9
- package/src/scan/search.node.js +0 -83
- package/src/scan/search.preview.js +0 -49
- package/src/scan/search.rg.js +0 -145
- package/src/security/advisory-store.js +0 -172
- package/src/security/installed-jvm-rust.js +0 -46
- package/src/security/installed.js +0 -280
- package/src/security/malware-file-heuristics.js +0 -121
- package/src/security/malware-heuristics.exclusions.js +0 -134
- package/src/security/malware-heuristics.js +0 -15
- package/src/security/malware-heuristics.roots.js +0 -145
- package/src/security/malware-heuristics.scan.js +0 -89
- package/src/security/malware-heuristics.sweep.js +0 -122
- package/src/security/malware-scoring.js +0 -105
- package/src/security/match.js +0 -85
- package/src/security/registry-sig.classify.js +0 -137
- package/src/security/registry-sig.js +0 -18
- package/src/security/registry-sig.rules.js +0 -192
- package/src/security/rust-advisory-report.js +0 -60
- package/src/security/typosquat.js +0 -81
- package/src/util.js +0 -40
- package/src/version.js +0 -7
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
// Pure dependency checker (replaces depcheck + knip's dependency output) — set math over the graph's
|
|
2
|
-
// externalImports vs package.json. NO filesystem here: the fs wrapper is internal-audit.js, so this is
|
|
3
|
-
// fully unit-testable (same pattern as dead-check.js computeDead). See DEPS_SECURITY_PLAN.md (P1).
|
|
4
|
-
//
|
|
5
|
-
// Philosophy: bias to FALSE-NEGATIVES. A dep we can't prove unused stays unflagged (or drops to low
|
|
6
|
-
// confidence) — knip's ~100 framework plugins know config conventions we don't, so we compensate with
|
|
7
|
-
// script/config-text mention scanning + a config-ecosystem prefix rule, and we NEVER say "safe to
|
|
8
|
-
// auto-remove", only "review".
|
|
9
|
-
import { makeFinding } from "./findings.js";
|
|
10
|
-
import {FRAMEWORK_RUNTIME_PEERS, IMPLICIT_STYLE_COMPILERS} from './dependency/conventions.js'
|
|
11
|
-
import {createScopedDepFindings} from './dependency/scoped-dependencies.js'
|
|
12
|
-
import {createPackageSourceMatcher} from './dependency/source-references.js'
|
|
13
|
-
|
|
14
|
-
// Packages referenced by config CONVENTION, not imports (eslint extends "airbnb" → eslint-config-airbnb).
|
|
15
|
-
// Flagged only at low confidence when nothing mentions them anywhere.
|
|
16
|
-
const CONFIG_ECOSYSTEM_RE = /^(eslint-(config|plugin)-|@typescript-eslint\/|@eslint\/|prettier-plugin-|postcss-|autoprefixer$|tailwindcss$|babel-(plugin|preset)-|@babel\/(plugin|preset)-|stylelint-|@commitlint\/|commitlint-|remark-|rehype-|@semantic-release\/|karma-|grunt-|gulp-)/;
|
|
17
|
-
|
|
18
|
-
// CLI name → package name, for script commands whose binary doesn't equal the package
|
|
19
|
-
// (`tsc` comes from typescript, `depcruise` from dependency-cruiser, …).
|
|
20
|
-
const BIN_PKG = {
|
|
21
|
-
tsc: "typescript", depcruise: "dependency-cruiser", "vue-cli-service": "@vue/cli-service",
|
|
22
|
-
ng: "@angular/cli", nest: "@nestjs/cli", sb: "storybook",
|
|
23
|
-
"electron-rebuild": "@electron/rebuild", playwright: "@playwright/test",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const isStylesheetSpecifier = (spec) => /\.(?:css|scss|sass|less|styl(?:us)?)(?:[?#].*)?$/i.test(String(spec || ""));
|
|
27
|
-
const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
-
// word-ish mention: the name not embedded inside a longer identifier/path segment
|
|
29
|
-
const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob);
|
|
30
|
-
|
|
31
|
-
// computeDepFindings({ externalImports, pkg, workspacePkgNames, configTexts }) → { findings, usedPackages }
|
|
32
|
-
// externalImports — graph.json's array (P0): {file, spec, pkg, builtin, kind, line, dynamic?, unresolved?}
|
|
33
|
-
// pkg — parsed package.json ({} for non-JS repos → no dep findings)
|
|
34
|
-
// workspacePkgNames — Set of monorepo-local package names (never "missing")
|
|
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
|
|
37
|
-
export function computeDepFindings({
|
|
38
|
-
externalImports = [], pkg = {}, workspacePkgNames = new Set(), configTexts = new Map(),
|
|
39
|
-
sourceTexts = new Map(), aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
|
|
40
|
-
} = {}) {
|
|
41
|
-
const findings = [];
|
|
42
|
-
const meta = { scope: scope || ".", manifest };
|
|
43
|
-
const isAliasSpec = (spec) => aliases.some((a) => {
|
|
44
|
-
const s = String(spec || "");
|
|
45
|
-
const key = typeof a === "string" ? a : String(a?.key || "");
|
|
46
|
-
const prefix = typeof a === "string" ? a.replace(/\*.*$/, "") : String(a?.prefix || "");
|
|
47
|
-
const suffix = key.includes("*") ? key.slice(key.indexOf("*") + 1) : String(a?.suffix || "");
|
|
48
|
-
return key.includes("*")
|
|
49
|
-
? !!prefix && s.startsWith(prefix) && (!suffix || s.endsWith(suffix))
|
|
50
|
-
: !!key && s === key;
|
|
51
|
-
});
|
|
52
|
-
const sections = {
|
|
53
|
-
dependencies: pkg.dependencies || {},
|
|
54
|
-
devDependencies: pkg.devDependencies || {},
|
|
55
|
-
peerDependencies: pkg.peerDependencies || {},
|
|
56
|
-
optionalDependencies: pkg.optionalDependencies || {},
|
|
57
|
-
};
|
|
58
|
-
const allDeclared = new Set(Object.values(sections).flatMap((s) => Object.keys(s)));
|
|
59
|
-
const selfName = String(pkg.name || "");
|
|
60
|
-
const normRoots = (nonRuntimeRoots || []).map((root) => String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "")).filter(Boolean);
|
|
61
|
-
const isNonRuntimeFile = (file) => {
|
|
62
|
-
const normalized = String(file || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
63
|
-
return normRoots.some((root) => normalized === root || normalized.startsWith(`${root}/`));
|
|
64
|
-
};
|
|
65
|
-
const npmNameParts = (name) => {
|
|
66
|
-
const value = String(name || "");
|
|
67
|
-
const slash = value.startsWith("@") ? value.indexOf("/") : -1;
|
|
68
|
-
return slash > 0 ? { scope: value.slice(0, slash), base: value.slice(slash + 1) } : { scope: "", base: value };
|
|
69
|
-
};
|
|
70
|
-
const selfParts = npmNameParts(selfName);
|
|
71
|
-
const configuredNapi = npmNameParts(pkg.napi?.name || selfName);
|
|
72
|
-
const napiBinding = {
|
|
73
|
-
scope: configuredNapi.scope || (configuredNapi.base === selfParts.base ? selfParts.scope : ""),
|
|
74
|
-
base: configuredNapi.base,
|
|
75
|
-
};
|
|
76
|
-
const NAPI_PLATFORM_SUFFIX = /^(?:android-(?:arm64|arm-eabi)|win32-(?:x64|ia32|arm64)-msvc|darwin-(?:universal|x64|arm64)|freebsd-(?:x64|arm64)|linux-(?:x64|arm64|arm|riscv64|s390x|ppc64|loong64)-(?:gnu|musl|gnueabihf|musleabihf)|aix-ppc64|sunos-x64|wasm32-wasi(?:-preview1-threads)?)$/;
|
|
77
|
-
const isGeneratedNapiPackage = (name, files) => {
|
|
78
|
-
if (!pkg.napi || !napiBinding.base || !files.every((file) => /(^|\/)index\.[cm]?js$/i.test(String(file || "").replace(/\\/g, "/")))) return false;
|
|
79
|
-
const candidate = npmNameParts(name);
|
|
80
|
-
if (candidate.scope !== napiBinding.scope || !candidate.base.startsWith(`${napiBinding.base}-`)) return false;
|
|
81
|
-
return NAPI_PLATFORM_SUFFIX.test(candidate.base.slice(napiBinding.base.length + 1));
|
|
82
|
-
};
|
|
83
|
-
const isGeneratedNapiBinary = (entry) => !!pkg.napi
|
|
84
|
-
&& /(^|\/)index\.[cm]?js$/i.test(String(entry.file || "").replace(/\\/g, "/"))
|
|
85
|
-
&& /^\.\/[^/]+\.node$/i.test(String(entry.spec || ""));
|
|
86
|
-
// Some frameworks consume required peers internally, so application source legitimately never imports
|
|
87
|
-
// them. Keep this deliberately narrow: react-dom is a required Next.js runtime peer in the same package
|
|
88
|
-
// scope, not a general React exemption and not a repo-wide whitelist.
|
|
89
|
-
const frameworkRuntime = new Set();
|
|
90
|
-
for (const [provider, peers] of FRAMEWORK_RUNTIME_PEERS) {
|
|
91
|
-
if (allDeclared.has(provider)) for (const peer of peers) frameworkRuntime.add(peer);
|
|
92
|
-
}
|
|
93
|
-
const implicitCompilerUsage = new Set();
|
|
94
|
-
for (const [name, sourcePattern] of IMPLICIT_STYLE_COMPILERS) {
|
|
95
|
-
if (allDeclared.has(name) && sourceFiles.some((file) => sourcePattern.test(String(file || "")))) {
|
|
96
|
-
implicitCompilerUsage.add(name);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ---- usage index from the graph's recorded imports ----
|
|
101
|
-
const usedPackages = new Map(); // pkgName -> { files:Set, lines:Map(file→first line), kinds/specs:Set, typeOnly:boolean }
|
|
102
|
-
let builtinUsed = false;
|
|
103
|
-
for (const e of externalImports) {
|
|
104
|
-
if (e.ecosystem && e.ecosystem !== "npm") continue; // go/python imports have their own checkers
|
|
105
|
-
if (isAliasSpec(e.spec)) continue;
|
|
106
|
-
if (e.unresolved) continue;
|
|
107
|
-
if (e.builtin) { builtinUsed = true; continue; }
|
|
108
|
-
if (!e.pkg) continue;
|
|
109
|
-
let u = usedPackages.get(e.pkg);
|
|
110
|
-
if (!u) usedPackages.set(e.pkg, (u = { files: new Set(), lines: new Map(), kinds: new Set(), specs: new Set(), typeOnly: true }));
|
|
111
|
-
u.files.add(e.file);
|
|
112
|
-
if (!u.lines.has(e.file)) u.lines.set(e.file, e.line || 0);
|
|
113
|
-
u.kinds.add(e.kind);
|
|
114
|
-
u.specs.add(e.spec || e.pkg);
|
|
115
|
-
if (!e.typeOnly) u.typeOnly = false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const scriptBlob = Object.values(pkg.scripts || {}).join("\n");
|
|
119
|
-
const configBlob = scriptBlob + "\n" + [...configTexts.values()].join("\n");
|
|
120
|
-
const scriptTokens = new Set(scriptBlob.split(/[^\w@/.:-]+/).filter(Boolean));
|
|
121
|
-
const binReferenced = (name) => {
|
|
122
|
-
if (scriptTokens.has(name)) return true;
|
|
123
|
-
for (const [bin, p] of Object.entries(BIN_PKG)) if (p === name && scriptTokens.has(bin)) return true;
|
|
124
|
-
return false;
|
|
125
|
-
};
|
|
126
|
-
const typesBase = (name) => (name.startsWith("@types/") ? name.slice(7).replace(/^(.+?)__(.+)$/, "@$1/$2") : null); // @types/babel__core → @babel/core
|
|
127
|
-
const sourceReferenced = createPackageSourceMatcher(sourceTexts);
|
|
128
|
-
|
|
129
|
-
// ---- unused dependencies (per section; prod vs dev differ in severity/confidence) ----
|
|
130
|
-
for (const [section, deps] of Object.entries(sections)) {
|
|
131
|
-
if (section === "peerDependencies") continue; // peers are consumer-facing contracts, not usage
|
|
132
|
-
for (const name of Object.keys(deps)) {
|
|
133
|
-
if (name === selfName || usedPackages.has(name) || frameworkRuntime.has(name) || implicitCompilerUsage.has(name) || sourceReferenced(name)) continue;
|
|
134
|
-
const tb = typesBase(name);
|
|
135
|
-
if (tb) { // @types/x is used iff x is used (or it types the Node builtins)
|
|
136
|
-
if (tb === "node" ? builtinUsed : usedPackages.has(tb) || frameworkRuntime.has(tb) || mentioned(configBlob, tb)) continue;
|
|
137
|
-
}
|
|
138
|
-
if (binReferenced(name) || mentioned(configBlob, name)) continue; // scripts/config keep it alive
|
|
139
|
-
const ecosystem = CONFIG_ECOSYSTEM_RE.test(name);
|
|
140
|
-
const dev = section !== "dependencies";
|
|
141
|
-
if (ecosystem && dev) continue; // config-convention devDeps: too FP-prone to flag at all
|
|
142
|
-
findings.push(makeFinding({
|
|
143
|
-
category: "unused",
|
|
144
|
-
rule: "unused-dep",
|
|
145
|
-
severity: dev ? "info" : "low",
|
|
146
|
-
confidence: dev || ecosystem ? "low" : "medium",
|
|
147
|
-
title: `Unused ${section === "dependencies" ? "dependency" : section.replace(/ies$/, "y")}: ${name}`,
|
|
148
|
-
reason: "No indexed package import, package-script command, recognized config mention, framework peer contract, or implicit style-compiler input uses this declaration.",
|
|
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,
|
|
151
|
-
package: name,
|
|
152
|
-
evidence: [{ file: manifest, line: 0, snippet: `declared in ${section}` }],
|
|
153
|
-
source: "internal",
|
|
154
|
-
actionability: "MANIFEST_REVIEW_REQUIRED",
|
|
155
|
-
autoRemove: false,
|
|
156
|
-
verification: {
|
|
157
|
-
evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
|
|
158
|
-
manifestDeclaration: { status: "FOUND", file: manifest, section, version: String(deps[name] || "") },
|
|
159
|
-
indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0 },
|
|
160
|
-
packageScripts: { status: "CHECKED", matched: false },
|
|
161
|
-
knownConfigs: { status: "CHECKED", filesScanned: configTexts.size, matched: false },
|
|
162
|
-
frameworkPeerContract: "NOT_FOUND",
|
|
163
|
-
implicitCompilerInput: "NOT_FOUND",
|
|
164
|
-
dynamicOrPluginUsage: "NOT_PROVEN_ABSENT",
|
|
165
|
-
decision: "REVIEW_REQUIRED",
|
|
166
|
-
},
|
|
167
|
-
fixHint: `Review manifest/config/plugin usage; if confirmed unused, remove ${name} with the package manager and run targeted tests`,
|
|
168
|
-
...meta,
|
|
169
|
-
}));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ---- missing (phantom) dependencies: imported but declared nowhere ----
|
|
174
|
-
for (const [name, use] of usedPackages) {
|
|
175
|
-
if (allDeclared.has(name) || name === selfName || workspacePkgNames.has(name)) continue;
|
|
176
|
-
const files = [...use.files];
|
|
177
|
-
if (files.every(isNonRuntimeFile) || isGeneratedNapiPackage(name, files)) continue;
|
|
178
|
-
const testOnly = files.every((f) => /(^|[/\\])(test|tests|__tests__|spec|e2e|__mocks__)([/\\]|$)|[._-](test|spec)\.[a-z]+$/i.test(f));
|
|
179
|
-
const stylesheetOnly = [...use.specs].length > 0 && [...use.specs].every(isStylesheetSpecifier);
|
|
180
|
-
const usageReason = stylesheetOnly
|
|
181
|
-
? `Direct stylesheet import(s) resolve through "${name}"; CSS-only imports are build/runtime inputs even without JavaScript bindings.`
|
|
182
|
-
: use.typeOnly
|
|
183
|
-
? `Direct type-only import(s) resolve through "${name}"; the compiler still requires a declared package.`
|
|
184
|
-
: `Direct source import(s) resolve through "${name}", but the nearest package manifest does not declare it.`;
|
|
185
|
-
findings.push(makeFinding({
|
|
186
|
-
category: "unused",
|
|
187
|
-
rule: "missing-dep",
|
|
188
|
-
severity: testOnly ? "low" : "medium",
|
|
189
|
-
confidence: "high",
|
|
190
|
-
title: `Missing dependency: ${name}`,
|
|
191
|
-
reason: usageReason,
|
|
192
|
-
detail: `"${name}" is imported by ${files.length} file(s) in scope ${meta.scope} but not declared in ${manifest} — it only works via a transitive install (phantom dependency) and can break on any lockfile change.`,
|
|
193
|
-
package: name,
|
|
194
|
-
file: files[0],
|
|
195
|
-
line: use.lines.get(files[0]) || 0,
|
|
196
|
-
evidence: files.slice(0, 5).map((f) => ({ file: f, line: use.lines.get(f) || 0, snippet: "" })),
|
|
197
|
-
source: "internal",
|
|
198
|
-
actionability: "STRONG_MANIFEST_MISMATCH",
|
|
199
|
-
autoInstall: false,
|
|
200
|
-
verification: {
|
|
201
|
-
evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
|
|
202
|
-
manifestDeclaration: { status: "NOT_FOUND", file: manifest, checkedSections: Object.keys(sections) },
|
|
203
|
-
indexedSourceImports: {
|
|
204
|
-
status: "FOUND",
|
|
205
|
-
completeness: "COMPLETE_FOR_GRAPH_SCOPE",
|
|
206
|
-
count: files.length,
|
|
207
|
-
files: files.slice(0, 20),
|
|
208
|
-
truncated: files.length > 20,
|
|
209
|
-
typeOnly: use.typeOnly,
|
|
210
|
-
stylesheetOnly,
|
|
211
|
-
testOnly,
|
|
212
|
-
},
|
|
213
|
-
packageScope: meta.scope,
|
|
214
|
-
transitiveResolutionRisk: "PRESENT",
|
|
215
|
-
decision: "DECLARE_AFTER_SCOPE_REVIEW",
|
|
216
|
-
},
|
|
217
|
-
fixHint: `npm install ${testOnly ? "--save-dev " : ""}${name}`,
|
|
218
|
-
...meta,
|
|
219
|
-
}));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ---- duplicate declarations (same package in several sections) ----
|
|
223
|
-
const seenIn = new Map();
|
|
224
|
-
for (const [section, deps] of Object.entries(sections)) for (const name of Object.keys(deps)) (seenIn.get(name) || seenIn.set(name, []).get(name)).push(section);
|
|
225
|
-
for (const [name, ss] of seenIn) {
|
|
226
|
-
if (ss.length < 2) continue;
|
|
227
|
-
if (ss.includes("peerDependencies") && ss.includes("devDependencies") && ss.length === 2) continue; // standard lib-author pattern
|
|
228
|
-
findings.push(makeFinding({
|
|
229
|
-
category: "unused",
|
|
230
|
-
rule: "duplicate-dep",
|
|
231
|
-
severity: "info",
|
|
232
|
-
confidence: "high",
|
|
233
|
-
title: `Duplicate declaration: ${name}`,
|
|
234
|
-
reason: `The same package is declared in multiple manifest sections: ${ss.join(" + ")}.`,
|
|
235
|
-
detail: `"${name}" is declared in ${ss.join(" + ")} — npm resolves one of them; keep a single section.`,
|
|
236
|
-
package: name,
|
|
237
|
-
source: "internal",
|
|
238
|
-
actionability: "MANIFEST_REVIEW_REQUIRED",
|
|
239
|
-
autoRemove: false,
|
|
240
|
-
verification: {
|
|
241
|
-
evidenceModel: "MANIFEST_ONLY",
|
|
242
|
-
manifestDeclaration: { status: "DUPLICATE", file: manifest, sections: ss },
|
|
243
|
-
decision: "REVIEW_REQUIRED",
|
|
244
|
-
},
|
|
245
|
-
...meta,
|
|
246
|
-
}));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ---- unresolved local imports (broken relative/alias paths) ----
|
|
250
|
-
const unresolvedSeen = new Set();
|
|
251
|
-
let unresolvedCount = 0;
|
|
252
|
-
for (const e of externalImports) {
|
|
253
|
-
if (!e.unresolved) continue;
|
|
254
|
-
if (isNonRuntimeFile(e.file) || isGeneratedNapiBinary(e)) continue;
|
|
255
|
-
unresolvedCount++;
|
|
256
|
-
const key = e.file + "|" + e.spec;
|
|
257
|
-
if (unresolvedSeen.has(key) || unresolvedSeen.size >= 100) continue; // cap the findings, keep the count honest
|
|
258
|
-
unresolvedSeen.add(key);
|
|
259
|
-
findings.push(makeFinding({
|
|
260
|
-
category: "structure",
|
|
261
|
-
rule: "unresolved-import",
|
|
262
|
-
severity: "low",
|
|
263
|
-
confidence: "medium",
|
|
264
|
-
title: `Unresolved import: ${e.spec}`,
|
|
265
|
-
detail: `${e.file}:${e.line} imports "${e.spec}", which resolves to no file in the repo (broken path, missing alias target, or a file type the graph doesn't index).`,
|
|
266
|
-
file: e.file,
|
|
267
|
-
line: e.line || 0,
|
|
268
|
-
source: "internal",
|
|
269
|
-
...meta,
|
|
270
|
-
}));
|
|
271
|
-
}
|
|
272
|
-
if (unresolvedCount > unresolvedSeen.size) {
|
|
273
|
-
findings.push(makeFinding({
|
|
274
|
-
category: "structure",
|
|
275
|
-
rule: "unresolved-import",
|
|
276
|
-
severity: "info",
|
|
277
|
-
confidence: "high",
|
|
278
|
-
title: `…and ${unresolvedCount - unresolvedSeen.size} more unresolved imports`,
|
|
279
|
-
detail: "Finding list capped at 100 unique unresolved imports; the count above is the true total.",
|
|
280
|
-
source: "internal",
|
|
281
|
-
...meta,
|
|
282
|
-
}));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return { findings, usedPackages, declared: allDeclared };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export const computeScopedDepFindings = createScopedDepFindings(computeDepFindings)
|
|
289
|
-
|
|
290
|
-
export { computeGoDepFindings, computePyDepFindings } from "./dep-check-ecosystems.js";
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Stable facade for structural dependency analysis.
|
|
2
|
-
export {
|
|
3
|
-
buildFileImportGraph,
|
|
4
|
-
checkBoundaries,
|
|
5
|
-
findOrphans,
|
|
6
|
-
findSccs,
|
|
7
|
-
globToRe,
|
|
8
|
-
representativeCycle,
|
|
9
|
-
} from './structure/dependency-graph.js'
|
|
10
|
-
export {computeStructureFindings, isRustModuleTreeComponent} from './structure/findings.js'
|
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
])
|
|
@@ -1,51 +0,0 @@
|
|
|
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}/`)
|
|
9
|
-
|
|
10
|
-
// Bind the ecosystem-specific core without introducing a facade/helper import cycle.
|
|
11
|
-
export function createScopedDepFindings(computeDepFindings) {
|
|
12
|
-
return function computeScopedDepFindings({
|
|
13
|
-
externalImports = [], packageScopes = [], workspacePkgNames = new Set(), configTexts = new Map(),
|
|
14
|
-
sourceTexts = new Map(), nonRuntimeRoots = [], sourceFiles = [],
|
|
15
|
-
} = {}) {
|
|
16
|
-
const scopes = packageScopes.length
|
|
17
|
-
? packageScopes.map((scope) => ({...scope, root: normalizeDependencyScope(scope.root)})).sort((a, b) => b.root.length - a.root.length)
|
|
18
|
-
: [{root: '', manifest: 'package.json', pkg: {}, aliases: []}]
|
|
19
|
-
const importsByScope = new Map(scopes.map((scope) => [scope, []]))
|
|
20
|
-
const sourceFilesByScope = new Map(scopes.map((scope) => [scope, []]))
|
|
21
|
-
const sourceTextsByScope = new Map(scopes.map((scope) => [scope, new Map()]))
|
|
22
|
-
for (const item of externalImports) {
|
|
23
|
-
const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, item.file)) || scopes[scopes.length - 1]
|
|
24
|
-
importsByScope.get(owner).push(item)
|
|
25
|
-
}
|
|
26
|
-
for (const file of sourceFiles) {
|
|
27
|
-
const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, file)) || scopes[scopes.length - 1]
|
|
28
|
-
sourceFilesByScope.get(owner).push(file)
|
|
29
|
-
}
|
|
30
|
-
for (const [file, text] of sourceTexts) {
|
|
31
|
-
const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, file)) || scopes[scopes.length - 1]
|
|
32
|
-
sourceTextsByScope.get(owner).set(file, text)
|
|
33
|
-
}
|
|
34
|
-
const configOwner = new Map()
|
|
35
|
-
for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => dependencyScopeOwnsFile(scope.root, file)) || scopes[scopes.length - 1])
|
|
36
|
-
const findings = [], usedPackages = new Map(), declared = new Set()
|
|
37
|
-
for (const scope of scopes) {
|
|
38
|
-
const scopeConfig = new Map([...configTexts].filter(([file]) => configOwner.get(file) === scope))
|
|
39
|
-
const result = computeDepFindings({
|
|
40
|
-
externalImports: importsByScope.get(scope), pkg: scope.pkg || {}, workspacePkgNames,
|
|
41
|
-
configTexts: scopeConfig, sourceTexts: sourceTextsByScope.get(scope), aliases: scope.aliases || [], scope: scope.root,
|
|
42
|
-
manifest: scope.manifest || (scope.root ? `${scope.root}/package.json` : 'package.json'),
|
|
43
|
-
nonRuntimeRoots, sourceFiles: sourceFilesByScope.get(scope),
|
|
44
|
-
})
|
|
45
|
-
findings.push(...result.findings)
|
|
46
|
-
for (const [name, use] of result.usedPackages) usedPackages.set(`${scope.root || '.'}:${name}`, use)
|
|
47
|
-
for (const name of result.declared) declared.add(`${scope.root || '.'}:${name}`)
|
|
48
|
-
}
|
|
49
|
-
return {findings, usedPackages, declared}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import {createHash} from 'node:crypto'
|
|
2
|
-
import {writeFileSync} from 'node:fs'
|
|
3
|
-
import {join} from 'node:path'
|
|
4
|
-
import {computeDuplicates} from './duplicates.js'
|
|
5
|
-
import {withGitRefCheckout} from './git-ref-graph.js'
|
|
6
|
-
import {buildInternalGraph} from '../graph/internal-builder.js'
|
|
7
|
-
import {filterGraphForMode} from '../graph/graph-filter.js'
|
|
8
|
-
|
|
9
|
-
const NON_PRODUCT = new Set(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
10
|
-
const eligible = (fragment, settings) => {
|
|
11
|
-
const classes = new Set(fragment.classes || [])
|
|
12
|
-
if (fragment.n < settings.tokMin) return false
|
|
13
|
-
if (settings.skipTests && (fragment.test || classes.has('test') || classes.has('e2e'))) return false
|
|
14
|
-
return settings.includeClassified || !(fragment.excluded || [...classes].some((name) => NON_PRODUCT.has(name)))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function groupPairs(data, settings) {
|
|
18
|
-
const fragments = data.frags || []
|
|
19
|
-
const pairs = (data.modes?.[settings.mode] || []).filter(([a, b, similarity]) => similarity >= settings.simMin && eligible(fragments[a], settings) && eligible(fragments[b], settings))
|
|
20
|
-
const parent = new Map()
|
|
21
|
-
const find = (value) => { let root = value; while (parent.get(root) !== root) root = parent.get(root); return root }
|
|
22
|
-
for (const [a, b] of pairs) {
|
|
23
|
-
if (!parent.has(a)) parent.set(a, a)
|
|
24
|
-
if (!parent.has(b)) parent.set(b, b)
|
|
25
|
-
parent.set(find(a), find(b))
|
|
26
|
-
}
|
|
27
|
-
const groups = new Map()
|
|
28
|
-
for (const [a, b, similarity] of pairs) {
|
|
29
|
-
const root = find(a)
|
|
30
|
-
if (!groups.has(root)) groups.set(root, {members: new Set(), maxSim: 0})
|
|
31
|
-
const group = groups.get(root)
|
|
32
|
-
group.members.add(a); group.members.add(b); group.maxSim = Math.max(group.maxSim, similarity)
|
|
33
|
-
}
|
|
34
|
-
return [...groups.values()].map((group) => {
|
|
35
|
-
const members = [...group.members].map((index) => fragments[index]).sort((a, b) => b.n - a.n)
|
|
36
|
-
return {members, maxSim: group.maxSim, tokens: members.reduce((sum, item) => sum + item.n, 0)}
|
|
37
|
-
}).sort((a, b) => b.tokens - a.tokens)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function isFrameworkBoilerplateCloneGroup(group) {
|
|
41
|
-
const members = Array.isArray(group?.members) ? group.members : []
|
|
42
|
-
if (members.length < 2) return false
|
|
43
|
-
return members.every((member) => /(?:^|\/)\w[^/]*\.router\.[cm]?[jt]s$/i.test(String(member.file || '').replace(/\\/g, '/'))
|
|
44
|
-
&& /^(?:router|routes)\(?\)?$/i.test(String(member.label || '').trim()))
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function isDeclarativeCatalogCloneGroup(group) {
|
|
48
|
-
const members = Array.isArray(group?.members) ? group.members : []
|
|
49
|
-
return members.length >= 2 && members.every((member) => member.declarative === true)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function analyzeDuplicateGroups(repoRoot, graphPath, args = {}) {
|
|
53
|
-
const settings = {
|
|
54
|
-
simMin: Math.min(100, Math.max(50, Number(args.min_similarity) || 80)),
|
|
55
|
-
tokMin: Math.min(400, Math.max(12, Number(args.min_tokens) || 50)),
|
|
56
|
-
mode: args.mode === 'strict' ? 'strict' : 'renamed',
|
|
57
|
-
skipTests: args.include_tests !== true,
|
|
58
|
-
includeClassified: args.include_classified === true || args.include_non_product === true,
|
|
59
|
-
includeBoilerplate: args.include_boilerplate === true,
|
|
60
|
-
includeDeclarative: args.include_declarative === true,
|
|
61
|
-
}
|
|
62
|
-
const data = computeDuplicates(repoRoot, graphPath, {includeStrings: args.include_strings === true, minTokens: settings.tokMin})
|
|
63
|
-
const allGroups = groupPairs(data, settings)
|
|
64
|
-
const afterBoilerplate = settings.includeBoilerplate ? allGroups : allGroups.filter((group) => !isFrameworkBoilerplateCloneGroup(group))
|
|
65
|
-
const groups = settings.includeDeclarative ? afterBoilerplate : afterBoilerplate.filter((group) => !isDeclarativeCatalogCloneGroup(group))
|
|
66
|
-
const suppressed = (data.frags || []).filter((fragment) => !eligible(fragment, settings)).length
|
|
67
|
-
return {
|
|
68
|
-
settings, groups, suppressed,
|
|
69
|
-
boilerplateSuppressed: allGroups.length - afterBoilerplate.length,
|
|
70
|
-
declarativeSuppressed: afterBoilerplate.length - groups.length,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const digest = (value) => createHash('sha256').update(value).digest('hex').slice(0, 20)
|
|
75
|
-
function groupKey(group, mode) {
|
|
76
|
-
const members = group.members.map((fragment) => {
|
|
77
|
-
const fingerprints = Array.isArray(fragment.fp?.[mode]) ? [...fragment.fp[mode]].sort() : []
|
|
78
|
-
return `${String(fragment.file).replace(/\\/g, '/')}\0${fragment.label || ''}\0${digest(JSON.stringify(fingerprints))}`
|
|
79
|
-
}).sort()
|
|
80
|
-
return digest(members.join('\n'))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function compareDuplicateGroups({repoRoot, graphPath, currentGraph, baseRef, changedFiles = [], args = {}}) {
|
|
84
|
-
const current = analyzeDuplicateGroups(repoRoot, graphPath, args)
|
|
85
|
-
const mode = ['full', 'no-tests', 'tests-only'].includes(currentGraph?.graphBuildMode) ? currentGraph.graphBuildMode : 'full'
|
|
86
|
-
const baseline = await withGitRefCheckout(repoRoot, baseRef, async (checkout) => {
|
|
87
|
-
let graph = await buildInternalGraph(checkout)
|
|
88
|
-
if (mode !== 'full') graph = filterGraphForMode(graph, mode, {repoRoot: checkout})
|
|
89
|
-
graph.graphBuildMode = mode
|
|
90
|
-
const baselineGraphPath = join(checkout, '.weavatrix-verified-graph.json')
|
|
91
|
-
writeFileSync(baselineGraphPath, JSON.stringify(graph))
|
|
92
|
-
return analyzeDuplicateGroups(checkout, baselineGraphPath, args)
|
|
93
|
-
})
|
|
94
|
-
if (!baseline.ok) return {state: 'UNKNOWN', reason: baseline.error, currentGroups: current.groups.length}
|
|
95
|
-
const baselineKeys = new Set(baseline.value.groups.map((group) => groupKey(group, current.settings.mode)))
|
|
96
|
-
const changed = new Set(changedFiles.map((file) => String(file).replace(/\\/g, '/')))
|
|
97
|
-
const added = current.groups.filter((group) => !baselineKeys.has(groupKey(group, current.settings.mode)))
|
|
98
|
-
const scoped = added.filter((group) => group.members.some((member) => changed.has(String(member.file).replace(/\\/g, '/'))))
|
|
99
|
-
return {
|
|
100
|
-
state: scoped.length ? 'BLOCKED' : 'PASS', baseline: {ref: baseline.ref, commit: baseline.commit},
|
|
101
|
-
currentGroups: current.groups.length, baselineGroups: baseline.value.groups.length, newGroups: added.length,
|
|
102
|
-
scopedNewGroups: scoped.slice(0, 20).map((group) => ({similarity: group.maxSim, tokens: group.tokens, members: group.members.slice(0, 8).map((item) => ({file: item.file, start: item.start, end: item.end, label: item.label}))})),
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// duplicates-worker.js — runs clone detection OFF Electron's main thread (same rationale as
|
|
2
|
-
// graph/build-worker.js: a blocked main event loop freezes every window on Windows). The fingerprint
|
|
3
|
-
// sets never leave the worker; only slim fragment metadata + similarity pairs cross back.
|
|
4
|
-
import { parentPort, workerData } from "node:worker_threads";
|
|
5
|
-
import { computeDuplicates } from "./duplicates.js";
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
parentPort.postMessage(computeDuplicates(workerData.repoPath, workerData.graphJsonPath));
|
|
9
|
-
} catch (e) {
|
|
10
|
-
parentPort.postMessage({ ok: false, error: (e && e.message) || String(e) });
|
|
11
|
-
}
|