weavatrix 0.3.13 → 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.
Files changed (303) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +231 -199
  3. package/bin/native/darwin-arm64/weavatrix +0 -0
  4. package/bin/native/darwin-x64/weavatrix +0 -0
  5. package/bin/native/linux-arm64/weavatrix +0 -0
  6. package/bin/native/linux-x64/weavatrix +0 -0
  7. package/bin/native/win32-arm64/weavatrix.exe +0 -0
  8. package/bin/native/win32-x64/weavatrix.exe +0 -0
  9. package/bin/resolve-binary.mjs +73 -0
  10. package/bin/run-native.mjs +26 -0
  11. package/bin/weavatrix-mcp.mjs +7 -6
  12. package/bin/weavatrix.mjs +5 -0
  13. package/package.json +8 -65
  14. package/skill/SKILL.md +62 -374
  15. package/skill/agents/openai.yaml +4 -0
  16. package/skill/references/tool-routing.md +45 -0
  17. package/SECURITY.md +0 -101
  18. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  19. package/docs/agent-task-benchmark.md +0 -58
  20. package/docs/releases/v0.2.19.md +0 -33
  21. package/docs/releases/v0.3.0.md +0 -43
  22. package/docs/releases/v0.3.1.md +0 -30
  23. package/docs/releases/v0.3.10.md +0 -76
  24. package/docs/releases/v0.3.11.md +0 -55
  25. package/docs/releases/v0.3.12.md +0 -44
  26. package/docs/releases/v0.3.13.md +0 -34
  27. package/docs/releases/v0.3.2.md +0 -15
  28. package/docs/releases/v0.3.3.md +0 -25
  29. package/docs/releases/v0.3.4.md +0 -40
  30. package/docs/releases/v0.3.5.md +0 -50
  31. package/docs/releases/v0.3.6.md +0 -44
  32. package/docs/releases/v0.3.7.md +0 -37
  33. package/docs/releases/v0.3.8.md +0 -21
  34. package/docs/releases/v0.3.9.md +0 -36
  35. package/scripts/run-agent-task-benchmark.mjs +0 -118
  36. package/src/analysis/allowed-test-runner.js +0 -70
  37. package/src/analysis/architecture/contract-graph.js +0 -119
  38. package/src/analysis/architecture/contract-schema.js +0 -110
  39. package/src/analysis/architecture/contract-storage.js +0 -35
  40. package/src/analysis/architecture/contract-verification.js +0 -168
  41. package/src/analysis/architecture-contract.js +0 -16
  42. package/src/analysis/audit-debt.js +0 -94
  43. package/src/analysis/audit-extensions.js +0 -60
  44. package/src/analysis/cargo-dependency-evidence.js +0 -108
  45. package/src/analysis/cargo-manifests.js +0 -81
  46. package/src/analysis/change-classification/diff-parser.js +0 -103
  47. package/src/analysis/change-classification/options.js +0 -40
  48. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  49. package/src/analysis/change-classification.js +0 -188
  50. package/src/analysis/coverage-reports.js +0 -235
  51. package/src/analysis/cycle-route.js +0 -14
  52. package/src/analysis/data-flow-evidence.js +0 -114
  53. package/src/analysis/dead-check.js +0 -300
  54. package/src/analysis/dead-code-review/candidates.js +0 -194
  55. package/src/analysis/dead-code-review/policy.js +0 -23
  56. package/src/analysis/dead-code-review.js +0 -119
  57. package/src/analysis/dep-check-ecosystems.js +0 -205
  58. package/src/analysis/dep-check.js +0 -290
  59. package/src/analysis/dep-rules.js +0 -10
  60. package/src/analysis/dependency/conventions.js +0 -16
  61. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  62. package/src/analysis/dependency/source-references.js +0 -21
  63. package/src/analysis/duplicate-groups.js +0 -104
  64. package/src/analysis/duplicates-worker.js +0 -11
  65. package/src/analysis/duplicates.compute.js +0 -269
  66. package/src/analysis/duplicates.js +0 -15
  67. package/src/analysis/duplicates.run.js +0 -52
  68. package/src/analysis/duplicates.tokenize.js +0 -199
  69. package/src/analysis/endpoints/common.js +0 -62
  70. package/src/analysis/endpoints/extract.js +0 -107
  71. package/src/analysis/endpoints/inventory.js +0 -84
  72. package/src/analysis/endpoints/mounts.js +0 -129
  73. package/src/analysis/endpoints-java.js +0 -260
  74. package/src/analysis/endpoints-rust.js +0 -124
  75. package/src/analysis/endpoints.js +0 -3
  76. package/src/analysis/findings.js +0 -70
  77. package/src/analysis/git-history/analytics.js +0 -177
  78. package/src/analysis/git-history/collector.js +0 -67
  79. package/src/analysis/git-history/options.js +0 -66
  80. package/src/analysis/git-history.js +0 -143
  81. package/src/analysis/git-ref-graph.js +0 -76
  82. package/src/analysis/go-dependency-evidence.js +0 -62
  83. package/src/analysis/graph-analysis.aggregate.js +0 -295
  84. package/src/analysis/graph-analysis.edges.js +0 -24
  85. package/src/analysis/graph-analysis.js +0 -7
  86. package/src/analysis/graph-analysis.refs.js +0 -154
  87. package/src/analysis/graph-analysis.summaries.js +0 -65
  88. package/src/analysis/health-capabilities.js +0 -83
  89. package/src/analysis/hot-path-review.js +0 -266
  90. package/src/analysis/http-contract-wrappers.js +0 -241
  91. package/src/analysis/http-contracts/analysis.js +0 -179
  92. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  93. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  94. package/src/analysis/http-contracts/graph-context.js +0 -143
  95. package/src/analysis/http-contracts/matching.js +0 -61
  96. package/src/analysis/http-contracts/shared.js +0 -85
  97. package/src/analysis/http-contracts.js +0 -6
  98. package/src/analysis/internal-audit/dependency-health.js +0 -113
  99. package/src/analysis/internal-audit/python-manifests.js +0 -78
  100. package/src/analysis/internal-audit/repo-files.js +0 -50
  101. package/src/analysis/internal-audit/supply-chain.js +0 -149
  102. package/src/analysis/internal-audit.collect.js +0 -213
  103. package/src/analysis/internal-audit.js +0 -9
  104. package/src/analysis/internal-audit.reach.js +0 -173
  105. package/src/analysis/internal-audit.run.js +0 -256
  106. package/src/analysis/java-source.js +0 -36
  107. package/src/analysis/jvm-artifact-index.js +0 -87
  108. package/src/analysis/jvm-dependency-evidence.js +0 -138
  109. package/src/analysis/jvm-manifests.js +0 -114
  110. package/src/analysis/manifests.js +0 -123
  111. package/src/analysis/package-reachability.js +0 -39
  112. package/src/analysis/source-complexity.ast.js +0 -97
  113. package/src/analysis/source-complexity.constants.js +0 -55
  114. package/src/analysis/source-complexity.js +0 -14
  115. package/src/analysis/source-complexity.report.js +0 -81
  116. package/src/analysis/source-complexity.walk.js +0 -228
  117. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  118. package/src/analysis/source-correctness.js +0 -222
  119. package/src/analysis/static-test-reachability.js +0 -150
  120. package/src/analysis/structure/dependency-graph.js +0 -151
  121. package/src/analysis/structure/findings.js +0 -162
  122. package/src/analysis/task-retrieval.js +0 -107
  123. package/src/analysis/transport-contracts.js +0 -224
  124. package/src/analysis/transport-runtime-evidence.js +0 -155
  125. package/src/analysis-kit.mjs +0 -17
  126. package/src/bounds.js +0 -4
  127. package/src/build-graph.js +0 -209
  128. package/src/child-env.js +0 -7
  129. package/src/config.js +0 -5
  130. package/src/extension/local-services.mjs +0 -75
  131. package/src/git-exec.js +0 -67
  132. package/src/graph/build-worker.js +0 -49
  133. package/src/graph/builder/go-receiver-resolution.js +0 -112
  134. package/src/graph/builder/js/dynamic-import.js +0 -26
  135. package/src/graph/builder/js/queries.js +0 -48
  136. package/src/graph/builder/lang-csharp.js +0 -40
  137. package/src/graph/builder/lang-css.js +0 -29
  138. package/src/graph/builder/lang-go.js +0 -140
  139. package/src/graph/builder/lang-html.js +0 -29
  140. package/src/graph/builder/lang-java-calls.js +0 -142
  141. package/src/graph/builder/lang-java.js +0 -215
  142. package/src/graph/builder/lang-js.js +0 -291
  143. package/src/graph/builder/lang-python.js +0 -235
  144. package/src/graph/builder/lang-rust-calls.js +0 -63
  145. package/src/graph/builder/lang-rust.js +0 -299
  146. package/src/graph/builder/lang-solidity.js +0 -126
  147. package/src/graph/builder/lang-sql.js +0 -263
  148. package/src/graph/builder/pass2-resolution.js +0 -81
  149. package/src/graph/builder/spec-pkg.js +0 -87
  150. package/src/graph/community.js +0 -27
  151. package/src/graph/edge-provenance.js +0 -31
  152. package/src/graph/file-lock.js +0 -69
  153. package/src/graph/freshness-probe.js +0 -142
  154. package/src/graph/graph-filter.js +0 -63
  155. package/src/graph/incremental-refresh.js +0 -235
  156. package/src/graph/internal-builder.barrels.js +0 -235
  157. package/src/graph/internal-builder.build.js +0 -300
  158. package/src/graph/internal-builder.java.js +0 -33
  159. package/src/graph/internal-builder.js +0 -12
  160. package/src/graph/internal-builder.langs.js +0 -157
  161. package/src/graph/internal-builder.pass2.js +0 -163
  162. package/src/graph/internal-builder.resolvers.js +0 -286
  163. package/src/graph/layout.js +0 -51
  164. package/src/graph/node-id.js +0 -7
  165. package/src/graph/parser-artifact-boundary.js +0 -55
  166. package/src/graph/relations.js +0 -4
  167. package/src/graph/repo-registry.js +0 -124
  168. package/src/graph/resolvers/rust.js +0 -117
  169. package/src/infra/infra-items.js +0 -255
  170. package/src/infra/infra-registry.js +0 -184
  171. package/src/infra/infra.detect.js +0 -119
  172. package/src/infra/infra.js +0 -38
  173. package/src/infra/infra.match.js +0 -102
  174. package/src/infra/infra.scan.js +0 -93
  175. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  176. package/src/mcp/architecture-bootstrap.mjs +0 -168
  177. package/src/mcp/architecture-starter.mjs +0 -234
  178. package/src/mcp/catalog.mjs +0 -233
  179. package/src/mcp/company-contract-page.mjs +0 -142
  180. package/src/mcp/company-contract-reconcile.mjs +0 -87
  181. package/src/mcp/company-contract-verdict.mjs +0 -42
  182. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  183. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  184. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  185. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  186. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  187. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  188. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  189. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  190. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  191. package/src/mcp/evidence-snapshot.mjs +0 -50
  192. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  193. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  194. package/src/mcp/extension-api.mjs +0 -77
  195. package/src/mcp/git-output.mjs +0 -7
  196. package/src/mcp/graph/context-core.mjs +0 -111
  197. package/src/mcp/graph/context-seeds.mjs +0 -207
  198. package/src/mcp/graph/context-state.mjs +0 -85
  199. package/src/mcp/graph/reverse-reach.mjs +0 -42
  200. package/src/mcp/graph/tools-core.mjs +0 -143
  201. package/src/mcp/graph/tools-query.mjs +0 -276
  202. package/src/mcp/graph-context.mjs +0 -5
  203. package/src/mcp/graph-diff.mjs +0 -218
  204. package/src/mcp/health/audit-format.mjs +0 -199
  205. package/src/mcp/health/audit.mjs +0 -174
  206. package/src/mcp/health/dead-code.mjs +0 -110
  207. package/src/mcp/health/duplicates.mjs +0 -85
  208. package/src/mcp/health/endpoints.mjs +0 -71
  209. package/src/mcp/health/structure.mjs +0 -219
  210. package/src/mcp/runtime-version.mjs +0 -31
  211. package/src/mcp/server/auto-refresh.mjs +0 -66
  212. package/src/mcp/server/runtime-config.mjs +0 -43
  213. package/src/mcp/server/shutdown.mjs +0 -40
  214. package/src/mcp/staleness-notice.mjs +0 -20
  215. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  216. package/src/mcp/sync/evidence-common.mjs +0 -88
  217. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  218. package/src/mcp/sync/evidence-health.mjs +0 -56
  219. package/src/mcp/sync/evidence-packages.mjs +0 -95
  220. package/src/mcp/sync/payload-common.mjs +0 -97
  221. package/src/mcp/sync/payload-v2.mjs +0 -54
  222. package/src/mcp/sync/payload-v3.mjs +0 -79
  223. package/src/mcp/sync-evidence.mjs +0 -23
  224. package/src/mcp/sync-payload.mjs +0 -70
  225. package/src/mcp/tool-result.mjs +0 -54
  226. package/src/mcp/tools-actions.mjs +0 -11
  227. package/src/mcp/tools-architecture.mjs +0 -177
  228. package/src/mcp/tools-company.mjs +0 -251
  229. package/src/mcp/tools-context.mjs +0 -222
  230. package/src/mcp/tools-endpoints.mjs +0 -207
  231. package/src/mcp/tools-graph-hubs.mjs +0 -78
  232. package/src/mcp/tools-graph.mjs +0 -17
  233. package/src/mcp/tools-health.mjs +0 -24
  234. package/src/mcp/tools-history.mjs +0 -28
  235. package/src/mcp/tools-impact-change.mjs +0 -220
  236. package/src/mcp/tools-impact-precision.mjs +0 -89
  237. package/src/mcp/tools-impact.mjs +0 -169
  238. package/src/mcp/tools-source.mjs +0 -238
  239. package/src/mcp/tools-verified-change.mjs +0 -200
  240. package/src/mcp-rg.mjs +0 -50
  241. package/src/mcp-runtime.mjs +0 -2
  242. package/src/mcp-server.mjs +0 -298
  243. package/src/mcp-source-tools.mjs +0 -163
  244. package/src/path-classification.js +0 -220
  245. package/src/path-ignore.js +0 -69
  246. package/src/precision/lsp-client/constants.js +0 -12
  247. package/src/precision/lsp-client/environment.js +0 -20
  248. package/src/precision/lsp-client/errors.js +0 -15
  249. package/src/precision/lsp-client/lifecycle.js +0 -114
  250. package/src/precision/lsp-client/message-parser.js +0 -81
  251. package/src/precision/lsp-client/protocol.js +0 -212
  252. package/src/precision/lsp-client/registry.js +0 -59
  253. package/src/precision/lsp-client/repo-uri.js +0 -60
  254. package/src/precision/lsp-client/stdio-client.js +0 -140
  255. package/src/precision/lsp-client.js +0 -10
  256. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  257. package/src/precision/lsp-overlay/build.js +0 -261
  258. package/src/precision/lsp-overlay/contract.js +0 -61
  259. package/src/precision/lsp-overlay/reference-results.js +0 -108
  260. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  261. package/src/precision/lsp-overlay/source-session.js +0 -142
  262. package/src/precision/lsp-overlay/store.js +0 -150
  263. package/src/precision/lsp-overlay/target-index.js +0 -152
  264. package/src/precision/lsp-overlay/target-query.js +0 -55
  265. package/src/precision/lsp-overlay.js +0 -11
  266. package/src/precision/symbol-query.js +0 -224
  267. package/src/precision/typescript-lsp-provider.js +0 -12
  268. package/src/precision/typescript-provider/client.js +0 -89
  269. package/src/precision/typescript-provider/discovery.js +0 -124
  270. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  271. package/src/precision/typescript-provider/project-host.js +0 -254
  272. package/src/precision/typescript-provider/project-safety.js +0 -166
  273. package/src/precision/typescript-provider/reference-usage.js +0 -53
  274. package/src/process.js +0 -95
  275. package/src/repo-path.js +0 -53
  276. package/src/scan/discover.inventory.js +0 -78
  277. package/src/scan/discover.js +0 -5
  278. package/src/scan/discover.list.js +0 -79
  279. package/src/scan/discover.stack.js +0 -227
  280. package/src/scan/search.core.js +0 -24
  281. package/src/scan/search.git.js +0 -102
  282. package/src/scan/search.js +0 -9
  283. package/src/scan/search.node.js +0 -83
  284. package/src/scan/search.preview.js +0 -49
  285. package/src/scan/search.rg.js +0 -145
  286. package/src/security/advisory-store.js +0 -172
  287. package/src/security/installed-jvm-rust.js +0 -46
  288. package/src/security/installed.js +0 -280
  289. package/src/security/malware-file-heuristics.js +0 -121
  290. package/src/security/malware-heuristics.exclusions.js +0 -134
  291. package/src/security/malware-heuristics.js +0 -15
  292. package/src/security/malware-heuristics.roots.js +0 -145
  293. package/src/security/malware-heuristics.scan.js +0 -89
  294. package/src/security/malware-heuristics.sweep.js +0 -122
  295. package/src/security/malware-scoring.js +0 -105
  296. package/src/security/match.js +0 -85
  297. package/src/security/registry-sig.classify.js +0 -137
  298. package/src/security/registry-sig.js +0 -18
  299. package/src/security/registry-sig.rules.js +0 -192
  300. package/src/security/rust-advisory-report.js +0 -60
  301. package/src/security/typosquat.js +0 -81
  302. package/src/util.js +0 -40
  303. package/src/version.js +0 -7
