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,124 +0,0 @@
1
- const RUST_ROUTE_METHOD = "get|post|put|patch|delete|head|options|trace|connect|any";
2
-
3
- // Find a call's closing parenthesis without mistaking parens inside strings for syntax. This is deliberately
4
- // small (not a Rust parser), but keeps route-handler expressions bounded instead of using a cross-statement
5
- // regex that could turn an unrelated function call into a route.
6
- function closingParen(text, openAt) {
7
- let depth = 0, quote = "", escaped = false, blockComment = 0;
8
- for (let i = openAt; i < text.length; i++) {
9
- const ch = text[i];
10
- if (quote) {
11
- if (escaped) escaped = false;
12
- else if (ch === "\\") escaped = true;
13
- else if (ch === quote) quote = "";
14
- continue;
15
- }
16
- if (blockComment) {
17
- if (ch === "/" && text[i + 1] === "*") { blockComment++; i++; }
18
- else if (ch === "*" && text[i + 1] === "/") { blockComment--; i++; }
19
- continue;
20
- }
21
- if (ch === "/" && text[i + 1] === "/") {
22
- const newline = text.indexOf("\n", i + 2);
23
- if (newline < 0) return -1;
24
- i = newline;
25
- continue;
26
- }
27
- if (ch === "/" && text[i + 1] === "*") { blockComment = 1; i++; continue; }
28
- if (ch === '"') { quote = ch; continue; }
29
- if (ch === "'") {
30
- // Skip a Rust character literal, but do not mistake a lifetime (`'a`) for an unterminated string.
31
- const char = /^'(?:\\.|[^\\'\r\n])'/.exec(text.slice(i));
32
- if (char) { i += char[0].length - 1; continue; }
33
- }
34
- if (ch === "(") depth++;
35
- else if (ch === ")" && --depth === 0) return i;
36
- }
37
- return -1;
38
- }
39
-
40
- // Parse axum's MethodRouter expression, including direct method chains:
41
- // get(list).post(create) / axum::routing::get(show).delete(remove)
42
- // Calls inside a handler closure are not scanned and therefore cannot become fake endpoint methods.
43
- function axumRoutes(expr) {
44
- const found = [];
45
- let rest = String(expr || ""), offset = 0;
46
- const firstRe = new RegExp(`^\\s*(?:(?:axum\\s*::\\s*)?routing\\s*::\\s*)?(${RUST_ROUTE_METHOD})\\s*\\(`, "i");
47
- let match = firstRe.exec(rest);
48
- if (!match) return found;
49
-
50
- while (match) {
51
- const openAt = offset + match.index + match[0].lastIndexOf("(");
52
- const closeAt = closingParen(expr, openAt);
53
- if (closeAt < 0) break;
54
- found.push({ method: match[1], handler: expr.slice(openAt + 1, closeAt), index: openAt });
55
- offset = closeAt + 1;
56
- rest = expr.slice(offset);
57
- match = new RegExp(`^\\s*\\.\\s*(${RUST_ROUTE_METHOD})\\s*\\(`, "i").exec(rest);
58
- }
59
- return found;
60
- }
61
-
62
- // Actix's builder form is structurally distinct from axum and from HTTP clients:
63
- // web::get().to(handler) / actix_web::web::post().to(handler)
64
- function actixRoutes(expr) {
65
- const found = [];
66
- const re = new RegExp(`(?:actix_web\\s*::\\s*)?web\\s*::\\s*(${RUST_ROUTE_METHOD})\\s*\\(\\s*\\)\\s*\\.\\s*to\\s*\\(`, "gi");
67
- let match;
68
- while ((match = re.exec(expr))) {
69
- const openAt = re.lastIndex - 1;
70
- const closeAt = closingParen(expr, openAt);
71
- if (closeAt < 0) continue;
72
- found.push({ method: match[1], handler: expr.slice(openAt + 1, closeAt), index: match.index });
73
- re.lastIndex = closeAt + 1;
74
- }
75
- return found;
76
- }
77
-
78
- export function extractRustEndpoints(text, add) {
79
- let match;
80
-
81
- // Axum and actix both expose a path-first `.route(path, ...)` builder. Requiring either an axum
82
- // MethodRouter or actix's web::<method>().to(...) avoids treating arbitrary Rust APIs named `route` as HTTP.
83
- const routeRe = /\.\s*route\s*\(\s*"(\/(?:\\.|[^"\\])*)"\s*,/g;
84
- while ((match = routeRe.exec(text))) {
85
- const openAt = text.indexOf("(", match.index);
86
- const closeAt = closingParen(text, openAt);
87
- if (closeAt < 0) continue;
88
- const exprStart = routeRe.lastIndex;
89
- const expr = text.slice(exprStart, closeAt);
90
- const routes = [...axumRoutes(expr), ...actixRoutes(expr)];
91
- for (const route of routes) add(route.method, match[1], route.handler, exprStart + route.index);
92
- routeRe.lastIndex = closeAt + 1;
93
- }
94
-
95
- // actix `web::resource(path)` associates several method routes with one path.
96
- const resourceRe = /(?:actix_web\s*::\s*)?web\s*::\s*resource\s*\(\s*"(\/(?:\\.|[^"\\])*)"\s*\)/g;
97
- while ((match = resourceRe.exec(text))) {
98
- const tail = text.slice(resourceRe.lastIndex);
99
- const nextResource = tail.search(/(?:actix_web\s*::\s*)?web\s*::\s*resource\s*\(/);
100
- const semicolon = text.indexOf(";", resourceRe.lastIndex);
101
- let end = Math.min(text.length, resourceRe.lastIndex + 4000);
102
- if (nextResource >= 0) end = Math.min(end, resourceRe.lastIndex + nextResource);
103
- if (semicolon >= 0) end = Math.min(end, semicolon);
104
- const chain = text.slice(resourceRe.lastIndex, end);
105
- for (const route of actixRoutes(chain)) add(route.method, match[1], route.handler, resourceRe.lastIndex + route.index);
106
- }
107
-
108
- // Actix attribute macros. The route macro can declare more than one method for the same handler.
109
- const macroRe = new RegExp(`#\\s*\\[\\s*(?:(?:actix_web|actix)\\s*::\\s*)?(${RUST_ROUTE_METHOD})\\s*\\(\\s*"(\\/(?:\\\\.|[^"\\\\])*)"(?:\\s*,[^\\]]*)?\\s*\\)\\s*\\]`, "gi");
110
- while ((match = macroRe.exec(text))) {
111
- const after = text.slice(macroRe.lastIndex, macroRe.lastIndex + 600);
112
- const fn = /\b(?:pub(?:\s*\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)/.exec(after);
113
- add(match[1], match[2], fn ? fn[1] : "", match.index);
114
- }
115
-
116
- const multiMacroRe = /#\s*\[\s*(?:(?:actix_web|actix)\s*::\s*)?route\s*\(\s*"(\/(?:\\.|[^"\\])*)"([\s\S]*?)\)\s*\]/gi;
117
- while ((match = multiMacroRe.exec(text))) {
118
- const after = text.slice(multiMacroRe.lastIndex, multiMacroRe.lastIndex + 600);
119
- const fn = /\b(?:pub(?:\s*\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)/.exec(after);
120
- const methodRe = /\bmethod\s*=\s*"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|CONNECT)"/gi;
121
- let method;
122
- while ((method = methodRe.exec(match[2]))) add(method[1], match[1], fn ? fn[1] : "", match.index);
123
- }
124
- }
@@ -1,3 +0,0 @@
1
- // Stable public facade for server endpoint discovery.
2
- export { nextRoutePath, extractEndpointsFromText } from "./endpoints/extract.js";
3
- export { analyzeEndpointInventory, detectEndpoints } from "./endpoints/inventory.js";
@@ -1,70 +0,0 @@
1
- // Unified Finding factory + rollups for the internal dependency/security engine (DEPS_SECURITY_PLAN.md §2.3).
2
- // Every analyzer (unused / structure / vulnerability / malware) and the external-tool adapter emit THIS shape,
3
- // so the renderer and the AI summarizer consume one contract regardless of engine.
4
- import { createHash } from "node:crypto";
5
-
6
- const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
7
-
8
- export function dependencyVerification(manifest, imports, decision, mapping) {
9
- return {
10
- evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
11
- decision,
12
- manifestDeclaration: { status: "FOUND", file: manifest },
13
- indexedSourceImports: imports.length
14
- ? { status: "FOUND", count: imports.length, files: [...new Set(imports.map((item) => item.file))].slice(0, 10) }
15
- : { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] },
16
- mapping,
17
- };
18
- }
19
-
20
- // Stable id: survives re-runs so the UI can persist expand/dismiss state per finding.
21
- export function makeFinding(f) {
22
- const cycleIdentity = Array.isArray(f.cycleMembers)
23
- ? [...new Set(f.cycleMembers.map(String))].sort().join("\0")
24
- : "";
25
- const id = createHash("sha1")
26
- .update([
27
- f.category, f.rule, f.file || "", f.manifest || "", f.scope || "",
28
- f.package || "", f.symbol || "", cycleIdentity, f.title || "",
29
- ].join("|"))
30
- .digest("hex")
31
- .slice(0, 16);
32
- return {
33
- id,
34
- severity: "info",
35
- confidence: "medium",
36
- title: "",
37
- detail: "",
38
- reason: "",
39
- file: "",
40
- line: 0,
41
- symbol: "",
42
- package: "",
43
- version: "",
44
- graphNodeId: f.graphNodeId || f.file || "",
45
- evidence: [],
46
- source: "internal",
47
- fixHint: "",
48
- ...f,
49
- };
50
- }
51
-
52
- export function summarizeFindings(findings) {
53
- const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
54
- const byCategory = { unused: 0, structure: 0, vulnerability: 0, malware: 0 };
55
- for (const f of findings || []) {
56
- if (f.severity in bySeverity) bySeverity[f.severity]++;
57
- if (f.category in byCategory) byCategory[f.category]++;
58
- }
59
- return { bySeverity, byCategory };
60
- }
61
-
62
- export function sortFindings(findings) {
63
- const sev = new Map(SEVERITY_ORDER.map((s, i) => [s, i]));
64
- return [...(findings || [])].sort(
65
- (a, b) =>
66
- (sev.get(a.severity) ?? 9) - (sev.get(b.severity) ?? 9) ||
67
- String(a.category).localeCompare(String(b.category)) ||
68
- String(a.file || a.package || "").localeCompare(String(b.file || b.package || ""))
69
- );
70
- }
@@ -1,177 +0,0 @@
1
- import {isTestPath} from '../../graph/graph-filter.js'
2
- import {isStructuralRelation} from '../../graph/relations.js'
3
- import {
4
- boundedHistoryInteger,
5
- GIT_HISTORY_DEFAULTS as DEFAULTS,
6
- GIT_HISTORY_HARD_CAPS as HARD_CAPS,
7
- GIT_HISTORY_V,
8
- graphEndpoint,
9
- roundHistoryNumber as round,
10
- safeHistoryPath,
11
- } from './options.js'
12
-
13
- function graphFilesAndAdjacency(graph = {}) {
14
- const byId = new Map(), files = new Set()
15
- for (const node of graph.nodes || []) {
16
- const file = safeHistoryPath(node?.source_file)
17
- if (!file) continue
18
- byId.set(String(node.id), file)
19
- files.add(file)
20
- }
21
- const adjacency = new Map([...files].map((file) => [file, new Set()]))
22
- for (const link of graph.links || []) {
23
- if (isStructuralRelation(link?.relation) || link?.barrelProxy === true) continue
24
- const left = byId.get(String(graphEndpoint(link?.source)))
25
- const right = byId.get(String(graphEndpoint(link?.target)))
26
- if (!left || !right || left === right) continue
27
- adjacency.get(left)?.add(right)
28
- adjacency.get(right)?.add(left)
29
- }
30
- return {files, adjacency}
31
- }
32
-
33
- function graphDistanceAtMostTwo(left, right, adjacency) {
34
- if (left === right) return 0
35
- const neighbors = adjacency.get(left)
36
- if (!neighbors) return null
37
- if (neighbors.has(right)) return 1
38
- for (const middle of neighbors) if (adjacency.get(middle)?.has(right)) return 2
39
- return null
40
- }
41
-
42
- function percentile(value, positiveValues) {
43
- if (!(value > 0) || !positiveValues.length) return 0
44
- let atOrBelow = 0
45
- for (const candidate of positiveValues) if (candidate <= value) atOrBelow++
46
- return round(atOrBelow / positiveValues.length)
47
- }
48
-
49
- const pairSort = (left, right) => right.count - left.count
50
- || right.confidence - left.confidence
51
- || right.lift - left.lift
52
- || left.left.localeCompare(right.left)
53
- || left.right.localeCompare(right.right)
54
-
55
- const publicPair = (pair, graphDistance) => ({
56
- left: pair.left,
57
- right: pair.right,
58
- count: pair.count,
59
- jaccard: pair.jaccard,
60
- lift: pair.lift,
61
- confidence: pair.confidence,
62
- leftConfidence: pair.leftConfidence,
63
- rightConfidence: pair.rightConfidence,
64
- graphDistance,
65
- })
66
-
67
- function fileActivity(commits, graph) {
68
- const activity = new Map(), fileCommits = new Map()
69
- let additions = 0, deletions = 0, binaryChanges = 0
70
- for (const commit of commits) {
71
- const seen = new Set()
72
- for (const stat of commit.files) {
73
- if (seen.has(stat.file)) continue
74
- seen.add(stat.file)
75
- const entry = activity.get(stat.file) || {file: stat.file, commits: 0, additions: 0, deletions: 0, binaryChanges: 0}
76
- entry.commits++
77
- entry.additions += stat.additions
78
- entry.deletions += stat.deletions
79
- entry.binaryChanges += stat.binary ? 1 : 0
80
- activity.set(stat.file, entry)
81
- fileCommits.set(stat.file, (fileCommits.get(stat.file) || 0) + 1)
82
- additions += stat.additions
83
- deletions += stat.deletions
84
- if (stat.binary) binaryChanges++
85
- }
86
- }
87
- const {files: graphFiles, adjacency} = graphFilesAndAdjacency(graph)
88
- const raw = [...activity.values()].map((entry) => ({...entry, churn: entry.additions + entry.deletions}))
89
- const churnValues = raw.map((entry) => entry.churn).filter((value) => value > 0)
90
- const connectivityValues = [...graphFiles].map((file) => adjacency.get(file)?.size || 0).filter((value) => value > 0)
91
- const fileChurn = raw.map((entry) => {
92
- const connectivity = adjacency.get(entry.file)?.size || 0
93
- const churnPercentile = percentile(entry.churn, churnValues)
94
- const connectivityPercentile = percentile(connectivity, connectivityValues)
95
- return {...entry, connectivity, churnPercentile, connectivityPercentile, hotspotScore: round(Math.sqrt(churnPercentile * connectivityPercentile))}
96
- }).sort((a, b) => b.churn - a.churn || b.commits - a.commits || a.file.localeCompare(b.file))
97
- const hotspots = fileChurn.filter((entry) => entry.connectivity > 0)
98
- .sort((a, b) => b.hotspotScore - a.hotspotScore || b.churn - a.churn || b.connectivity - a.connectivity || a.file.localeCompare(b.file))
99
- return {fileCommits, graphFiles, adjacency, fileChurn, hotspots, additions, deletions, binaryChanges}
100
- }
101
-
102
- function coChangePairs(commits, fileCommits, limits) {
103
- const counts = new Map()
104
- let truncated = false
105
- for (const commit of commits) {
106
- const files = [...new Set(commit.files.map((entry) => entry.file))].sort()
107
- for (let left = 0; left < files.length; left++) for (let right = left + 1; right < files.length; right++) {
108
- const key = `${files[left]}\0${files[right]}`
109
- if (!counts.has(key) && counts.size >= limits.maxPairCandidates) { truncated = true; continue }
110
- counts.set(key, (counts.get(key) || 0) + 1)
111
- }
112
- }
113
- const pairs = []
114
- for (const [key, count] of counts) {
115
- if (count < limits.minPairCount || !commits.length) continue
116
- const split = key.indexOf('\0'), left = key.slice(0, split), right = key.slice(split + 1)
117
- const leftCount = fileCommits.get(left) || 0, rightCount = fileCommits.get(right) || 0
118
- const leftConfidence = count / leftCount, rightConfidence = count / rightCount
119
- pairs.push({
120
- left, right, count,
121
- jaccard: round(count / (leftCount + rightCount - count)),
122
- lift: round((count * commits.length) / (leftCount * rightCount)),
123
- confidence: round(Math.max(leftConfidence, rightConfidence)),
124
- leftConfidence: round(leftConfidence),
125
- rightConfidence: round(rightConfidence),
126
- })
127
- }
128
- return {pairs: pairs.sort(pairSort), totalCandidates: counts.size, truncated}
129
- }
130
-
131
- export function buildGitHistoryAnalytics({commits = [], graph = {}, window, limits = {}, status = 'complete'} = {}) {
132
- const maxPairs = boundedHistoryInteger(limits.maxPairs, DEFAULTS.maxPairs, 1, HARD_CAPS.maxPairs)
133
- const minPairCount = boundedHistoryInteger(limits.minPairCount, DEFAULTS.minPairCount, 1, 100)
134
- const maxPairCandidates = boundedHistoryInteger(limits.maxPairCandidates, DEFAULTS.maxPairCandidates, 100, HARD_CAPS.maxPairCandidates)
135
- const eligible = commits.filter((commit) => !commit.oversized && commit.files.length > 0)
136
- const skipped = commits.filter((commit) => commit.oversized)
137
- const activity = fileActivity(eligible, graph)
138
- const cochange = coChangePairs(eligible, activity.fileCommits, {minPairCount, maxPairCandidates})
139
- const observed = cochange.pairs.slice(0, maxPairs).map((pair) => publicPair(pair, graphDistanceAtMostTwo(pair.left, pair.right, activity.adjacency)))
140
- const expectedTestSource = cochange.pairs.filter((pair) => isTestPath(pair.left) !== isTestPath(pair.right)).slice(0, maxPairs).map((pair) => {
141
- const test = isTestPath(pair.left) ? pair.left : pair.right
142
- const source = test === pair.left ? pair.right : pair.left
143
- return {
144
- source, test, count: pair.count, jaccard: pair.jaccard, lift: pair.lift, confidence: pair.confidence,
145
- sourceConfidence: source === pair.left ? pair.leftConfidence : pair.rightConfidence,
146
- testConfidence: test === pair.left ? pair.leftConfidence : pair.rightConfidence,
147
- graphDistance: graphDistanceAtMostTwo(source, test, activity.adjacency),
148
- }
149
- })
150
- const hidden = cochange.pairs.filter((pair) => !isTestPath(pair.left) && !isTestPath(pair.right)
151
- && activity.graphFiles.has(pair.left) && activity.graphFiles.has(pair.right)
152
- && graphDistanceAtMostTwo(pair.left, pair.right, activity.adjacency) === null)
153
- .slice(0, maxPairs).map((pair) => publicPair(pair, null))
154
- const reasons = [
155
- skipped.length ? `${skipped.length} oversized change-set(s) excluded` : null,
156
- cochange.truncated ? 'co-change candidate cap reached' : null,
157
- status === 'partial' ? 'git output or commit window was truncated' : null,
158
- ].filter(Boolean)
159
- return {
160
- gitHistoryV: GIT_HISTORY_V,
161
- status: reasons.length ? 'partial' : status,
162
- window: window || null,
163
- limits: {maxCommits: limits.maxCommits ?? null, maxFilesPerCommit: limits.maxFilesPerCommit ?? null, maxPairs, minPairCount, maxPairCandidates},
164
- completeness: {complete: reasons.length === 0, reasons},
165
- totals: {
166
- commitsRead: commits.length, commitsAnalyzed: eligible.length, oversizedCommitsSkipped: skipped.length,
167
- files: activity.fileChurn.length, additions: activity.additions, deletions: activity.deletions,
168
- churn: activity.additions + activity.deletions, binaryChanges: activity.binaryChanges,
169
- ignoredFiles: commits.reduce((sum, commit) => sum + (commit.ignoredFiles || 0), 0),
170
- invalidPaths: commits.reduce((sum, commit) => sum + (commit.invalidPaths || 0), 0),
171
- graphFiles: activity.graphFiles.size,
172
- },
173
- fileChurn: activity.fileChurn,
174
- hotspots: activity.hotspots,
175
- coupling: {eligibleCommits: eligible.length, totalCandidates: cochange.totalCandidates, candidatesTruncated: cochange.truncated, observed, expectedTestSource, hidden},
176
- }
177
- }
@@ -1,67 +0,0 @@
1
- import {isWeavatrixIgnored} from '../../path-ignore.js'
2
- import {
3
- boundedHistoryInteger,
4
- GIT_HISTORY_DEFAULTS as DEFAULTS,
5
- GIT_HISTORY_HARD_CAPS as HARD_CAPS,
6
- safeHistoryPath,
7
- } from './options.js'
8
-
9
- const HEADER_SEPARATOR = '\x1e'
10
- const FIELD_SEPARATOR = '\x1f'
11
-
12
- // git subprocess execution (sync runGit + streaming boundedGitCommand) lives in ../../git-exec.js.
13
-
14
- const statNumber = (value) => value === '-'
15
- ? {value: 0, binary: true}
16
- : {value: Number(value), binary: false}
17
-
18
- export function parseGitNumstatLog(raw, options = {}) {
19
- const maxFiles = boundedHistoryInteger(options.maxFilesPerCommit, DEFAULTS.maxFilesPerCommit, 2, HARD_CAPS.maxFilesPerCommit)
20
- const ignoreRules = options.ignoreRules || []
21
- const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw || '')
22
- const segments = text.split(HEADER_SEPARATOR).slice(1)
23
- if (options.dropLastIncomplete && segments.length) segments.pop()
24
- const commits = []
25
- for (const segment of segments) {
26
- const firstNul = segment.indexOf('\0')
27
- if (firstNul < 0) continue
28
- const header = segment.slice(0, firstNul).replace(/^[\r\n]+/, '')
29
- const separator = header.indexOf(FIELD_SEPARATOR)
30
- if (separator < 0) continue
31
- const hash = header.slice(0, separator), timestamp = Number(header.slice(separator + 1))
32
- if (!/^[a-f0-9]{40,64}$/i.test(hash) || !Number.isInteger(timestamp) || timestamp < 0) continue
33
- const tokens = segment.slice(firstNul + 1).split('\0'), files = new Map()
34
- let fileCount = 0, ignoredFiles = 0, invalidPaths = 0, oversized = false
35
- for (let index = 0; index < tokens.length; index += 1) {
36
- const token = tokens[index].replace(/^[\r\n]+/, '')
37
- const match = /^(\d+|-)\t(\d+|-)\t(.*)$/s.exec(token)
38
- if (!match) continue
39
- let rawPath = match[3], renamedFrom = null
40
- if (!rawPath) {
41
- renamedFrom = safeHistoryPath(tokens[index + 1])
42
- rawPath = tokens[index + 2]
43
- index += 2
44
- }
45
- const path = safeHistoryPath(rawPath)
46
- if (!path) { invalidPaths++; continue }
47
- if (isWeavatrixIgnored(path, ignoreRules)) { ignoredFiles++; continue }
48
- const additions = statNumber(match[1]), deletions = statNumber(match[2])
49
- fileCount++
50
- if (fileCount > maxFiles) { oversized = true; files.clear(); continue }
51
- if (oversized) continue
52
- const previous = files.get(path)
53
- files.set(path, {
54
- file: path,
55
- additions: (previous?.additions || 0) + additions.value,
56
- deletions: (previous?.deletions || 0) + deletions.value,
57
- binary: Boolean(previous?.binary || additions.binary || deletions.binary),
58
- ...(renamedFrom ? {renamedFrom} : previous?.renamedFrom ? {renamedFrom: previous.renamedFrom} : {}),
59
- })
60
- }
61
- commits.push({
62
- hash, timestamp, fileCount, ignoredFiles, invalidPaths, oversized,
63
- files: oversized ? [] : [...files.values()].sort((a, b) => a.file.localeCompare(b.file)),
64
- })
65
- }
66
- return commits
67
- }
@@ -1,66 +0,0 @@
1
- import {isAbsolute} from 'node:path'
2
- import {boundedInteger} from '../../util.js'
3
-
4
- export const GIT_HISTORY_V = 1
5
- export const GIT_HISTORY_WINDOWS = Object.freeze([3, 6, 12])
6
- export const GIT_HISTORY_DEFAULTS = Object.freeze({
7
- months: 6,
8
- maxCommits: 500,
9
- maxFilesPerCommit: 80,
10
- maxPairs: 100,
11
- minPairCount: 2,
12
- maxPairCandidates: 100_000,
13
- maxOutputBytes: 16 * 1024 * 1024,
14
- timeoutMs: 20_000,
15
- })
16
- export const GIT_HISTORY_HARD_CAPS = Object.freeze({
17
- maxCommits: 2_000,
18
- maxFilesPerCommit: 200,
19
- maxPairs: 500,
20
- maxPairCandidates: 250_000,
21
- maxOutputBytes: 64 * 1024 * 1024,
22
- timeoutMs: 60_000,
23
- })
24
- export const GIT_FORMAT = '%x1e%H%x1f%ct'
25
-
26
- export const graphEndpoint = (value) => value && typeof value === 'object' ? value.id : value
27
- export const roundHistoryNumber = (value, digits = 4) => {
28
- if (!Number.isFinite(value)) return 0
29
- const scale = 10 ** digits
30
- return Math.round(value * scale) / scale
31
- }
32
- export const boundedHistoryInteger = boundedInteger
33
-
34
- export function safeHistoryPath(value) {
35
- const path = String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
36
- if (!path || path.includes('\0') || isAbsolute(path) || /^[a-z]:\//i.test(path)) return null
37
- if (/[\x00-\x1f\x7f]/.test(path)) return null
38
- const parts = path.split('/')
39
- return parts.some((part) => !part || part === '.' || part === '..') ? null : path
40
- }
41
-
42
- export function utcMonthsBefore(date, months) {
43
- const source = new Date(date)
44
- if (!Number.isFinite(source.getTime())) throw new Error('now must be a valid date')
45
- const targetMonth = source.getUTCMonth() - months
46
- const first = new Date(Date.UTC(source.getUTCFullYear(), targetMonth, 1, source.getUTCHours(), source.getUTCMinutes(), source.getUTCSeconds(), source.getUTCMilliseconds()))
47
- const lastDay = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate()
48
- first.setUTCDate(Math.min(source.getUTCDate(), lastDay))
49
- return first
50
- }
51
-
52
- export function normalizeGitHistoryOptions(input = {}) {
53
- const defaults = GIT_HISTORY_DEFAULTS, caps = GIT_HISTORY_HARD_CAPS
54
- const months = Number(input.months ?? defaults.months)
55
- if (!GIT_HISTORY_WINDOWS.includes(months)) throw new Error('months must be one of 3, 6 or 12')
56
- return {
57
- months,
58
- maxCommits: boundedHistoryInteger(input.maxCommits, defaults.maxCommits, 1, caps.maxCommits),
59
- maxFilesPerCommit: boundedHistoryInteger(input.maxFilesPerCommit, defaults.maxFilesPerCommit, 2, caps.maxFilesPerCommit),
60
- maxPairs: boundedHistoryInteger(input.maxPairs, defaults.maxPairs, 1, caps.maxPairs),
61
- minPairCount: boundedHistoryInteger(input.minPairCount, defaults.minPairCount, 1, 100),
62
- maxPairCandidates: boundedHistoryInteger(input.maxPairCandidates, defaults.maxPairCandidates, 100, caps.maxPairCandidates),
63
- maxOutputBytes: boundedHistoryInteger(input.maxOutputBytes, defaults.maxOutputBytes, 64 * 1024, caps.maxOutputBytes),
64
- timeoutMs: boundedHistoryInteger(input.timeoutMs, defaults.timeoutMs, 1_000, caps.timeoutMs),
65
- }
66
- }
@@ -1,143 +0,0 @@
1
- // Public facade and bounded local collector for behavioral Git-history intelligence.
2
- import {childProcessEnv} from '../child-env.js'
3
- import {createRepoBoundary} from '../repo-path.js'
4
- import {loadWeavatrixIgnore} from '../path-ignore.js'
5
- import {buildGitHistoryAnalytics} from './git-history/analytics.js'
6
- import {parseGitNumstatLog} from './git-history/collector.js'
7
- import {boundedGitCommand} from '../git-exec.js'
8
- import {
9
- boundedHistoryInteger,
10
- GIT_FORMAT,
11
- GIT_HISTORY_V,
12
- normalizeGitHistoryOptions,
13
- utcMonthsBefore,
14
- } from './git-history/options.js'
15
-
16
- export {buildGitHistoryAnalytics} from './git-history/analytics.js'
17
- export {parseGitNumstatLog} from './git-history/collector.js'
18
- export {GIT_HISTORY_V, GIT_HISTORY_WINDOWS} from './git-history/options.js'
19
-
20
- const unavailableResult = (window, limits, reason) => ({
21
- gitHistoryV: GIT_HISTORY_V,
22
- status: 'unavailable',
23
- window,
24
- limits,
25
- completeness: {complete: false, reasons: [reason]},
26
- totals: {
27
- commitsRead: 0, commitsAnalyzed: 0, oversizedCommitsSkipped: 0, files: 0,
28
- additions: 0, deletions: 0, churn: 0, binaryChanges: 0, ignoredFiles: 0,
29
- invalidPaths: 0, graphFiles: 0,
30
- },
31
- fileChurn: [],
32
- hotspots: [],
33
- coupling: {eligibleCommits: 0, totalCandidates: 0, candidatesTruncated: false, observed: [], expectedTestSource: [], hidden: []},
34
- })
35
-
36
- export async function analyzeGitHistory(input = {}) {
37
- const options = normalizeGitHistoryOptions(input)
38
- const now = new Date(input.now ?? Date.now())
39
- if (!Number.isFinite(now.getTime())) throw new Error('now must be a valid date')
40
- const since = utcMonthsBefore(now, options.months)
41
- const window = {months: options.months, since: since.toISOString(), until: now.toISOString()}
42
- const limits = {
43
- maxCommits: options.maxCommits,
44
- maxFilesPerCommit: options.maxFilesPerCommit,
45
- maxPairs: options.maxPairs,
46
- minPairCount: options.minPairCount,
47
- maxPairCandidates: options.maxPairCandidates,
48
- }
49
- const boundary = createRepoBoundary(input.repoRoot)
50
- if (!boundary.root) return unavailableResult(window, limits, 'repository root is unavailable')
51
- const args = [
52
- 'log', '--no-merges', '--numstat', '-z', `--format=${GIT_FORMAT}`,
53
- `--since=${window.since}`, `--until=${window.until}`, `--max-count=${options.maxCommits + 1}`,
54
- '--', '.',
55
- ]
56
- let execution
57
- try {
58
- execution = await (input.runner || boundedGitCommand)('git', args, {
59
- cwd: boundary.root,
60
- env: childProcessEnv(),
61
- timeoutMs: options.timeoutMs,
62
- maxOutputBytes: options.maxOutputBytes,
63
- })
64
- } catch (error) {
65
- return unavailableResult(window, limits, String(error?.message || 'git history collection failed').slice(0, 200))
66
- }
67
- if (execution.exitCode !== 0 && !execution.truncated) return unavailableResult(window, limits, 'git log failed')
68
- const raw = Buffer.isBuffer(execution.stdout) ? execution.stdout : Buffer.from(String(execution.stdout || ''))
69
- const tooLarge = raw.length > options.maxOutputBytes
70
- const truncated = Boolean(execution.truncated || tooLarge)
71
- const bounded = tooLarge ? raw.subarray(0, options.maxOutputBytes) : raw
72
- const parsed = parseGitNumstatLog(bounded, {
73
- maxFilesPerCommit: options.maxFilesPerCommit,
74
- ignoreRules: loadWeavatrixIgnore(boundary.root),
75
- dropLastIncomplete: truncated,
76
- })
77
- const commitsTruncated = parsed.length > options.maxCommits
78
- return buildGitHistoryAnalytics({
79
- commits: parsed.slice(0, options.maxCommits),
80
- graph: input.graph || {},
81
- window,
82
- limits,
83
- status: truncated || commitsTruncated ? 'partial' : 'complete',
84
- })
85
- }
86
-
87
- export function formatGitHistoryAnalytics(result, options = {}) {
88
- const topN = boundedHistoryInteger(options.topN, 10, 1, 50)
89
- if (!result || result.status === 'unavailable') {
90
- return `Git history intelligence: UNAVAILABLE — ${result?.completeness?.reasons?.[0] || 'history is unavailable'}`
91
- }
92
- const window = result.window
93
- ? `${result.window.months} months (${result.window.since.slice(0, 10)} → ${result.window.until.slice(0, 10)})`
94
- : 'configured window'
95
- const lines = [
96
- `Git history intelligence — ${window}`,
97
- `Status: ${String(result.status).toUpperCase()} · ${result.totals.commitsAnalyzed}/${result.totals.commitsRead} commits analyzed · ${result.totals.files} files · ${result.totals.churn} changed lines`,
98
- '',
99
- 'Hotspots (churn percentile × graph-connectivity percentile):',
100
- ]
101
- const hotspots = result.hotspots.slice(0, topN)
102
- if (!hotspots.length) lines.push('- none')
103
- for (const item of hotspots) lines.push(`- ${item.file}: score ${item.hotspotScore.toFixed(4)} · churn ${item.churn} in ${item.commits} commits · connectivity ${item.connectivity}`)
104
- lines.push('', 'Hidden co-change coupling (no graph path within 2 hops):')
105
- const hidden = result.coupling.hidden.slice(0, topN)
106
- if (!hidden.length) lines.push('- none')
107
- for (const pair of hidden) lines.push(`- ${pair.left} ↔ ${pair.right}: ${pair.count} commits · Jaccard ${pair.jaccard.toFixed(4)} · lift ${pair.lift.toFixed(4)} · confidence ${pair.confidence.toFixed(4)}`)
108
- lines.push('', 'Expected test/source co-change:')
109
- const expected = result.coupling.expectedTestSource.slice(0, topN)
110
- if (!expected.length) lines.push('- none')
111
- for (const pair of expected) lines.push(`- ${pair.test} ↔ ${pair.source}: ${pair.count} commits · confidence ${pair.confidence.toFixed(4)}`)
112
- if (result.completeness.reasons.length) lines.push('', `Partial: ${result.completeness.reasons.join('; ')}.`)
113
- return lines.join('\n')
114
- }
115
-
116
- export function boundGitHistoryAnalytics(result, options = {}) {
117
- const topN = boundedHistoryInteger(options.topN, 10, 1, 50)
118
- const source = result && typeof result === 'object' ? result : {}
119
- const coupling = source.coupling && typeof source.coupling === 'object' ? source.coupling : {}
120
- const collections = {}
121
- const cap = (name, value) => {
122
- const items = Array.isArray(value) ? value : []
123
- const bounded = items.slice(0, topN)
124
- collections[name] = {total: items.length, returned: bounded.length, truncated: items.length > bounded.length}
125
- return bounded
126
- }
127
- const bounded = {
128
- ...source,
129
- limits: {...(source.limits || {}), topN},
130
- fileChurn: cap('fileChurn', source.fileChurn),
131
- hotspots: cap('hotspots', source.hotspots),
132
- coupling: {
133
- ...coupling,
134
- observed: cap('coupling.observed', coupling.observed),
135
- expectedTestSource: cap('coupling.expectedTestSource', coupling.expectedTestSource),
136
- hidden: cap('coupling.hidden', coupling.hidden),
137
- },
138
- }
139
- return {
140
- result: bounded,
141
- page: {limit: topN, truncated: Object.values(collections).some((entry) => entry.truncated), collections},
142
- }
143
- }