weavatrix 0.3.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +231 -199
  3. package/bin/native/darwin-arm64/weavatrix +0 -0
  4. package/bin/native/darwin-x64/weavatrix +0 -0
  5. package/bin/native/linux-arm64/weavatrix +0 -0
  6. package/bin/native/linux-x64/weavatrix +0 -0
  7. package/bin/native/win32-arm64/weavatrix.exe +0 -0
  8. package/bin/native/win32-x64/weavatrix.exe +0 -0
  9. package/bin/resolve-binary.mjs +73 -0
  10. package/bin/run-native.mjs +26 -0
  11. package/bin/weavatrix-mcp.mjs +7 -6
  12. package/bin/weavatrix.mjs +5 -0
  13. package/package.json +8 -66
  14. package/skill/SKILL.md +62 -374
  15. package/skill/agents/openai.yaml +4 -0
  16. package/skill/references/tool-routing.md +45 -0
  17. package/SECURITY.md +0 -101
  18. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  19. package/docs/agent-task-benchmark.md +0 -58
  20. package/docs/releases/v0.2.19.md +0 -33
  21. package/docs/releases/v0.3.0.md +0 -43
  22. package/docs/releases/v0.3.1.md +0 -30
  23. package/docs/releases/v0.3.10.md +0 -76
  24. package/docs/releases/v0.3.11.md +0 -55
  25. package/docs/releases/v0.3.12.md +0 -44
  26. package/docs/releases/v0.3.13.md +0 -34
  27. package/docs/releases/v0.3.14.md +0 -37
  28. package/docs/releases/v0.3.2.md +0 -15
  29. package/docs/releases/v0.3.3.md +0 -25
  30. package/docs/releases/v0.3.4.md +0 -40
  31. package/docs/releases/v0.3.5.md +0 -50
  32. package/docs/releases/v0.3.6.md +0 -44
  33. package/docs/releases/v0.3.7.md +0 -37
  34. package/docs/releases/v0.3.8.md +0 -21
  35. package/docs/releases/v0.3.9.md +0 -36
  36. package/scripts/run-agent-task-benchmark.mjs +0 -118
  37. package/src/analysis/allowed-test-runner.js +0 -70
  38. package/src/analysis/architecture/contract-graph.js +0 -119
  39. package/src/analysis/architecture/contract-schema.js +0 -110
  40. package/src/analysis/architecture/contract-storage.js +0 -35
  41. package/src/analysis/architecture/contract-verification.js +0 -168
  42. package/src/analysis/architecture-contract.js +0 -16
  43. package/src/analysis/audit-debt.js +0 -94
  44. package/src/analysis/audit-extensions.js +0 -60
  45. package/src/analysis/cargo-dependency-evidence.js +0 -108
  46. package/src/analysis/cargo-manifests.js +0 -81
  47. package/src/analysis/change-classification/diff-parser.js +0 -103
  48. package/src/analysis/change-classification/options.js +0 -40
  49. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  50. package/src/analysis/change-classification.js +0 -188
  51. package/src/analysis/coverage-reports-tarpaulin.js +0 -112
  52. package/src/analysis/coverage-reports.js +0 -246
  53. package/src/analysis/cycle-route.js +0 -14
  54. package/src/analysis/data-flow-evidence.js +0 -114
  55. package/src/analysis/dead-check.js +0 -300
  56. package/src/analysis/dead-code-review/candidates.js +0 -194
  57. package/src/analysis/dead-code-review/policy.js +0 -23
  58. package/src/analysis/dead-code-review.js +0 -119
  59. package/src/analysis/dep-check-ecosystems.js +0 -205
  60. package/src/analysis/dep-check.js +0 -290
  61. package/src/analysis/dep-rules.js +0 -10
  62. package/src/analysis/dependency/conventions.js +0 -16
  63. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  64. package/src/analysis/dependency/source-references.js +0 -21
  65. package/src/analysis/duplicate-groups.js +0 -104
  66. package/src/analysis/duplicates-worker.js +0 -11
  67. package/src/analysis/duplicates.compute.js +0 -269
  68. package/src/analysis/duplicates.js +0 -15
  69. package/src/analysis/duplicates.run.js +0 -52
  70. package/src/analysis/duplicates.tokenize.js +0 -199
  71. package/src/analysis/endpoints/common.js +0 -79
  72. package/src/analysis/endpoints/extract.js +0 -120
  73. package/src/analysis/endpoints/inventory.js +0 -84
  74. package/src/analysis/endpoints/mounts.js +0 -129
  75. package/src/analysis/endpoints-java.js +0 -260
  76. package/src/analysis/endpoints-rust.js +0 -124
  77. package/src/analysis/endpoints.js +0 -3
  78. package/src/analysis/findings.js +0 -70
  79. package/src/analysis/git-history/analytics.js +0 -177
  80. package/src/analysis/git-history/collector.js +0 -67
  81. package/src/analysis/git-history/options.js +0 -66
  82. package/src/analysis/git-history.js +0 -143
  83. package/src/analysis/git-ref-graph.js +0 -76
  84. package/src/analysis/go-dependency-evidence.js +0 -62
  85. package/src/analysis/graph-analysis.aggregate.js +0 -296
  86. package/src/analysis/graph-analysis.edges.js +0 -24
  87. package/src/analysis/graph-analysis.js +0 -7
  88. package/src/analysis/graph-analysis.refs.js +0 -202
  89. package/src/analysis/graph-analysis.summaries.js +0 -65
  90. package/src/analysis/health-capabilities.js +0 -83
  91. package/src/analysis/hot-path-review.js +0 -266
  92. package/src/analysis/http-contract-wrappers.js +0 -241
  93. package/src/analysis/http-contracts/analysis.js +0 -179
  94. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  95. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  96. package/src/analysis/http-contracts/graph-context.js +0 -143
  97. package/src/analysis/http-contracts/matching.js +0 -61
  98. package/src/analysis/http-contracts/shared.js +0 -85
  99. package/src/analysis/http-contracts.js +0 -6
  100. package/src/analysis/internal-audit/dependency-health.js +0 -113
  101. package/src/analysis/internal-audit/python-manifests.js +0 -78
  102. package/src/analysis/internal-audit/repo-files.js +0 -50
  103. package/src/analysis/internal-audit/supply-chain.js +0 -149
  104. package/src/analysis/internal-audit.collect.js +0 -213
  105. package/src/analysis/internal-audit.js +0 -9
  106. package/src/analysis/internal-audit.reach.js +0 -201
  107. package/src/analysis/internal-audit.run.js +0 -256
  108. package/src/analysis/java-source.js +0 -36
  109. package/src/analysis/jvm-artifact-index.js +0 -87
  110. package/src/analysis/jvm-dependency-evidence.js +0 -138
  111. package/src/analysis/jvm-manifests.js +0 -114
  112. package/src/analysis/manifests.js +0 -123
  113. package/src/analysis/package-reachability.js +0 -39
  114. package/src/analysis/source-complexity.ast.js +0 -97
  115. package/src/analysis/source-complexity.constants.js +0 -55
  116. package/src/analysis/source-complexity.js +0 -14
  117. package/src/analysis/source-complexity.report.js +0 -81
  118. package/src/analysis/source-complexity.walk.js +0 -228
  119. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  120. package/src/analysis/source-correctness.js +0 -222
  121. package/src/analysis/static-test-reachability.js +0 -150
  122. package/src/analysis/structure/dependency-graph.js +0 -151
  123. package/src/analysis/structure/findings.js +0 -162
  124. package/src/analysis/task-retrieval.js +0 -107
  125. package/src/analysis/transport-contracts.js +0 -224
  126. package/src/analysis/transport-runtime-evidence.js +0 -155
  127. package/src/analysis-kit.mjs +0 -17
  128. package/src/bounds.js +0 -4
  129. package/src/build-graph.js +0 -209
  130. package/src/child-env.js +0 -7
  131. package/src/config.js +0 -5
  132. package/src/extension/local-services.mjs +0 -75
  133. package/src/git-exec.js +0 -67
  134. package/src/graph/build-worker.js +0 -49
  135. package/src/graph/builder/go-receiver-resolution.js +0 -112
  136. package/src/graph/builder/js/dynamic-import.js +0 -26
  137. package/src/graph/builder/js/queries.js +0 -48
  138. package/src/graph/builder/lang-csharp.js +0 -40
  139. package/src/graph/builder/lang-css.js +0 -29
  140. package/src/graph/builder/lang-go.js +0 -140
  141. package/src/graph/builder/lang-html.js +0 -29
  142. package/src/graph/builder/lang-java-calls.js +0 -142
  143. package/src/graph/builder/lang-java.js +0 -215
  144. package/src/graph/builder/lang-js.js +0 -291
  145. package/src/graph/builder/lang-python.js +0 -235
  146. package/src/graph/builder/lang-rust-calls.js +0 -63
  147. package/src/graph/builder/lang-rust.js +0 -299
  148. package/src/graph/builder/lang-solidity.js +0 -126
  149. package/src/graph/builder/lang-sql.js +0 -263
  150. package/src/graph/builder/pass2-resolution.js +0 -81
  151. package/src/graph/builder/spec-pkg.js +0 -87
  152. package/src/graph/community.js +0 -27
  153. package/src/graph/edge-provenance.js +0 -31
  154. package/src/graph/file-lock.js +0 -69
  155. package/src/graph/freshness-probe.js +0 -142
  156. package/src/graph/graph-filter.js +0 -63
  157. package/src/graph/incremental-refresh.js +0 -235
  158. package/src/graph/internal-builder.barrels.js +0 -235
  159. package/src/graph/internal-builder.build.js +0 -300
  160. package/src/graph/internal-builder.java.js +0 -33
  161. package/src/graph/internal-builder.js +0 -12
  162. package/src/graph/internal-builder.langs.js +0 -157
  163. package/src/graph/internal-builder.pass2.js +0 -163
  164. package/src/graph/internal-builder.resolvers.js +0 -286
  165. package/src/graph/layout.js +0 -51
  166. package/src/graph/node-id.js +0 -7
  167. package/src/graph/parser-artifact-boundary.js +0 -55
  168. package/src/graph/relations.js +0 -4
  169. package/src/graph/repo-registry.js +0 -124
  170. package/src/graph/resolvers/rust.js +0 -117
  171. package/src/infra/infra-items.js +0 -255
  172. package/src/infra/infra-registry.js +0 -184
  173. package/src/infra/infra.detect.js +0 -119
  174. package/src/infra/infra.js +0 -38
  175. package/src/infra/infra.match.js +0 -102
  176. package/src/infra/infra.scan.js +0 -93
  177. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  178. package/src/mcp/architecture-bootstrap.mjs +0 -168
  179. package/src/mcp/architecture-starter.mjs +0 -234
  180. package/src/mcp/catalog.mjs +0 -233
  181. package/src/mcp/company-contract-page.mjs +0 -142
  182. package/src/mcp/company-contract-reconcile.mjs +0 -87
  183. package/src/mcp/company-contract-verdict.mjs +0 -42
  184. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  185. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  186. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  187. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  188. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  189. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  190. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  191. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  192. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  193. package/src/mcp/evidence-snapshot.mjs +0 -50
  194. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  195. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  196. package/src/mcp/extension-api.mjs +0 -77
  197. package/src/mcp/git-output.mjs +0 -7
  198. package/src/mcp/graph/context-core.mjs +0 -111
  199. package/src/mcp/graph/context-seeds.mjs +0 -207
  200. package/src/mcp/graph/context-state.mjs +0 -85
  201. package/src/mcp/graph/reverse-reach.mjs +0 -42
  202. package/src/mcp/graph/tools-core.mjs +0 -143
  203. package/src/mcp/graph/tools-query.mjs +0 -276
  204. package/src/mcp/graph-context.mjs +0 -5
  205. package/src/mcp/graph-diff.mjs +0 -218
  206. package/src/mcp/health/audit-format.mjs +0 -199
  207. package/src/mcp/health/audit.mjs +0 -199
  208. package/src/mcp/health/dead-code.mjs +0 -110
  209. package/src/mcp/health/duplicates.mjs +0 -121
  210. package/src/mcp/health/endpoints.mjs +0 -71
  211. package/src/mcp/health/structure.mjs +0 -220
  212. package/src/mcp/runtime-version.mjs +0 -31
  213. package/src/mcp/server/auto-refresh.mjs +0 -66
  214. package/src/mcp/server/runtime-config.mjs +0 -43
  215. package/src/mcp/server/shutdown.mjs +0 -40
  216. package/src/mcp/staleness-notice.mjs +0 -20
  217. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  218. package/src/mcp/sync/evidence-common.mjs +0 -88
  219. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  220. package/src/mcp/sync/evidence-health.mjs +0 -56
  221. package/src/mcp/sync/evidence-packages.mjs +0 -95
  222. package/src/mcp/sync/payload-common.mjs +0 -97
  223. package/src/mcp/sync/payload-v2.mjs +0 -54
  224. package/src/mcp/sync/payload-v3.mjs +0 -79
  225. package/src/mcp/sync-evidence.mjs +0 -23
  226. package/src/mcp/sync-payload.mjs +0 -70
  227. package/src/mcp/tool-result.mjs +0 -54
  228. package/src/mcp/tools-actions.mjs +0 -11
  229. package/src/mcp/tools-architecture.mjs +0 -177
  230. package/src/mcp/tools-company.mjs +0 -251
  231. package/src/mcp/tools-context.mjs +0 -222
  232. package/src/mcp/tools-endpoints.mjs +0 -207
  233. package/src/mcp/tools-graph-hubs.mjs +0 -78
  234. package/src/mcp/tools-graph.mjs +0 -17
  235. package/src/mcp/tools-health.mjs +0 -24
  236. package/src/mcp/tools-history.mjs +0 -28
  237. package/src/mcp/tools-impact-change.mjs +0 -220
  238. package/src/mcp/tools-impact-precision.mjs +0 -89
  239. package/src/mcp/tools-impact.mjs +0 -169
  240. package/src/mcp/tools-source.mjs +0 -238
  241. package/src/mcp/tools-verified-change.mjs +0 -200
  242. package/src/mcp-rg.mjs +0 -50
  243. package/src/mcp-runtime.mjs +0 -2
  244. package/src/mcp-server.mjs +0 -298
  245. package/src/mcp-source-tools.mjs +0 -163
  246. package/src/path-classification.js +0 -226
  247. package/src/path-ignore.js +0 -69
  248. package/src/precision/lsp-client/constants.js +0 -12
  249. package/src/precision/lsp-client/environment.js +0 -20
  250. package/src/precision/lsp-client/errors.js +0 -15
  251. package/src/precision/lsp-client/lifecycle.js +0 -114
  252. package/src/precision/lsp-client/message-parser.js +0 -81
  253. package/src/precision/lsp-client/protocol.js +0 -212
  254. package/src/precision/lsp-client/registry.js +0 -59
  255. package/src/precision/lsp-client/repo-uri.js +0 -60
  256. package/src/precision/lsp-client/stdio-client.js +0 -140
  257. package/src/precision/lsp-client.js +0 -10
  258. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  259. package/src/precision/lsp-overlay/build.js +0 -261
  260. package/src/precision/lsp-overlay/contract.js +0 -61
  261. package/src/precision/lsp-overlay/reference-results.js +0 -108
  262. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  263. package/src/precision/lsp-overlay/source-session.js +0 -142
  264. package/src/precision/lsp-overlay/store.js +0 -150
  265. package/src/precision/lsp-overlay/target-index.js +0 -152
  266. package/src/precision/lsp-overlay/target-query.js +0 -55
  267. package/src/precision/lsp-overlay.js +0 -11
  268. package/src/precision/symbol-query.js +0 -224
  269. package/src/precision/typescript-lsp-provider.js +0 -12
  270. package/src/precision/typescript-provider/client.js +0 -89
  271. package/src/precision/typescript-provider/discovery.js +0 -124
  272. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  273. package/src/precision/typescript-provider/project-host.js +0 -254
  274. package/src/precision/typescript-provider/project-safety.js +0 -166
  275. package/src/precision/typescript-provider/reference-usage.js +0 -53
  276. package/src/process.js +0 -95
  277. package/src/repo-path.js +0 -53
  278. package/src/scan/discover.inventory.js +0 -78
  279. package/src/scan/discover.js +0 -5
  280. package/src/scan/discover.list.js +0 -79
  281. package/src/scan/discover.stack.js +0 -227
  282. package/src/scan/search.core.js +0 -24
  283. package/src/scan/search.git.js +0 -102
  284. package/src/scan/search.js +0 -9
  285. package/src/scan/search.node.js +0 -83
  286. package/src/scan/search.preview.js +0 -49
  287. package/src/scan/search.rg.js +0 -145
  288. package/src/security/advisory-store.js +0 -172
  289. package/src/security/installed-jvm-rust.js +0 -46
  290. package/src/security/installed.js +0 -280
  291. package/src/security/malware-file-heuristics.js +0 -121
  292. package/src/security/malware-heuristics.exclusions.js +0 -134
  293. package/src/security/malware-heuristics.js +0 -15
  294. package/src/security/malware-heuristics.roots.js +0 -145
  295. package/src/security/malware-heuristics.scan.js +0 -89
  296. package/src/security/malware-heuristics.sweep.js +0 -122
  297. package/src/security/malware-scoring.js +0 -105
  298. package/src/security/match.js +0 -85
  299. package/src/security/registry-sig.classify.js +0 -137
  300. package/src/security/registry-sig.js +0 -18
  301. package/src/security/registry-sig.rules.js +0 -192
  302. package/src/security/rust-advisory-report.js +0 -60
  303. package/src/security/typosquat.js +0 -81
  304. package/src/util.js +0 -40
  305. package/src/version.js +0 -7
