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,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
- }