@@ -1,119 +0,0 @@
1
- // Conservative, source-free review queue for statically unreferenced code. This deliberately builds
2
- // on computeDead instead of inventing a second liveness model. Candidates are evidence for review,
3
- // never deletion instructions: framework entry, dynamic loading, reflection and public API surfaces
4
- // lower confidence and remain explicit in the returned record. Per-candidate record shapes live in
5
- // dead-code-review/candidates.js; this module owns collection, filtering and the summary envelope.
6
- import { computeDead } from "./dead-check.js";
7
- import { createPathClassifier } from "../path-classification.js";
8
- import {
9
- DEAD_CODE_CONFIDENCE_RANK as CONFIDENCE_RANK, hasDynamicCode,
10
- REFLECTION_CODE_RE as REFLECTION_RE, deadCodePathAllowed as pathAllowed,
11
- normalizedReviewPath as normalizedPath,
12
- } from './dead-code-review/policy.js'
13
- import { symbolCandidate, fileCandidate } from './dead-code-review/candidates.js'
14
-
15
- // Pure review model. Filesystem collection/entry inference stays in the MCP adapter so tests can pass
16
- // exact source maps and convention evidence without touching the working tree.
17
- export function computeDeadCodeReview(graph, sources, options = {}) {
18
- const entrySet = options.entrySet instanceof Set ? options.entrySet : new Set(options.entrySet || []);
19
- const dynamicTargets = options.dynamicTargets instanceof Set ? options.dynamicTargets : new Set(options.dynamicTargets || []);
20
- const frameworkByFile = new Map((options.frameworkEvidence || []).map((entry) => [normalizedPath(entry.file), entry]));
21
- const classifier = options.pathClassifier || createPathClassifier(null);
22
- const classificationCache = new Map();
23
- const classify = (file, source) => {
24
- if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, { content: source }));
25
- return classificationCache.get(file);
26
- };
27
- const repoSignals = {
28
- dynamicLoading: (graph.externalImports || []).some((entry) => entry?.dynamic) || [...sources].some(([file, text]) => hasDynamicCode(text, file)),
29
- reflection: [...sources.values()].some((text) => REFLECTION_RE.test(String(text || ""))),
30
- };
31
- const includeTests = options.includeTests === true;
32
- const includeClassified = options.includeClassified === true;
33
- const minConfidence = Object.hasOwn(CONFIDENCE_RANK, options.minConfidence) ? options.minConfidence : "medium";
34
- const pathPrefix = normalizedPath(options.path || "").replace(/\/+$/, "");
35
- const requestedKinds = new Set(Array.isArray(options.kinds) && options.kinds.length ? options.kinds : ["file", "function", "method", "symbol"]);
36
- const exactNoReferenceIds = new Set(graph.precisionNoReferenceSymbols || options.exactNoReferenceIds || []);
37
- const context = { sources, entrySet, dynamicTargets, frameworkByFile, classify, repoSignals, exactNoReferenceIds };
38
- const dead = computeDead(graph, sources, { entrySet });
39
- const nodesById = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
40
- const rawSymbols = [...dead.deadSymbols, ...(dead.testOnlySymbols || []).map((item) => ({...item, testOnly: true}))]
41
- .map((item) => ({ item, node: nodesById.get(String(item.id)) }))
42
- .filter((entry) => entry.node)
43
- .map(({ item, node }) => symbolCandidate(item, node, context));
44
- const symbolsByFile = new Map();
45
- for (const candidate of rawSymbols) {
46
- if (!symbolsByFile.has(candidate.file)) symbolsByFile.set(candidate.file, []);
47
- symbolsByFile.get(candidate.file).push(candidate);
48
- }
49
- const rawFiles = dead.deadFiles.map((item) => fileCandidate(item, symbolsByFile.get(normalizedPath(item.file)) || [], context));
50
- const raw = [...rawSymbols, ...rawFiles];
51
- const suppressed = { tests: 0, classified: 0, confidence: 0, path: 0, kind: 0 };
52
- const candidates = [];
53
- for (const candidate of raw) {
54
- // Node-level test surfaces (Rust #[cfg(test)] symbols in production files) follow the same
55
- // include_tests policy as path-classified test files.
56
- if (!includeTests && nodesById.get(String(candidate.id))?.test_surface === true) { suppressed.tests += 1; continue; }
57
- const info = classify(candidate.file, sources.get(candidate.file));
58
- const allowed = pathAllowed(info, { includeTests, includeClassified });
59
- if (!allowed.ok) { suppressed[allowed.bucket] += 1; continue; }
60
- if (pathPrefix && candidate.file !== pathPrefix && !candidate.file.startsWith(`${pathPrefix}/`)) { suppressed.path += 1; continue; }
61
- if (!requestedKinds.has(candidate.kind)) { suppressed.kind += 1; continue; }
62
- if (CONFIDENCE_RANK[candidate.confidence] > CONFIDENCE_RANK[minConfidence]) { suppressed.confidence += 1; continue; }
63
- candidates.push(candidate);
64
- }
65
- candidates.sort((left, right) =>
66
- CONFIDENCE_RANK[left.confidence] - CONFIDENCE_RANK[right.confidence]
67
- || left.file.localeCompare(right.file)
68
- || left.line - right.line
69
- || left.id.localeCompare(right.id));
70
-
71
- const warnings = [{
72
- code: "STATIC_LIVENESS_IS_NOT_RUNTIME_PROOF",
73
- message: "No static reference is not proof of dead runtime code. Review every candidate; never bulk-delete or auto-delete.",
74
- }];
75
- if (repoSignals.dynamicLoading) warnings.push({
76
- code: "DYNAMIC_LOADING_PRESENT",
77
- message: "Dynamic loading exists in the repository; static callers may be incomplete.",
78
- });
79
- if (repoSignals.reflection) warnings.push({
80
- code: "REFLECTION_PRESENT",
81
- message: "Reflection-like APIs exist in the repository; public or name-addressed symbols may be invoked without graph edges.",
82
- });
83
- if (suppressed.confidence) warnings.push({
84
- code: "LOW_CONFIDENCE_SUPPRESSED",
85
- message: `${suppressed.confidence} low-confidence candidate(s), including public/framework-sensitive code, were suppressed; set min_confidence=low to review them explicitly.`,
86
- });
87
-
88
- return {
89
- candidates,
90
- warnings,
91
- suppressed,
92
- repoSignals,
93
- totals: {
94
- indexedSymbols: dead.stats.symbols,
95
- indexedFiles: dead.stats.files,
96
- rawDeadSymbols: rawSymbols.length,
97
- rawTestOnlySymbols: (dead.testOnlySymbols || []).length,
98
- rawDeadFiles: rawFiles.length,
99
- reviewCandidates: candidates.length,
100
- byConfidence: {
101
- high: candidates.filter((candidate) => candidate.confidence === "high").length,
102
- medium: candidates.filter((candidate) => candidate.confidence === "medium").length,
103
- low: candidates.filter((candidate) => candidate.confidence === "low").length,
104
- },
105
- byEvidenceTier: {
106
- strongStatic: candidates.filter((candidate) => candidate.evidenceTier === "STRONG_STATIC_EVIDENCE").length,
107
- boundedStatic: candidates.filter((candidate) => candidate.evidenceTier === "BOUNDED_STATIC_EVIDENCE").length,
108
- highUncertainty: candidates.filter((candidate) => candidate.evidenceTier === "HIGH_UNCERTAINTY").length,
109
- },
110
- },
111
- policy: {
112
- verdict: "REVIEW_REQUIRED",
113
- autoDelete: false,
114
- minConfidence,
115
- includeTests,
116
- includeClassified,
117
- },
118
- };
119
- }
@@ -1,205 +0,0 @@
1
- import { makeFinding } from "./findings.js";
2
-
3
- const TEST_PATH_RE = /(^|[/\\])(test|tests|__tests__|spec|e2e|__mocks__)([/\\]|$)|[._-](test|spec)\.[a-z0-9]+$|_test\.go$|(^|[/\\])test(?:_[^/\\]*)?\.py$/i;
4
- const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
- const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob);
6
-
7
- // ---- Go: set math over ecosystem:"Go" externalImports vs go.mod requires ----
8
- // goMod = parseGoMod() output. Only DIRECT requires can be "unused" (indirect ones belong to `go mod
9
- // tidy`); replace targets and the own module never count. Missing = imported module with no require
10
- // prefix — rare in Go (builds fail), so it usually flags vendored/replaced setups: keep it medium.
11
- export function computeGoDepFindings({ externalImports = [], goMod = null, nonRuntimeRoots = [] } = {}) {
12
- const findings = [];
13
- if (!goMod || !goMod.module) return { findings, declared: new Set() };
14
- const requires = goMod.requires || [];
15
- const declared = new Set(requires.map((r) => r.path));
16
- const replaced = new Set((goMod.replaces || []).map((r) => r.from));
17
- const moduleOf = (spec) => { let best = ""; for (const p of declared) if ((spec === p || spec.startsWith(p + "/")) && p.length > best.length) best = p; return best; };
18
-
19
- const usedModules = new Set();
20
- const missing = new Map(); // pkg → { files:Set, lines:Map }
21
- const inNonRuntimeRoot = (file) => (nonRuntimeRoots || []).some((root) => {
22
- const f = String(file || "").replace(/\\/g, "/"), r = String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
23
- return !!r && (f === r || f.startsWith(`${r}/`));
24
- });
25
- for (const e of externalImports) {
26
- if (e.ecosystem !== "Go" || e.builtin || e.unresolved || !e.pkg) continue;
27
- const mod = moduleOf(e.spec || e.pkg) || (declared.has(e.pkg) ? e.pkg : "");
28
- if (mod) { usedModules.add(mod); continue; }
29
- let m = missing.get(e.pkg);
30
- if (!m) missing.set(e.pkg, (m = { files: new Set(), lines: new Map() }));
31
- m.files.add(e.file);
32
- if (!m.lines.has(e.file)) m.lines.set(e.file, e.line || 0);
33
- }
34
-
35
- for (const r of requires) {
36
- if (r.indirect || usedModules.has(r.path)) continue;
37
- findings.push(makeFinding({
38
- category: "unused",
39
- rule: "unused-dep",
40
- severity: "low",
41
- confidence: "medium",
42
- title: `Unused Go module: ${r.path}`,
43
- reason: "A direct go.mod requirement has no matching recorded Go import; build-tagged usage remains possible.",
44
- detail: `"${r.path}" is required (direct) in go.mod but no .go file imports it or any of its packages. Build-tag-guarded files can hide usage — confirm with \`go mod tidy\` before removing.`,
45
- package: r.path,
46
- version: r.version,
47
- source: "internal",
48
- fixHint: "go mod tidy (drops requires nothing imports)",
49
- }));
50
- }
51
- for (const [pkg, use] of missing) {
52
- if (replaced.has(pkg)) continue;
53
- const files = [...use.files];
54
- if (files.every(inNonRuntimeRoot)) continue;
55
- findings.push(makeFinding({
56
- category: "unused",
57
- rule: "missing-dep",
58
- severity: "medium",
59
- confidence: "medium",
60
- title: `Missing Go module: ${pkg}`,
61
- reason: "A recorded Go import has no matching direct go.mod requirement or replace entry.",
62
- detail: `"${pkg}" is imported by ${files.length} file(s) but go.mod has no matching require — a replace/workspace/vendor setup, or the module was never added.`,
63
- package: pkg,
64
- file: files[0],
65
- line: use.lines.get(files[0]) || 0,
66
- evidence: files.slice(0, 5).map((f) => ({ file: f, line: use.lines.get(f) || 0, snippet: "" })),
67
- source: "internal",
68
- fixHint: `go get ${pkg}`,
69
- }));
70
- }
71
- return { findings, declared };
72
- }
73
-
74
- // ---- Python: ecosystem:"PyPI" externalImports vs requirements/pyproject/Pipfile ----
75
- // Import→dist naming is heuristic (yaml→PyYAML, python-X/X-python variants), so matching is GENEROUS for
76
- // suppression and every unused finding stays low-confidence. CLI-only tools (pytest, black, gunicorn …)
77
- // and stub/plugin conventions (types-*, *-stubs, pytest-*, flake8-*) are never flagged unused.
78
- const PY_TOOL_DISTS = new Set(("pytest tox nox black ruff flake8 pylint mypy pyright isort bandit coverage pre-commit pip setuptools wheel build twine poetry poetry-core " +
79
- "pip-tools uv virtualenv pipenv gunicorn uwsgi supervisor ipython jupyter jupyterlab notebook ipykernel codecov autopep8 yapf commitizen detect-secrets safety pip-audit hatchling flit flit-core pdm").split(" "));
80
- const pyNorm = (n) => String(n || "").toLowerCase().replace(/[-_.]+/g, "-");
81
-
82
- function computePyDepFindingsFlat({
83
- externalImports = [], pyManifest = null, configTexts = new Map(),
84
- managedDependencies = [], ignoredDependencies = [], nonRuntimeRoots = [],
85
- } = {}) {
86
- const findings = [];
87
- const deps = (pyManifest && pyManifest.deps) || [];
88
- const present = !!(pyManifest && pyManifest.present);
89
- const declared = new Set(deps.map((d) => pyNorm(d.name)));
90
- const managed = new Set((managedDependencies || []).map(pyNorm));
91
- const ignored = new Set((ignoredDependencies || []).map(pyNorm));
92
- const inNonRuntimeRoot = (file) => (nonRuntimeRoots || []).some((root) => {
93
- const f = String(file || "").replace(/\\/g, "/"), r = String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
94
- return !!r && (f === r || f.startsWith(`${r}/`));
95
- });
96
- for (const name of managed) declared.add(name);
97
-
98
- const used = new Map(); // top import → { dist, files:Set, lines:Map }
99
- for (const e of externalImports) {
100
- if (e.ecosystem !== "PyPI" || e.builtin || e.unresolved || !e.pkg) continue;
101
- const top = String(e.spec || e.pkg).split(".")[0];
102
- let u = used.get(top);
103
- if (!u) used.set(top, (u = { dist: e.pkg, files: new Set(), lines: new Map() }));
104
- u.files.add(e.file);
105
- if (!u.lines.has(e.file)) u.lines.set(e.file, e.line || 0);
106
- }
107
- // generous match: does declared dist D cover import top t (dist guess g)?
108
- const covers = (D, t, g) => {
109
- const d = pyNorm(D), nt = pyNorm(t), ng = pyNorm(g);
110
- return d === nt || d === ng || d === `python-${nt}` || d === `${nt}-python` || d === `${nt}-binary` || d.replace(/\d+$/, "") === nt.replace(/\d+$/, "");
111
- };
112
-
113
- const configBlob = [...configTexts.values()].join("\n");
114
- if (present) {
115
- for (const d of deps) {
116
- const n = pyNorm(d.name);
117
- if (managed.has(n) || ignored.has(n)) continue;
118
- if (d.buildSystem || PY_TOOL_DISTS.has(n) || /^types-|-stubs$|^pytest-|^flake8-|^sphinx/.test(n)) continue;
119
- let hit = false;
120
- for (const [top, u] of used) if (covers(d.name, top, u.dist)) { hit = true; break; }
121
- if (hit || mentioned(configBlob, d.name)) continue;
122
- findings.push(makeFinding({
123
- category: "unused",
124
- rule: "unused-dep",
125
- severity: d.dev ? "info" : "low",
126
- confidence: "low",
127
- title: `Unused Python dependency: ${d.name}`,
128
- reason: "No recorded Python import maps to this declared distribution; import-to-distribution mapping and plugin loading are heuristic.",
129
- detail: `"${d.name}" is declared but no .py file imports a module that maps to it. Import-name↔package-name mapping is heuristic and plugins/CLI tools load dynamically — review before removing.`,
130
- package: d.name,
131
- source: "internal",
132
- fixHint: `remove "${d.name}" from the manifest after confirming nothing imports or shells out to it`,
133
- }));
134
- }
135
- }
136
- for (const [top, u] of used) {
137
- if (ignored.has(pyNorm(top)) || ignored.has(pyNorm(u.dist)) || managed.has(pyNorm(top)) || managed.has(pyNorm(u.dist))) continue;
138
- let hit = false;
139
- for (const D of declared) if (covers(D, top, u.dist)) { hit = true; break; }
140
- if (hit) continue;
141
- const files = [...u.files];
142
- if (files.every(inNonRuntimeRoot)) continue;
143
- const testOnly = files.every((f) => TEST_PATH_RE.test(f));
144
- findings.push(makeFinding({
145
- category: "unused",
146
- rule: "missing-dep",
147
- severity: present ? (testOnly ? "low" : "medium") : "low",
148
- confidence: present ? "medium" : "low",
149
- title: `Missing Python dependency: ${u.dist}`,
150
- reason: present
151
- ? `A recorded Python import maps to "${u.dist}", but no declared distribution covers it.`
152
- : `A recorded Python import maps to "${u.dist}", but no Python dependency manifest is present; a managed runtime may provide it.`,
153
- detail: `"${top}" is imported by ${files.length} file(s) but ${present ? "no declared dependency provides it" : "the repo has no Python dependency manifest (requirements.txt / pyproject / Pipfile); a bundled or managed runtime may provide it"}${u.dist !== top ? ` (PyPI package is likely "${u.dist}")` : ""}.${present ? "" : " If this is intentional, declare it under python.managedDependencies in .weavatrix-deps.json."}`,
154
- package: u.dist,
155
- file: files[0],
156
- line: u.lines.get(files[0]) || 0,
157
- evidence: files.slice(0, 5).map((f) => ({ file: f, line: u.lines.get(f) || 0, snippet: "" })),
158
- source: "internal",
159
- fixHint: present ? `pip install ${u.dist} (and add it to requirements.txt / pyproject)` : `add ${u.dist} to a Python manifest, or declare it as a managed runtime dependency`,
160
- }));
161
- }
162
- return { findings, declared, managed };
163
- }
164
-
165
- const normPyScope = (root) => String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
166
- const pyScopeOwns = (root, file) => !root || file === root || String(file || "").replace(/\\/g, "/").startsWith(`${root}/`);
167
-
168
- export function computePyDepFindings(options = {}) {
169
- const scopes = Array.isArray(options.pyManifest?.scopes) ? options.pyManifest.scopes : [];
170
- if (!scopes.length) return computePyDepFindingsFlat(options);
171
- const normalized = scopes.map((scope) => ({ ...scope, root: normPyScope(scope.root) }))
172
- .sort((left, right) => right.root.length - left.root.length);
173
- if (!normalized.some((scope) => !scope.root)) normalized.push({ root: "", present: false, deps: [], manifests: [] });
174
- const importsByScope = new Map(normalized.map((scope) => [scope, []]));
175
- const configByScope = new Map(normalized.map((scope) => [scope, new Map()]));
176
- for (const entry of options.externalImports || []) {
177
- const owner = normalized.find((scope) => pyScopeOwns(scope.root, entry.file)) || normalized.at(-1);
178
- importsByScope.get(owner).push(entry);
179
- }
180
- for (const [file, text] of options.configTexts || new Map()) {
181
- const owner = normalized.find((scope) => pyScopeOwns(scope.root, file)) || normalized.at(-1);
182
- configByScope.get(owner).set(file, text);
183
- }
184
- const findings = [];
185
- const declared = new Set();
186
- const managed = new Set();
187
- for (const scope of normalized) {
188
- const result = computePyDepFindingsFlat({
189
- ...options,
190
- externalImports: importsByScope.get(scope),
191
- configTexts: configByScope.get(scope),
192
- pyManifest: {present: scope.present, deps: scope.deps || []},
193
- });
194
- findings.push(...result.findings.map((finding) => ({
195
- ...finding,
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" },
200
- })));
201
- for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
202
- for (const name of result.managed) managed.add(name);
203
- }
204
- return { findings, declared, managed };
205
- }
@@ -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
- ])