@@ -1,65 +0,0 @@
1
- // Reading a built graph.json and summarizing it for the UI cards: named communities and degree
2
- // hotspots. Both read the graph file from disk and return [] on any failure.
3
- import { readFileSync } from "node:fs";
4
- import { communityTerritoryOf } from "../graph/community.js";
5
- import { isStructuralRelation } from "../graph/relations.js";
6
-
7
- // graph-builder labels communities "Community N" without an LLM. Derive a real name from each
8
- // community's dominant folder + sample files so the UI shows modules, not bare numbers.
9
- export function summarizeCommunities(graphJsonPath, max = 40) {
10
- try {
11
- const graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
12
- const byCommunity = new Map();
13
- for (const node of graph.nodes || []) {
14
- if (node.file_type !== "code") continue;
15
- const community = node.community;
16
- if (community === undefined || community === null) continue;
17
- if (!byCommunity.has(community)) byCommunity.set(community, { id: community, size: 0, dirs: new Map(), files: [] });
18
- const entry = byCommunity.get(community);
19
- entry.size += 1;
20
- const parts = String(node.source_file || "").split(/[\\/]/).filter(Boolean);
21
- const dir = communityTerritoryOf(node.source_file);
22
- entry.dirs.set(dir, (entry.dirs.get(dir) || 0) + 1);
23
- if (entry.files.length < 4) entry.files.push(parts[parts.length - 1] || node.source_file || "");
24
- }
25
- return [...byCommunity.values()]
26
- .map((entry) => {
27
- const dominant = [...entry.dirs.entries()].sort((left, right) => right[1] - left[1])[0];
28
- return { id: entry.id, size: entry.size, name: dominant ? dominant[0] : "(mixed)", files: entry.files };
29
- })
30
- .sort((left, right) => right.size - left.size)
31
- .slice(0, max);
32
- } catch {
33
- return [];
34
- }
35
- }
36
-
37
- // Top nodes by total degree (in+out) — the "load-bearing" / refactor-candidate hotspots.
38
- export function summarizeHotspots(graphJsonPath, max = 15) {
39
- try {
40
- const graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
41
- const nodes = graph.nodes || [];
42
- const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
43
- const inDeg = new Map();
44
- const outDeg = new Map();
45
- for (const link of graph.links || []) {
46
- if (isStructuralRelation(link.relation) || link.barrelProxy === true) continue;
47
- const s = endpoint(link.source);
48
- const t = endpoint(link.target);
49
- outDeg.set(s, (outDeg.get(s) || 0) + 1);
50
- inDeg.set(t, (inDeg.get(t) || 0) + 1);
51
- }
52
- return nodes
53
- .filter((node) => node.file_type === "code")
54
- .map((node) => {
55
- const inbound = inDeg.get(node.id) || 0;
56
- const outbound = outDeg.get(node.id) || 0;
57
- return { label: node.label || node.norm_label || node.id, file: node.source_file || "", in: inbound, out: outbound, degree: inbound + outbound };
58
- })
59
- .filter((node) => node.degree > 0)
60
- .sort((left, right) => right.degree - left.degree)
61
- .slice(0, max);
62
- } catch {
63
- return [];
64
- }
65
- }
@@ -1,83 +0,0 @@
1
- // Honest Health capability coverage. `status` says whether a check actually ran for this repository;
2
- // `completeness` says whether its evidence can support a repository-wide conclusion.
3
- const capability = (status, completeness, detail, extra = {}) => ({ status, completeness, detail, ...extra });
4
-
5
- const sourceLanguages = (files) => {
6
- const languages = new Set();
7
- for (const file of files || []) {
8
- if (/\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/i.test(file)) languages.add("javascript/typescript");
9
- else if (/\.py$/i.test(file)) languages.add("python");
10
- else if (/\.go$/i.test(file)) languages.add("go");
11
- else if (/\.java$/i.test(file)) languages.add("java");
12
- else if (/\.rs$/i.test(file)) languages.add("rust");
13
- else if (/\.cs$/i.test(file)) languages.add("csharp");
14
- }
15
- return languages;
16
- };
17
-
18
- const checkCapability = (check, supported, label) => {
19
- if (!supported) return capability("NOT_SUPPORTED", "PARTIAL", `${label} is not supported for the discovered package ecosystem.`);
20
- if (check?.status === "OK") return capability("CHECKED", "COMPLETE", check.detail || `${label} completed.`);
21
- if (check?.status === "PARTIAL") return capability("NOT_CHECKED", "PARTIAL", check.detail || `${label} evidence is partial.`);
22
- if (check?.status === "ERROR") return capability("NOT_CHECKED", "PARTIAL", check.detail || `${label} failed.`);
23
- return capability("NOT_CHECKED", "PARTIAL", check?.detail || `${label} was not requested.`);
24
- };
25
-
26
- export function buildHealthCapabilityMatrix({
27
- graphComplete,
28
- dependencyStatus,
29
- dependencyEcosystems = {},
30
- checks = {},
31
- sourceFiles = [],
32
- correctnessCoverage = {},
33
- measuredCoverageFiles = 0,
34
- } = {}) {
35
- const languages = sourceLanguages(sourceFiles);
36
- const ecosystemRows = Object.values(dependencyEcosystems).filter((item) => item?.present);
37
- const supportedDependencyRows = ecosystemRows.filter((item) => item.status === "CHECKED");
38
- const unsupportedDependencyRows = ecosystemRows.filter((item) => item.status === "NOT_SUPPORTED");
39
- const noDependencyEvidence = ecosystemRows.length === 0;
40
- const onlyUnsupportedDependencies = unsupportedDependencyRows.length > 0 && supportedDependencyRows.length === 0;
41
- const advisorySupported = supportedDependencyRows.some((item) => ["npm", "go", "python", "rust", "maven", "gradle"].includes(item.ecosystem));
42
- const onlyRustAdvisories = supportedDependencyRows.length > 0 && supportedDependencyRows.every((item) => item.ecosystem === "rust");
43
- const malwareSupported = supportedDependencyRows.some((item) => ["npm", "go", "python"].includes(item.ecosystem));
44
- const coverageSupported = [...languages].some((language) => ["javascript/typescript", "python", "go"].includes(language));
45
- const runtimeFiles = Number(correctnessCoverage.runtimeCorrectnessFiles || 0);
46
- const concurrencyFiles = Number(correctnessCoverage.concurrencyFiles || 0);
47
-
48
- return {
49
- capabilityMatrixV: 1,
50
- structure: capability(
51
- "CHECKED",
52
- graphComplete ? "COMPLETE" : "PARTIAL",
53
- graphComplete
54
- ? "Graph cycles, orphans and configured boundaries were checked over the complete graph."
55
- : "Structure checks ran, but the graph is scoped or excludes part of the repository.",
56
- ),
57
- dependencies: capability(
58
- noDependencyEvidence ? "NOT_CHECKED" : onlyUnsupportedDependencies ? "NOT_SUPPORTED" : "CHECKED",
59
- !noDependencyEvidence && dependencyStatus === "COMPLETE" ? "COMPLETE" : "PARTIAL",
60
- noDependencyEvidence
61
- ? "No dependency manifest was discovered, so manifest-to-import verification did not run."
62
- : onlyUnsupportedDependencies
63
- ? "Dependency manifests were detected, but every discovered build ecosystem lacks import-to-artifact verification."
64
- : unsupportedDependencyRows.length
65
- ? `Supported ecosystems were checked; ${unsupportedDependencyRows.map((item) => item.ecosystem).join(", ")} remains NOT_SUPPORTED.`
66
- : "Discovered supported manifests were compared with indexed imports at their documented evidence level.",
67
- { ecosystems: dependencyEcosystems },
68
- ),
69
- runtimeCorrectness: runtimeFiles
70
- ? capability("CHECKED", "PARTIAL", `Checked ${runtimeFiles} product source file(s) for the bounded Go/Java/retry correctness patterns. This is not compiler or runtime proof.`, { checks: correctnessCoverage.checks || {} })
71
- : capability("NOT_SUPPORTED", "PARTIAL", "No source file matched the currently supported bounded correctness patterns."),
72
- concurrency: concurrencyFiles
73
- ? capability("CHECKED", "PARTIAL", `Checked ${concurrencyFiles} Java source file(s) for direct InterruptedException restore/rethrow evidence. No race detector ran; race freedom is not claimed.`)
74
- : capability("NOT_SUPPORTED", "PARTIAL", "No supported Java interruption check applied. No race detector ran; race freedom is not claimed."),
75
- advisories: checkCapability(onlyRustAdvisories && checks.rustsec?.status === "OK" ? checks.rustsec : checks.osv, advisorySupported, onlyRustAdvisories ? "RustSec advisory matching" : "OSV advisory matching"),
76
- malware: checkCapability(checks.malware, malwareSupported, "Installed-package malware scanning"),
77
- coverage: measuredCoverageFiles > 0
78
- ? capability("CHECKED", "COMPLETE", `Mapped an existing supported coverage report to ${measuredCoverageFiles} file(s).`)
79
- : coverageSupported
80
- ? capability("NOT_CHECKED", "PARTIAL", "No supported measured coverage report was found; static test reachability is not coverage.")
81
- : capability("NOT_SUPPORTED", "PARTIAL", `No supported coverage format applies to the discovered language set${languages.size ? ` (${[...languages].join(", ")})` : ""}.`),
82
- };
83
- }
@@ -1,266 +0,0 @@
1
- // Bounded local hot-path review over parser-derived complexity facts. This deliberately keeps
2
- // local syntax cost, graph coupling and test evidence separate: it is a review queue, not a runtime
3
- // profiler or an interprocedural Big-O proof.
4
- import {readCoverageForRepo, normalizeRepoParts} from './coverage-reports.js'
5
- import {computeStaticTestReachability} from './static-test-reachability.js'
6
- import {createPathClassifier, hasPathClass} from '../path-classification.js'
7
- import {isStructuralRelation} from '../graph/relations.js'
8
- import {boundedInteger} from '../util.js'
9
-
10
- const NON_PRODUCT = ['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
11
- const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value || '')
12
- const normalize = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
13
- const boundedInt = boundedInteger
14
- const round = (value) => Math.round(Number(value || 0) * 100) / 100
15
- const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number(value) || 0))
16
-
17
- function normalizeScope(value) {
18
- const path = normalize(value)
19
- if (!path) return {ok: true, path: ''}
20
- if (path.includes('\0') || /^[a-z]:\//i.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
21
- return {ok: false, error: 'path must be repository-relative and cannot contain traversal segments'}
22
- }
23
- return {ok: true, path}
24
- }
25
-
26
- function coverageForRange(record, startLine, endLine) {
27
- if (!record) return null
28
- if (!(record.lines instanceof Map) || !record.lines.size) return Number.isFinite(record.pct) ? record.pct : null
29
- let total = 0
30
- let covered = 0
31
- for (let line = Math.max(1, startLine); line <= Math.max(startLine, endLine); line++) {
32
- if (!record.lines.has(line)) continue
33
- total++
34
- if (Number(record.lines.get(line)) > 0) covered++
35
- }
36
- return total ? covered / total : Number.isFinite(record.pct) ? record.pct : null
37
- }
38
-
39
- function graphRiskBySymbol(graph, symbolIds, testCallerIds) {
40
- const state = new Map([...symbolIds].map((id) => [id, {
41
- incoming: 0, outgoing: 0, callers: new Set(), callees: new Set(),
42
- }]))
43
- for (const link of graph?.links || []) {
44
- if (!link || link.typeOnly === true || link.compileOnly === true || link.barrelProxy === true || isStructuralRelation(link.relation)) continue
45
- const source = endpoint(link.source)
46
- const target = endpoint(link.target)
47
- if (!source || !target || source === target) continue
48
- if (state.has(source)) {
49
- const item = state.get(source)
50
- item.outgoing++
51
- item.callees.add(target)
52
- }
53
- // A test caller (inline #[cfg(test)] symbol or a test-path file) must not inflate a production
54
- // symbol's fan-in — the same reason test symbols are excluded as candidates. Skipped only for the
55
- // incoming/coupling side; unless the caller opted tests in (testCallerIds is then null).
56
- if (state.has(target) && !(testCallerIds && testCallerIds.has(source))) {
57
- const item = state.get(target)
58
- item.incoming++
59
- item.callers.add(source)
60
- }
61
- }
62
- return new Map([...state].map(([id, item]) => [id, {
63
- fanIn: item.callers.size,
64
- fanOut: item.callees.size,
65
- incomingEdges: item.incoming,
66
- outgoingEdges: item.outgoing,
67
- }]))
68
- }
69
-
70
- function localSyntaxScore(complexity, thresholds) {
71
- const insideLoop = Number(complexity.allocationsInLoops || 0)
72
- + Number(complexity.copiesInLoops || 0)
73
- + Number(complexity.linearOpsInLoops || 0)
74
- + Number(complexity.sortsInLoops || 0) * 2
75
- + Number(complexity.recursionInLoops || 0) * 2
76
- const score = Number(complexity.timeRank || 0) * 12
77
- + Number(complexity.memoryRank || 0) * 5
78
- + Math.min(18, Number(complexity.cyclomatic || 1) / thresholds.cyclomatic * 10)
79
- + Math.min(12, Number(complexity.callCount || 0) / thresholds.calls * 6)
80
- + Math.min(18, Number(complexity.maxLoopDepth || 0) * 5)
81
- + Math.min(24, insideLoop * 5)
82
- + (complexity.recursion ? 6 : 0)
83
- return round(clamp(score))
84
- }
85
-
86
- function couplingScore(risk) {
87
- return round(clamp(Math.log2(1 + risk.fanIn) * 20 + Math.log2(1 + risk.fanOut) * 10))
88
- }
89
-
90
- function reasonsFor(complexity, thresholds) {
91
- const reasons = []
92
- if (Number(complexity.timeRank || 0) >= thresholds.timeRank) reasons.push(String(complexity.timeLabel || `local time rank ${complexity.timeRank}`))
93
- if (Number(complexity.cyclomatic || 0) >= thresholds.cyclomatic) reasons.push(`cyclomatic ${complexity.cyclomatic} >= ${thresholds.cyclomatic}`)
94
- if (Number(complexity.callCount || 0) >= thresholds.calls) reasons.push(`${complexity.callCount} local call sites >= ${thresholds.calls}`)
95
- if (Number(complexity.maxLoopDepth || 0) >= thresholds.loopDepth) reasons.push(`loop depth ${complexity.maxLoopDepth} >= ${thresholds.loopDepth}`)
96
- if (complexity.allocationsInLoops) reasons.push(`${complexity.allocationsInLoops} allocation(s) inside iteration`)
97
- if (complexity.copiesInLoops) reasons.push(`${complexity.copiesInLoops} copy/copies inside iteration`)
98
- if (complexity.linearOpsInLoops) reasons.push(`${complexity.linearOpsInLoops} linear operation(s) inside iteration`)
99
- if (complexity.sortsInLoops) reasons.push(`${complexity.sortsInLoops} sort(s) inside iteration`)
100
- if (complexity.recursionInLoops) reasons.push(`${complexity.recursionInLoops} recursive call(s) inside iteration`)
101
- if (complexity.recursion) reasons.push('direct recursion; bound remains unknown')
102
- return [...new Set(reasons)].slice(0, 12)
103
- }
104
-
105
- export function computeHotPathReview(graph, options = {}) {
106
- const scope = normalizeScope(options.path)
107
- if (!scope.ok) return {ok: false, error: scope.error}
108
- const thresholds = {
109
- cyclomatic: boundedInt(options.cyclomaticThreshold, 8, 2, 1000),
110
- calls: boundedInt(options.callThreshold, 12, 1, 10000),
111
- loopDepth: boundedInt(options.loopDepthThreshold, 2, 1, 10),
112
- timeRank: boundedInt(options.timeRankThreshold, 2, 0, 5),
113
- minScore: boundedInt(options.minScore, 85, 0, 100),
114
- }
115
- const defaultFocus = options.minScore == null
116
- const topN = boundedInt(options.topN, 20, 1, 100)
117
- const classifier = createPathClassifier(options.repoRoot || null)
118
- const knownFiles = [...new Set((graph?.nodes || []).map((node) => normalize(node?.source_file)).filter(Boolean))]
119
- const measuredCoverage = options.repoRoot ? readCoverageForRepo(options.repoRoot, knownFiles) : new Map()
120
- const coverageSources = [...new Set([...measuredCoverage.values()].map((item) => item?.source).filter(Boolean))].sort()
121
- const staticTests = measuredCoverage.size || !options.repoRoot
122
- ? null
123
- : computeStaticTestReachability(graph, {repoRoot: options.repoRoot, path: scope.path})
124
- const staticByFile = new Map((staticTests?.reachable || []).map((item) => [normalize(item.file), item]))
125
-
126
- const eligible = []
127
- const excluded = {tests: 0, classified: 0, outOfScope: 0}
128
- for (const node of graph?.nodes || []) {
129
- const complexity = node?.complexity
130
- const file = normalize(node?.source_file)
131
- if (!complexity || !file || !String(node?.id || '').includes('#')) continue
132
- if (scope.path && file !== scope.path && !file.startsWith(`${scope.path}/`)) { excluded.outOfScope++; continue }
133
- const classification = classifier.explain(file)
134
- const test = node?.test_surface === true || hasPathClass(classification, 'test', 'e2e')
135
- const classified = classification.excluded || hasPathClass(classification, ...NON_PRODUCT)
136
- if (test && options.includeTests !== true) { excluded.tests++; continue }
137
- if (classified && options.includeClassified !== true) { excluded.classified++; continue }
138
- eligible.push({node, complexity, file, classification: test ? 'test' : classified ? 'classified' : 'production'})
139
- }
140
-
141
- // Test callers (inline test_surface symbols and test-path files) that must not count toward a
142
- // production symbol's fan-in. Built over ALL nodes because a caller may sit outside the review scope.
143
- const testCallerIds = options.includeTests === true ? null : (() => {
144
- const ids = new Set()
145
- const classifyCache = new Map()
146
- for (const node of graph?.nodes || []) {
147
- const id = String(node?.id || '')
148
- if (!id.includes('#')) continue
149
- let isTest = node?.test_surface === true
150
- const file = normalize(node?.source_file)
151
- if (!isTest && file) {
152
- let info = classifyCache.get(file)
153
- if (info === undefined) classifyCache.set(file, (info = classifier.explain(file)))
154
- isTest = hasPathClass(info, 'test', 'e2e')
155
- }
156
- if (isTest) ids.add(id)
157
- }
158
- return ids
159
- })()
160
- const riskById = graphRiskBySymbol(graph, new Set(eligible.map(({node}) => String(node.id))), testCallerIds)
161
- const candidates = []
162
- for (const entry of eligible) {
163
- const {node, complexity, file} = entry
164
- const reasons = reasonsFor(complexity, thresholds)
165
- if (!reasons.length) continue
166
- const risk = riskById.get(String(node.id)) || {fanIn: 0, fanOut: 0, incomingEdges: 0, outgoingEdges: 0}
167
- const syntaxScore = localSyntaxScore(complexity, thresholds)
168
- const graphScore = couplingScore(risk)
169
- const startLine = Number(complexity.startLine) || Number(String(node.source_location || '').replace(/^L/, '')) || 0
170
- const endLine = Number(complexity.endLine) || Number(String(node.source_end || '').replace(/^L/, '')) || startLine
171
- const coverageRecord = measuredCoverage.get(normalizeRepoParts(file)) || null
172
- const actualCoverage = coverageForRange(coverageRecord, startLine, endLine)
173
- const nearest = staticByFile.get(file)?.nearestTests?.[0] || null
174
- const coverageRisk = actualCoverage == null ? 0 : (1 - actualCoverage) * 100
175
- const score = round(clamp(syntaxScore * 0.72 + graphScore * 0.2 + coverageRisk * 0.08))
176
- const directHotEvidence = Array.isArray(complexity.hotEvidence) ? complexity.hotEvidence.slice(0, 12) : []
177
- // The default queue is intentionally narrow. A small, locally expensive function can still be
178
- // important even with little graph fan-in, so retain only a bounded strong-local fallback. An
179
- // explicit minScore disables this fallback and gives the caller a strict numeric gate.
180
- const bodyLines = startLine > 0 && endLine >= startLine ? endLine - startLine + 1 : Number.POSITIVE_INFINITY
181
- const strongLocalEvidence = defaultFocus && directHotEvidence.length > 0 && bodyLines <= 80 && (
182
- Number(complexity.recursionInLoops || 0) > 0
183
- || (bodyLines <= 40 && (
184
- Number(complexity.sortsInLoops || 0) > 0
185
- || (Number(complexity.timeRank || 0) >= 4 && Number(complexity.maxLoopDepth || 0) >= 2)
186
- ))
187
- )
188
- if (score < thresholds.minScore && !strongLocalEvidence) continue
189
- const confidence = directHotEvidence.length ? 'HIGH' : complexity.recursion ? 'LOW' : 'MEDIUM'
190
- candidates.push({
191
- id: String(node.id),
192
- label: String(node.label || node.norm_label || node.id),
193
- kind: String(node.symbol_kind || 'symbol'),
194
- file,
195
- startLine,
196
- endLine,
197
- classification: entry.classification,
198
- score,
199
- selection: score >= thresholds.minScore ? 'SCORE_THRESHOLD' : 'STRONG_LOCAL_EVIDENCE',
200
- confidence,
201
- localSyntax: {
202
- score: syntaxScore,
203
- timeRank: Number(complexity.timeRank || 0),
204
- timeLabel: String(complexity.timeLabel || ''),
205
- memoryRank: Number(complexity.memoryRank || 0),
206
- memoryLabel: String(complexity.memoryLabel || ''),
207
- cyclomatic: Number(complexity.cyclomatic || 0),
208
- calls: Number(complexity.callCount || 0),
209
- loops: Number(complexity.loops || 0),
210
- maxLoopDepth: Number(complexity.maxLoopDepth || 0),
211
- allocationsInLoops: Number(complexity.allocationsInLoops || 0),
212
- copiesInLoops: Number(complexity.copiesInLoops || 0),
213
- linearOpsInLoops: Number(complexity.linearOpsInLoops || 0),
214
- sortsInLoops: Number(complexity.sortsInLoops || 0),
215
- recursionInLoops: Number(complexity.recursionInLoops || 0),
216
- recursion: complexity.recursion === true,
217
- },
218
- graphRisk: {...risk, score: graphScore},
219
- testEvidence: actualCoverage != null
220
- ? {actualCoverage, source: coverageRecord?.source || 'coverage report'}
221
- : nearest
222
- ? {actualCoverage: 'NOT_AVAILABLE', staticReachable: true, nearestTest: nearest.test, distance: nearest.distance, confidence: nearest.confidence}
223
- : {actualCoverage: 'NOT_AVAILABLE', staticReachable: false},
224
- reasons,
225
- sourceEvidence: directHotEvidence,
226
- })
227
- }
228
- candidates.sort((left, right) => right.score - left.score
229
- || right.localSyntax.score - left.localSyntax.score
230
- || right.graphRisk.fanIn - left.graphRisk.fanIn
231
- || left.id.localeCompare(right.id))
232
- const hotspots = candidates.slice(0, topN)
233
- return {
234
- ok: true,
235
- modelVersion: 1,
236
- complexityVersion: Number(graph?.complexityV) || 0,
237
- scope: {
238
- path: scope.path || null,
239
- includeTests: options.includeTests === true,
240
- includeClassified: options.includeClassified === true,
241
- },
242
- thresholds,
243
- selectionPolicy: {
244
- mode: defaultFocus ? 'FOCUSED_DEFAULT' : 'EXPLICIT_SCORE_THRESHOLD',
245
- strongLocalFallback: defaultFocus,
246
- broadenWith: 'Set min_score lower (0 restores the full diagnostic candidate set).',
247
- },
248
- analyzedSymbols: eligible.length,
249
- candidateSymbols: candidates.length,
250
- coverage: measuredCoverage.size
251
- ? {actualCoverage: 'AVAILABLE', measuredFiles: measuredCoverage.size, sources: coverageSources}
252
- : {actualCoverage: 'NOT_AVAILABLE', staticReachability: staticTests ? {
253
- reachableFiles: staticTests.reachableFiles,
254
- productFiles: staticTests.productFiles,
255
- truncated: staticTests.bounds.truncated,
256
- } : null},
257
- excluded,
258
- hotspots,
259
- bounds: {topN, returned: hotspots.length, totalCandidates: candidates.length, truncated: candidates.length > hotspots.length},
260
- caveats: [
261
- 'Scores rank parser-derived local syntax cost; they are not profiler measurements.',
262
- 'Graph risk is reported separately and does not propagate loop complexity through callees.',
263
- 'Recursion bounds, CFG reachability, dead stores and taint flow are not inferred by this model.',
264
- ],
265
- }
266
- }
@@ -1,241 +0,0 @@
1
- // Bounded HTTP-wrapper configuration and conservative auto-discovery. A wrapper is accepted only
2
- // when its URL position and HTTP method are statically known; no source is evaluated.
3
- import { statSync } from "node:fs";
4
- import { createRepoBoundary } from "../repo-path.js";
5
- import { safeRead } from "../util.js";
6
-
7
- export const DEFAULT_HTTP_CLIENT_NAMES = Object.freeze([
8
- "axios", "http", "https", "$http", "httpClient", "apiClient", "restClient",
9
- ]);
10
-
11
- const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
12
- const MAX_CONFIG_BYTES = 64 * 1024;
13
- const MAX_WRAPPERS = 100;
14
- const MAX_CLIENT_NAMES = 40;
15
- const MAX_DISCOVERY_BODY = 2_000;
16
- const identifier = (value) => {
17
- const text = String(value || "").trim();
18
- return /^[A-Za-z_$][\w$]{0,127}$/.test(text) ? text : null;
19
- };
20
- const method = (value) => {
21
- const text = String(value || "").trim().toUpperCase();
22
- return HTTP_METHODS.has(text) ? text : null;
23
- };
24
- const argumentIndex = (value) => {
25
- const number = Number(value ?? 0);
26
- return Number.isInteger(number) && number >= 0 && number <= 5 ? number : null;
27
- };
28
-
29
- export function normalizeHttpClientNames(values) {
30
- return [...new Set((Array.isArray(values) ? values : [])
31
- .slice(0, MAX_CLIENT_NAMES)
32
- .map(identifier)
33
- .filter(Boolean))];
34
- }
35
-
36
- export function normalizeHttpWrapperDescriptors(values, source = "input") {
37
- const wrappers = [];
38
- for (const raw of (Array.isArray(values) ? values : []).slice(0, MAX_WRAPPERS)) {
39
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
40
- const urlArgument = argumentIndex(raw.urlArgument ?? raw.url_argument);
41
- const fixedMethod = method(raw.method);
42
- if (urlArgument == null || !fixedMethod) continue;
43
- const call = identifier(raw.call);
44
- const object = identifier(raw.object);
45
- const member = identifier(raw.member);
46
- if (call && !object && !member) {
47
- wrappers.push({ kind: "function", call, method: fixedMethod, urlArgument, source });
48
- } else if (!call && object && member) {
49
- wrappers.push({ kind: "member", object, member, method: fixedMethod, urlArgument, source });
50
- }
51
- }
52
- const seen = new Set();
53
- return wrappers.filter((wrapper) => {
54
- const key = `${wrapper.kind}\0${wrapper.call || wrapper.object}\0${wrapper.member || ""}\0${wrapper.method}\0${wrapper.urlArgument}`;
55
- if (seen.has(key)) return false;
56
- seen.add(key);
57
- return true;
58
- });
59
- }
60
-
61
- export function loadHttpContractConfig(repoRoot) {
62
- const empty = { clientNames: [], wrappers: [], autoDiscoverWrappers: true, loaded: false, warnings: [] };
63
- const resolved = createRepoBoundary(repoRoot).resolve(".weavatrix.json");
64
- if (!resolved.ok) return empty;
65
- try {
66
- if (statSync(resolved.path).size > MAX_CONFIG_BYTES) return { ...empty, error: "config-too-large" };
67
- const raw = JSON.parse(safeRead(resolved.path));
68
- const config = raw?.httpContracts;
69
- if (config == null) return { ...empty, loaded: true };
70
- if (!config || typeof config !== "object" || Array.isArray(config)) return { ...empty, error: "invalid-http-contract-config" };
71
- const rawNames = Array.isArray(config.clientNames) ? config.clientNames : [];
72
- const rawWrappers = Array.isArray(config.wrappers) ? config.wrappers : [];
73
- const clientNames = normalizeHttpClientNames(rawNames);
74
- const wrappers = normalizeHttpWrapperDescriptors(rawWrappers, "config");
75
- const warnings = [];
76
- if (config.clientNames != null && !Array.isArray(config.clientNames)) warnings.push("httpContracts.clientNames must be an array");
77
- if (rawNames.length > MAX_CLIENT_NAMES) warnings.push("httpContracts.clientNames cap reached");
78
- if (clientNames.length < Math.min(rawNames.length, MAX_CLIENT_NAMES)) warnings.push("invalid httpContracts.clientNames entries skipped");
79
- if (config.wrappers != null && !Array.isArray(config.wrappers)) warnings.push("httpContracts.wrappers must be an array");
80
- if (rawWrappers.length > MAX_WRAPPERS) warnings.push("httpContracts.wrappers cap reached");
81
- if (wrappers.length < Math.min(rawWrappers.length, MAX_WRAPPERS)) warnings.push("invalid or duplicate httpContracts.wrappers entries skipped");
82
- if (config.autoDiscoverWrappers != null && typeof config.autoDiscoverWrappers !== "boolean") warnings.push("httpContracts.autoDiscoverWrappers must be boolean");
83
- return {
84
- clientNames,
85
- wrappers,
86
- autoDiscoverWrappers: config.autoDiscoverWrappers !== false,
87
- loaded: true,
88
- warnings,
89
- };
90
- } catch {
91
- return { ...empty, error: "invalid-config" };
92
- }
93
- }
94
-
95
- // Masks comments and strings while retaining indices and newlines. Wrapper discovery needs only
96
- // declarations and a direct `client.verb(urlParam)` delegation, never literal contents.
97
- function codeMask(text) {
98
- const chars = String(text || "").split("");
99
- let index = 0;
100
- while (index < chars.length) {
101
- const char = chars[index], next = chars[index + 1];
102
- if (char === "/" && next === "/") {
103
- chars[index++] = " "; chars[index++] = " ";
104
- while (index < chars.length && chars[index] !== "\n") chars[index++] = " ";
105
- } else if (char === "/" && next === "*") {
106
- chars[index++] = " "; chars[index++] = " ";
107
- while (index < chars.length) {
108
- if (chars[index] === "*" && chars[index + 1] === "/") { chars[index++] = " "; chars[index++] = " "; break; }
109
- if (chars[index] !== "\n") chars[index] = " ";
110
- index += 1;
111
- }
112
- } else if (char === "'" || char === '"' || char === "`") {
113
- const quote = char;
114
- chars[index++] = " ";
115
- while (index < chars.length) {
116
- if (chars[index] === "\\") {
117
- chars[index++] = " ";
118
- if (index < chars.length && chars[index] !== "\n") chars[index] = " ";
119
- index += 1;
120
- continue;
121
- }
122
- const closes = chars[index] === quote;
123
- if (chars[index] !== "\n") chars[index] = " ";
124
- index += 1;
125
- if (closes) break;
126
- }
127
- } else index += 1;
128
- }
129
- return chars.join("");
130
- }
131
-
132
- function matchingBrace(mask, open) {
133
- let depth = 0;
134
- for (let index = open; index < Math.min(mask.length, open + MAX_DISCOVERY_BODY); index += 1) {
135
- if (mask[index] === "{") depth += 1;
136
- else if (mask[index] === "}" && --depth === 0) return index + 1;
137
- }
138
- return Math.min(mask.length, open + MAX_DISCOVERY_BODY);
139
- }
140
-
141
- function parameterNames(raw) {
142
- const parts = [];
143
- let start = 0, round = 0, square = 0, curly = 0, angle = 0;
144
- const text = String(raw || "");
145
- for (let index = 0; index <= text.length; index += 1) {
146
- const char = text[index];
147
- if (char === "(") round += 1;
148
- else if (char === ")") round -= 1;
149
- else if (char === "[") square += 1;
150
- else if (char === "]") square -= 1;
151
- else if (char === "{") curly += 1;
152
- else if (char === "}") curly -= 1;
153
- else if (char === "<") angle += 1;
154
- else if (char === ">") angle = Math.max(0, angle - 1);
155
- if ((char === "," && round === 0 && square === 0 && curly === 0 && angle === 0) || index === text.length) {
156
- parts.push(text.slice(start, index));
157
- start = index + 1;
158
- }
159
- }
160
- const names = [];
161
- for (const part of parts.slice(0, 6)) {
162
- const match = /^\s*([A-Za-z_$][\w$]*)(?=\s*(?:[?:=]|$))/.exec(part);
163
- if (!match) return [];
164
- names.push(match[1]);
165
- }
166
- return names;
167
- }
168
-
169
- function escaped(value) {
170
- return String(value).replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
171
- }
172
-
173
- function delegatedWrapper(mask, definition, clientNames) {
174
- const body = mask.slice(definition.bodyStart, definition.bodyEnd);
175
- const clients = clientNames.map(escaped).join("|");
176
- if (!clients) return null;
177
- const delegate = new RegExp(`\\b(?:${clients})\\s*(?:\\?\\.|\\.)\\s*(get|post|put|patch|delete|head|options)\\s*(?:<[^>\\n]{1,200}>)?\\s*\\(\\s*([A-Za-z_$][\\w$]*)\\s*(?=[,)])`, "gi");
178
- const matches = [];
179
- let found;
180
- while ((found = delegate.exec(body)) && matches.length < 3) {
181
- const urlArgument = definition.parameters.indexOf(found[2]);
182
- if (urlArgument >= 0) matches.push({ method: found[1].toUpperCase(), urlArgument });
183
- }
184
- // Common typed clients keep response/error handling in one helper and pass the concrete
185
- // axios/http method plus its argument array: api(axios.get, [url, options]). The HTTP method and
186
- // first transport argument are still statically fixed, so this is as deterministic as a direct
187
- // axios.get(url) delegation without evaluating the helper.
188
- const methodReference = new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\(\\s*(?:${clients})\\s*(?:\\?\\.|\\.)\\s*(get|post|put|patch|delete|head|options)\\s*,\\s*\\[\\s*([A-Za-z_$][\\w$]*)\\s*(?=[,\\]])`, "gi");
189
- while ((found = methodReference.exec(body)) && matches.length < 3) {
190
- const urlArgument = definition.parameters.indexOf(found[2]);
191
- if (urlArgument >= 0) matches.push({ method: found[1].toUpperCase(), urlArgument });
192
- }
193
- const unique = [...new Map(matches.map((item) => [`${item.method}\0${item.urlArgument}`, item])).values()];
194
- return unique.length === 1 ? unique[0] : null;
195
- }
196
-
197
- export function discoverHttpWrappers(sources, clientNames = []) {
198
- const allowedClients = [...new Set([...DEFAULT_HTTP_CLIENT_NAMES, ...normalizeHttpClientNames(clientNames)])];
199
- const candidates = [];
200
- let truncated = false;
201
- for (const source of Array.isArray(sources) ? sources : []) {
202
- if (candidates.length >= MAX_WRAPPERS) { truncated = true; break; }
203
- const mask = codeMask(source.text);
204
- const definitions = [];
205
- const functions = /\b(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]{0,300})\)\s*(?::[^\n{]{1,200})?\s*\{/g;
206
- let match;
207
- while ((match = functions.exec(mask)) && definitions.length < MAX_WRAPPERS) {
208
- const open = functions.lastIndex - 1;
209
- definitions.push({ call: match[1], parameters: parameterNames(match[2]), bodyStart: open + 1, bodyEnd: matchingBrace(mask, open) - 1 });
210
- }
211
- const arrows = /\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:<[^>\n]{1,200}>\s*)?(?:\(([^)]{0,300})\)\s*(?::[^\n=]{1,200})?|([A-Za-z_$][\w$]*))\s*=>\s*/g;
212
- while ((match = arrows.exec(mask)) && definitions.length < MAX_WRAPPERS) {
213
- const parameters = parameterNames(match[2] ?? match[3]);
214
- const open = mask.indexOf("{", arrows.lastIndex);
215
- const block = open >= 0 && /^\s*\{/.test(mask.slice(arrows.lastIndex, open + 1));
216
- const bodyStart = block ? open + 1 : arrows.lastIndex;
217
- const bodyEnd = block ? matchingBrace(mask, open) - 1 : Math.min(mask.length, bodyStart + MAX_DISCOVERY_BODY, mask.indexOf(";", bodyStart) >= 0 ? mask.indexOf(";", bodyStart) : mask.length);
218
- definitions.push({ call: match[1], parameters, bodyStart, bodyEnd });
219
- }
220
- for (const definition of definitions) {
221
- if (!definition.parameters.length) continue;
222
- const delegated = delegatedWrapper(mask, definition, allowedClients);
223
- if (delegated && candidates.length < MAX_WRAPPERS) candidates.push({ kind: "function", call: definition.call, ...delegated, source: "auto", definitionFile: source.file });
224
- else if (delegated) truncated = true;
225
- }
226
- }
227
-
228
- const byCall = new Map();
229
- for (const candidate of candidates) {
230
- const entries = byCall.get(candidate.call) || [];
231
- entries.push(candidate);
232
- byCall.set(candidate.call, entries);
233
- }
234
- const wrappers = [], ambiguous = [];
235
- for (const [call, entries] of [...byCall.entries()].sort(([left], [right]) => left.localeCompare(right))) {
236
- const unique = [...new Map(entries.map((entry) => [`${entry.method}\0${entry.urlArgument}\0${entry.definitionFile}`, entry])).values()];
237
- if (unique.length === 1) wrappers.push(unique[0]);
238
- else ambiguous.push({ call, definitions: unique.length });
239
- }
240
- return { wrappers, ambiguous, truncated };
241
- }