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,103 +0,0 @@
1
- import {changeLimits, normalizeChangePath} from './options.js'
2
-
3
- function decodeGitQuoted(value) {
4
- const input = String(value || '').trim()
5
- if (!input.startsWith('"')) return input.split('\t', 1)[0]
6
- try { return JSON.parse(input) } catch { /* Git octal escapes are not JSON. */ }
7
- const bytes = []
8
- for (let index = 1; index < input.length - 1; index++) {
9
- const char = input[index]
10
- if (char !== '\\') { bytes.push(...Buffer.from(char)); continue }
11
- const next = input[++index] || ''
12
- if (/[0-7]/.test(next)) {
13
- let octal = next
14
- while (octal.length < 3 && /[0-7]/.test(input[index + 1] || '')) octal += input[++index]
15
- bytes.push(parseInt(octal, 8))
16
- } else {
17
- const escapes = {n: 10, r: 13, t: 9, b: 8, f: 12, v: 11, '\\': 92, '"': 34}
18
- bytes.push(escapes[next] ?? next.charCodeAt(0))
19
- }
20
- }
21
- return Buffer.from(bytes).toString('utf8')
22
- }
23
-
24
- function diffPath(raw, prefix) {
25
- const decoded = decodeGitQuoted(raw)
26
- if (!decoded || decoded === '/dev/null') return null
27
- return normalizeChangePath(decoded.startsWith(`${prefix}/`) ? decoded.slice(2) : decoded)
28
- }
29
-
30
- function headerPaths(line) {
31
- const match = /^diff --git ("(?:\\.|[^"])*"|\S+) ("(?:\\.|[^"])*"|\S+)$/.exec(line)
32
- return match
33
- ? {oldPath: diffPath(match[1], 'a'), newPath: diffPath(match[2], 'b')}
34
- : {oldPath: null, newPath: null}
35
- }
36
-
37
- const emptyFile = (paths = {}) => ({
38
- oldPath: paths.oldPath || null,
39
- newPath: paths.newPath || null,
40
- newFile: false,
41
- deletedFile: false,
42
- renamed: false,
43
- binary: false,
44
- hunks: [],
45
- additions: [],
46
- removals: [],
47
- })
48
-
49
- export function parseZeroContextDiff(diffText, options = {}) {
50
- const limits = changeLimits(options)
51
- const original = String(diffText ?? '')
52
- const byteLength = Buffer.byteLength(original)
53
- const oversized = byteLength > limits.maxDiffBytes
54
- const text = oversized ? original.slice(0, limits.maxDiffBytes) : original
55
- const files = []
56
- let file = null, hunk = null, changedLines = 0, truncated = oversized
57
- const finish = () => {
58
- if (!file) return
59
- if ((file.oldPath || file.newPath) && files.length < limits.maxFiles) files.push(file)
60
- else if (files.length >= limits.maxFiles) truncated = true
61
- file = null; hunk = null
62
- }
63
- const addChange = (kind, line, oldLine, newLine, mappedNewLine) => {
64
- changedLines++
65
- if (changedLines > limits.maxChangedLines) { truncated = true; return }
66
- const change = {
67
- kind,
68
- text: String(line).slice(0, limits.maxLineLength),
69
- ...(oldLine != null ? {oldLine} : {}),
70
- ...(newLine != null ? {newLine} : {}),
71
- mappedNewLine,
72
- }
73
- ;(kind === 'added' ? file.additions : file.removals).push(change)
74
- }
75
- for (const line of text.split(/\r?\n/)) {
76
- if (line.startsWith('diff --git ')) { finish(); file = emptyFile(headerPaths(line)); continue }
77
- if (!file && line.startsWith('--- ')) file = emptyFile()
78
- if (!file) continue
79
- if (line.startsWith('new file mode ')) { file.newFile = true; continue }
80
- if (line.startsWith('deleted file mode ')) { file.deletedFile = true; continue }
81
- if (line.startsWith('rename from ')) { file.oldPath = normalizeChangePath(decodeGitQuoted(line.slice(12))); file.renamed = true; continue }
82
- if (line.startsWith('rename to ')) { file.newPath = normalizeChangePath(decodeGitQuoted(line.slice(10))); file.renamed = true; continue }
83
- if (line.startsWith('Binary files ') || line === 'GIT binary patch') { file.binary = true; continue }
84
- if (!hunk && line.startsWith('--- ')) { file.oldPath = diffPath(line.slice(4), 'a'); if (!file.oldPath) file.newFile = true; continue }
85
- if (!hunk && line.startsWith('+++ ')) { file.newPath = diffPath(line.slice(4), 'b'); if (!file.newPath) file.deletedFile = true; continue }
86
- const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line)
87
- if (match) {
88
- hunk = {
89
- oldStart: Number(match[1]), oldCount: match[2] == null ? 1 : Number(match[2]),
90
- newStart: Number(match[3]), newCount: match[4] == null ? 1 : Number(match[4]),
91
- oldCursor: Number(match[1]), newCursor: Number(match[3]),
92
- }
93
- file.hunks.push({oldStart: hunk.oldStart, oldCount: hunk.oldCount, newStart: hunk.newStart, newCount: hunk.newCount})
94
- continue
95
- }
96
- if (!hunk || line.startsWith('\\ No newline')) continue
97
- if (line.startsWith('+')) { addChange('added', line.slice(1), null, hunk.newCursor, hunk.newCursor); hunk.newCursor++ }
98
- else if (line.startsWith('-')) { addChange('removed', line.slice(1), hunk.oldCursor, null, hunk.newCursor); hunk.oldCursor++ }
99
- else { hunk.oldCursor++; hunk.newCursor++ }
100
- }
101
- finish()
102
- return {files, byteLength, changedLines: Math.min(changedLines, limits.maxChangedLines), truncated, oversized, limits}
103
- }
@@ -1,40 +0,0 @@
1
- export const CHANGE_CLASSIFICATION_LIMITS = Object.freeze({
2
- maxDiffBytes: 2 * 1024 * 1024,
3
- maxFiles: 500,
4
- maxChangedLines: 20_000,
5
- maxLineLength: 4_000,
6
- maxSymbolsPerFile: 250,
7
- maxSeeds: 1_000,
8
- })
9
-
10
- export const CHANGE_CLASS_RANK = Object.freeze({
11
- 'metadata-only': 0,
12
- 'test-only': 0,
13
- added: 1,
14
- 'body-changed': 2,
15
- 'signature-changed': 3,
16
- removed: 4,
17
- unknown: 5,
18
- })
19
-
20
- export const normalizeChangePath = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
21
- export const changeLineNumber = (value) => Number((String(value || '').match(/(?:^L|@)(\d+)$/) || [])[1] || 0)
22
- export const bareGraphLabel = (value) => String(value || '').replace(/\(.*$/, '').replace(/[^A-Za-z0-9_$].*$/, '').trim()
23
-
24
- export function changeLimits(value = {}) {
25
- const bounded = (item, fallback, min, max) => Math.max(min, Math.min(max, Number(item) || fallback))
26
- const defaults = CHANGE_CLASSIFICATION_LIMITS
27
- return {
28
- maxDiffBytes: bounded(value.maxDiffBytes, defaults.maxDiffBytes, 1_024, defaults.maxDiffBytes),
29
- maxFiles: bounded(value.maxFiles, defaults.maxFiles, 1, defaults.maxFiles),
30
- maxChangedLines: bounded(value.maxChangedLines, defaults.maxChangedLines, 10, defaults.maxChangedLines),
31
- maxLineLength: bounded(value.maxLineLength, defaults.maxLineLength, 80, defaults.maxLineLength),
32
- maxSymbolsPerFile: bounded(value.maxSymbolsPerFile, defaults.maxSymbolsPerFile, 1, defaults.maxSymbolsPerFile),
33
- maxSeeds: bounded(value.maxSeeds, defaults.maxSeeds, 1, defaults.maxSeeds),
34
- }
35
- }
36
-
37
- export function uniqueChangeSeeds(values, limit) {
38
- const all = [...new Set(values.filter(Boolean).map(String))].sort((a, b) => a.localeCompare(b))
39
- return {items: all.slice(0, limit), truncated: all.length > limit, total: all.length}
40
- }
@@ -1,177 +0,0 @@
1
- import {hasPathClass} from '../../path-classification.js'
2
- import {bareGraphLabel, CHANGE_CLASS_RANK, changeLineNumber, normalizeChangePath} from './options.js'
3
-
4
- export function indexChangeGraph(graph, limits) {
5
- const byFile = new Map()
6
- for (const node of graph?.nodes || []) {
7
- const file = normalizeChangePath(node?.source_file || (!String(node?.id || '').includes('#') ? node?.id : ''))
8
- if (!file) continue
9
- if (!byFile.has(file)) byFile.set(file, {path: file, fileNodeId: null, symbols: []})
10
- const record = byFile.get(file)
11
- if (!String(node.id).includes('#')) record.fileNodeId = String(node.id)
12
- else if (record.symbols.length < limits.maxSymbolsPerFile) {
13
- const start = changeLineNumber(node.source_location) || changeLineNumber(node.id)
14
- if (!start) continue
15
- record.symbols.push({
16
- id: String(node.id), label: String(node.label || node.id), start,
17
- end: changeLineNumber(node.source_end), exported: node.exported === true,
18
- symbolKind: node.symbol_kind || null,
19
- })
20
- }
21
- }
22
- for (const record of byFile.values()) {
23
- record.symbols.sort((a, b) => a.start - b.start || a.id.localeCompare(b.id))
24
- for (let index = 0; index < record.symbols.length; index++) {
25
- const symbol = record.symbols[index]
26
- if (!symbol.end || symbol.end < symbol.start) {
27
- const next = record.symbols[index + 1]?.start
28
- symbol.end = next ? Math.max(symbol.start, next - 1) : symbol.start + 400
29
- }
30
- symbol.end = Math.min(symbol.end, symbol.start + 2_000)
31
- }
32
- }
33
- return byFile
34
- }
35
-
36
- const isMetadataLine = (text) => {
37
- const value = String(text || '').trim()
38
- return !value || /^(?:\/\/|\/\*|\*|\*\/|#(?!include\b)|<!--|-->|"""|''')/.test(value)
39
- }
40
-
41
- function signatureText(text, symbol) {
42
- const value = String(text || '').trim(), name = bareGraphLabel(symbol.label)
43
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
44
- const hasName = name && new RegExp(`\\b${escaped}\\b`, 'i').test(value)
45
- return hasName && /\b(?:export|default|declare|abstract|async|function|class|interface|type|enum|const|let|var|def|func|fn|struct|trait|impl|public|private|protected|static)\b/.test(value)
46
- }
47
-
48
- function signaturePosition(change, symbol) {
49
- const candidates = change.kind === 'removed' ? [change.mappedNewLine, change.oldLine] : [change.newLine]
50
- if (candidates.some((line) => line === symbol.start)) return true
51
- if (!candidates.some((line) => Number.isFinite(line) && line > symbol.start && line <= symbol.start + 4)) return false
52
- const value = String(change.text || '').trim()
53
- if (/^(?:return|throw|yield|if|for|while|switch|match|const|let|var|this\.|self\.|[A-Za-z_$][\w$]*\s*=)/.test(value)) return false
54
- return /^(?:[A-Za-z_$][\w$]*\??\s*:\s*[^;]+[,)]?|[A-Za-z_$][\w$<>,.?\[\] :*&]+\s+[A-Za-z_$][\w$]*\s*[,)]|[),:<>{}\[\]|&?]+)$/.test(value)
55
- }
56
-
57
- const moduleSignatureText = (text) => /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+\w+\s*=\s*require\b|using\b|package\b|#include\b|mod\b|pub\s+use\b)/.test(String(text || ''))
58
-
59
- function chooseSymbol(record, change) {
60
- if (!record?.symbols?.length) return null
61
- const lines = change.kind === 'removed'
62
- ? [...new Set([change.mappedNewLine, change.oldLine].filter((line) => Number.isFinite(line) && line > 0))]
63
- : [change.newLine]
64
- const candidates = []
65
- for (const symbol of record.symbols) {
66
- if (!lines.some((line) => line >= symbol.start && line <= symbol.end)) continue
67
- const label = bareGraphLabel(symbol.label)
68
- const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
69
- candidates.push({symbol, mentions: label && new RegExp(`\\b${escaped}\\b`).test(change.text), span: symbol.end - symbol.start})
70
- }
71
- return candidates.sort((a, b) => Number(b.mentions) - Number(a.mentions) || a.span - b.span || a.symbol.id.localeCompare(b.symbol.id))[0]?.symbol || null
72
- }
73
-
74
- const strongest = (classes) => [...classes]
75
- .sort((a, b) => (CHANGE_CLASS_RANK[b] ?? 5) - (CHANGE_CLASS_RANK[a] ?? 5) || a.localeCompare(b))[0] || 'metadata-only'
76
-
77
- function symbolChanges(parsed, record, includeAddedSeeds) {
78
- const grouped = new Map(), unmapped = []
79
- for (const change of [...parsed.additions, ...parsed.removals]) {
80
- const symbol = chooseSymbol(record, change)
81
- if (!symbol) { unmapped.push(change); continue }
82
- if (!grouped.has(symbol.id)) grouped.set(symbol.id, {symbol, additions: [], removals: []})
83
- grouped.get(symbol.id)[change.kind === 'added' ? 'additions' : 'removals'].push(change)
84
- }
85
- const symbols = [], seedIds = []
86
- for (const group of [...grouped.values()].sort((a, b) => a.symbol.start - b.symbol.start || a.symbol.id.localeCompare(b.symbol.id))) {
87
- const addedCode = group.additions.filter((change) => !isMetadataLine(change.text))
88
- const removedCode = group.removals.filter((change) => !isMetadataLine(change.text))
89
- const addedDeclaration = addedCode.some((change) => signaturePosition(change, group.symbol) || signatureText(change.text, group.symbol))
90
- const removedDeclaration = removedCode.some((change) => signaturePosition(change, group.symbol) || signatureText(change.text, group.symbol))
91
- let classification
92
- if (parsed.newFile || (addedDeclaration && !removedCode.length)) classification = 'added'
93
- else if (parsed.deletedFile || (removedDeclaration && !addedCode.length)) classification = 'removed'
94
- else if (addedDeclaration || removedDeclaration) classification = 'signature-changed'
95
- else if (addedCode.length || removedCode.length) classification = 'body-changed'
96
- else classification = 'metadata-only'
97
- const reasons = {
98
- added: 'new declaration; existing callers cannot depend on it yet',
99
- removed: 'declaration removed; existing callers may break',
100
- 'signature-changed': 'declaration/signature line changed',
101
- 'body-changed': 'executable lines changed inside the symbol body',
102
- 'metadata-only': 'only comment/blank lines changed in this symbol',
103
- }
104
- const symbolSeeds = []
105
- if (classification === 'added') { if (includeAddedSeeds) symbolSeeds.push(group.symbol.id) }
106
- else if (classification !== 'metadata-only') {
107
- symbolSeeds.push(group.symbol.id)
108
- if (['removed', 'signature-changed'].includes(classification) && group.symbol.exported && record?.fileNodeId) symbolSeeds.push(record.fileNodeId)
109
- }
110
- seedIds.push(...symbolSeeds)
111
- symbols.push({
112
- id: group.symbol.id, label: group.symbol.label, start: group.symbol.start, end: group.symbol.end,
113
- exported: group.symbol.exported, classification, reason: reasons[classification],
114
- addedLines: group.additions.map((change) => change.newLine).filter(Boolean),
115
- removedLines: group.removals.map((change) => change.oldLine).filter(Boolean),
116
- seedIds: [...new Set(symbolSeeds)].sort(),
117
- })
118
- }
119
- return {symbols, seedIds, unmapped}
120
- }
121
-
122
- export function analyzeParsedFile(parsed, indexed, {includeAddedSeeds}) {
123
- const path = parsed.newPath || parsed.oldPath || '(unknown)'
124
- const record = indexed.get(parsed.newPath) || indexed.get(parsed.oldPath) || null
125
- const grouped = symbolChanges(parsed, record, includeAddedSeeds)
126
- const unmappedCode = grouped.unmapped.filter((change) => !isMetadataLine(change.text))
127
- const unmappedMetadata = grouped.unmapped.length > 0 && !unmappedCode.length
128
- let classification, reason
129
- if (parsed.binary) { classification = 'unknown'; reason = 'binary diff has no line-level evidence' }
130
- else if (parsed.deletedFile) { classification = 'removed'; reason = 'file removed' }
131
- else if (parsed.renamed) { classification = 'signature-changed'; reason = 'file rename changes module identity' }
132
- else if (parsed.newFile) { classification = 'added'; reason = 'new file; no existing dependent can target it yet' }
133
- else if (unmappedCode.some((change) => moduleSignatureText(change.text))) { classification = 'signature-changed'; reason = 'module import/export surface changed outside a mapped symbol' }
134
- else if (unmappedCode.length) { classification = 'unknown'; reason = 'executable diff lines could not be mapped to a graph symbol' }
135
- else if (grouped.symbols.length) {
136
- classification = strongest(grouped.symbols.map((symbol) => symbol.classification))
137
- reason = grouped.symbols.length === 1 ? grouped.symbols[0].reason : `${grouped.symbols.length} mapped symbols; strongest change is ${classification}`
138
- } else if (unmappedMetadata || parsed.hunks.length || parsed.additions.length || parsed.removals.length) {
139
- classification = 'metadata-only'; reason = 'only comment/blank metadata changed outside symbols'
140
- } else { classification = 'metadata-only'; reason = 'file metadata changed without textual hunks' }
141
-
142
- const seedIds = grouped.seedIds
143
- if (parsed.binary || parsed.deletedFile || parsed.renamed || classification === 'unknown') {
144
- if (record?.fileNodeId) seedIds.push(record.fileNodeId)
145
- if (parsed.binary || parsed.deletedFile || classification === 'unknown') seedIds.push(...(record?.symbols || []).map((symbol) => symbol.id))
146
- } else if (classification === 'signature-changed' && !grouped.symbols.length && record?.fileNodeId) seedIds.push(record.fileNodeId)
147
- else if (classification === 'added' && includeAddedSeeds) {
148
- if (record?.fileNodeId) seedIds.push(record.fileNodeId)
149
- seedIds.push(...grouped.symbols.map((symbol) => symbol.id))
150
- }
151
- return {
152
- path, oldPath: parsed.oldPath, newPath: parsed.newPath, classification, reason,
153
- binary: parsed.binary, renamed: parsed.renamed,
154
- addedLines: parsed.additions.length, removedLines: parsed.removals.length,
155
- symbols: grouped.symbols, seedIds: [...new Set(seedIds)].sort(),
156
- }
157
- }
158
-
159
- export function unknownChangedFile(path, indexed, reason) {
160
- const normalized = normalizeChangePath(path), record = indexed.get(normalized)
161
- return {
162
- path: normalized, oldPath: normalized, newPath: normalized, classification: 'unknown', reason,
163
- binary: false, renamed: false, addedLines: 0, removedLines: 0, symbols: [],
164
- seedIds: [record?.fileNodeId, ...(record?.symbols || []).map((symbol) => symbol.id)].filter(Boolean).sort(),
165
- }
166
- }
167
-
168
- export function classifyTestSurface(file, pathClassifier) {
169
- const explanation = pathClassifier.explain(file.path)
170
- if (!hasPathClass(explanation, 'test', 'e2e')) return file
171
- const surface = explanation.classes.includes('e2e') ? 'e2e' : 'test'
172
- return {
173
- ...file, classification: 'test-only', changeClassification: file.classification,
174
- reason: `${surface} path; excluded from the product blast-radius seed set`,
175
- pathClasses: explanation.classes, seedIds: [],
176
- }
177
- }
@@ -1,188 +0,0 @@
1
- // Symbol-aware git-diff classification for change_impact.
2
- import {runGit} from '../git-exec.js'
3
- import {createPathClassifier} from '../path-classification.js'
4
- import {parseZeroContextDiff} from './change-classification/diff-parser.js'
5
- import {
6
- CHANGE_CLASSIFICATION_LIMITS,
7
- CHANGE_CLASS_RANK,
8
- changeLimits,
9
- normalizeChangePath,
10
- uniqueChangeSeeds,
11
- } from './change-classification/options.js'
12
- import {
13
- analyzeParsedFile,
14
- classifyTestSurface,
15
- indexChangeGraph,
16
- unknownChangedFile,
17
- } from './change-classification/symbol-classifier.js'
18
-
19
- export {parseZeroContextDiff} from './change-classification/diff-parser.js'
20
- export {CHANGE_CLASSIFICATION_LIMITS} from './change-classification/options.js'
21
-
22
- const VERDICT_RANK = Object.freeze({LOW: 0, MEDIUM: 1, HIGH: 2})
23
-
24
- function recoverBoundedDiff(repoRoot, base, files, limits) {
25
- const deadline = Date.now() + 12_000
26
- let text = ''
27
- const fallbackFiles = []
28
- for (let index = 0; index < files.length; index++) {
29
- const remainingMs = deadline - Date.now()
30
- const remainingBytes = limits.maxDiffBytes - Buffer.byteLength(text)
31
- if (remainingMs <= 0 || remainingBytes < 1_024) {
32
- fallbackFiles.push(...files.slice(index))
33
- break
34
- }
35
- const result = runGit(repoRoot, [
36
- 'diff', '--no-ext-diff', '--find-renames', '--no-color',
37
- '--unified=0', String(base), '--', files[index],
38
- ], {timeout: Math.min(2_000, remainingMs), maxBuffer: remainingBytes + 1})
39
- const chunk = String(result.stdout || '')
40
- if (result.status !== 0 || !chunk || Buffer.byteLength(chunk) > remainingBytes) {
41
- fallbackFiles.push(files[index])
42
- continue
43
- }
44
- text += chunk
45
- }
46
- return {text, fallbackFiles}
47
- }
48
-
49
- function runGitDiff(repoRoot, base, limits) {
50
- const args = ['diff', '--no-ext-diff', '--find-renames', '--no-color', '--unified=0', String(base), '--']
51
- const result = runGit(repoRoot, args, {timeout: 12_000, maxBuffer: limits.maxDiffBytes + 1})
52
- const output = String(result.stdout || '')
53
- const oversized = result.error?.code === 'ENOBUFS' || Buffer.byteLength(output) > limits.maxDiffBytes
54
- if (result.status === 0 && !oversized) return {available: true, text: output, error: null}
55
- let fallbackFiles = []
56
- let fallbackTruncated = false
57
- let recoveredText = ''
58
- if (oversized) {
59
- const names = runGit(repoRoot, [
60
- 'diff', '--name-only', '-z', '--no-ext-diff', '--find-renames', String(base), '--',
61
- ], {timeout: 12_000, maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096)})
62
- if (names.status === 0) {
63
- const all = String(names.stdout || '').split('\0').map(normalizeChangePath).filter(Boolean)
64
- const boundedFiles = [...new Set(all)].sort((a, b) => a.localeCompare(b)).slice(0, limits.maxFiles)
65
- fallbackTruncated = all.length > limits.maxFiles
66
- const recovered = recoverBoundedDiff(repoRoot, base, boundedFiles, limits)
67
- recoveredText = recovered.text
68
- fallbackFiles = recovered.fallbackFiles
69
- }
70
- }
71
- return {
72
- available: recoveredText.length > 0,
73
- text: recoveredText,
74
- oversized,
75
- partialFallback: recoveredText.length > 0,
76
- fallbackFiles,
77
- fallbackTruncated,
78
- error: oversized
79
- ? 'git diff exceeded the byte limit'
80
- : String(result.stderr || result.error?.message || 'git diff unavailable').trim(),
81
- }
82
- }
83
-
84
- const validExplicitFiles = (files) => [...new Set((Array.isArray(files) ? files : [])
85
- .map(normalizeChangePath)
86
- .filter((file) => file && !file.startsWith('../') && !file.includes('/../') && !file.startsWith('-')))]
87
- .sort((a, b) => a.localeCompare(b))
88
-
89
- function collectDiffInput({diffText, repoRoot, base, explicitFiles, limits}) {
90
- if (typeof diffText === 'string') return {source: 'provided-diff', available: true, text: diffText, reason: '', oversized: false}
91
- if (repoRoot && base) {
92
- const result = runGitDiff(repoRoot, base, limits)
93
- return {
94
- source: 'git-diff', available: result.available, text: result.text,
95
- reason: result.error || '', oversized: result.oversized === true,
96
- fallbackFiles: result.fallbackFiles || [], fallbackTruncated: result.fallbackTruncated === true,
97
- partialFallback: result.partialFallback === true,
98
- }
99
- }
100
- return {
101
- source: 'files-only', available: false, text: '', oversized: false,
102
- reason: 'no unified diff was provided and no repoRoot/base pair was available',
103
- fallbackFiles: [], fallbackTruncated: false, partialFallback: false,
104
- }
105
- }
106
-
107
- function classificationReasons(counts, {available, unavailableReason, truncated, includeAddedSeeds}) {
108
- const reasons = []
109
- if (!available) reasons.push(`Diff unavailable: ${unavailableReason || 'unknown error'}; using conservative file/symbol seeds.`)
110
- if (truncated) reasons.push('Diff exceeded a safety bound; incomplete evidence is classified HIGH/unknown.')
111
- if (counts.removed || counts['signature-changed']) reasons.push(`${counts.removed} removed and ${counts['signature-changed']} signature/module-surface file change(s) can break existing callers.`)
112
- if (counts['body-changed']) reasons.push(`${counts['body-changed']} file(s) contain mapped executable body changes.`)
113
- if (counts.added && !includeAddedSeeds) reasons.push(`${counts.added} purely additive file change(s) create no dependent seeds by default.`)
114
- if (counts['metadata-only']) reasons.push(`${counts['metadata-only']} metadata-only file change(s) create no dependent seeds.`)
115
- if (counts['test-only']) reasons.push(`${counts['test-only']} test-only file change(s) are labelled explicitly and create no product blast-radius seeds.`)
116
- if (counts.unknown) reasons.push(`${counts.unknown} file change(s) remain unknown and are seeded conservatively.`)
117
- if (!reasons.length) reasons.push('No changed files were present in the supplied diff.')
118
- return reasons
119
- }
120
-
121
- export function classifyChangeImpact({
122
- repoRoot = '', graph = {}, base = '', diffText, files = [], includeAddedSeeds = false,
123
- limits: requestedLimits = {},
124
- } = {}) {
125
- const limits = changeLimits(requestedLimits)
126
- const explicitFiles = validExplicitFiles(files)
127
- const input = collectDiffInput({diffText, repoRoot, base, explicitFiles, limits})
128
- const fallbackFiles = validExplicitFiles([...explicitFiles, ...(input.fallbackFiles || [])]).slice(0, limits.maxFiles)
129
- const indexed = indexChangeGraph(graph, limits)
130
- const pathClassifier = createPathClassifier(repoRoot)
131
- const parsed = input.available
132
- ? parseZeroContextDiff(input.text, limits)
133
- : {files: [], changedLines: 0, byteLength: 0, truncated: input.oversized, oversized: input.oversized, limits}
134
- const analyzed = parsed.files.map((file) => classifyTestSurface(analyzeParsedFile(file, indexed, {includeAddedSeeds}), pathClassifier))
135
- const represented = new Set(analyzed.flatMap((file) => [file.oldPath, file.newPath].filter(Boolean)))
136
- for (const file of fallbackFiles) if (!represented.has(file)) analyzed.push(classifyTestSurface(
137
- unknownChangedFile(file, indexed, input.available ? 'explicitly changed file had no textual hunk' : input.reason),
138
- pathClassifier,
139
- ))
140
- if (!input.available && !fallbackFiles.length) analyzed.push(unknownChangedFile('(diff unavailable)', indexed, input.reason))
141
- analyzed.sort((a, b) => a.path.localeCompare(b.path))
142
-
143
- const incomplete = parsed.truncated || parsed.oversized || input.oversized || input.fallbackTruncated
144
- const parsedEvidenceIncomplete = parsed.truncated || parsed.oversized || (input.oversized && !input.partialFallback)
145
- if (parsedEvidenceIncomplete) for (const file of analyzed) {
146
- if (file.classification === 'test-only') continue
147
- file.classification = 'unknown'
148
- file.reason = 'diff was truncated/oversized; symbol-level classification is incomplete'
149
- const record = indexed.get(file.newPath) || indexed.get(file.oldPath)
150
- file.seedIds = [...new Set([record?.fileNodeId, ...(record?.symbols || []).map((symbol) => symbol.id), ...file.seedIds].filter(Boolean))].sort()
151
- }
152
-
153
- const seeds = uniqueChangeSeeds(analyzed.flatMap((file) => file.seedIds), limits.maxSeeds)
154
- let verdict = 'LOW'
155
- for (const file of analyzed) {
156
- const next = ['removed', 'signature-changed', 'unknown'].includes(file.classification)
157
- ? 'HIGH' : file.classification === 'body-changed' ? 'MEDIUM' : 'LOW'
158
- if (VERDICT_RANK[next] > VERDICT_RANK[verdict]) verdict = next
159
- }
160
- if (!input.available || input.oversized || parsed.truncated || seeds.truncated) verdict = 'HIGH'
161
- const counts = Object.fromEntries(Object.keys(CHANGE_CLASS_RANK)
162
- .map((name) => [name, analyzed.filter((file) => file.classification === name).length]))
163
- return {
164
- ok: input.available && !input.oversized && !parsed.truncated && !seeds.truncated,
165
- source: input.source,
166
- verdict,
167
- reasons: classificationReasons(counts, {
168
- available: input.available,
169
- unavailableReason: input.reason,
170
- truncated: incomplete,
171
- includeAddedSeeds,
172
- }),
173
- seedIds: seeds.items,
174
- files: analyzed,
175
- summary: {
176
- files: analyzed.length,
177
- symbols: analyzed.reduce((sum, file) => sum + file.symbols.length, 0),
178
- counts,
179
- seeds: seeds.items.length,
180
- totalSeedsBeforeCap: seeds.total,
181
- },
182
- bounds: {
183
- ...limits, diffBytes: parsed.byteLength, changedLines: parsed.changedLines,
184
- fallbackFiles: input.fallbackFiles?.length || 0,
185
- truncated: input.oversized === true || !!parsed.truncated || seeds.truncated || input.fallbackTruncated === true,
186
- },
187
- }
188
- }
@@ -1,112 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
-
3
- function pct01(value) {
4
- const n = Number(value);
5
- if (!Number.isFinite(n)) return null;
6
- return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
7
- }
8
-
9
- function pctFromCounts(covered, total) {
10
- const c = Number(covered);
11
- const t = Number(total);
12
- if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
13
- return Math.max(0, Math.min(1, c / t));
14
- }
15
-
16
- function addLineHit(lines, line, hit) {
17
- const value = Number(line);
18
- if (!Number.isFinite(value) || value <= 0) return;
19
- const n = Math.round(value);
20
- if (n < 1) return;
21
- const current = lines.get(n);
22
- lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
23
- }
24
-
25
- function numericLineList(value) {
26
- const input = Array.isArray(value) ? value : [];
27
- return input
28
- .map((line) => Number(line))
29
- .filter((line) => Number.isFinite(line) && line > 0)
30
- .map((line) => Math.round(line));
31
- }
32
-
33
- function legacyLineGroups(record) {
34
- if (!record || typeof record !== "object") return { covered: [], uncovered: [] };
35
- const lines = record.lines && typeof record.lines === "object" ? record.lines : {};
36
- return {
37
- covered: [
38
- ...numericLineList(record.covered),
39
- ...numericLineList(record.covered_lines),
40
- ...numericLineList(record.coveredLines),
41
- ...numericLineList(lines.covered),
42
- ...numericLineList(lines.covered_lines),
43
- ],
44
- uncovered: [
45
- ...numericLineList(record.uncovered),
46
- ...numericLineList(record.uncovered_lines),
47
- ...numericLineList(record.uncoveredLines),
48
- ...numericLineList(record.missing),
49
- ...numericLineList(record.missing_lines),
50
- ...numericLineList(lines.uncovered),
51
- ...numericLineList(lines.uncovered_lines),
52
- ...numericLineList(lines.missing),
53
- ],
54
- };
55
- }
56
-
57
- function recordPath(record) {
58
- const raw = record?.path || record?.file || record?.filename || record?.name;
59
- if (Array.isArray(raw)) return raw.map(String).filter(Boolean).join("/");
60
- return raw;
61
- }
62
-
63
- function reportEntries(json) {
64
- const files = json?.files || json?.coverage || json;
65
- if (Array.isArray(files)) {
66
- return files.map((record) => [recordPath(record), record]).filter(([raw]) => raw);
67
- }
68
- if (files && typeof files === "object") return Object.entries(files);
69
- return [];
70
- }
71
-
72
- function coveragePct(record, covered, total) {
73
- const summary = record?.summary && typeof record.summary === "object" ? record.summary : {};
74
- return pct01(
75
- record?.coverage
76
- ?? record?.coverage_percentage
77
- ?? record?.line_coverage
78
- ?? summary.coverage
79
- ?? summary.coverage_percentage
80
- ?? summary.percent
81
- )
82
- ?? pctFromCounts(record?.covered, record?.coverable)
83
- ?? pctFromCounts(covered, total);
84
- }
85
-
86
- export function readTarpaulinCoverage(filePath) {
87
- const json = JSON.parse(readFileSync(filePath, "utf8"));
88
- return reportEntries(json).map(([path, record]) => {
89
- const lines = new Map();
90
- for (const trace of Array.isArray(record?.traces) ? record.traces : []) {
91
- if (!trace?.stats || typeof trace.stats !== "object" || !("Line" in trace.stats)) continue;
92
- addLineHit(lines, trace.line, trace.stats.Line);
93
- }
94
- const legacy = legacyLineGroups(record);
95
- for (const line of legacy.uncovered) addLineHit(lines, line, 0);
96
- for (const line of legacy.covered) addLineHit(lines, line, 1);
97
- const measuredTotal = lines.size;
98
- const measuredCovered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
99
- const reportedTotal = Number(record?.coverable);
100
- const reportedCovered = Number(record?.covered);
101
- return {
102
- path,
103
- entry: {
104
- pct: coveragePct(record, measuredCovered, measuredTotal),
105
- total: Number.isFinite(reportedTotal) ? reportedTotal : measuredTotal,
106
- covered: Number.isFinite(reportedCovered) ? reportedCovered : measuredCovered,
107
- lines,
108
- source: "tarpaulin-report.json",
109
- },
110
- };
111
- });
112
- }