@orangepro/orangepro-mcp 0.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,518 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ const GO_COVERAGE_CANDIDATES = new Set([
4
+ "coverage.out",
5
+ "cover.out",
6
+ "coverprofile.out",
7
+ "go.coverprofile",
8
+ ".orangepro/coverage/go.coverprofile",
9
+ "coverage/coverage.out",
10
+ "coverage/go.coverprofile"
11
+ ]);
12
+ const RUNTIME_COVERAGE_CANDIDATES = new Set([
13
+ ...GO_COVERAGE_CANDIDATES,
14
+ "coverage/lcov.info",
15
+ "lcov.info",
16
+ "coverage.xml",
17
+ "target/site/jacoco/jacoco.xml",
18
+ "build/reports/jacoco/test/jacocoTestReport.xml"
19
+ ]);
20
+ const COVERAGE_SCAN_SKIP_DIRS = new Set([".git", ".orangepro", "node_modules", "vendor", "dist", "build", "target"]);
21
+ function coverageFormatForPath(rel) {
22
+ const normalized = rel.replace(/\\/g, "/");
23
+ const base = path.posix.basename(normalized).toLowerCase();
24
+ if (GO_COVERAGE_CANDIDATES.has(normalized) || /(^|\/)(coverage|cover)(profile)?\.(out|cov)$/i.test(normalized) || /\.coverprofile$/i.test(base)) {
25
+ return "go-coverprofile";
26
+ }
27
+ if (base === "lcov.info" || base.endsWith("-lcov.info") || base.endsWith(".lcov.info"))
28
+ return "lcov";
29
+ if (base === "coverage.xml" || base.endsWith(".coverage.xml"))
30
+ return "coverage-py";
31
+ if (base === "jacoco.xml" || normalized.endsWith("/jacocoTestReport.xml"))
32
+ return "jacoco";
33
+ return null;
34
+ }
35
+ function pct(part, total) {
36
+ return total > 0 ? Math.round((part / total) * 1000) / 10 : 0;
37
+ }
38
+ function symbolLanguage(n) {
39
+ const file = typeof n.properties.file === "string" ? n.properties.file : n.external_id;
40
+ if (file.includes(".go#") || file.endsWith(".go"))
41
+ return "go";
42
+ if (/\.(ts|tsx|js|jsx)(#|$)/.test(file))
43
+ return "tsjs";
44
+ if (/\.py(#|$)/.test(file))
45
+ return "python";
46
+ if (/\.java(#|$)/.test(file))
47
+ return "java";
48
+ return "other";
49
+ }
50
+ function artifactCandidates(root, files) {
51
+ const out = new Map();
52
+ const add = (candidate) => {
53
+ if (!existsSync(path.join(root, candidate.path)))
54
+ return;
55
+ out.set(candidate.path, candidate);
56
+ };
57
+ for (const f of files) {
58
+ const format = coverageFormatForPath(f.relPath);
59
+ if (format)
60
+ add({ path: f.relPath, format });
61
+ }
62
+ for (const rel of RUNTIME_COVERAGE_CANDIDATES) {
63
+ const format = coverageFormatForPath(rel);
64
+ if (format && existsSync(path.join(root, rel)))
65
+ add({ path: rel, format });
66
+ }
67
+ for (const candidate of discoverNestedCoverageArtifacts(root))
68
+ add(candidate);
69
+ try {
70
+ for (const entry of readdirSync(path.join(root, ".orangepro/coverage"), { withFileTypes: true })) {
71
+ if (!entry.isFile())
72
+ continue;
73
+ const rel = `.orangepro/coverage/${entry.name}`;
74
+ const format = coverageFormatForPath(rel);
75
+ if (format)
76
+ add({ path: rel, format });
77
+ }
78
+ }
79
+ catch {
80
+ /* no generated coverage dir */
81
+ }
82
+ return [...out.values()].sort((a, b) => a.path.localeCompare(b.path));
83
+ }
84
+ function discoverNestedCoverageArtifacts(root) {
85
+ const out = [];
86
+ const addLcov = (rel) => {
87
+ if (existsSync(path.join(root, rel)))
88
+ out.push({ path: rel, format: "lcov" });
89
+ };
90
+ const visit = (dirRel) => {
91
+ let entries;
92
+ try {
93
+ entries = readdirSync(path.join(root, dirRel), { withFileTypes: true });
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ for (const entry of entries) {
99
+ if (!entry.isDirectory())
100
+ continue;
101
+ if (COVERAGE_SCAN_SKIP_DIRS.has(entry.name))
102
+ continue;
103
+ const childRel = dirRel ? path.posix.join(dirRel, entry.name) : entry.name;
104
+ if (entry.name.toLowerCase() === "coverage") {
105
+ addLcov(path.posix.join(childRel, "lcov.info"));
106
+ continue;
107
+ }
108
+ visit(childRel);
109
+ }
110
+ };
111
+ visit("");
112
+ return out;
113
+ }
114
+ function goModuleForDir(dir, goModulesByDir) {
115
+ for (;;) {
116
+ const module = goModulesByDir.get(dir);
117
+ if (module)
118
+ return { dir, module };
119
+ if (!dir)
120
+ return null;
121
+ dir = path.posix.dirname(dir);
122
+ if (dir === ".")
123
+ dir = "";
124
+ }
125
+ }
126
+ function relDir(rel) {
127
+ const dir = path.posix.dirname(rel);
128
+ return dir === "." ? "" : dir;
129
+ }
130
+ function coverageArtifactBaseDir(rel) {
131
+ const dir = relDir(rel.replace(/\\/g, "/"));
132
+ return path.posix.basename(dir).toLowerCase() === "coverage" ? relDir(dir) : dir;
133
+ }
134
+ function sameModuleOwner(fileRel, artifactRel, goModulesByDir) {
135
+ if (goModulesByDir.size <= 1)
136
+ return true;
137
+ const artifactOwner = goModuleForDir(relDir(artifactRel), goModulesByDir);
138
+ const fileOwner = goModuleForDir(relDir(fileRel), goModulesByDir);
139
+ return Boolean(artifactOwner && fileOwner && artifactOwner.dir === fileOwner.dir && artifactOwner.module === fileOwner.module);
140
+ }
141
+ function directGoCoverageMatch(candidate, artifactRel, goModulesByDir, codeFiles) {
142
+ if (candidate.startsWith("../") || !codeFiles.has(candidate))
143
+ return null;
144
+ return sameModuleOwner(candidate, artifactRel, goModulesByDir) ? candidate : null;
145
+ }
146
+ function normalizeGoCoverageFile(rawFile, root, artifactRel, goModulesByDir, codeFiles) {
147
+ let raw = rawFile.replace(/\\/g, "/");
148
+ if (path.isAbsolute(raw)) {
149
+ const rel = path.relative(root, raw).replace(/\\/g, "/");
150
+ const direct = directGoCoverageMatch(rel, artifactRel, goModulesByDir, codeFiles);
151
+ if (direct)
152
+ return direct;
153
+ }
154
+ raw = raw.replace(/^\.\//, "");
155
+ const directMatches = new Set();
156
+ for (const candidate of [raw, relDir(artifactRel) ? path.posix.normalize(path.posix.join(relDir(artifactRel), raw)) : raw]) {
157
+ const direct = directGoCoverageMatch(candidate, artifactRel, goModulesByDir, codeFiles);
158
+ if (direct)
159
+ directMatches.add(direct);
160
+ }
161
+ if (directMatches.size === 1)
162
+ return [...directMatches][0];
163
+ const moduleMatches = new Set();
164
+ const modules = [...goModulesByDir.entries()].sort((a, b) => b[1].length - a[1].length || b[0].length - a[0].length);
165
+ for (const [dir, moduleName] of modules) {
166
+ if (!moduleName || !raw.startsWith(`${moduleName}/`))
167
+ continue;
168
+ const moduleRel = raw.slice(moduleName.length + 1);
169
+ const rel = path.posix.normalize(dir ? path.posix.join(dir, moduleRel) : moduleRel);
170
+ if (rel.startsWith("../") || (dir && rel !== dir && !rel.startsWith(`${dir}/`)))
171
+ continue;
172
+ if (!codeFiles.has(rel))
173
+ continue;
174
+ if (goModuleForDir(path.posix.dirname(rel), goModulesByDir)?.module !== moduleName)
175
+ continue;
176
+ moduleMatches.add(rel);
177
+ }
178
+ return moduleMatches.size === 1 ? [...moduleMatches][0] : null;
179
+ }
180
+ function parseGoCoverprofile(root, rel, goModulesByDir, codeFiles) {
181
+ const abs = path.join(root, rel);
182
+ let stat;
183
+ try {
184
+ stat = statSync(abs);
185
+ }
186
+ catch {
187
+ return { ranges: [] };
188
+ }
189
+ if (!stat.isFile())
190
+ return { ranges: [] };
191
+ if (stat.size > 20_000_000) {
192
+ return { ranges: [], skipped: { path: rel, format: "go-coverprofile", reason: "coverage artifact exceeds 20MB size cap" } };
193
+ }
194
+ let content = "";
195
+ try {
196
+ content = readFileSync(abs, "utf8");
197
+ }
198
+ catch {
199
+ return { ranges: [] };
200
+ }
201
+ const lines = content.split(/\r?\n/);
202
+ if (!/^mode:\s+(set|count|atomic)\s*$/.test(lines[0] ?? ""))
203
+ return { ranges: [] };
204
+ const ranges = [];
205
+ let positiveRanges = 0;
206
+ for (const line of lines.slice(1)) {
207
+ const m = line.match(/^(.+):(\d+)\.\d+,(\d+)\.\d+\s+\d+\s+(\d+)$/);
208
+ if (!m || Number(m[4]) <= 0)
209
+ continue;
210
+ positiveRanges++;
211
+ const file = normalizeGoCoverageFile(m[1], root, rel, goModulesByDir, codeFiles);
212
+ if (!file)
213
+ continue;
214
+ ranges.push({
215
+ file,
216
+ start_line: Number(m[2]),
217
+ end_line: Number(m[3]),
218
+ artifact: rel,
219
+ format: "go-coverprofile"
220
+ });
221
+ }
222
+ return {
223
+ ranges,
224
+ ...(positiveRanges > 0 && ranges.length === 0
225
+ ? {
226
+ skipped: {
227
+ path: rel,
228
+ format: "go-coverprofile",
229
+ reason: "coverage artifact had positive ranges, but none matched scanned Go files"
230
+ }
231
+ }
232
+ : {})
233
+ };
234
+ }
235
+ function languageForFormat(format) {
236
+ if (format === "go-coverprofile")
237
+ return "go";
238
+ if (format === "lcov")
239
+ return "tsjs";
240
+ if (format === "coverage-py")
241
+ return "python";
242
+ return "java";
243
+ }
244
+ function normalizeCoverageFile(rawFile, root, artifactRel, codeFiles, allowedExt) {
245
+ let raw = xmlDecode(rawFile.trim()).replace(/\\/g, "/");
246
+ if (!raw || (raw.includes("://") && !raw.startsWith("file://")))
247
+ return null;
248
+ if (raw.startsWith("file://"))
249
+ raw = raw.slice("file://".length);
250
+ const allowedFiles = [...codeFiles].filter((f) => allowedExt.test(f));
251
+ const direct = (candidate) => {
252
+ const rel = candidate.replace(/\\/g, "/").replace(/^\.\//, "");
253
+ if (rel.startsWith("../") || path.posix.isAbsolute(rel))
254
+ return null;
255
+ return codeFiles.has(rel) && allowedExt.test(rel) ? rel : null;
256
+ };
257
+ if (path.isAbsolute(raw)) {
258
+ const rel = path.relative(root, raw).replace(/\\/g, "/");
259
+ return direct(rel);
260
+ }
261
+ const candidateSet = new Set([raw]);
262
+ const artifactDir = relDir(artifactRel);
263
+ if (artifactDir)
264
+ candidateSet.add(path.posix.normalize(path.posix.join(artifactDir, raw)));
265
+ const artifactBaseDir = coverageArtifactBaseDir(artifactRel);
266
+ if (artifactBaseDir)
267
+ candidateSet.add(path.posix.normalize(path.posix.join(artifactBaseDir, raw)));
268
+ for (const candidate of candidateSet) {
269
+ const match = direct(candidate);
270
+ if (match)
271
+ return match;
272
+ }
273
+ const suffix = raw.replace(/^\.\//, "");
274
+ if (!suffix.includes("/"))
275
+ return null;
276
+ const matches = allowedFiles.filter((f) => f === suffix || f.endsWith(`/${suffix}`));
277
+ return matches.length === 1 ? matches[0] : null;
278
+ }
279
+ function readCoverageText(root, rel, format) {
280
+ const abs = path.join(root, rel);
281
+ let stat;
282
+ try {
283
+ stat = statSync(abs);
284
+ }
285
+ catch {
286
+ return null;
287
+ }
288
+ if (!stat.isFile())
289
+ return null;
290
+ if (stat.size > 20_000_000) {
291
+ return { content: "", skipped: { path: rel, format, reason: "coverage artifact exceeds 20MB size cap" } };
292
+ }
293
+ try {
294
+ return { content: readFileSync(abs, "utf8") };
295
+ }
296
+ catch {
297
+ return null;
298
+ }
299
+ }
300
+ function parseLcov(root, rel, codeFiles) {
301
+ const read = readCoverageText(root, rel, "lcov");
302
+ if (!read)
303
+ return { ranges: [] };
304
+ if (read.skipped)
305
+ return { ranges: [], skipped: read.skipped };
306
+ const ranges = [];
307
+ let currentFile = null;
308
+ let positiveRanges = 0;
309
+ for (const line of read.content.split(/\r?\n/)) {
310
+ if (line.startsWith("SF:")) {
311
+ const rawFile = line.slice(3).trim();
312
+ currentFile = rawFile ? normalizeCoverageFile(rawFile, root, rel, codeFiles, /\.(ts|tsx|js|jsx)$/i) : null;
313
+ continue;
314
+ }
315
+ const m = line.match(/^DA:(\d+),(\d+)/);
316
+ if (!m || Number(m[2]) <= 0)
317
+ continue;
318
+ positiveRanges++;
319
+ if (!currentFile)
320
+ continue;
321
+ const lineNo = Number(m[1]);
322
+ ranges.push({ file: currentFile, start_line: lineNo, end_line: lineNo, artifact: rel, format: "lcov" });
323
+ }
324
+ return {
325
+ ranges,
326
+ ...(positiveRanges > 0 && ranges.length === 0
327
+ ? { skipped: { path: rel, format: "lcov", reason: "coverage artifact had positive ranges, but none matched scanned TypeScript/JavaScript files" } }
328
+ : {})
329
+ };
330
+ }
331
+ function xmlAttr(tag, name) {
332
+ const m = tag.match(new RegExp(`\\b${name}=["']([^"']*)["']`));
333
+ return m ? xmlDecode(m[1]) : undefined;
334
+ }
335
+ function xmlDecode(s) {
336
+ return s
337
+ .replace(/&quot;/g, '"')
338
+ .replace(/&apos;/g, "'")
339
+ .replace(/&lt;/g, "<")
340
+ .replace(/&gt;/g, ">")
341
+ .replace(/&amp;/g, "&");
342
+ }
343
+ function parseXmlSources(content) {
344
+ const out = [];
345
+ for (const m of content.matchAll(/<source>([\s\S]*?)<\/source>/g)) {
346
+ const value = xmlDecode(m[1].trim());
347
+ if (value)
348
+ out.push(value);
349
+ }
350
+ return out;
351
+ }
352
+ function parseCoveragePyXml(root, rel, codeFiles) {
353
+ const read = readCoverageText(root, rel, "coverage-py");
354
+ if (!read)
355
+ return { ranges: [] };
356
+ if (read.skipped)
357
+ return { ranges: [], skipped: read.skipped };
358
+ const ranges = [];
359
+ const sources = parseXmlSources(read.content);
360
+ let positiveRanges = 0;
361
+ for (const m of read.content.matchAll(/<class\b([^>]*)>([\s\S]*?)<\/class>/g)) {
362
+ const filename = xmlAttr(m[1], "filename");
363
+ if (!filename)
364
+ continue;
365
+ const fileCandidates = [filename, ...sources.map((source) => path.isAbsolute(source) ? path.join(source, filename) : path.posix.join(source.replace(/\\/g, "/"), filename))];
366
+ let file = null;
367
+ for (const candidate of fileCandidates) {
368
+ file = normalizeCoverageFile(candidate, root, rel, codeFiles, /\.py$/i);
369
+ if (file)
370
+ break;
371
+ }
372
+ for (const line of m[2].matchAll(/<line\b([^>]*)\/?>/g)) {
373
+ const number = Number(xmlAttr(line[1], "number"));
374
+ const hits = Number(xmlAttr(line[1], "hits") ?? "0");
375
+ if (!Number.isFinite(number) || hits <= 0)
376
+ continue;
377
+ positiveRanges++;
378
+ if (!file)
379
+ continue;
380
+ ranges.push({ file, start_line: number, end_line: number, artifact: rel, format: "coverage-py" });
381
+ }
382
+ }
383
+ return {
384
+ ranges,
385
+ ...(positiveRanges > 0 && ranges.length === 0
386
+ ? { skipped: { path: rel, format: "coverage-py", reason: "coverage artifact had positive ranges, but none matched scanned Python files" } }
387
+ : {})
388
+ };
389
+ }
390
+ function parseJacocoXml(root, rel, codeFiles) {
391
+ const read = readCoverageText(root, rel, "jacoco");
392
+ if (!read)
393
+ return { ranges: [] };
394
+ if (read.skipped)
395
+ return { ranges: [], skipped: read.skipped };
396
+ const ranges = [];
397
+ let positiveRanges = 0;
398
+ for (const pkg of read.content.matchAll(/<package\b([^>]*)>([\s\S]*?)<\/package>/g)) {
399
+ const packageName = (xmlAttr(pkg[1], "name") ?? "").replace(/\./g, "/");
400
+ for (const sf of pkg[2].matchAll(/<sourcefile\b([^>]*)>([\s\S]*?)<\/sourcefile>/g)) {
401
+ const name = xmlAttr(sf[1], "name");
402
+ if (!name)
403
+ continue;
404
+ const rawFile = packageName ? `${packageName}/${name}` : name;
405
+ const file = normalizeCoverageFile(rawFile, root, rel, codeFiles, /\.java$/i);
406
+ for (const line of sf[2].matchAll(/<line\b([^>]*)\/?>/g)) {
407
+ const number = Number(xmlAttr(line[1], "nr"));
408
+ const coveredInstructions = Number(xmlAttr(line[1], "ci") ?? "0");
409
+ const coveredBranches = Number(xmlAttr(line[1], "cb") ?? "0");
410
+ if (!Number.isFinite(number) || coveredInstructions + coveredBranches <= 0)
411
+ continue;
412
+ positiveRanges++;
413
+ if (!file)
414
+ continue;
415
+ ranges.push({ file, start_line: number, end_line: number, artifact: rel, format: "jacoco" });
416
+ }
417
+ }
418
+ }
419
+ return {
420
+ ranges,
421
+ ...(positiveRanges > 0 && ranges.length === 0
422
+ ? { skipped: { path: rel, format: "jacoco", reason: "coverage artifact had positive ranges, but none matched scanned Java files" } }
423
+ : {})
424
+ };
425
+ }
426
+ function parseCoverageArtifact(root, candidate, goModulesByDir, codeFiles) {
427
+ if (candidate.format === "go-coverprofile")
428
+ return parseGoCoverprofile(root, candidate.path, goModulesByDir, codeFiles);
429
+ if (candidate.format === "lcov")
430
+ return parseLcov(root, candidate.path, codeFiles);
431
+ if (candidate.format === "coverage-py")
432
+ return parseCoveragePyXml(root, candidate.path, codeFiles);
433
+ return parseJacocoXml(root, candidate.path, codeFiles);
434
+ }
435
+ function overlaps(aStart, aEnd, bStart, bEnd) {
436
+ return aStart <= bEnd && bStart <= aEnd;
437
+ }
438
+ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
439
+ const codeFiles = new Set(nodes
440
+ .filter((n) => n.kind === "CodeSymbol" && typeof n.properties.file === "string")
441
+ .map((n) => n.properties.file));
442
+ const artifacts = artifactCandidates(root, files);
443
+ if (artifacts.length === 0)
444
+ return undefined;
445
+ const rangesByFile = new Map();
446
+ const artifactStats = new Map();
447
+ const skippedArtifacts = [];
448
+ for (const artifact of artifacts) {
449
+ const { ranges, skipped } = parseCoverageArtifact(root, artifact, goModulesByDir, codeFiles);
450
+ if (skipped)
451
+ skippedArtifacts.push(skipped);
452
+ if (ranges.length === 0)
453
+ continue;
454
+ const stat = artifactStats.get(artifact.path) ?? { files: new Set(), covered_ranges: 0, format: artifact.format };
455
+ for (const range of ranges) {
456
+ const list = rangesByFile.get(range.file);
457
+ if (list)
458
+ list.push(range);
459
+ else
460
+ rangesByFile.set(range.file, [range]);
461
+ stat.files.add(range.file);
462
+ stat.covered_ranges++;
463
+ }
464
+ artifactStats.set(artifact.path, stat);
465
+ }
466
+ if (artifactStats.size === 0 && skippedArtifacts.length === 0)
467
+ return undefined;
468
+ const byLanguage = {};
469
+ const coveredSymbols = new Set();
470
+ let totalEligible = 0;
471
+ let symbolsWithSpans = 0;
472
+ const ingestedLanguages = new Set([...artifactStats.values()].map((s) => languageForFormat(s.format)));
473
+ for (const n of nodes) {
474
+ if (n.kind !== "CodeSymbol" || n.denominator_eligible !== true || n.stale === true)
475
+ continue;
476
+ const lang = symbolLanguage(n);
477
+ if (!ingestedLanguages.has(lang))
478
+ continue;
479
+ totalEligible++;
480
+ const bucket = (byLanguage[lang] ??= { eligible: 0, symbols_with_spans: 0, covered: 0, covered_pct: 0 });
481
+ bucket.eligible++;
482
+ const file = typeof n.properties.file === "string" ? n.properties.file : "";
483
+ const start = typeof n.properties.start_line === "number" ? n.properties.start_line : null;
484
+ const end = typeof n.properties.end_line === "number" ? n.properties.end_line : null;
485
+ if (!file || start == null || end == null)
486
+ continue;
487
+ symbolsWithSpans++;
488
+ bucket.symbols_with_spans++;
489
+ const ranges = rangesByFile.get(file) ?? [];
490
+ const overlapping = ranges.filter((r) => overlaps(start, end, r.start_line, r.end_line));
491
+ if (overlapping.length === 0)
492
+ continue;
493
+ const formats = [...new Set(overlapping.map((r) => r.format))].sort();
494
+ coveredSymbols.add(n.external_id);
495
+ bucket.covered++;
496
+ n.properties = {
497
+ ...n.properties,
498
+ runtime_covered: true,
499
+ runtime_coverage_formats: formats
500
+ };
501
+ }
502
+ for (const bucket of Object.values(byLanguage))
503
+ bucket.covered_pct = pct(bucket.covered, bucket.eligible);
504
+ return {
505
+ artifacts: [...artifactStats.entries()].map(([path, s]) => ({
506
+ path,
507
+ format: s.format,
508
+ files: s.files.size,
509
+ covered_ranges: s.covered_ranges
510
+ })),
511
+ ...(skippedArtifacts.length > 0 ? { skipped_artifacts: skippedArtifacts } : {}),
512
+ total_eligible_symbols: totalEligible,
513
+ symbols_with_spans: symbolsWithSpans,
514
+ covered_symbols: coveredSymbols.size,
515
+ covered_pct: pct(coveredSymbols.size, totalEligible),
516
+ by_language: byLanguage
517
+ };
518
+ }