weavatrix 0.3.14 → 1.1.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 (306) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +132 -200
  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 +10 -67
  14. package/server.json +38 -0
  15. package/skill/SKILL.md +62 -374
  16. package/skill/agents/openai.yaml +4 -0
  17. package/skill/references/tool-routing.md +45 -0
  18. package/SECURITY.md +0 -101
  19. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  20. package/docs/agent-task-benchmark.md +0 -58
  21. package/docs/releases/v0.2.19.md +0 -33
  22. package/docs/releases/v0.3.0.md +0 -43
  23. package/docs/releases/v0.3.1.md +0 -30
  24. package/docs/releases/v0.3.10.md +0 -76
  25. package/docs/releases/v0.3.11.md +0 -55
  26. package/docs/releases/v0.3.12.md +0 -44
  27. package/docs/releases/v0.3.13.md +0 -34
  28. package/docs/releases/v0.3.14.md +0 -37
  29. package/docs/releases/v0.3.2.md +0 -15
  30. package/docs/releases/v0.3.3.md +0 -25
  31. package/docs/releases/v0.3.4.md +0 -40
  32. package/docs/releases/v0.3.5.md +0 -50
  33. package/docs/releases/v0.3.6.md +0 -44
  34. package/docs/releases/v0.3.7.md +0 -37
  35. package/docs/releases/v0.3.8.md +0 -21
  36. package/docs/releases/v0.3.9.md +0 -36
  37. package/scripts/run-agent-task-benchmark.mjs +0 -118
  38. package/src/analysis/allowed-test-runner.js +0 -70
  39. package/src/analysis/architecture/contract-graph.js +0 -119
  40. package/src/analysis/architecture/contract-schema.js +0 -110
  41. package/src/analysis/architecture/contract-storage.js +0 -35
  42. package/src/analysis/architecture/contract-verification.js +0 -168
  43. package/src/analysis/architecture-contract.js +0 -16
  44. package/src/analysis/audit-debt.js +0 -94
  45. package/src/analysis/audit-extensions.js +0 -60
  46. package/src/analysis/cargo-dependency-evidence.js +0 -108
  47. package/src/analysis/cargo-manifests.js +0 -81
  48. package/src/analysis/change-classification/diff-parser.js +0 -103
  49. package/src/analysis/change-classification/options.js +0 -40
  50. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  51. package/src/analysis/change-classification.js +0 -188
  52. package/src/analysis/coverage-reports-tarpaulin.js +0 -112
  53. package/src/analysis/coverage-reports.js +0 -246
  54. package/src/analysis/cycle-route.js +0 -14
  55. package/src/analysis/data-flow-evidence.js +0 -114
  56. package/src/analysis/dead-check.js +0 -300
  57. package/src/analysis/dead-code-review/candidates.js +0 -194
  58. package/src/analysis/dead-code-review/policy.js +0 -23
  59. package/src/analysis/dead-code-review.js +0 -119
  60. package/src/analysis/dep-check-ecosystems.js +0 -205
  61. package/src/analysis/dep-check.js +0 -290
  62. package/src/analysis/dep-rules.js +0 -10
  63. package/src/analysis/dependency/conventions.js +0 -16
  64. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  65. package/src/analysis/dependency/source-references.js +0 -21
  66. package/src/analysis/duplicate-groups.js +0 -104
  67. package/src/analysis/duplicates-worker.js +0 -11
  68. package/src/analysis/duplicates.compute.js +0 -269
  69. package/src/analysis/duplicates.js +0 -15
  70. package/src/analysis/duplicates.run.js +0 -52
  71. package/src/analysis/duplicates.tokenize.js +0 -199
  72. package/src/analysis/endpoints/common.js +0 -79
  73. package/src/analysis/endpoints/extract.js +0 -120
  74. package/src/analysis/endpoints/inventory.js +0 -84
  75. package/src/analysis/endpoints/mounts.js +0 -129
  76. package/src/analysis/endpoints-java.js +0 -260
  77. package/src/analysis/endpoints-rust.js +0 -124
  78. package/src/analysis/endpoints.js +0 -3
  79. package/src/analysis/findings.js +0 -70
  80. package/src/analysis/git-history/analytics.js +0 -177
  81. package/src/analysis/git-history/collector.js +0 -67
  82. package/src/analysis/git-history/options.js +0 -66
  83. package/src/analysis/git-history.js +0 -143
  84. package/src/analysis/git-ref-graph.js +0 -76
  85. package/src/analysis/go-dependency-evidence.js +0 -62
  86. package/src/analysis/graph-analysis.aggregate.js +0 -296
  87. package/src/analysis/graph-analysis.edges.js +0 -24
  88. package/src/analysis/graph-analysis.js +0 -7
  89. package/src/analysis/graph-analysis.refs.js +0 -202
  90. package/src/analysis/graph-analysis.summaries.js +0 -65
  91. package/src/analysis/health-capabilities.js +0 -83
  92. package/src/analysis/hot-path-review.js +0 -266
  93. package/src/analysis/http-contract-wrappers.js +0 -241
  94. package/src/analysis/http-contracts/analysis.js +0 -179
  95. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  96. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  97. package/src/analysis/http-contracts/graph-context.js +0 -143
  98. package/src/analysis/http-contracts/matching.js +0 -61
  99. package/src/analysis/http-contracts/shared.js +0 -85
  100. package/src/analysis/http-contracts.js +0 -6
  101. package/src/analysis/internal-audit/dependency-health.js +0 -113
  102. package/src/analysis/internal-audit/python-manifests.js +0 -78
  103. package/src/analysis/internal-audit/repo-files.js +0 -50
  104. package/src/analysis/internal-audit/supply-chain.js +0 -149
  105. package/src/analysis/internal-audit.collect.js +0 -213
  106. package/src/analysis/internal-audit.js +0 -9
  107. package/src/analysis/internal-audit.reach.js +0 -201
  108. package/src/analysis/internal-audit.run.js +0 -256
  109. package/src/analysis/java-source.js +0 -36
  110. package/src/analysis/jvm-artifact-index.js +0 -87
  111. package/src/analysis/jvm-dependency-evidence.js +0 -138
  112. package/src/analysis/jvm-manifests.js +0 -114
  113. package/src/analysis/manifests.js +0 -123
  114. package/src/analysis/package-reachability.js +0 -39
  115. package/src/analysis/source-complexity.ast.js +0 -97
  116. package/src/analysis/source-complexity.constants.js +0 -55
  117. package/src/analysis/source-complexity.js +0 -14
  118. package/src/analysis/source-complexity.report.js +0 -81
  119. package/src/analysis/source-complexity.walk.js +0 -228
  120. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  121. package/src/analysis/source-correctness.js +0 -222
  122. package/src/analysis/static-test-reachability.js +0 -150
  123. package/src/analysis/structure/dependency-graph.js +0 -151
  124. package/src/analysis/structure/findings.js +0 -162
  125. package/src/analysis/task-retrieval.js +0 -107
  126. package/src/analysis/transport-contracts.js +0 -224
  127. package/src/analysis/transport-runtime-evidence.js +0 -155
  128. package/src/analysis-kit.mjs +0 -17
  129. package/src/bounds.js +0 -4
  130. package/src/build-graph.js +0 -209
  131. package/src/child-env.js +0 -7
  132. package/src/config.js +0 -5
  133. package/src/extension/local-services.mjs +0 -75
  134. package/src/git-exec.js +0 -67
  135. package/src/graph/build-worker.js +0 -49
  136. package/src/graph/builder/go-receiver-resolution.js +0 -112
  137. package/src/graph/builder/js/dynamic-import.js +0 -26
  138. package/src/graph/builder/js/queries.js +0 -48
  139. package/src/graph/builder/lang-csharp.js +0 -40
  140. package/src/graph/builder/lang-css.js +0 -29
  141. package/src/graph/builder/lang-go.js +0 -140
  142. package/src/graph/builder/lang-html.js +0 -29
  143. package/src/graph/builder/lang-java-calls.js +0 -142
  144. package/src/graph/builder/lang-java.js +0 -215
  145. package/src/graph/builder/lang-js.js +0 -291
  146. package/src/graph/builder/lang-python.js +0 -235
  147. package/src/graph/builder/lang-rust-calls.js +0 -63
  148. package/src/graph/builder/lang-rust.js +0 -299
  149. package/src/graph/builder/lang-solidity.js +0 -126
  150. package/src/graph/builder/lang-sql.js +0 -263
  151. package/src/graph/builder/pass2-resolution.js +0 -81
  152. package/src/graph/builder/spec-pkg.js +0 -87
  153. package/src/graph/community.js +0 -27
  154. package/src/graph/edge-provenance.js +0 -31
  155. package/src/graph/file-lock.js +0 -69
  156. package/src/graph/freshness-probe.js +0 -142
  157. package/src/graph/graph-filter.js +0 -63
  158. package/src/graph/incremental-refresh.js +0 -235
  159. package/src/graph/internal-builder.barrels.js +0 -235
  160. package/src/graph/internal-builder.build.js +0 -300
  161. package/src/graph/internal-builder.java.js +0 -33
  162. package/src/graph/internal-builder.js +0 -12
  163. package/src/graph/internal-builder.langs.js +0 -157
  164. package/src/graph/internal-builder.pass2.js +0 -163
  165. package/src/graph/internal-builder.resolvers.js +0 -286
  166. package/src/graph/layout.js +0 -51
  167. package/src/graph/node-id.js +0 -7
  168. package/src/graph/parser-artifact-boundary.js +0 -55
  169. package/src/graph/relations.js +0 -4
  170. package/src/graph/repo-registry.js +0 -124
  171. package/src/graph/resolvers/rust.js +0 -117
  172. package/src/infra/infra-items.js +0 -255
  173. package/src/infra/infra-registry.js +0 -184
  174. package/src/infra/infra.detect.js +0 -119
  175. package/src/infra/infra.js +0 -38
  176. package/src/infra/infra.match.js +0 -102
  177. package/src/infra/infra.scan.js +0 -93
  178. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  179. package/src/mcp/architecture-bootstrap.mjs +0 -168
  180. package/src/mcp/architecture-starter.mjs +0 -234
  181. package/src/mcp/catalog.mjs +0 -233
  182. package/src/mcp/company-contract-page.mjs +0 -142
  183. package/src/mcp/company-contract-reconcile.mjs +0 -87
  184. package/src/mcp/company-contract-verdict.mjs +0 -42
  185. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  186. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  187. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  188. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  189. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  190. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  191. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  192. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  193. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  194. package/src/mcp/evidence-snapshot.mjs +0 -50
  195. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  196. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  197. package/src/mcp/extension-api.mjs +0 -77
  198. package/src/mcp/git-output.mjs +0 -7
  199. package/src/mcp/graph/context-core.mjs +0 -111
  200. package/src/mcp/graph/context-seeds.mjs +0 -207
  201. package/src/mcp/graph/context-state.mjs +0 -85
  202. package/src/mcp/graph/reverse-reach.mjs +0 -42
  203. package/src/mcp/graph/tools-core.mjs +0 -143
  204. package/src/mcp/graph/tools-query.mjs +0 -276
  205. package/src/mcp/graph-context.mjs +0 -5
  206. package/src/mcp/graph-diff.mjs +0 -218
  207. package/src/mcp/health/audit-format.mjs +0 -199
  208. package/src/mcp/health/audit.mjs +0 -199
  209. package/src/mcp/health/dead-code.mjs +0 -110
  210. package/src/mcp/health/duplicates.mjs +0 -121
  211. package/src/mcp/health/endpoints.mjs +0 -71
  212. package/src/mcp/health/structure.mjs +0 -220
  213. package/src/mcp/runtime-version.mjs +0 -31
  214. package/src/mcp/server/auto-refresh.mjs +0 -66
  215. package/src/mcp/server/runtime-config.mjs +0 -43
  216. package/src/mcp/server/shutdown.mjs +0 -40
  217. package/src/mcp/staleness-notice.mjs +0 -20
  218. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  219. package/src/mcp/sync/evidence-common.mjs +0 -88
  220. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  221. package/src/mcp/sync/evidence-health.mjs +0 -56
  222. package/src/mcp/sync/evidence-packages.mjs +0 -95
  223. package/src/mcp/sync/payload-common.mjs +0 -97
  224. package/src/mcp/sync/payload-v2.mjs +0 -54
  225. package/src/mcp/sync/payload-v3.mjs +0 -79
  226. package/src/mcp/sync-evidence.mjs +0 -23
  227. package/src/mcp/sync-payload.mjs +0 -70
  228. package/src/mcp/tool-result.mjs +0 -54
  229. package/src/mcp/tools-actions.mjs +0 -11
  230. package/src/mcp/tools-architecture.mjs +0 -177
  231. package/src/mcp/tools-company.mjs +0 -251
  232. package/src/mcp/tools-context.mjs +0 -222
  233. package/src/mcp/tools-endpoints.mjs +0 -207
  234. package/src/mcp/tools-graph-hubs.mjs +0 -78
  235. package/src/mcp/tools-graph.mjs +0 -17
  236. package/src/mcp/tools-health.mjs +0 -24
  237. package/src/mcp/tools-history.mjs +0 -28
  238. package/src/mcp/tools-impact-change.mjs +0 -220
  239. package/src/mcp/tools-impact-precision.mjs +0 -89
  240. package/src/mcp/tools-impact.mjs +0 -169
  241. package/src/mcp/tools-source.mjs +0 -238
  242. package/src/mcp/tools-verified-change.mjs +0 -200
  243. package/src/mcp-rg.mjs +0 -50
  244. package/src/mcp-runtime.mjs +0 -2
  245. package/src/mcp-server.mjs +0 -298
  246. package/src/mcp-source-tools.mjs +0 -163
  247. package/src/path-classification.js +0 -226
  248. package/src/path-ignore.js +0 -69
  249. package/src/precision/lsp-client/constants.js +0 -12
  250. package/src/precision/lsp-client/environment.js +0 -20
  251. package/src/precision/lsp-client/errors.js +0 -15
  252. package/src/precision/lsp-client/lifecycle.js +0 -114
  253. package/src/precision/lsp-client/message-parser.js +0 -81
  254. package/src/precision/lsp-client/protocol.js +0 -212
  255. package/src/precision/lsp-client/registry.js +0 -59
  256. package/src/precision/lsp-client/repo-uri.js +0 -60
  257. package/src/precision/lsp-client/stdio-client.js +0 -140
  258. package/src/precision/lsp-client.js +0 -10
  259. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  260. package/src/precision/lsp-overlay/build.js +0 -261
  261. package/src/precision/lsp-overlay/contract.js +0 -61
  262. package/src/precision/lsp-overlay/reference-results.js +0 -108
  263. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  264. package/src/precision/lsp-overlay/source-session.js +0 -142
  265. package/src/precision/lsp-overlay/store.js +0 -150
  266. package/src/precision/lsp-overlay/target-index.js +0 -152
  267. package/src/precision/lsp-overlay/target-query.js +0 -55
  268. package/src/precision/lsp-overlay.js +0 -11
  269. package/src/precision/symbol-query.js +0 -224
  270. package/src/precision/typescript-lsp-provider.js +0 -12
  271. package/src/precision/typescript-provider/client.js +0 -89
  272. package/src/precision/typescript-provider/discovery.js +0 -124
  273. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  274. package/src/precision/typescript-provider/project-host.js +0 -254
  275. package/src/precision/typescript-provider/project-safety.js +0 -166
  276. package/src/precision/typescript-provider/reference-usage.js +0 -53
  277. package/src/process.js +0 -95
  278. package/src/repo-path.js +0 -53
  279. package/src/scan/discover.inventory.js +0 -78
  280. package/src/scan/discover.js +0 -5
  281. package/src/scan/discover.list.js +0 -79
  282. package/src/scan/discover.stack.js +0 -227
  283. package/src/scan/search.core.js +0 -24
  284. package/src/scan/search.git.js +0 -102
  285. package/src/scan/search.js +0 -9
  286. package/src/scan/search.node.js +0 -83
  287. package/src/scan/search.preview.js +0 -49
  288. package/src/scan/search.rg.js +0 -145
  289. package/src/security/advisory-store.js +0 -172
  290. package/src/security/installed-jvm-rust.js +0 -46
  291. package/src/security/installed.js +0 -280
  292. package/src/security/malware-file-heuristics.js +0 -121
  293. package/src/security/malware-heuristics.exclusions.js +0 -134
  294. package/src/security/malware-heuristics.js +0 -15
  295. package/src/security/malware-heuristics.roots.js +0 -145
  296. package/src/security/malware-heuristics.scan.js +0 -89
  297. package/src/security/malware-heuristics.sweep.js +0 -122
  298. package/src/security/malware-scoring.js +0 -105
  299. package/src/security/match.js +0 -85
  300. package/src/security/registry-sig.classify.js +0 -137
  301. package/src/security/registry-sig.js +0 -18
  302. package/src/security/registry-sig.rules.js +0 -192
  303. package/src/security/rust-advisory-report.js +0 -60
  304. package/src/security/typosquat.js +0 -81
  305. package/src/util.js +0 -40
  306. package/src/version.js +0 -7
@@ -1,246 +0,0 @@
1
- // graph-builder-coverage.js — coverage-report readers for the graph analysis. Parses Istanbul (summary/final),
2
- // lcov, Python coverage.json, and Go coverage.out into per-file line-hit maps, resolving report paths onto the
3
- // graph's known files. Split out of graph-builder-analysis.js (pure except for reading the repo's coverage files).
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { join, relative, resolve } from "node:path";
6
- import { createRepoBoundary } from "../repo-path.js";
7
- import { readTarpaulinCoverage } from "./coverage-reports-tarpaulin.js";
8
-
9
- export function normRepoPath(value) {
10
- return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
11
- }
12
-
13
- export function dirOfRepoPath(value) {
14
- const p = normRepoPath(value);
15
- const i = p.lastIndexOf("/");
16
- return i >= 0 ? p.slice(0, i) : "";
17
- }
18
-
19
- export function normalizeRepoParts(value) {
20
- const out = [];
21
- for (const part of normRepoPath(value).split("/")) {
22
- if (!part || part === ".") continue;
23
- if (part === "..") out.pop();
24
- else out.push(part);
25
- }
26
- return out.join("/");
27
- }
28
-
29
- function pct01(value) {
30
- const n = Number(value);
31
- if (!Number.isFinite(n)) return null;
32
- return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
33
- }
34
-
35
- export function pctFromCounts(covered, total) {
36
- const c = Number(covered);
37
- const t = Number(total);
38
- if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
39
- return Math.max(0, Math.min(1, c / t));
40
- }
41
-
42
- function resolveCoverageFile(rawPath, knownFiles, repoRoot) {
43
- const known = (knownFiles || []).map(normalizeRepoParts).filter(Boolean);
44
- const knownSet = new Set(known);
45
- const candidates = [];
46
- const add = (value) => {
47
- const p = normalizeRepoParts(value);
48
- if (p && !candidates.includes(p)) candidates.push(p);
49
- };
50
- const raw = String(rawPath || "").replace(/^file:\/\//i, "").replace(/[?#].*$/, "");
51
- const normalizedRaw = normRepoPath(raw);
52
- add(normalizedRaw);
53
- add(normalizedRaw.replace(/^\.\//, ""));
54
- if (repoRoot) {
55
- try {
56
- const rel = normRepoPath(relative(repoRoot, raw));
57
- if (rel && !rel.startsWith("..")) add(rel);
58
- } catch {
59
- /* best effort */
60
- }
61
- const rootPrefix = `${normRepoPath(resolve(repoRoot)).toLowerCase()}/`;
62
- const lowerRaw = normalizedRaw.toLowerCase();
63
- if (lowerRaw.startsWith(rootPrefix)) add(normalizedRaw.slice(rootPrefix.length));
64
- }
65
- for (const candidate of candidates) if (knownSet.has(candidate)) return candidate;
66
- const lower = normalizedRaw.toLowerCase();
67
- const suffixHit = known
68
- .slice()
69
- .sort((a, b) => b.length - a.length)
70
- .find((file) => lower.endsWith(`/${file.toLowerCase()}`) || lower === file.toLowerCase());
71
- return suffixHit || candidates[0] || "";
72
- }
73
-
74
- function mergeCoverageEntry(map, rawPath, entry, knownFiles, repoRoot) {
75
- const file = resolveCoverageFile(rawPath, knownFiles, repoRoot);
76
- if (!file) return;
77
- const current = map.get(file) || {};
78
- map.set(file, {
79
- pct: entry.pct != null ? entry.pct : current.pct ?? null,
80
- total: entry.total != null ? entry.total : current.total ?? null,
81
- covered: entry.covered != null ? entry.covered : current.covered ?? null,
82
- lines: entry.lines || current.lines || null,
83
- source: current.source && entry.source ? `${current.source}, ${entry.source}` : entry.source || current.source || ""
84
- });
85
- }
86
-
87
- function addLineHit(lines, line, hit) {
88
- const value = Number(line);
89
- if (!Number.isFinite(value) || value <= 0) return;
90
- const n = Math.round(value);
91
- if (n < 1) return;
92
- const current = lines.get(n);
93
- lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
94
- }
95
-
96
- function parseIstanbulSummary(map, filePath, knownFiles, repoRoot) {
97
- const json = JSON.parse(readFileSync(filePath, "utf8"));
98
- for (const [raw, value] of Object.entries(json || {})) {
99
- if (raw === "total" || !value || typeof value !== "object") continue;
100
- const lines = value.lines || value.statements || {};
101
- mergeCoverageEntry(map, raw, {
102
- pct: pct01(lines.pct),
103
- total: Number.isFinite(Number(lines.total)) ? Number(lines.total) : null,
104
- covered: Number.isFinite(Number(lines.covered)) ? Number(lines.covered) : null,
105
- source: "coverage-summary.json"
106
- }, knownFiles, repoRoot);
107
- }
108
- }
109
-
110
- function parseIstanbulFinal(map, filePath, knownFiles, repoRoot) {
111
- const json = JSON.parse(readFileSync(filePath, "utf8"));
112
- for (const [raw, record] of Object.entries(json || {})) {
113
- if (!record || typeof record !== "object") continue;
114
- const statementMap = record.statementMap || {};
115
- const hits = record.s || {};
116
- const lines = new Map();
117
- for (const [id, loc] of Object.entries(statementMap)) {
118
- const hit = Number(hits[id]) || 0;
119
- const start = Math.max(1, Math.round(Number(loc?.start?.line) || 0));
120
- const end = Math.max(start, Math.round(Number(loc?.end?.line) || start));
121
- for (let line = start; line <= end; line++) addLineHit(lines, line, hit);
122
- }
123
- const total = lines.size;
124
- const covered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
125
- mergeCoverageEntry(map, record.path || raw, {
126
- pct: pctFromCounts(covered, total),
127
- total,
128
- covered,
129
- lines,
130
- source: "coverage-final.json"
131
- }, knownFiles, repoRoot);
132
- }
133
- }
134
-
135
- function parseLcov(map, filePath, knownFiles, repoRoot) {
136
- const flush = (record) => {
137
- if (!record || !record.file) return;
138
- const total = record.total ?? record.lines.size;
139
- const covered = record.covered ?? [...record.lines.values()].filter((hit) => Number(hit) > 0).length;
140
- mergeCoverageEntry(map, record.file, {
141
- pct: pctFromCounts(covered, total),
142
- total,
143
- covered,
144
- lines: record.lines,
145
- source: "lcov.info"
146
- }, knownFiles, repoRoot);
147
- };
148
- let current = null;
149
- for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
150
- const line = rawLine.trim();
151
- if (line.startsWith("SF:")) {
152
- flush(current);
153
- current = { file: line.slice(3), lines: new Map(), total: null, covered: null };
154
- } else if (current && line.startsWith("DA:")) {
155
- const [lineNo, count] = line.slice(3).split(",");
156
- addLineHit(current.lines, lineNo, count);
157
- } else if (current && line.startsWith("LF:")) {
158
- current.total = Number(line.slice(3)) || current.total;
159
- } else if (current && line.startsWith("LH:")) {
160
- current.covered = Number(line.slice(3)) || current.covered;
161
- } else if (line === "end_of_record") {
162
- flush(current);
163
- current = null;
164
- }
165
- }
166
- flush(current);
167
- }
168
-
169
- function parseCoveragePyJson(map, filePath, knownFiles, repoRoot) {
170
- const json = JSON.parse(readFileSync(filePath, "utf8"));
171
- for (const [raw, file] of Object.entries(json?.files || {})) {
172
- const lines = new Map();
173
- for (const line of file.executed_lines || []) addLineHit(lines, line, 1);
174
- for (const line of file.missing_lines || []) if (!lines.has(Number(line))) addLineHit(lines, line, 0);
175
- const summary = file.summary || {};
176
- const total = Number(summary.num_statements) || lines.size;
177
- const covered = Number(summary.covered_lines) || [...lines.values()].filter((hit) => Number(hit) > 0).length;
178
- mergeCoverageEntry(map, raw, {
179
- pct: pct01(summary.percent_covered ?? summary.percent_covered_display) ?? pctFromCounts(covered, total),
180
- total,
181
- covered,
182
- lines,
183
- source: "coverage.json"
184
- }, knownFiles, repoRoot);
185
- }
186
- }
187
-
188
- function parseGoCoverage(map, filePath, knownFiles, repoRoot) {
189
- const byFile = new Map();
190
- for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
191
- const line = rawLine.trim();
192
- if (!line || line.startsWith("mode:")) continue;
193
- const m = line.match(/^(.+):(\d+)\.\d+,(\d+)\.\d+\s+\d+\s+(\d+)$/);
194
- if (!m) continue;
195
- const record = byFile.get(m[1]) || { lines: new Map() };
196
- const start = Math.max(1, Number(m[2]) || 1);
197
- const end = Math.max(start, Number(m[3]) || start);
198
- for (let n = start; n <= end; n++) addLineHit(record.lines, n, Number(m[4]) || 0);
199
- byFile.set(m[1], record);
200
- }
201
- for (const [raw, record] of byFile) {
202
- const total = record.lines.size;
203
- const covered = [...record.lines.values()].filter((hit) => Number(hit) > 0).length;
204
- mergeCoverageEntry(map, raw, {
205
- pct: pctFromCounts(covered, total),
206
- total,
207
- covered,
208
- lines: record.lines,
209
- source: "coverage.out"
210
- }, knownFiles, repoRoot);
211
- }
212
- }
213
-
214
- function parseTarpaulinJson(map, filePath, knownFiles, repoRoot) {
215
- for (const record of readTarpaulinCoverage(filePath)) {
216
- mergeCoverageEntry(map, record.path, record.entry, knownFiles, repoRoot);
217
- }
218
- }
219
-
220
- export function readCoverageForRepo(repoRoot, knownFiles) {
221
- const out = new Map();
222
- if (!repoRoot) return out;
223
- const boundary = createRepoBoundary(repoRoot);
224
- const candidates = [
225
- ["coverage/coverage-summary.json", parseIstanbulSummary],
226
- ["coverage/coverage-final.json", parseIstanbulFinal],
227
- ["coverage/lcov.info", parseLcov],
228
- ["lcov.info", parseLcov],
229
- ["coverage/coverage.json", parseCoveragePyJson],
230
- ["coverage.json", parseCoveragePyJson],
231
- ["tarpaulin-report.json", parseTarpaulinJson],
232
- ["coverage/tarpaulin-report.json", parseTarpaulinJson],
233
- ["coverage.out", parseGoCoverage],
234
- ["cover.out", parseGoCoverage]
235
- ];
236
- for (const [candidate, parser] of candidates) {
237
- const resolved = boundary.resolve(candidate);
238
- if (!resolved.ok || !existsSync(resolved.path)) continue;
239
- try {
240
- parser(out, resolved.path, knownFiles, repoRoot);
241
- } catch {
242
- /* malformed/foreign coverage report: ignore */
243
- }
244
- }
245
- return out;
246
- }
@@ -1,14 +0,0 @@
1
- const MAX_REPRESENTATIVE_CYCLE_FILES = 64;
2
-
3
- // Keep the complete closed route for normal cycles. Very large SCCs still get a closed, deterministic
4
- // head/tail route without letting one finding consume the whole audit response.
5
- export function formatRepresentativeCycle(cycle, maxFiles = MAX_REPRESENTATIVE_CYCLE_FILES) {
6
- const raw = Array.isArray(cycle) ? cycle.map(String).filter(Boolean) : [];
7
- if (!raw.length) return "";
8
- const closed = raw.length > 1 && raw[0] === raw[raw.length - 1] ? raw : [...raw, raw[0]];
9
- const cap = Math.max(4, Number(maxFiles) || MAX_REPRESENTATIVE_CYCLE_FILES);
10
- if (closed.length <= cap + 1) return closed.join(" → ");
11
- const side = Math.max(2, Math.floor(cap / 2));
12
- const omitted = closed.length - side * 2;
13
- return `${closed.slice(0, side).join(" → ")} → … ${omitted} file(s) omitted … → ${closed.slice(-side).join(" → ")}`;
14
- }
@@ -1,114 +0,0 @@
1
- import {readFileSync} from 'node:fs'
2
- import {extname} from 'node:path'
3
- import ts from 'typescript'
4
- import {createRepoBoundary} from '../repo-path.js'
5
-
6
- const lineOfNode = (node) => Number(String(node?.source_location || '').match(/^L(\d+)/i)?.[1] || String(node?.id || '').match(/@(\d+)$/)?.[1] || 0)
7
- const fileOfNode = (node) => String(node?.source_file || String(node?.id || '').split('#')[0]).replace(/\\/g, '/')
8
- const cleanName = (value) => String(value || '').replace(/\(\)$/, '').split('.').pop()
9
- const callName = (node) => node.expression?.name?.text || node.expression?.text || node.expression?.escapedText || ''
10
-
11
- function scriptKind(file) {
12
- const ext = extname(file).toLowerCase()
13
- if (ext === '.tsx') return ts.ScriptKind.TSX
14
- if (ext === '.jsx') return ts.ScriptKind.JSX
15
- if (ext === '.ts' || ext === '.mts' || ext === '.cts') return ts.ScriptKind.TS
16
- return ts.ScriptKind.JS
17
- }
18
-
19
- function parse(cache, boundary, file) {
20
- if (cache.has(file)) return cache.get(file)
21
- if (!/\.[cm]?[jt]sx?$/i.test(file)) return null
22
- const resolved = boundary.resolve(file)
23
- if (!resolved.ok) return null
24
- try {
25
- const source = ts.createSourceFile(file, readFileSync(resolved.path, 'utf8'), ts.ScriptTarget.Latest, true, scriptKind(file))
26
- cache.set(file, source)
27
- return source
28
- } catch { return null }
29
- }
30
-
31
- function parametersFor(source, target) {
32
- const expected = cleanName(target?.label)
33
- const expectedLine = lineOfNode(target)
34
- const choices = []
35
- const visit = (node) => {
36
- if (Array.isArray(node.parameters) && node.name) {
37
- const name = cleanName(node.name.text || node.name.getText?.(source))
38
- if (!expected || name === expected) {
39
- const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
40
- choices.push({line, parameters: node.parameters.map((item) => item.name.getText(source).slice(0, 80))})
41
- }
42
- }
43
- ts.forEachChild(node, visit)
44
- }
45
- visit(source)
46
- choices.sort((a, b) => Math.abs(a.line - expectedLine) - Math.abs(b.line - expectedLine))
47
- return choices[0]?.parameters || []
48
- }
49
-
50
- function callsAt(source, line, expectedName) {
51
- const found = []
52
- const visit = (node) => {
53
- if (ts.isCallExpression(node)) {
54
- const at = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
55
- if ((!line || at === line) && (!expectedName || cleanName(callName(node)) === expectedName)) found.push({node, line: at})
56
- }
57
- ts.forEachChild(node, visit)
58
- }
59
- visit(source)
60
- return found
61
- }
62
-
63
- // Bounded interprocedural evidence: maps arguments at graph callsites to the callee's declared
64
- // parameters. This is not a control-flow graph, value propagation, or taint-analysis claim.
65
- export function extractCallArgumentEvidence({graph, repoRoot, seedIds = [], depth = 2, maxEdges = 40} = {}) {
66
- const boundary = createRepoBoundary(repoRoot)
67
- const cache = new Map()
68
- const queue = (seedIds || []).map((id) => ({id: String(id), hop: 0}))
69
- const visited = new Set()
70
- const seenCalls = new Set()
71
- const edges = []
72
- let unsupported = 0
73
- while (queue.length && edges.length < maxEdges) {
74
- const current = queue.shift()
75
- if (visited.has(current.id) || current.hop >= depth) continue
76
- visited.add(current.id)
77
- const calls = [
78
- ...(graph.out.get(current.id) || []).filter((edge) => edge.relation === 'calls')
79
- .map((edge) => ({fromId: current.id, toId: String(edge.id), line: Number(edge.line) || 0, nextId: String(edge.id)})),
80
- ...(graph.inn.get(current.id) || []).filter((edge) => edge.relation === 'calls')
81
- .map((edge) => ({fromId: String(edge.id), toId: current.id, line: Number(edge.line) || 0, nextId: String(edge.id)})),
82
- ]
83
- for (const callEdge of calls) {
84
- const callKey = `${callEdge.fromId}\0${callEdge.toId}\0${callEdge.line}`
85
- if (seenCalls.has(callKey)) continue
86
- seenCalls.add(callKey)
87
- const from = graph.byId.get(callEdge.fromId)
88
- const to = graph.byId.get(callEdge.toId)
89
- const callerSource = parse(cache, boundary, fileOfNode(from))
90
- const targetSource = parse(cache, boundary, fileOfNode(to))
91
- if (!from || !to || !callerSource || !targetSource) { unsupported++; continue }
92
- const params = parametersFor(targetSource, to)
93
- const callsites = callsAt(callerSource, callEdge.line, cleanName(to.label))
94
- if (!callsites.length) { unsupported++; continue }
95
- for (const call of callsites.slice(0, 3)) {
96
- edges.push({
97
- from: callEdge.fromId, to: callEdge.toId, hop: current.hop + 1,
98
- file: fileOfNode(from), line: call.line,
99
- arguments: call.node.arguments.slice(0, 12).map((argument, index) => ({
100
- index, expression: argument.getText(callerSource).slice(0, 160), parameter: params[index] || null,
101
- })),
102
- state: 'EXTRACTED',
103
- })
104
- if (edges.length >= maxEdges) break
105
- }
106
- if (current.hop + 1 < depth) queue.push({id: callEdge.nextId, hop: current.hop + 1})
107
- }
108
- }
109
- return {
110
- model: 'bounded call-argument-to-parameter evidence (not CFG or taint analysis)',
111
- status: edges.length ? (unsupported ? 'PARTIAL' : 'COMPLETE') : 'UNAVAILABLE',
112
- edges, unsupportedEdges: unsupported, capped: edges.length >= maxEdges,
113
- }
114
- }
@@ -1,300 +0,0 @@
1
- // Portable, language-agnostic DEAD-code checker. A symbol is DEAD when it has NO inbound graph edge (nothing
2
- // calls/imports/references/inherits it) AND its bare name appears NOWHERE in the repo outside its own file.
3
- // A file is DEAD when nothing imports it, it isn't an entry point, and all its symbols are dead.
4
- //
5
- // Pure core `computeDead(graph, sources)` (sources = Map<fileRel, text>) works on any graph-builder-schema graph
6
- // and is fully testable with no filesystem. It only needs {nodes, links} + source text. See [[graph-builder-internalization]].
7
- import { posix } from "node:path";
8
- import { isStructuralRelation } from "../graph/relations.js";
9
- import { createSqlDeadVerdict } from "../graph/builder/lang-sql.js";
10
- import { createPathClassifier, hasPathClass } from "../path-classification.js";
11
-
12
- const IDENT_RE = /[A-Za-z_$][\w$]*/g;
13
- const bareName = (label) => String(label || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
14
- // Ignore comment-only lexical mentions, which are documentation rather than callers. Keep strings on
15
- // purpose: registries/reflection often address a live symbol by name, and static liveness must stay
16
- // conservative there. This only strips comments that start a logical line, avoiding a language parser
17
- // guess around inline comment markers, regex literals, or URLs.
18
- const lexicalEvidenceText = (value) => {
19
- let inBlockComment = false;
20
- return String(value || "").split(/\r?\n/).map((line) => {
21
- let rest = line;
22
- while (true) {
23
- if (inBlockComment) {
24
- const end = rest.indexOf("*/");
25
- if (end < 0) return "";
26
- inBlockComment = false;
27
- rest = rest.slice(end + 2);
28
- }
29
- const trimmed = rest.trimStart();
30
- if (trimmed.startsWith("//") || trimmed.startsWith("#")) return "";
31
- if (!trimmed.startsWith("/*")) return rest;
32
- inBlockComment = true;
33
- rest = trimmed.slice(2);
34
- }
35
- }).join("\n");
36
- };
37
- // entry surfaces are never dead even with no inbound edge (framework/CLI/HTTP enter them externally).
38
- // Exported for internal-audit.js (reachability entry set) — keep the two in lockstep.
39
- export const ENTRY_FILE = /(^|[\\/])(index|main|app|server|cli|cmd|bootstrap|entry|run|__main__|manage|wsgi|asgi|setup|conftest)\.[a-z0-9]+$|(^|[\\/])(bin|cmd)[\\/]|(^|[\\/])main\.go$/i;
40
- const defaultPathClassifier = createPathClassifier(null);
41
- const isTestFile = (file) => hasPathClass(defaultPathClassifier.explain(file), "test", "e2e");
42
-
43
- // Framework-owned entry modules are invoked by convention rather than a source import. Keep this narrow:
44
- // these are Next.js App/Pages Router surfaces and framework metadata files, not every file under `app/`.
45
- const NEXT_ENTRY_FILE = /(^|\/)(?:src\/)?app\/(?:.*\/)?(?:page|layout|template|loading|error|global-error|not-found|default|route|robots|sitemap|manifest|opengraph-image|twitter-image|icon|apple-icon)\.[cm]?[jt]sx?$|(^|\/)(?:src\/)?pages\/(?!.*\/(?:components?|lib|utils?)\/).+\.[cm]?[jt]sx?$|(^|\/)(?:middleware|instrumentation)\.[cm]?[jt]s$/i;
46
- const RUST_ENTRY_FILE = /(^|\/)(?:build\.rs|src\/(?:lib|main)\.rs)$/i;
47
- export const isFrameworkEntryFile = (file) => {
48
- const normalized = String(file || "").replace(/\\/g, "/");
49
- return NEXT_ENTRY_FILE.test(normalized) || RUST_ENTRY_FILE.test(normalized);
50
- };
51
-
52
- const lineOfNode = (n) => {
53
- const m = /@(\d+)$/.exec(String(n.id || "")) || /L(\d+)/.exec(String(n.source_location || ""));
54
- return m ? Number(m[1]) : 0;
55
- };
56
-
57
- // Older graphs marked every method of an exported class as exported. Verify that an `exported` node is
58
- // actually a module-surface declaration before reporting it. New graphs expose symbol_kind/member_of and
59
- // already keep members unexported, but the source check preserves correctness for cached v0.1.2 graphs.
60
- function hasModuleExport(source, node, name) {
61
- if (node.member_of || node.symbol_kind === "method") return false;
62
- const lines = String(source || "").split(/\r?\n/);
63
- const line = lines[Math.max(0, lineOfNode(node) - 1)] || "";
64
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65
- const direct = new RegExp(`\\bexport\\s+(?:default\\s+)?(?:(?:declare|abstract|async)\\s+)*(?:function|class|const|let|var|enum|interface|type|namespace)\\s+${escaped}\\b`);
66
- if (direct.test(line) || direct.test(String(source || ""))) return true;
67
- const exportDefault = new RegExp(`\\bexport\\s+default\\s+${escaped}\\b`);
68
- if (exportDefault.test(String(source || ""))) return true;
69
- // `export { local }`, `export { local as publicName }` and CommonJS explicit exports.
70
- const exportList = new RegExp(`\\bexport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}`, "s");
71
- const commonJs = new RegExp(`(?:module\\.exports\\s*=\\s*\\{[^}]*\\b${escaped}\\b|(?:module\\.exports|exports)\\.${escaped}\\s*=)`, "s");
72
- return exportList.test(String(source || "")) || commonJs.test(String(source || ""));
73
- }
74
-
75
- function namespaceConsumedFiles(sources, graph) {
76
- const files = new Set(sources.keys());
77
- const consumed = new Set();
78
- const extensions = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
79
- const resolve = (from, spec) => {
80
- if (!spec.startsWith(".")) return "";
81
- const base = posix.normalize(posix.join(posix.dirname(from.replace(/\\/g, "/")), spec));
82
- for (const ext of extensions) {
83
- const p = `${base}${ext}`;
84
- if (files.has(p)) return p;
85
- }
86
- for (const ext of extensions.slice(1)) {
87
- const p = `${base}/index${ext}`;
88
- if (files.has(p)) return p;
89
- }
90
- return "";
91
- };
92
- for (const [file, textValue] of sources) {
93
- const text = String(textValue || "");
94
- const patterns = [
95
- /\bimport\s+\*\s+as\s+[A-Za-z_$][\w$]*\s+from\s*(["'])([^"']+)\1/g,
96
- /\bexport\s+\*\s+from\s*(["'])([^"']+)\1/g,
97
- /\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*require\(\s*(["'])([^"']+)\1\s*\)/g,
98
- ];
99
- for (const re of patterns) {
100
- let m;
101
- while ((m = re.exec(text))) {
102
- const target = resolve(file, m[2]);
103
- if (target) consumed.add(target);
104
- }
105
- }
106
- }
107
- // New graphs retain the exact specifier on file import edges, which also resolves namespace imports
108
- // expressed through tsconfig aliases (the relative-path fallback above intentionally cannot guess those).
109
- for (const link of graph.links || []) {
110
- if (link.relation !== "imports" && link.relation !== "re_exports") continue;
111
- const from = String(link.source?.id || link.source || ""), target = String(link.target?.id || link.target || "");
112
- if (!link.specifier || from.includes("#") || target.includes("#") || !sources.has(from)) continue;
113
- const spec = String(link.specifier).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
- const text = String(sources.get(from) || "");
115
- if (new RegExp(`(?:import\\s+\\*\\s+as\\s+[A-Za-z_$][\\w$]*\\s+from|export\\s+\\*\\s+from)\\s*["']${spec}["']`).test(text)) consumed.add(target);
116
- }
117
- return consumed;
118
- }
119
-
120
- export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
121
- const nodes = graph.nodes || [], links = graph.links || [];
122
- const ep = (v) => (v && typeof v === "object" ? v.id : v);
123
- const inbound = new Set();
124
- const inboundSources = new Map();
125
- const nodesById = new Map(nodes.map((node) => [String(node.id), node]));
126
- for (const l of links) if (!isStructuralRelation(l.relation)) {
127
- const target = String(ep(l.target));
128
- const source = String(ep(l.source));
129
- inbound.add(target);
130
- const values = inboundSources.get(target) || [];
131
- values.push(source);
132
- inboundSources.set(target, values);
133
- }
134
-
135
- // whole-repo identifier frequency: a symbol whose name appears MORE than once total (its definition + at least
136
- // one use, same-file OR cross-file) is referenced. Errs toward "alive" (common-named symbols never flagged).
137
- const globalFreq = new Map();
138
- for (const [, text] of sources) for (const m of lexicalEvidenceText(text).matchAll(IDENT_RE)) { const n = m[0]; globalFreq.set(n, (globalFreq.get(n) || 0) + 1); }
139
-
140
- const symById = new Map();
141
- const symsByFile = new Map();
142
- for (const n of nodes) {
143
- if (!String(n.id).includes("#")) continue;
144
- symById.set(n.id, n);
145
- (symsByFile.get(n.source_file) || symsByFile.set(n.source_file, []).get(n.source_file)).push(n);
146
- }
147
-
148
- const symbolNames = new Set([...symById.values()].map((node) => bareName(node.label)).filter(Boolean));
149
- const occurrenceFiles = new Map();
150
- const occurrenceCounts = new Map();
151
- for (const [file, text] of sources) for (const match of lexicalEvidenceText(text).matchAll(IDENT_RE)) {
152
- const name = match[0];
153
- if (!symbolNames.has(name)) continue;
154
- const files = occurrenceFiles.get(name) || new Set();
155
- files.add(file);
156
- occurrenceFiles.set(name, files);
157
- const counts = occurrenceCounts.get(name) || new Map();
158
- counts.set(file, (counts.get(file) || 0) + 1);
159
- occurrenceCounts.set(name, counts);
160
- }
161
-
162
- const declarationCounts = new Map();
163
- for (const node of symById.values()) {
164
- const name = bareName(node.label);
165
- if (!name) continue;
166
- const key = `${node.source_file}\0${name}`;
167
- declarationCounts.set(key, (declarationCounts.get(key) || 0) + 1);
168
- }
169
- const exactReferenceIds = new Set(graph.precisionReferenceSymbols || []);
170
- const exactProductionReferenceIds = new Set(graph.precisionProductionReferenceSymbols || []);
171
- const exactTestReferenceIds = new Set(graph.precisionTestReferenceSymbols || []);
172
-
173
- // decorated defs (@app.route/@app.event/@pytest.fixture…) are entered by the framework: trust the
174
- // builder's flag when present, else walk the source line(s) above the definition (graph-builder graphs).
175
- const lineCache = new Map();
176
- const linesOf = (f) => { let l = lineCache.get(f); if (!l) { l = String(sources.get(f) || "").split(/\r?\n/); lineCache.set(f, l); } return l; };
177
- const symLine = (n) => { const m = /@(\d+)$/.exec(String(n.id)) || /L(\d+)/.exec(String(n.source_location || "")); return m ? Number(m[1]) : 0; };
178
- const isDecorated = (n) => {
179
- if (n.decorated) return true;
180
- const ln = symLine(n);
181
- if (!ln) return false;
182
- const lines = linesOf(n.source_file);
183
- for (let i = ln - 2; i >= 0 && i >= ln - 6; i--) {
184
- const t = (lines[i] || "").trim();
185
- if (t.startsWith("@")) return true;
186
- if (t === "" || t.startsWith("#")) continue;
187
- break;
188
- }
189
- return false;
190
- };
191
-
192
- const isReferenced = (n) => {
193
- if (inbound.has(n.id)) return true; // a real graph edge targets it
194
- if (exactReferenceIds.has(String(n.id))) return true; // revision-bound point-query evidence found a caller
195
- const name = bareName(n.label);
196
- if (!name || !/^[A-Za-z_$]/.test(name)) return true; // selectors/odd labels → don't flag
197
- if (/^__\w+__$/.test(name)) return true; // dunders are invoked implicitly (with/str/==/iter…), never spelled
198
- if ((globalFreq.get(name) || 0) > 1) return true; // name appears beyond its single definition
199
- return isDecorated(n); // framework-registered via decorator
200
- };
201
-
202
- const sqlVerdict = createSqlDeadVerdict({ nodes, links, ep, bareName }); // gated SQL verdicts — ORM blindness stays silent
203
- const deadSymbols = [];
204
- for (const n of symById.values()) {
205
- if (isReferenced(n) || sqlVerdict.veto(n)) continue;
206
- deadSymbols.push({ id: n.id, file: n.source_file, label: n.label, test: isTestFile(n.source_file) || n.test_surface === true, reason: sqlVerdict.reason(n) || "no inbound edge and name unreferenced outside its file" });
207
- }
208
- const deadSet = new Set(deadSymbols.map((s) => s.id));
209
-
210
- // A production declaration consumed only by tests is live to the raw graph but dead to production.
211
- // Keep it in a separate review class: this is useful evidence, never an automatic delete verdict.
212
- const testOnlySymbols = [];
213
- const consumerFileOf = (id) => nodesById.get(id)?.source_file || (id.includes("#") ? id.split("#")[0] : id);
214
- const isTestConsumer = (id) => nodesById.get(id)?.test_surface === true || isTestFile(consumerFileOf(id));
215
- for (const n of symById.values()) {
216
- if (deadSet.has(n.id) || isTestFile(n.source_file) || n.test_surface === true || isDecorated(n)) continue;
217
- const sourcesForSymbol = inboundSources.get(String(n.id)) || [];
218
- const hasTestInbound = sourcesForSymbol.some(isTestConsumer);
219
- const hasProductionInbound = sourcesForSymbol.some((id) => consumerFileOf(id) && !isTestConsumer(id));
220
- const name = bareName(n.label);
221
- const occurrenceSet = occurrenceFiles.get(name) || new Set();
222
- const externalOccurrences = [...occurrenceSet].filter((file) => file !== n.source_file);
223
- const localOccurrences = occurrenceCounts.get(name)?.get(n.source_file) || 0;
224
- const localDeclarations = declarationCounts.get(`${n.source_file}\0${name}`) || 0;
225
- const hasLocalProductionUse = localOccurrences > localDeclarations;
226
- const lexicalTestOnly = externalOccurrences.length > 0 && externalOccurrences.every((file) => isTestFile(file));
227
- const hasExactProductionInbound = exactProductionReferenceIds.has(String(n.id));
228
- const hasExactTestInbound = exactTestReferenceIds.has(String(n.id));
229
- if (hasProductionInbound || hasExactProductionInbound || hasLocalProductionUse
230
- || (!hasTestInbound && !hasExactTestInbound && !lexicalTestOnly)) continue;
231
- testOnlySymbols.push({
232
- id: n.id,
233
- file: n.source_file,
234
- label: n.label,
235
- test: false,
236
- reason: "referenced only from test/e2e code; no production consumer was found",
237
- testConsumerFiles: [...new Set(sourcesForSymbol.filter(isTestConsumer).map(consumerFileOf).filter(Boolean))].sort(),
238
- evidence: hasTestInbound ? "graph" : hasExactTestInbound ? "exact-semantic" : "lexical",
239
- publicApi: n.exported === true || ["public", "protected"].includes(String(n.visibility || "").toLowerCase()),
240
- });
241
- }
242
-
243
- const deadFiles = [];
244
- for (const n of nodes) {
245
- if (String(n.id).includes("#")) continue; // file nodes only
246
- if (inbound.has(n.id) || ENTRY_FILE.test(n.source_file) || isFrameworkEntryFile(n.source_file) || entrySet.has(n.source_file)) continue;
247
- const syms = symsByFile.get(n.source_file) || [];
248
- if (syms.length && syms.every((s) => deadSet.has(s.id))) deadFiles.push({ file: n.source_file, reason: "not imported; all symbols unreferenced" });
249
- }
250
-
251
- return {
252
- deadSymbols,
253
- testOnlySymbols,
254
- deadFiles,
255
- referencedIds: [...symById.values()].filter(isReferenced).map((n) => n.id),
256
- stats: { symbols: symById.size, deadSymbols: deadSymbols.length, testOnlySymbols: testOnlySymbols.length, files: nodes.filter((n) => !String(n.id).includes("#")).length, deadFiles: deadFiles.length },
257
- };
258
- }
259
-
260
- // Export-scoped unused check (knip's "unused exports"): an EXPORTED symbol (P0's exported:true flag)
261
- // with no inbound non-contains edge whose bare name occurs in NO file other than its own. Same-file-only
262
- // usage means the symbol is alive but the export is not — still worth surfacing. Pure like computeDead.
263
- // dynamicTargets = files reached via dynamic import() — their exports are consumed at runtime, skip them.
264
- export function computeUnusedExports(graph, sources, { dynamicTargets = new Set(), entrySet = new Set() } = {}) {
265
- const nodes = graph.nodes || [], links = graph.links || [];
266
- const ep = (v) => (v && typeof v === "object" ? v.id : v);
267
- const inbound = new Set();
268
- for (const l of links) if (!isStructuralRelation(l.relation)) inbound.add(ep(l.target));
269
-
270
- const namespaceConsumed = namespaceConsumedFiles(sources, graph);
271
- const candidates = [];
272
- const wanted = new Set();
273
- for (const n of nodes) {
274
- if (!n.exported || !String(n.id).includes("#") || inbound.has(n.id)) continue;
275
- if (ENTRY_FILE.test(n.source_file) || isFrameworkEntryFile(n.source_file) || entrySet.has(n.source_file) || dynamicTargets.has(n.source_file) || namespaceConsumed.has(n.source_file)) continue;
276
- const nm = bareName(n.label);
277
- if (!nm || !/^[A-Za-z_$]/.test(nm)) continue;
278
- if (!hasModuleExport(sources.get(n.source_file), n, nm)) continue;
279
- candidates.push({ n, nm });
280
- wanted.add(nm);
281
- }
282
- if (!candidates.length) return [];
283
-
284
- const occursIn = new Map(); // name -> Set(files whose text contains it)
285
- for (const [file, text] of sources) {
286
- for (const m of String(text || "").matchAll(IDENT_RE)) {
287
- const w = m[0];
288
- if (!wanted.has(w)) continue;
289
- (occursIn.get(w) || occursIn.set(w, new Set()).get(w)).add(file);
290
- }
291
- }
292
-
293
- const out = [];
294
- for (const { n, nm } of candidates) {
295
- const occ = occursIn.get(nm);
296
- if (occ && [...occ].some((f) => f !== n.source_file)) continue; // referenced beyond its own file → alive
297
- out.push({ id: n.id, file: n.source_file, label: n.label, test: isTestFile(n.source_file), reason: "exported but never imported or referenced outside its own file" });
298
- }
299
- return out;
300
- }