api-tests-coverage 1.0.24 → 1.0.26

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/dist/dashboard/dist/assets/_basePickBy-BHjg34fk.js +1 -0
  2. package/dist/dashboard/dist/assets/_basePickBy-DIphIltc.js +1 -0
  3. package/dist/dashboard/dist/assets/_baseUniq-D57u2_9m.js +1 -0
  4. package/dist/dashboard/dist/assets/_baseUniq-DxJYHd7T.js +1 -0
  5. package/dist/dashboard/dist/assets/arc-DQosMxPM.js +1 -0
  6. package/dist/dashboard/dist/assets/arc-DcXkmNi0.js +1 -0
  7. package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-CNbtIqHR.js +36 -0
  8. package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-ChMY32ql.js +36 -0
  9. package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-DVhWtRxG.js +122 -0
  10. package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-NgdJaQvK.js +122 -0
  11. package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-B6esYq70.js +10 -0
  12. package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-ChTe70Dn.js +10 -0
  13. package/dist/dashboard/dist/assets/channel-B3Mj1BTw.js +1 -0
  14. package/dist/dashboard/dist/assets/channel-Df6s6dhy.js +1 -0
  15. package/dist/dashboard/dist/assets/chunk-4BX2VUAB-B7Pkx3C3.js +1 -0
  16. package/dist/dashboard/dist/assets/chunk-4BX2VUAB-BS3-4dfL.js +1 -0
  17. package/dist/dashboard/dist/assets/chunk-55IACEB6-8ClDkPsD.js +1 -0
  18. package/dist/dashboard/dist/assets/chunk-55IACEB6-BCczdImM.js +1 -0
  19. package/dist/dashboard/dist/assets/chunk-B4BG7PRW--cjprmFF.js +165 -0
  20. package/dist/dashboard/dist/assets/chunk-B4BG7PRW-D6Mi4ccz.js +165 -0
  21. package/dist/dashboard/dist/assets/chunk-DI55MBZ5-B0tOisd5.js +220 -0
  22. package/dist/dashboard/dist/assets/chunk-DI55MBZ5-D9bxNSnS.js +220 -0
  23. package/dist/dashboard/dist/assets/chunk-FMBD7UC4-CSek7h3u.js +15 -0
  24. package/dist/dashboard/dist/assets/chunk-FMBD7UC4-RSShKwSG.js +15 -0
  25. package/dist/dashboard/dist/assets/chunk-QN33PNHL-BRCzcTtl.js +1 -0
  26. package/dist/dashboard/dist/assets/chunk-QN33PNHL-DFyjAoyD.js +1 -0
  27. package/dist/dashboard/dist/assets/chunk-QZHKN3VN-ARq4habW.js +1 -0
  28. package/dist/dashboard/dist/assets/chunk-QZHKN3VN-TFdw1-iS.js +1 -0
  29. package/dist/dashboard/dist/assets/chunk-TZMSLE5B-CWotsEVz.js +1 -0
  30. package/dist/dashboard/dist/assets/chunk-TZMSLE5B-DrmzpdLp.js +1 -0
  31. package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-Dr8j2BkV.js +1 -0
  32. package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-cvlgQ4cC.js +1 -0
  33. package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-Dr8j2BkV.js +1 -0
  34. package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-cvlgQ4cC.js +1 -0
  35. package/dist/dashboard/dist/assets/clone-D-A0zWrx.js +1 -0
  36. package/dist/dashboard/dist/assets/clone-DRiM0_7k.js +1 -0
  37. package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-L06bC_vI.js +1 -0
  38. package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-_dXvVagP.js +1 -0
  39. package/dist/dashboard/dist/assets/dagre-6UL2VRFP-BfhkcdcZ.js +4 -0
  40. package/dist/dashboard/dist/assets/dagre-6UL2VRFP-LQJxsDjp.js +4 -0
  41. package/dist/dashboard/dist/assets/diagram-PSM6KHXK-Bguvtjhb.js +24 -0
  42. package/dist/dashboard/dist/assets/diagram-PSM6KHXK-C8bgfsC2.js +24 -0
  43. package/dist/dashboard/dist/assets/diagram-QEK2KX5R-CDM-bAUc.js +43 -0
  44. package/dist/dashboard/dist/assets/diagram-QEK2KX5R-SPnyk4NX.js +43 -0
  45. package/dist/dashboard/dist/assets/diagram-S2PKOQOG-Cv8CAseP.js +24 -0
  46. package/dist/dashboard/dist/assets/diagram-S2PKOQOG-DNQuKOCA.js +24 -0
  47. package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-CgAEujxC.js +60 -0
  48. package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-DHMIYnca.js +60 -0
  49. package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-B-9A_TD6.js +162 -0
  50. package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-C8juupCT.js +162 -0
  51. package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-BzXOAiOm.js +267 -0
  52. package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-DDmcIEO0.js +267 -0
  53. package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-6Rn0oWgA.js +65 -0
  54. package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-D4YFQ0Qf.js +65 -0
  55. package/dist/dashboard/dist/assets/graph-DI2MOSai.js +1 -0
  56. package/dist/dashboard/dist/assets/graph-VO6A5Zyb.js +1 -0
  57. package/dist/dashboard/dist/assets/index-BD_Ue7zI.js +777 -0
  58. package/dist/dashboard/dist/assets/index-BQfUzgMV.js +778 -0
  59. package/dist/dashboard/dist/assets/index-Bpho1Ov5.css +1 -0
  60. package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-BEOgUULT.js +2 -0
  61. package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-BULYGXV8.js +2 -0
  62. package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CBFUW_L2.js +139 -0
  63. package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CpGd67rs.js +139 -0
  64. package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-BXpodEnf.js +89 -0
  65. package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-CXjvcKGc.js +89 -0
  66. package/dist/dashboard/dist/assets/layout-Cpj8l95P.js +1 -0
  67. package/dist/dashboard/dist/assets/layout-D5qgY_UX.js +1 -0
  68. package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-CKZrc1IF.js +68 -0
  69. package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-_3DZbNEl.js +68 -0
  70. package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-B--OM1Gs.js +30 -0
  71. package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-uIOJq-u0.js +30 -0
  72. package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-CDx0v76p.js +7 -0
  73. package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-D5g_wTRC.js +7 -0
  74. package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-BLpGY-Om.js +64 -0
  75. package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-CbvZ1a-7.js +64 -0
  76. package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-D-fji9s3.js +10 -0
  77. package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-E0klRQfk.js +10 -0
  78. package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-Byy1IdkL.js +145 -0
  79. package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-CWB1Ub2x.js +145 -0
  80. package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-J-c1KNJ7.js +1 -0
  81. package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-x3lHxmNY.js +1 -0
  82. package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-D0gUM6SR.js +1 -0
  83. package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-DRL2jF9p.js +1 -0
  84. package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-D490JqJU.js +61 -0
  85. package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-LOxOovzx.js +61 -0
  86. package/dist/dashboard/dist/assets/treemap-GDKQZRPO-C5Nk6dQh.js +162 -0
  87. package/dist/dashboard/dist/assets/treemap-GDKQZRPO-C6DntuKu.js +162 -0
  88. package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-BKisDUaz.js +7 -0
  89. package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-CYsIKi3H.js +7 -0
  90. package/dist/dashboard/dist/index.html +2 -2
  91. package/dist/src/analyzeHelpers.d.ts +60 -0
  92. package/dist/src/analyzeHelpers.d.ts.map +1 -0
  93. package/dist/src/analyzeHelpers.js +670 -0
  94. package/dist/src/config/defaultConfig.js +1 -1
  95. package/dist/src/index.js +553 -615
  96. package/dist/src/reporting.d.ts +10 -0
  97. package/dist/src/reporting.d.ts.map +1 -1
  98. package/dist/src/reporting.js +3 -0
  99. package/dist/src/summary/evaluateMetrics.js +3 -2
  100. package/dist/src/unitAnalysis.d.ts +132 -1
  101. package/dist/src/unitAnalysis.d.ts.map +1 -1
  102. package/dist/src/unitAnalysis.js +1139 -185
  103. package/package.json +1 -1
@@ -44,40 +44,76 @@ exports.parseCoverageXml = parseCoverageXml;
44
44
  exports.parseGoCoverage = parseGoCoverage;
45
45
  exports.parseStrykerReport = parseStrykerReport;
46
46
  exports.parseSlowTests = parseSlowTests;
47
+ exports.calculateDynamicPassRate = calculateDynamicPassRate;
48
+ exports.calculateUnitScore = calculateUnitScore;
47
49
  exports.detectTestSmells = detectTestSmells;
50
+ exports.calculateWeightedCoveredItems = calculateWeightedCoveredItems;
48
51
  const fs = __importStar(require("fs"));
49
52
  const path = __importStar(require("path"));
50
- const child_process_1 = require("child_process");
51
53
  const fast_glob_1 = __importDefault(require("fast-glob"));
52
54
  const COVERAGE_REPORT_CANDIDATES = [
53
- 'target/site/jacoco/jacoco.xml',
54
55
  'build/reports/jacoco/test/jacocoTestReport.xml',
55
- 'coverage/coverage-summary.json',
56
+ 'build/reports/jacoco/jacocoTestReport.xml',
57
+ 'build/reports/jacoco/**/*.xml',
58
+ 'target/site/jacoco/*.xml',
56
59
  'coverage/lcov.info',
60
+ 'coverage/coverage-summary.json',
61
+ '.nyc_output/**/*.json',
57
62
  'coverage.xml',
58
63
  '.coverage',
64
+ 'coverage/.resultset.json',
59
65
  'coverage.out',
66
+ 'build/reports/kover/**/*.xml',
67
+ 'build/reports/clover.xml',
60
68
  ];
61
69
  const TEST_RESULT_GLOBS = [
62
- 'target/surefire-reports/*.xml',
63
70
  'build/test-results/**/*.xml',
64
- 'reports/junit*.xml',
71
+ 'target/surefire-reports/**/*.xml',
72
+ 'build/reports/tests/**/*.xml',
73
+ 'test-results/**/*.xml',
65
74
  'junit*.xml',
66
- 'results.xml',
67
- 'jest-results.json',
68
- 'reports/jest-results.json',
75
+ '.test-results/**/*.xml',
69
76
  ];
70
- const PIT_REPORT_GLOBS = [
71
- 'target/pit-reports/**/mutations.xml',
77
+ const MUTATION_REPORT_GLOBS = [
72
78
  'build/reports/pitest/**/mutations.xml',
79
+ 'target/pit-reports/**/mutations.xml',
80
+ 'build/reports/pitest/**/*.xml',
81
+ 'reports/mutation/**/*.json',
82
+ 'reports/stryker/**/*.json',
83
+ 'stryker-tmp/**/*.json',
84
+ '.mutmut-cache',
85
+ 'reports/infection.json',
73
86
  ];
74
- const STRYKER_REPORT_GLOBS = [
75
- 'reports/mutation/mutation-report.json',
76
- 'reports/stryker/mutation-report.json',
77
- 'StrykerOutput/**/mutation-report.json',
87
+ const CONTRACT_REPORT_GLOBS = [
88
+ 'target/pacts/**/*.json',
89
+ 'pacts/**/*.json',
90
+ 'build/pacts/**/*.json',
91
+ '.pact/pacts/**/*.json',
78
92
  ];
93
+ const AST_ASSERTION_FULL_SCORE = 20;
94
+ const AST_ASSERTION_PARTIAL_SCORE = 10;
95
+ const AST_ASSERTION_PARTIAL_THRESHOLD = 0.1;
96
+ const AST_ISOLATION_FULL_SCORE = 20;
97
+ const AST_ISOLATION_PARTIAL_SCORE = 10;
98
+ const AST_ISOLATION_FULL_THRESHOLD = 80;
99
+ const AST_ISOLATION_PARTIAL_THRESHOLD = 50;
100
+ const AST_NAMING_FULL_SCORE = 20;
101
+ const AST_NAMING_PARTIAL_SCORE = 10;
102
+ const AST_NAMING_FULL_THRESHOLD = 80;
103
+ const AST_NAMING_PARTIAL_THRESHOLD = 50;
104
+ const AST_DISABLED_FULL_SCORE = 20;
105
+ const AST_DISABLED_PARTIAL_SCORE = 10;
106
+ const AST_DISABLED_PARTIAL_THRESHOLD = 2;
107
+ const AST_CRITICAL_SMELL_BASE_SCORE = 20;
108
+ const AST_CRITICAL_SMELL_DEDUCTION = 5;
109
+ const SCORE_WEIGHTS = {
110
+ astOnly: { ast: 1 },
111
+ astMutation: { ast: 0.6, mutation: 0.4 },
112
+ astRuntime: { ast: 0.4, line: 0.4, branch: 0.2 },
113
+ astRuntimeMutation: { ast: 0.3, line: 0.3, branch: 0.2, mutation: 0.2 },
114
+ };
79
115
  async function analyzeUnitTests(options) {
80
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
116
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8;
81
117
  const rootDir = path.resolve(options.rootDir);
82
118
  const unitConfig = options.config;
83
119
  const testAnalyses = options.testFiles.map((filePath) => analyzeTestFile(rootDir, filePath));
@@ -87,7 +123,8 @@ async function analyzeUnitTests(options) {
87
123
  const smells = ((_a = unitConfig.smellDetection) === null || _a === void 0 ? void 0 : _a.enabled) !== false
88
124
  ? detectTestSmells(rootDir, testAnalyses)
89
125
  : [];
90
- const slowTests = parseSlowTests(rootDir, unitConfig);
126
+ const ast = buildAstSummary(testAnalyses, smells);
127
+ const dynamicDiscovery = discoverDynamicResults(rootDir, unitConfig);
91
128
  const independence = ((_b = unitConfig.independenceCheck) === null || _b === void 0 ? void 0 : _b.enabled) !== false
92
129
  ? computeIndependence(rootDir, testAnalyses)
93
130
  : null;
@@ -95,14 +132,23 @@ async function analyzeUnitTests(options) {
95
132
  ? discoverCoverage(rootDir)
96
133
  : null;
97
134
  const mutationTesting = ((_d = unitConfig.mutationTesting) === null || _d === void 0 ? void 0 : _d.enabled)
98
- ? runMutationTesting(rootDir, (_e = options.languages) !== null && _e !== void 0 ? _e : [], unitConfig)
99
- : null;
135
+ ? discoverMutationTesting(rootDir)
136
+ : { summary: null };
137
+ const contractTesting = discoverContractTesting(rootDir);
100
138
  const failures = [];
101
139
  const warnings = [];
102
140
  const messages = [];
141
+ const consoleLines = buildUnitAnalysisConsoleLines({
142
+ ast,
143
+ dynamic: dynamicDiscovery.summary,
144
+ coverage: (_e = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _e !== void 0 ? _e : null,
145
+ mutation: mutationTesting.summary,
146
+ contract: contractTesting.summary,
147
+ languages: (_f = options.languages) !== null && _f !== void 0 ? _f : [],
148
+ });
103
149
  if (coverageDiscovery) {
104
150
  if (coverageDiscovery.summary) {
105
- const coverageThresholds = (_f = unitConfig.codeCoverage) === null || _f === void 0 ? void 0 : _f.thresholds;
151
+ const coverageThresholds = (_g = unitConfig.codeCoverage) === null || _g === void 0 ? void 0 : _g.thresholds;
106
152
  if ((coverageThresholds === null || coverageThresholds === void 0 ? void 0 : coverageThresholds.line) !== undefined && coverageDiscovery.summary.lineCoverage !== null && coverageDiscovery.summary.lineCoverage < coverageThresholds.line) {
107
153
  failures.push(`Unit line coverage ${coverageDiscovery.summary.lineCoverage.toFixed(1)}% is below threshold ${coverageThresholds.line}%`);
108
154
  }
@@ -118,51 +164,102 @@ async function analyzeUnitTests(options) {
118
164
  messages.push(coverageDiscovery.missing.hint);
119
165
  }
120
166
  }
121
- if (mutationTesting === null || mutationTesting === void 0 ? void 0 : mutationTesting.summary) {
122
- const threshold = (_h = (_g = unitConfig.mutationTesting) === null || _g === void 0 ? void 0 : _g.threshold) !== null && _h !== void 0 ? _h : 70;
167
+ if (mutationTesting.summary) {
168
+ const threshold = (_j = (_h = unitConfig.mutationTesting) === null || _h === void 0 ? void 0 : _h.threshold) !== null && _j !== void 0 ? _j : 70;
123
169
  if (mutationTesting.summary.mutationScore < threshold) {
124
170
  failures.push(`Mutation score ${mutationTesting.summary.mutationScore.toFixed(1)}% is below threshold ${threshold}%`);
125
171
  }
126
172
  }
127
- if (mutationTesting === null || mutationTesting === void 0 ? void 0 : mutationTesting.warning) {
173
+ if (mutationTesting.warning) {
128
174
  warnings.push(mutationTesting.warning);
129
175
  }
130
- const smellFailOn = (_k = (_j = unitConfig.smellDetection) === null || _j === void 0 ? void 0 : _j.failOn) !== null && _k !== void 0 ? _k : 'CRITICAL';
176
+ const smellFailOn = (_l = (_k = unitConfig.smellDetection) === null || _k === void 0 ? void 0 : _k.failOn) !== null && _l !== void 0 ? _l : 'CRITICAL';
131
177
  if (smellFailOn !== 'none') {
132
178
  const hasBlockingSmell = smells.some((smell) => severityAtOrAbove(smell.severity, smellFailOn));
133
179
  if (hasBlockingSmell) {
134
180
  failures.push(`Detected test smells at or above ${smellFailOn} severity`);
135
181
  }
136
182
  }
137
- if (independence && ((_l = unitConfig.independenceCheck) === null || _l === void 0 ? void 0 : _l.minScore) !== undefined && independence.score < unitConfig.independenceCheck.minScore) {
183
+ if (independence && ((_m = unitConfig.independenceCheck) === null || _m === void 0 ? void 0 : _m.minScore) !== undefined && independence.score < unitConfig.independenceCheck.minScore) {
138
184
  failures.push(`Test independence score ${independence.score} is below minimum ${unitConfig.independenceCheck.minScore}`);
139
185
  }
186
+ const layersApplied = ['ast'];
187
+ if (dynamicDiscovery.summary)
188
+ layersApplied.push('dynamic');
189
+ if (coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary)
190
+ layersApplied.push('coverage');
191
+ if (mutationTesting.summary)
192
+ layersApplied.push('mutation');
193
+ if (contractTesting.summary)
194
+ layersApplied.push('contract');
195
+ const scoreInput = {
196
+ astQuality: ast.qualityScore,
197
+ dynamicPassRate: dynamicDiscovery.summary ? calculateDynamicPassRate(dynamicDiscovery.summary) : null,
198
+ lineCoverage: (_p = (_o = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _o === void 0 ? void 0 : _o.lineCoverage) !== null && _p !== void 0 ? _p : null,
199
+ branchCoverage: (_r = (_q = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _q === void 0 ? void 0 : _q.branchCoverage) !== null && _r !== void 0 ? _r : null,
200
+ mutationScore: (_t = (_s = mutationTesting.summary) === null || _s === void 0 ? void 0 : _s.mutationScore) !== null && _t !== void 0 ? _t : null,
201
+ layersApplied,
202
+ };
203
+ const weightedScore = calculateUnitScore(scoreInput);
204
+ const scoreWeighting = describeAppliedWeighting(scoreInput);
205
+ const appliedScoreWeights = getAppliedUnitScoreWeights(scoreInput);
206
+ consoleLines.push('');
207
+ consoleLines.push(` Final score: ${weightedScore}% (${scoreWeighting})`);
208
+ appendDetectedSmells(consoleLines, ast);
209
+ appendLowCoverageFiles(consoleLines, (_u = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _u !== void 0 ? _u : null);
210
+ const totalItems = ast.testCount;
211
+ const qualityPassingTests = countPassingQualityTests(testAnalyses);
212
+ const coveredItems = calculateWeightedCoveredItems(totalItems, weightedScore);
140
213
  const details = {
141
214
  items: unitItems,
142
- codeCoverage: (_m = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _m !== void 0 ? _m : null,
215
+ layersApplied,
216
+ ast,
217
+ dynamic: dynamicDiscovery.summary,
218
+ coverage: (_v = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _v !== void 0 ? _v : null,
219
+ mutation: mutationTesting.summary,
220
+ contract: contractTesting.summary,
221
+ codeCoverage: (_w = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _w !== void 0 ? _w : null,
143
222
  codeCoverageMissing: coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.missing,
144
- mutationTesting: (_o = mutationTesting === null || mutationTesting === void 0 ? void 0 : mutationTesting.summary) !== null && _o !== void 0 ? _o : null,
223
+ mutationTesting: mutationTesting.summary,
224
+ filesScanned: ast.filesScanned,
225
+ smells,
226
+ enrichedByRuntime: Boolean(appliedScoreWeights.line || appliedScoreWeights.branch),
227
+ enrichedByMutation: Boolean(appliedScoreWeights.mutation),
228
+ lineCoverage: (_x = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _x === void 0 ? void 0 : _x.lineCoverage,
229
+ branchCoverage: (_y = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _y === void 0 ? void 0 : _y.branchCoverage,
230
+ classCoverage: (_z = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _z === void 0 ? void 0 : _z.classCoverage,
231
+ methodCoverage: (_0 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _0 === void 0 ? void 0 : _0.methodCoverage,
232
+ uncoveredFiles: (_1 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _1 === void 0 ? void 0 : _1.uncoveredFiles,
233
+ lowCoverageFiles: (_2 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _2 === void 0 ? void 0 : _2.lowCoverageFiles,
234
+ runtimeReportPath: (_3 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _3 === void 0 ? void 0 : _3.reportPath,
235
+ mutationScore: (_4 = mutationTesting.summary) === null || _4 === void 0 ? void 0 : _4.mutationScore,
236
+ mutationSurvivedCount: (_5 = mutationTesting.summary) === null || _5 === void 0 ? void 0 : _5.survivedMutants,
237
+ mutationKilledCount: (_6 = mutationTesting.summary) === null || _6 === void 0 ? void 0 : _6.killedMutants,
238
+ worstMutationFiles: (_7 = mutationTesting.summary) === null || _7 === void 0 ? void 0 : _7.worstMutationFiles,
239
+ mutationReportPath: (_8 = mutationTesting.summary) === null || _8 === void 0 ? void 0 : _8.reportPath,
145
240
  testSmells: {
146
241
  bySeverity: summarizeSmells(smells),
147
242
  items: smells,
148
243
  },
149
- slowTests,
244
+ slowTests: dynamicDiscovery.summary,
150
245
  independence,
151
246
  assertionDensity,
152
247
  messages,
248
+ weightedScore,
249
+ scoreWeighting,
250
+ qualityPassingTests,
153
251
  };
154
252
  return {
155
253
  result: {
156
254
  type: 'unit',
157
- totalItems: unitItems.length,
158
- coveredItems: unitItems.filter((item) => item.covered).length,
159
- coveragePercent: unitItems.length > 0
160
- ? Number(((unitItems.filter((item) => item.covered).length / unitItems.length) * 100).toFixed(1))
161
- : 0,
255
+ totalItems,
256
+ coveredItems,
257
+ coveragePercent: clampScore(weightedScore),
162
258
  details,
163
259
  },
164
260
  failures,
165
261
  warnings,
262
+ consoleLines,
166
263
  };
167
264
  }
168
265
  function analyzeTestFile(rootDir, filePath) {
@@ -170,14 +267,35 @@ function analyzeTestFile(rootDir, filePath) {
170
267
  const language = detectTestLanguage(filePath);
171
268
  const blocks = extractTestBlocks(language, content);
172
269
  const assertionCount = countAssertions(language, content);
270
+ const isolationUsage = blocks.filter((block) => testUsesIsolation(language, block.body)).length;
173
271
  return {
174
272
  filePath,
175
273
  relativePath: normalizePath(path.relative(rootDir, filePath)),
176
274
  language,
177
275
  content,
276
+ testBlocks: blocks,
277
+ testCount: blocks.length,
178
278
  qualityScore: scoreTestQuality(content),
179
279
  parameterizedExpansions: countParameterizedExpansions(content),
180
280
  assertionCount,
281
+ testClassCount: countTestClasses(language, content),
282
+ mockCount: countMockSignals(content),
283
+ stubCount: countStubSignals(content),
284
+ spyCount: countSpySignals(content),
285
+ testsUsingIsolation: isolationUsage,
286
+ descriptiveTestNameCount: blocks.filter((block) => isDescriptiveTestName(block.name)).length,
287
+ genericNameExamples: blocks
288
+ .map((block) => block.name)
289
+ .filter((name) => isGenericTestName(name))
290
+ .slice(0, 5),
291
+ emptyTestBodies: blocks
292
+ .filter((block) => isEmptyTestBody(block.body))
293
+ .map((block) => `${normalizePath(path.relative(rootDir, filePath))}#${block.name}`),
294
+ threadSleepUsage: collectLineMatches(rootDir, filePath, content, /Thread\.sleep\(|time\.sleep\(|setTimeout\(|\bSleep\(/),
295
+ systemOutUsage: collectLineMatches(rootDir, filePath, content, /System\.out\.println\(|console\.log\(|print\(/),
296
+ hardcodedCredentials: collectLineMatches(rootDir, filePath, content, /\b(?:password|secret|token|api[_-]?key)\b\s*[:=]\s*['"][^'"]+['"]/i),
297
+ staticMutableStateModified: hasStaticMutableStateMutation(language, content),
298
+ sharedMutableFields: collectSharedMutableFields(language, content),
181
299
  mockLibrary: detectMockLibrary(content),
182
300
  layer: classifyLayer(content, filePath),
183
301
  coveredMethods: extractCoveredMethods(language, content),
@@ -271,34 +389,155 @@ function buildUnitCoverageItems(sourceMethods, testAnalyses) {
271
389
  }
272
390
  return Array.from(byMethod.values()).sort((a, b) => a.id.localeCompare(b.id));
273
391
  }
392
+ function buildAstSummary(testAnalyses, smells) {
393
+ const totalTests = testAnalyses.reduce((sum, test) => sum + test.testCount, 0);
394
+ const parameterizedTestCount = testAnalyses.reduce((sum, test) => sum + countParameterizedTests(test.language, test.content), 0);
395
+ const testClassCount = testAnalyses.reduce((sum, test) => sum + test.testClassCount, 0);
396
+ const mockCount = testAnalyses.reduce((sum, test) => sum + test.mockCount, 0);
397
+ const stubCount = testAnalyses.reduce((sum, test) => sum + test.stubCount, 0);
398
+ const spyCount = testAnalyses.reduce((sum, test) => sum + test.spyCount, 0);
399
+ const isolatedTests = testAnalyses.reduce((sum, test) => sum + test.testsUsingIsolation, 0);
400
+ const assertionCount = testAnalyses.reduce((sum, test) => sum + test.assertionCount, 0);
401
+ const testsWithZeroAssertions = testAnalyses.flatMap((test) => test.testBlocks
402
+ .filter((block) => countAssertions(test.language, block.body) === 0 && !hasVerificationCall(test.language, block.body))
403
+ .map((block) => `${test.relativePath}#${block.name}`));
404
+ const testsWithTooManyAssertions = testAnalyses.flatMap((test) => test.testBlocks
405
+ .filter((block) => countAssertions(test.language, block.body) > 10)
406
+ .map((block) => `${test.relativePath}#${block.name}`));
407
+ const descriptiveNameCount = testAnalyses.reduce((sum, test) => sum + test.descriptiveTestNameCount, 0);
408
+ const genericNameExamples = dedupeStrings(testAnalyses.flatMap((test) => test.genericNameExamples)).slice(0, 5);
409
+ const disabledTests = testAnalyses.flatMap((test) => test.testBlocks
410
+ .filter((block) => isDisabledBlock(test.language, block))
411
+ .map((block) => `${test.relativePath}#${block.name}`));
412
+ const emptyTestBodies = testAnalyses.flatMap((test) => test.emptyTestBodies);
413
+ const threadSleepUsage = testAnalyses.flatMap((test) => test.threadSleepUsage);
414
+ const systemOutUsage = testAnalyses.flatMap((test) => test.systemOutUsage);
415
+ const hardcodedCredentials = testAnalyses.flatMap((test) => test.hardcodedCredentials);
416
+ const sharedMutableFields = dedupeStrings(testAnalyses.flatMap((test) => test.sharedMutableFields));
417
+ const staticMutableStateModified = testAnalyses.some((test) => test.staticMutableStateModified);
418
+ // Scoring mirrors the cascaded unit-analysis specification:
419
+ // 20 points when every test asserts, 10 points when fewer than 10% miss assertions,
420
+ // otherwise 0 so AST-only mode still surfaces weak test suites clearly.
421
+ const assertionCompleteness = testsWithZeroAssertions.length === 0
422
+ ? AST_ASSERTION_FULL_SCORE
423
+ : totalTests > 0 && (testsWithZeroAssertions.length / totalTests) < AST_ASSERTION_PARTIAL_THRESHOLD
424
+ ? AST_ASSERTION_PARTIAL_SCORE
425
+ : 0;
426
+ const isolationPercent = totalTests > 0 ? Number(((isolatedTests / totalTests) * 100).toFixed(1)) : 0;
427
+ const testIsolation = isolationPercent >= AST_ISOLATION_FULL_THRESHOLD
428
+ ? AST_ISOLATION_FULL_SCORE
429
+ : isolationPercent >= AST_ISOLATION_PARTIAL_THRESHOLD
430
+ ? AST_ISOLATION_PARTIAL_SCORE
431
+ : 0;
432
+ const descriptiveNamePercent = totalTests > 0 ? Number(((descriptiveNameCount / totalTests) * 100).toFixed(1)) : 0;
433
+ const namingQuality = descriptiveNamePercent >= AST_NAMING_FULL_THRESHOLD
434
+ ? AST_NAMING_FULL_SCORE
435
+ : descriptiveNamePercent >= AST_NAMING_PARTIAL_THRESHOLD
436
+ ? AST_NAMING_PARTIAL_SCORE
437
+ : 0;
438
+ const disabledTestsScore = disabledTests.length === 0
439
+ ? AST_DISABLED_FULL_SCORE
440
+ : disabledTests.length <= AST_DISABLED_PARTIAL_THRESHOLD
441
+ ? AST_DISABLED_PARTIAL_SCORE
442
+ : 0;
443
+ // Critical smells share equal deductions because each one represents a strong signal
444
+ // that the test is timing-dependent, empty, or leaking credentials.
445
+ const criticalSmells = Math.max(0, AST_CRITICAL_SMELL_BASE_SCORE - ((threadSleepUsage.length + emptyTestBodies.length + hardcodedCredentials.length) * AST_CRITICAL_SMELL_DEDUCTION));
446
+ const qualityScore = clampScore(assertionCompleteness + testIsolation + namingQuality + disabledTestsScore + criticalSmells);
447
+ return {
448
+ filesScanned: testAnalyses.length,
449
+ testCount: totalTests,
450
+ parameterizedTestCount,
451
+ testClassCount,
452
+ mockCount,
453
+ stubCount,
454
+ spyCount,
455
+ isolationPercent,
456
+ assertionCount,
457
+ assertionsPerTest: totalTests > 0 ? Number((assertionCount / totalTests).toFixed(1)) : 0,
458
+ testsWithZeroAssertions,
459
+ testsWithTooManyAssertions,
460
+ descriptiveNamePercent,
461
+ genericNameExamples,
462
+ qualityScore,
463
+ dimensionScores: {
464
+ assertionCompleteness,
465
+ testIsolation,
466
+ namingQuality,
467
+ disabledTests: disabledTestsScore,
468
+ criticalSmells,
469
+ },
470
+ smells,
471
+ disabledTests,
472
+ emptyTestBodies,
473
+ threadSleepUsage,
474
+ systemOutUsage,
475
+ hardcodedCredentials,
476
+ staticMutableStateModified,
477
+ sharedMutableFields,
478
+ };
479
+ }
480
+ function discoverFirstMatchingPaths(rootDir, patterns) {
481
+ for (const pattern of patterns) {
482
+ const matchedPaths = fast_glob_1.default.sync(pattern, {
483
+ cwd: rootDir,
484
+ absolute: true,
485
+ onlyFiles: true,
486
+ unique: true,
487
+ }).sort((a, b) => a.length - b.length || a.localeCompare(b));
488
+ if (matchedPaths.length > 0) {
489
+ return {
490
+ summary: null,
491
+ matchedPaths,
492
+ checkedPaths: patterns.map((candidate) => normalizePath(candidate)),
493
+ };
494
+ }
495
+ }
496
+ return {
497
+ summary: null,
498
+ matchedPaths: [],
499
+ checkedPaths: patterns.map((candidate) => normalizePath(candidate)),
500
+ };
501
+ }
274
502
  function discoverCoverage(rootDir) {
275
- const checkedPaths = COVERAGE_REPORT_CANDIDATES.map((candidate) => normalizePath(candidate));
276
- for (const candidate of COVERAGE_REPORT_CANDIDATES) {
277
- const fullPath = path.join(rootDir, candidate);
278
- if (!fs.existsSync(fullPath))
279
- continue;
280
- if (candidate.endsWith('jacoco.xml') || candidate.endsWith('jacocoTestReport.xml')) {
281
- return { summary: parseJacocoXml(fullPath) };
503
+ const discovery = discoverFirstMatchingPaths(rootDir, COVERAGE_REPORT_CANDIDATES);
504
+ const [reportPath] = discovery.matchedPaths;
505
+ if (reportPath) {
506
+ const relativePath = normalizePath(path.relative(rootDir, reportPath));
507
+ if (/jacoco|kover/i.test(relativePath)) {
508
+ return { summary: { ...parseJacocoXml(reportPath), reportPath: relativePath } };
509
+ }
510
+ if (relativePath.endsWith('coverage-summary.json')) {
511
+ return { summary: { ...parseIstanbulSummary(reportPath), reportPath: relativePath } };
512
+ }
513
+ if (relativePath.endsWith('lcov.info')) {
514
+ return { summary: { ...parseLcovInfo(reportPath), reportPath: relativePath } };
515
+ }
516
+ if (relativePath.includes('.nyc_output/') && relativePath.endsWith('.json')) {
517
+ return { summary: { ...parseNycCoverageJson(reportPath), reportPath: relativePath } };
282
518
  }
283
- if (candidate.endsWith('coverage-summary.json')) {
284
- return { summary: parseIstanbulSummary(fullPath) };
519
+ if (relativePath.endsWith('coverage.xml')) {
520
+ return { summary: { ...parseCoverageXml(reportPath), reportPath: relativePath } };
285
521
  }
286
- if (candidate.endsWith('lcov.info')) {
287
- return { summary: parseLcovInfo(fullPath) };
522
+ if (relativePath.endsWith('.coverage')) {
523
+ return { summary: { ...parseCoveragePyBinary(reportPath), reportPath: relativePath } };
288
524
  }
289
- if (candidate === 'coverage.xml') {
290
- return { summary: parseCoverageXml(fullPath) };
525
+ if (relativePath.endsWith('coverage/.resultset.json')) {
526
+ return { summary: { ...parseSimpleCovJson(reportPath), reportPath: relativePath } };
291
527
  }
292
- if (candidate === 'coverage.out') {
293
- return { summary: parseGoCoverage(fullPath) };
528
+ if (relativePath.endsWith('coverage.out')) {
529
+ return { summary: { ...parseGoCoverage(reportPath), reportPath: relativePath } };
530
+ }
531
+ if (relativePath.endsWith('clover.xml')) {
532
+ return { summary: { ...parseCloverXml(reportPath), reportPath: relativePath } };
294
533
  }
295
534
  }
296
535
  return {
297
536
  summary: null,
298
537
  missing: {
299
538
  message: 'Run tests with coverage enabled to see report',
300
- hint: 'Add jacoco plugin to build.gradle',
301
- checkedPaths,
539
+ hint: 'Write JaCoCo, Kover, LCOV, coverage.py, or Clover reports to one of the supported paths',
540
+ checkedPaths: discovery.checkedPaths,
302
541
  },
303
542
  };
304
543
  }
@@ -307,12 +546,25 @@ function parseJacocoXml(reportPath) {
307
546
  const xml = safeRead(reportPath);
308
547
  const counters = extractCounters(xml);
309
548
  const uncoveredMethods = [];
549
+ const uncoveredClasses = [];
550
+ const fileCoverage = [];
310
551
  const classRegex = /<class\b([^>]*?)name="([^"]+)"([^>]*?)sourcefilename="([^"]+)"([^>]*)>([\s\S]*?)<\/class>/g;
311
552
  let classMatch;
312
553
  while ((classMatch = classRegex.exec(xml)) !== null) {
313
554
  const className = (_a = classMatch[2].split('/').pop()) !== null && _a !== void 0 ? _a : classMatch[2];
314
555
  const file = classMatch[4];
315
556
  const classBody = classMatch[6];
557
+ const classBodyWithoutMethods = classBody.replace(/<method\b[\s\S]*?<\/method>/g, '');
558
+ const classLineCounter = /<counter\b[^>]*type="LINE"[^>]*missed="(\d+)"[^>]*covered="(\d+)"/.exec(classBodyWithoutMethods);
559
+ const classLineCoverage = classLineCounter
560
+ ? safePercent(Number(classLineCounter[2]), Number(classLineCounter[1]) + Number(classLineCounter[2]))
561
+ : null;
562
+ if (classLineCoverage !== null) {
563
+ fileCoverage.push({ file: normalizePath(file), lineCoverage: classLineCoverage });
564
+ }
565
+ if (classLineCounter && Number(classLineCounter[2]) === 0 && Number(classLineCounter[1]) > 0) {
566
+ uncoveredClasses.push(className);
567
+ }
316
568
  const methodRegex = /<method\b[^>]*name="([^"]+)"[^>]*line="(\d+)"[^>]*>([\s\S]*?)<\/method>/g;
317
569
  let methodMatch;
318
570
  while ((methodMatch = methodRegex.exec(classBody)) !== null) {
@@ -331,6 +583,9 @@ function parseJacocoXml(reportPath) {
331
583
  branchCoverage: coveragePercent(counters.BRANCH),
332
584
  methodCoverage: coveragePercent(counters.METHOD),
333
585
  classCoverage: coveragePercent(counters.CLASS),
586
+ ...buildCoverageFileFields(fileCoverage),
587
+ fileCoverage: sortCoverageFiles(fileCoverage),
588
+ uncoveredClasses: dedupeStrings(uncoveredClasses),
334
589
  uncoveredMethods,
335
590
  reportPath: normalizePath(reportPath),
336
591
  };
@@ -339,18 +594,34 @@ function parseIstanbulSummary(reportPath) {
339
594
  var _a;
340
595
  const json = JSON.parse(safeRead(reportPath));
341
596
  const total = ((_a = json['total']) !== null && _a !== void 0 ? _a : {});
597
+ const fileCoverage = Object.entries(json)
598
+ .filter(([key, value]) => key !== 'total' && typeof value === 'object' && value !== null)
599
+ .map(([key, value]) => {
600
+ var _a;
601
+ return ({
602
+ file: normalizePath(key),
603
+ lineCoverage: (_a = readPct(value, 'lines')) !== null && _a !== void 0 ? _a : 0,
604
+ });
605
+ });
606
+ const uncoveredClasses = Object.entries(json)
607
+ .filter(([key, value]) => key !== 'total' && typeof value === 'object' && value !== null)
608
+ .filter(([, value]) => readPct(value, 'lines') === 0)
609
+ .map(([key]) => path.basename(key, path.extname(key)));
342
610
  return {
343
611
  tool: 'istanbul',
344
612
  lineCoverage: readPct(total, 'lines'),
345
613
  branchCoverage: readPct(total, 'branches'),
346
614
  methodCoverage: readPct(total, 'functions'),
347
615
  classCoverage: readPct(total, 'statements'),
616
+ ...buildCoverageFileFields(fileCoverage),
617
+ fileCoverage: sortCoverageFiles(fileCoverage),
618
+ uncoveredClasses: dedupeStrings(uncoveredClasses),
348
619
  uncoveredMethods: [],
349
620
  reportPath: normalizePath(reportPath),
350
621
  };
351
622
  }
352
623
  function parseLcovInfo(reportPath) {
353
- var _a, _b;
624
+ var _a, _b, _c;
354
625
  const content = safeRead(reportPath);
355
626
  const files = content.split('end_of_record').map((chunk) => chunk.trim()).filter(Boolean);
356
627
  let totalLines = 0;
@@ -360,20 +631,25 @@ function parseLcovInfo(reportPath) {
360
631
  let totalFunctions = 0;
361
632
  let hitFunctions = 0;
362
633
  let coveredFiles = 0;
634
+ const uncoveredClasses = [];
363
635
  const uncoveredMethods = [];
636
+ const fileCoverage = [];
364
637
  for (const fileChunk of files) {
365
638
  const lines = fileChunk.split('\n');
366
639
  const file = (_b = (_a = lines.find((line) => line.startsWith('SF:'))) === null || _a === void 0 ? void 0 : _a.slice(3)) !== null && _b !== void 0 ? _b : 'unknown';
367
640
  const fnMap = new Map();
368
- let fileCovered = false;
641
+ let fileTotalLines = 0;
642
+ let fileHitLines = 0;
369
643
  for (const line of lines) {
370
- if (line.startsWith('LF:'))
371
- totalLines += Number(line.slice(3));
644
+ if (line.startsWith('LF:')) {
645
+ const count = Number(line.slice(3));
646
+ totalLines += count;
647
+ fileTotalLines += count;
648
+ }
372
649
  if (line.startsWith('LH:')) {
373
650
  const hits = Number(line.slice(3));
374
651
  hitLines += hits;
375
- if (hits > 0)
376
- fileCovered = true;
652
+ fileHitLines += hits;
377
653
  }
378
654
  if (line.startsWith('BRF:'))
379
655
  totalBranches += Number(line.slice(4));
@@ -394,8 +670,13 @@ function parseLcovInfo(reportPath) {
394
670
  entry.hits = Number(hits);
395
671
  }
396
672
  }
397
- if (fileCovered)
673
+ const fileLineCoverage = (_c = safePercent(fileHitLines, fileTotalLines)) !== null && _c !== void 0 ? _c : 0;
674
+ fileCoverage.push({ file: normalizePath(file), lineCoverage: fileLineCoverage });
675
+ if (fileLineCoverage > 0)
398
676
  coveredFiles += 1;
677
+ if (fileLineCoverage === 0) {
678
+ uncoveredClasses.push(path.basename(file, path.extname(file)));
679
+ }
399
680
  for (const [name, data] of fnMap.entries()) {
400
681
  if (data.hits === 0) {
401
682
  uncoveredMethods.push({
@@ -413,10 +694,71 @@ function parseLcovInfo(reportPath) {
413
694
  branchCoverage: safePercent(hitBranches, totalBranches),
414
695
  methodCoverage: safePercent(hitFunctions, totalFunctions),
415
696
  classCoverage: files.length > 0 ? Number(((coveredFiles / files.length) * 100).toFixed(1)) : null,
697
+ ...buildCoverageFileFields(fileCoverage),
698
+ fileCoverage: sortCoverageFiles(fileCoverage),
699
+ uncoveredClasses: dedupeStrings(uncoveredClasses),
416
700
  uncoveredMethods,
417
701
  reportPath: normalizePath(reportPath),
418
702
  };
419
703
  }
704
+ function parseNycCoverageJson(reportPath) {
705
+ const json = JSON.parse(safeRead(reportPath));
706
+ const fileCoverage = Object.entries(json).map(([file, value]) => {
707
+ var _a, _b;
708
+ const statements = Object.values((_a = value.s) !== null && _a !== void 0 ? _a : {});
709
+ const coveredStatements = statements.filter((count) => count > 0).length;
710
+ return {
711
+ file: normalizePath(file),
712
+ lineCoverage: (_b = safePercent(coveredStatements, statements.length)) !== null && _b !== void 0 ? _b : 0,
713
+ };
714
+ });
715
+ const statementCounts = Object.values(json).flatMap((value) => { var _a; return Object.values((_a = value.s) !== null && _a !== void 0 ? _a : {}); });
716
+ const branchCounts = Object.values(json).flatMap((value) => { var _a; return Object.values((_a = value.b) !== null && _a !== void 0 ? _a : {}).flat(); });
717
+ const functionCounts = Object.values(json).flatMap((value) => { var _a; return Object.values((_a = value.f) !== null && _a !== void 0 ? _a : {}); });
718
+ return {
719
+ tool: 'nyc',
720
+ lineCoverage: safePercent(statementCounts.filter((count) => count > 0).length, statementCounts.length),
721
+ branchCoverage: safePercent(branchCounts.filter((count) => count > 0).length, branchCounts.length),
722
+ methodCoverage: safePercent(functionCounts.filter((count) => count > 0).length, functionCounts.length),
723
+ classCoverage: null,
724
+ ...buildCoverageFileFields(fileCoverage),
725
+ fileCoverage: sortCoverageFiles(fileCoverage),
726
+ uncoveredClasses: [],
727
+ uncoveredMethods: [],
728
+ reportPath: normalizePath(reportPath),
729
+ };
730
+ }
731
+ function parseSimpleCovJson(reportPath) {
732
+ var _a, _b;
733
+ const json = JSON.parse(safeRead(reportPath));
734
+ const fileCoverage = [];
735
+ let totalExecutable = 0;
736
+ let totalCovered = 0;
737
+ for (const run of Object.values(json)) {
738
+ for (const [file, hits] of Object.entries((_a = run.coverage) !== null && _a !== void 0 ? _a : {})) {
739
+ const executable = hits.filter((hit) => hit !== null).length;
740
+ const covered = hits.filter((hit) => typeof hit === 'number' && hit > 0).length;
741
+ totalExecutable += executable;
742
+ totalCovered += covered;
743
+ fileCoverage.push({
744
+ file: normalizePath(file),
745
+ lineCoverage: (_b = safePercent(covered, executable)) !== null && _b !== void 0 ? _b : 0,
746
+ });
747
+ }
748
+ }
749
+ return {
750
+ tool: 'simplecov',
751
+ lineCoverage: safePercent(totalCovered, totalExecutable),
752
+ branchCoverage: null,
753
+ methodCoverage: null,
754
+ classCoverage: null,
755
+ ...buildCoverageFileFields(fileCoverage),
756
+ fileCoverage: sortCoverageFiles(fileCoverage),
757
+ uncoveredClasses: [],
758
+ uncoveredMethods: [],
759
+ reportPath: normalizePath(reportPath),
760
+ };
761
+ }
420
762
  function parseCoverageXml(reportPath) {
421
763
  var _a;
422
764
  const xml = safeRead(reportPath);
@@ -426,6 +768,16 @@ function parseCoverageXml(reportPath) {
426
768
  const branchesCovered = readNumericAttribute(xml, 'branches-covered');
427
769
  const classes = [...xml.matchAll(/<class\b[^>]*name="([^"]+)"[^>]*filename="([^"]+)"[\s\S]*?<\/class>/g)];
428
770
  const coveredClasses = classes.filter((match) => /line-rate="(?!0(?:\.0+)?)\d*\.?\d+"/.test(match[0])).length;
771
+ const uncoveredClasses = classes
772
+ .filter((match) => /line-rate="0(?:\.0+)?"/.test(match[0]))
773
+ .map((match) => { var _a; return (_a = String(match[1]).split('.').pop()) !== null && _a !== void 0 ? _a : String(match[1]); });
774
+ const fileCoverage = classes.map((match) => {
775
+ var _a, _b;
776
+ return ({
777
+ file: normalizePath(String(match[2])),
778
+ lineCoverage: Number((Number((_b = (_a = /line-rate="([^"]+)"/.exec(match[0])) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0) * 100).toFixed(1)),
779
+ });
780
+ });
429
781
  const uncoveredMethods = [];
430
782
  const methodRegex = /<method\b[^>]*name="([^"]+)"[^>]*line-rate="([^"]+)"[^>]*>([\s\S]*?)<\/method>/g;
431
783
  let methodMatch;
@@ -448,14 +800,19 @@ function parseCoverageXml(reportPath) {
448
800
  branchCoverage: safePercent(branchesCovered, branchesValid),
449
801
  methodCoverage: safePercent(coveredMethods, totalMethods),
450
802
  classCoverage: classes.length > 0 ? Number(((coveredClasses / classes.length) * 100).toFixed(1)) : null,
803
+ ...buildCoverageFileFields(fileCoverage),
804
+ fileCoverage: sortCoverageFiles(fileCoverage),
805
+ uncoveredClasses: dedupeStrings(uncoveredClasses),
451
806
  uncoveredMethods,
452
807
  reportPath: normalizePath(reportPath),
453
808
  };
454
809
  }
455
810
  function parseGoCoverage(reportPath) {
811
+ var _a;
456
812
  const content = safeRead(reportPath);
457
813
  let total = 0;
458
814
  let covered = 0;
815
+ const fileStats = new Map();
459
816
  for (const line of content.split('\n')) {
460
817
  if (!line || line.startsWith('mode:'))
461
818
  continue;
@@ -463,121 +820,102 @@ function parseGoCoverage(reportPath) {
463
820
  if (parts.length < 3)
464
821
  continue;
465
822
  const count = Number(parts[2]);
466
- const range = parts[1];
823
+ const file = normalizePath(parts[0].split(':')[0]);
824
+ const range = parts[0].includes(':') ? parts[0].slice(parts[0].indexOf(':') + 1) : parts[1];
467
825
  const lineSpan = range.split(',').map((segment) => segment.split('.'));
468
826
  const startLine = Number(lineSpan[0][0]);
469
827
  const endLine = Number(lineSpan[1][0]);
470
828
  const lines = Math.max(1, endLine - startLine + 1);
471
829
  total += lines;
472
- if (count > 0)
830
+ const stat = (_a = fileStats.get(file)) !== null && _a !== void 0 ? _a : { total: 0, covered: 0 };
831
+ stat.total += lines;
832
+ if (count > 0) {
473
833
  covered += lines;
834
+ stat.covered += lines;
835
+ }
836
+ fileStats.set(file, stat);
474
837
  }
838
+ const fileCoverage = Array.from(fileStats.entries()).map(([file, stat]) => {
839
+ var _a;
840
+ return ({
841
+ file,
842
+ lineCoverage: (_a = safePercent(stat.covered, stat.total)) !== null && _a !== void 0 ? _a : 0,
843
+ });
844
+ });
475
845
  return {
476
846
  tool: 'go-cover',
477
847
  lineCoverage: safePercent(covered, total),
478
848
  branchCoverage: null,
479
849
  methodCoverage: null,
480
850
  classCoverage: null,
851
+ ...buildCoverageFileFields(fileCoverage),
852
+ fileCoverage: sortCoverageFiles(fileCoverage),
853
+ uncoveredClasses: [],
481
854
  uncoveredMethods: [],
482
855
  reportPath: normalizePath(reportPath),
483
856
  };
484
857
  }
485
- function runMutationTesting(rootDir, languages, config) {
486
- var _a, _b;
487
- const selectedTool = resolveMutationTool((_b = (_a = config.mutationTesting) === null || _a === void 0 ? void 0 : _a.tool) !== null && _b !== void 0 ? _b : 'auto', languages, rootDir);
488
- if (!selectedTool) {
489
- return { summary: null, warning: 'Mutation testing is enabled but no supported mutation tool could be resolved for this project.' };
490
- }
491
- const command = mutationCommandFor(rootDir, selectedTool);
492
- const result = (0, child_process_1.spawnSync)(command.command, command.args, {
493
- cwd: rootDir,
494
- encoding: 'utf-8',
495
- shell: false,
496
- });
497
- const summary = parseMutationReport(rootDir, selectedTool, result.stdout, result.stderr);
498
- if (!summary) {
499
- const stderr = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
858
+ function parseCoveragePyBinary(reportPath) {
859
+ return {
860
+ tool: 'coverage.py',
861
+ lineCoverage: null,
862
+ branchCoverage: null,
863
+ methodCoverage: null,
864
+ classCoverage: null,
865
+ uncoveredFiles: [],
866
+ lowCoverageFiles: [],
867
+ fileCoverage: [],
868
+ uncoveredClasses: [],
869
+ uncoveredMethods: [],
870
+ reportPath: normalizePath(reportPath),
871
+ };
872
+ }
873
+ function parseCloverXml(reportPath) {
874
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
875
+ const xml = safeRead(reportPath);
876
+ const lineRate = safePercent(Number((_b = (_a = /coveredstatements="(\d+)"/.exec(xml)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0), Number((_d = (_c = /statements="(\d+)"/.exec(xml)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 0));
877
+ const branchRate = safePercent(Number((_f = (_e = /coveredconditionals="(\d+)"/.exec(xml)) === null || _e === void 0 ? void 0 : _e[1]) !== null && _f !== void 0 ? _f : 0), Number((_h = (_g = /conditionals="(\d+)"/.exec(xml)) === null || _g === void 0 ? void 0 : _g[1]) !== null && _h !== void 0 ? _h : 0));
878
+ const methodRate = safePercent(Number((_k = (_j = /coveredmethods="(\d+)"/.exec(xml)) === null || _j === void 0 ? void 0 : _j[1]) !== null && _k !== void 0 ? _k : 0), Number((_m = (_l = /methods="(\d+)"/.exec(xml)) === null || _l === void 0 ? void 0 : _l[1]) !== null && _m !== void 0 ? _m : 0));
879
+ return {
880
+ tool: 'clover',
881
+ lineCoverage: lineRate,
882
+ branchCoverage: branchRate,
883
+ methodCoverage: methodRate,
884
+ classCoverage: null,
885
+ uncoveredFiles: [],
886
+ lowCoverageFiles: [],
887
+ fileCoverage: [],
888
+ uncoveredClasses: [],
889
+ uncoveredMethods: [],
890
+ reportPath: normalizePath(reportPath),
891
+ };
892
+ }
893
+ function discoverMutationTesting(rootDir) {
894
+ const discovery = discoverFirstMatchingPaths(rootDir, MUTATION_REPORT_GLOBS);
895
+ const [reportPath] = discovery.matchedPaths;
896
+ if (!reportPath) {
897
+ return { summary: null };
898
+ }
899
+ try {
900
+ const relativePath = normalizePath(path.relative(rootDir, reportPath));
901
+ if (reportPath.endsWith('.xml')) {
902
+ return { summary: { ...parsePitestXml(reportPath), reportPath: relativePath } };
903
+ }
904
+ return { summary: { ...parseStrykerReport(reportPath, /stryker/i.test(reportPath) ? 'stryker' : 'mutation'), reportPath: relativePath } };
905
+ }
906
+ catch (error) {
500
907
  return {
501
908
  summary: null,
502
- warning: stderr || `Mutation testing tool ${selectedTool} did not produce a supported report.`,
909
+ warning: `Mutation report ${normalizePath(path.relative(rootDir, reportPath))} could not be parsed: ${error instanceof Error ? error.message : String(error)}`,
503
910
  };
504
911
  }
505
- return { summary };
506
- }
507
- function resolveMutationTool(tool, languages, rootDir) {
508
- if (tool !== 'auto')
509
- return tool;
510
- const lowerLanguages = languages.map((language) => language.toLowerCase());
511
- if (lowerLanguages.includes('java') || lowerLanguages.includes('kotlin'))
512
- return 'pitest';
513
- if (lowerLanguages.includes('typescript') || lowerLanguages.includes('javascript'))
514
- return 'stryker';
515
- if (lowerLanguages.includes('python'))
516
- return 'mutmut';
517
- if (lowerLanguages.includes('go'))
518
- return 'go-mutesting';
519
- if (lowerLanguages.includes('csharp') || lowerLanguages.includes('dotnet'))
520
- return 'dotnet-stryker';
521
- if (fs.existsSync(path.join(rootDir, 'package.json')))
522
- return 'stryker';
523
- if (fs.existsSync(path.join(rootDir, 'pom.xml')))
524
- return 'pitest';
525
- if (fs.existsSync(path.join(rootDir, 'build.gradle')) || fs.existsSync(path.join(rootDir, 'build.gradle.kts')))
526
- return 'pitest';
527
- if (fs.existsSync(path.join(rootDir, 'pyproject.toml')) || fs.existsSync(path.join(rootDir, 'setup.py')))
528
- return 'mutmut';
529
- if (fs.existsSync(path.join(rootDir, 'go.mod')))
530
- return 'go-mutesting';
531
- if (fast_glob_1.default.sync(['*.sln', '*.csproj'], { cwd: rootDir }).length > 0)
532
- return 'dotnet-stryker';
533
- return null;
534
- }
535
- function mutationCommandFor(rootDir, tool) {
536
- switch (tool) {
537
- case 'pitest':
538
- return fs.existsSync(path.join(rootDir, 'gradlew'))
539
- ? { command: path.join(rootDir, 'gradlew'), args: ['pitest'] }
540
- : fs.existsSync(path.join(rootDir, 'build.gradle'))
541
- ? { command: 'gradle', args: ['pitest'] }
542
- : { command: 'mvn', args: ['pitest:mutationCoverage'] };
543
- case 'stryker':
544
- return { command: 'npx', args: ['stryker', 'run'] };
545
- case 'mutmut':
546
- return { command: 'mutmut', args: ['run'] };
547
- case 'go-mutesting':
548
- return { command: 'go-mutesting', args: ['./...'] };
549
- case 'dotnet-stryker':
550
- return { command: 'dotnet', args: ['stryker'] };
551
- default:
552
- return { command: 'true', args: [] };
553
- }
554
- }
555
- function parseMutationReport(rootDir, tool, stdout, stderr) {
556
- if (tool === 'pitest') {
557
- for (const reportPath of fast_glob_1.default.sync([...PIT_REPORT_GLOBS], { cwd: rootDir, absolute: true })) {
558
- return parsePitestXml(reportPath);
559
- }
560
- }
561
- if (tool === 'stryker' || tool === 'dotnet-stryker') {
562
- for (const reportPath of fast_glob_1.default.sync([...STRYKER_REPORT_GLOBS], { cwd: rootDir, absolute: true })) {
563
- return parseStrykerReport(reportPath, tool === 'dotnet-stryker' ? 'dotnet stryker' : 'stryker');
564
- }
565
- }
566
- if (tool === 'mutmut') {
567
- const summary = parseMutmutOutput(stdout || stderr);
568
- return summary ? { ...summary, tool: 'mutmut' } : null;
569
- }
570
- if (tool === 'go-mutesting') {
571
- const summary = parseGoMutestingOutput(stdout || stderr);
572
- return summary ? { ...summary, tool: 'go-mutesting' } : null;
573
- }
574
- return null;
575
912
  }
576
913
  function parsePitestXml(reportPath) {
577
- var _a, _b, _c, _d, _e, _f, _g;
914
+ var _a, _b, _c, _d, _e, _f, _g, _h;
578
915
  const xml = safeRead(reportPath);
579
916
  const mutations = [...xml.matchAll(/<mutation\b([^>]*)>([\s\S]*?)<\/mutation>/g)];
580
917
  const survivedByFile = new Map();
918
+ const fileStats = new Map();
581
919
  let total = 0;
582
920
  let killed = 0;
583
921
  let survived = 0;
@@ -588,16 +926,20 @@ function parsePitestXml(reportPath) {
588
926
  const statusMatch = /status="([^"]+)"/.exec(attrs);
589
927
  const status = (_a = statusMatch === null || statusMatch === void 0 ? void 0 : statusMatch[1]) !== null && _a !== void 0 ? _a : 'UNKNOWN';
590
928
  const file = (_b = extractXmlText(body, 'sourceFile')) !== null && _b !== void 0 ? _b : 'unknown';
591
- const line = Number((_c = extractXmlText(body, 'lineNumber')) !== null && _c !== void 0 ? _c : '0');
592
- const mutator = (_d = extractXmlText(body, 'mutator')) !== null && _d !== void 0 ? _d : 'mutation';
593
- const description = (_e = extractXmlText(body, 'description')) !== null && _e !== void 0 ? _e : mutator;
929
+ const stats = (_c = fileStats.get(file)) !== null && _c !== void 0 ? _c : { total: 0, killed: 0 };
930
+ stats.total += 1;
931
+ fileStats.set(file, stats);
932
+ const line = Number((_d = extractXmlText(body, 'lineNumber')) !== null && _d !== void 0 ? _d : '0');
933
+ const mutator = (_e = extractXmlText(body, 'mutator')) !== null && _e !== void 0 ? _e : 'mutation';
934
+ const description = (_f = extractXmlText(body, 'description')) !== null && _f !== void 0 ? _f : mutator;
594
935
  const killingTest = extractXmlText(body, 'killingTest');
595
936
  if (status === 'KILLED') {
596
937
  killed += 1;
938
+ stats.killed += 1;
597
939
  }
598
940
  if (status === 'SURVIVED' || status === 'NO_COVERAGE' || status === 'TIMED_OUT') {
599
941
  survived += 1;
600
- const entries = (_f = survivedByFile.get(file)) !== null && _f !== void 0 ? _f : [];
942
+ const entries = (_g = survivedByFile.get(file)) !== null && _g !== void 0 ? _g : [];
601
943
  entries.push({
602
944
  line,
603
945
  mutation: description,
@@ -608,38 +950,45 @@ function parsePitestXml(reportPath) {
608
950
  }
609
951
  return {
610
952
  tool: 'pitest',
611
- mutationScore: (_g = safePercent(killed, total)) !== null && _g !== void 0 ? _g : 0,
953
+ mutationScore: (_h = safePercent(killed, total)) !== null && _h !== void 0 ? _h : 0,
612
954
  totalMutants: total,
613
955
  killedMutants: killed,
614
956
  survivedMutants: survived,
957
+ worstMutationFiles: rankWorstMutationFiles(fileStats),
958
+ weakestFiles: rankWeakestMutationFiles(fileStats),
615
959
  survivedByFile: Array.from(survivedByFile.entries()).map(([file, items]) => ({ file, survived: items })),
616
960
  reportPath: normalizePath(reportPath),
617
961
  };
618
962
  }
619
963
  function parseStrykerReport(reportPath, toolName) {
620
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
964
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
621
965
  const json = JSON.parse(safeRead(reportPath));
622
966
  const files = ((_a = json['files']) !== null && _a !== void 0 ? _a : {});
623
967
  let total = 0;
624
968
  let killed = 0;
625
969
  let survived = 0;
970
+ const fileStats = new Map();
626
971
  const survivedByFile = [];
627
972
  for (const [file, fileValue] of Object.entries(files)) {
628
973
  const fileRecord = fileValue;
629
974
  const mutants = ((_b = fileRecord['mutants']) !== null && _b !== void 0 ? _b : []);
630
975
  const survivors = [];
976
+ const stats = (_c = fileStats.get(file)) !== null && _c !== void 0 ? _c : { total: 0, killed: 0 };
631
977
  for (const mutant of mutants) {
632
978
  total += 1;
633
- const status = String((_c = mutant['status']) !== null && _c !== void 0 ? _c : 'Unknown');
634
- if (/Killed/i.test(status))
979
+ stats.total += 1;
980
+ const status = String((_d = mutant['status']) !== null && _d !== void 0 ? _d : 'Unknown');
981
+ if (/Killed/i.test(status)) {
635
982
  killed += 1;
983
+ stats.killed += 1;
984
+ }
636
985
  if (/Survived|NoCoverage|Timeout/i.test(status)) {
637
986
  survived += 1;
638
- const location = ((_d = mutant['location']) !== null && _d !== void 0 ? _d : {});
639
- const start = ((_e = location['start']) !== null && _e !== void 0 ? _e : {});
987
+ const location = ((_e = mutant['location']) !== null && _e !== void 0 ? _e : {});
988
+ const start = ((_f = location['start']) !== null && _f !== void 0 ? _f : {});
640
989
  survivors.push({
641
- line: Number((_f = start['line']) !== null && _f !== void 0 ? _f : 0),
642
- mutation: String((_h = (_g = mutant['mutatorName']) !== null && _g !== void 0 ? _g : mutant['id']) !== null && _h !== void 0 ? _h : 'mutation'),
990
+ line: Number((_g = start['line']) !== null && _g !== void 0 ? _g : 0),
991
+ mutation: String((_j = (_h = mutant['mutatorName']) !== null && _h !== void 0 ? _h : mutant['id']) !== null && _j !== void 0 ? _j : 'mutation'),
643
992
  originalCode: typeof mutant['replacement'] === 'string' ? String(mutant['replacement']) : undefined,
644
993
  mutatedCode: typeof mutant['description'] === 'string' ? String(mutant['description']) : undefined,
645
994
  testsThatShouldHaveCaught: [],
@@ -649,13 +998,16 @@ function parseStrykerReport(reportPath, toolName) {
649
998
  if (survivors.length > 0) {
650
999
  survivedByFile.push({ file: normalizePath(file), survived: survivors });
651
1000
  }
1001
+ fileStats.set(file, stats);
652
1002
  }
653
1003
  return {
654
1004
  tool: toolName,
655
- mutationScore: (_j = safePercent(killed, total)) !== null && _j !== void 0 ? _j : 0,
1005
+ mutationScore: (_k = safePercent(killed, total)) !== null && _k !== void 0 ? _k : 0,
656
1006
  totalMutants: total,
657
1007
  killedMutants: killed,
658
1008
  survivedMutants: survived,
1009
+ worstMutationFiles: rankWorstMutationFiles(fileStats),
1010
+ weakestFiles: rankWeakestMutationFiles(fileStats),
659
1011
  survivedByFile,
660
1012
  reportPath: normalizePath(reportPath),
661
1013
  };
@@ -673,6 +1025,8 @@ function parseMutmutOutput(output) {
673
1025
  totalMutants: total,
674
1026
  killedMutants: killed,
675
1027
  survivedMutants: survived,
1028
+ worstMutationFiles: [],
1029
+ weakestFiles: [],
676
1030
  survivedByFile: [],
677
1031
  };
678
1032
  }
@@ -688,25 +1042,78 @@ function parseGoMutestingOutput(output) {
688
1042
  totalMutants: total,
689
1043
  killedMutants: killed,
690
1044
  survivedMutants: survived,
1045
+ worstMutationFiles: [],
1046
+ weakestFiles: [],
691
1047
  survivedByFile: [],
692
1048
  };
693
1049
  }
1050
+ function discoverDynamicResults(rootDir, config) {
1051
+ var _a;
1052
+ const discovery = discoverFirstMatchingPaths(rootDir, TEST_RESULT_GLOBS);
1053
+ if (discovery.matchedPaths.length === 0) {
1054
+ return discovery;
1055
+ }
1056
+ const junitReportPaths = discovery.matchedPaths.filter((reportPath) => isLikelyJunitXml(reportPath));
1057
+ if (junitReportPaths.length === 0) {
1058
+ return {
1059
+ matchedPaths: [],
1060
+ checkedPaths: discovery.checkedPaths,
1061
+ summary: null,
1062
+ };
1063
+ }
1064
+ const tests = junitReportPaths.flatMap((reportPath) => parseJunitXmlResults(reportPath));
1065
+ const durations = tests.map((test) => ({
1066
+ file: test.file,
1067
+ method: test.method,
1068
+ durationMs: test.durationMs,
1069
+ }));
1070
+ const slowSummary = buildSlowTestSummary(durations, junitReportPaths.map((reportPath) => normalizePath(path.relative(rootDir, reportPath))), config);
1071
+ const failures = tests
1072
+ .filter((test) => test.status === 'failed' && test.failureMessage)
1073
+ .map((test) => {
1074
+ var _a;
1075
+ return ({
1076
+ file: test.file,
1077
+ method: test.method,
1078
+ message: (_a = test.failureMessage) !== null && _a !== void 0 ? _a : 'Test failed',
1079
+ stackTrace: test.stackTrace,
1080
+ });
1081
+ });
1082
+ const byTest = new Map();
1083
+ for (const test of tests) {
1084
+ const key = `${test.file}#${test.method}`;
1085
+ const statuses = (_a = byTest.get(key)) !== null && _a !== void 0 ? _a : new Set();
1086
+ statuses.add(test.status);
1087
+ byTest.set(key, statuses);
1088
+ }
1089
+ return {
1090
+ matchedPaths: junitReportPaths,
1091
+ checkedPaths: discovery.checkedPaths,
1092
+ summary: {
1093
+ passed: tests.filter((test) => test.status === 'passed').length,
1094
+ failed: tests.filter((test) => test.status === 'failed').length,
1095
+ skipped: tests.filter((test) => test.status === 'skipped').length,
1096
+ flakyTests: Array.from(byTest.entries())
1097
+ .filter(([, statuses]) => statuses.has('passed') && statuses.has('failed'))
1098
+ .map(([key]) => key),
1099
+ failures,
1100
+ ...slowSummary,
1101
+ },
1102
+ };
1103
+ }
1104
+ function isLikelyJunitXml(reportPath) {
1105
+ if (path.extname(reportPath).toLowerCase() !== '.xml') {
1106
+ return false;
1107
+ }
1108
+ const xml = safeReadHead(reportPath);
1109
+ return /<testsuites?\b|<testcase\b/.test(xml);
1110
+ }
694
1111
  function parseSlowTests(rootDir, config) {
1112
+ const discovery = discoverDynamicResults(rootDir, config);
1113
+ return discovery.summary;
1114
+ }
1115
+ function buildSlowTestSummary(durations, reportPaths, config) {
695
1116
  var _a, _b;
696
- const reportPaths = fast_glob_1.default.sync([...TEST_RESULT_GLOBS], { cwd: rootDir, absolute: true });
697
- if (reportPaths.length === 0)
698
- return null;
699
- const durations = [];
700
- for (const reportPath of reportPaths) {
701
- if (reportPath.endsWith('.json')) {
702
- durations.push(...parseJestJsonDurations(reportPath));
703
- }
704
- else {
705
- durations.push(...parseJunitXmlDurations(reportPath));
706
- }
707
- }
708
- if (durations.length === 0)
709
- return null;
710
1117
  const unitThreshold = (_b = (_a = config.slowTestThreshold) === null || _a === void 0 ? void 0 : _a.unit) !== null && _b !== void 0 ? _b : 200;
711
1118
  const slowTests = durations
712
1119
  .filter((item) => item.durationMs > unitThreshold)
@@ -716,32 +1123,43 @@ function parseSlowTests(rootDir, config) {
716
1123
  hint: buildSlowTestHint(item.durationMs),
717
1124
  }))
718
1125
  .sort((a, b) => b.durationMs - a.durationMs);
719
- const totalDurationMs = durations.reduce((sum, item) => sum + item.durationMs, 0);
1126
+ const totalDurationMs = durations.length > 0
1127
+ ? durations.reduce((sum, item) => sum + item.durationMs, 0)
1128
+ : 0;
720
1129
  const fastCount = durations.filter((item) => item.durationMs < 50).length;
721
1130
  const slowCount = durations.filter((item) => item.durationMs >= unitThreshold).length;
722
1131
  return {
723
1132
  slowTests,
724
- avgDurationMs: Math.round(totalDurationMs / durations.length),
1133
+ avgDurationMs: durations.length > 0 ? Math.round(totalDurationMs / durations.length) : 0,
725
1134
  totalDurationMs,
726
- fastPercent: Math.round((fastCount / durations.length) * 100),
727
- slowPercent: Math.round((slowCount / durations.length) * 100),
728
- reportPaths: reportPaths.map((reportPath) => normalizePath(path.relative(rootDir, String(reportPath)))),
1135
+ fastPercent: durations.length > 0 ? Math.round((fastCount / durations.length) * 100) : 0,
1136
+ slowPercent: durations.length > 0 ? Math.round((slowCount / durations.length) * 100) : 0,
1137
+ reportPaths,
729
1138
  };
730
1139
  }
731
- function parseJunitXmlDurations(reportPath) {
1140
+ function parseJunitXmlResults(reportPath) {
732
1141
  const xml = safeRead(reportPath);
733
- const testcases = [...xml.matchAll(/<testcase\b([^>]*)\/?>(?:[\s\S]*?<\/testcase>)?/g)];
1142
+ const testcases = [...xml.matchAll(/<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g)];
734
1143
  return testcases.map((match) => {
735
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1144
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
736
1145
  const attrs = match[1];
737
- const name = (_b = (_a = /\bname="([^"]+)"/.exec(attrs)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 'unknown';
738
- const className = (_d = (_c = /\bclassname="([^"]+)"/.exec(attrs)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : '';
739
- const file = (_g = (_f = (_e = /\bfile="([^"]+)"/.exec(attrs)) === null || _e === void 0 ? void 0 : _e[1]) !== null && _f !== void 0 ? _f : className) !== null && _g !== void 0 ? _g : 'unknown';
740
- const timeSeconds = Number((_j = (_h = /\btime="([^"]+)"/.exec(attrs)) === null || _h === void 0 ? void 0 : _h[1]) !== null && _j !== void 0 ? _j : 0);
1146
+ const body = (_a = match[2]) !== null && _a !== void 0 ? _a : '';
1147
+ const name = (_c = (_b = /\bname="([^"]+)"/.exec(attrs)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : 'unknown';
1148
+ const className = ((_d = /\bclassname="([^"]+)"/.exec(attrs)) === null || _d === void 0 ? void 0 : _d[1]) || 'unknown';
1149
+ const file = ((_e = /\bfile="([^"]+)"/.exec(attrs)) === null || _e === void 0 ? void 0 : _e[1]) || className;
1150
+ const timeSeconds = Number((_g = (_f = /\btime="([^"]+)"/.exec(attrs)) === null || _f === void 0 ? void 0 : _f[1]) !== null && _g !== void 0 ? _g : 0);
1151
+ const failureMatch = /<(failure|error)\b([^>]*)>([\s\S]*?)<\/\1>/.exec(body);
1152
+ const skippedMatch = /<skipped\b/.exec(body);
1153
+ const failureAttrs = (_h = failureMatch === null || failureMatch === void 0 ? void 0 : failureMatch[2]) !== null && _h !== void 0 ? _h : '';
1154
+ const failureBody = sanitizeXmlText((_j = failureMatch === null || failureMatch === void 0 ? void 0 : failureMatch[3]) !== null && _j !== void 0 ? _j : '');
1155
+ const failureMessage = (_l = (_k = /\bmessage="([^"]+)"/.exec(failureAttrs)) === null || _k === void 0 ? void 0 : _k[1]) !== null && _l !== void 0 ? _l : failureBody.split('\n').map((line) => line.trim()).find(Boolean);
741
1156
  return {
742
1157
  file: normalizePath(file),
743
1158
  method: name,
1159
+ status: failureMatch ? 'failed' : skippedMatch ? 'skipped' : 'passed',
744
1160
  durationMs: Math.round(timeSeconds * 1000),
1161
+ failureMessage,
1162
+ stackTrace: failureBody || undefined,
745
1163
  };
746
1164
  });
747
1165
  }
@@ -763,6 +1181,40 @@ function parseJestJsonDurations(reportPath) {
763
1181
  }
764
1182
  return durations;
765
1183
  }
1184
+ function discoverContractTesting(rootDir) {
1185
+ const discovery = discoverFirstMatchingPaths(rootDir, CONTRACT_REPORT_GLOBS);
1186
+ if (discovery.matchedPaths.length === 0) {
1187
+ return discovery;
1188
+ }
1189
+ const failedVerifications = [];
1190
+ let verifiedContracts = 0;
1191
+ for (const contractPath of discovery.matchedPaths) {
1192
+ try {
1193
+ const json = JSON.parse(safeRead(contractPath));
1194
+ const contractName = formatContractName(json, contractPath);
1195
+ const verification = extractContractVerificationStatus(json);
1196
+ if (verification === true) {
1197
+ verifiedContracts += 1;
1198
+ }
1199
+ if (verification === false) {
1200
+ failedVerifications.push(`${contractName}: ${extractContractFailureReason(json)}`);
1201
+ }
1202
+ }
1203
+ catch {
1204
+ failedVerifications.push(`${normalizePath(path.relative(rootDir, contractPath))}: invalid JSON`);
1205
+ }
1206
+ }
1207
+ return {
1208
+ matchedPaths: discovery.matchedPaths,
1209
+ checkedPaths: discovery.checkedPaths,
1210
+ summary: {
1211
+ totalContracts: discovery.matchedPaths.length,
1212
+ verifiedContracts,
1213
+ failedVerifications,
1214
+ contractPaths: discovery.matchedPaths.map((contractPath) => normalizePath(path.relative(rootDir, contractPath))),
1215
+ },
1216
+ };
1217
+ }
766
1218
  function buildSlowTestHint(durationMs) {
767
1219
  if (durationMs > 5000) {
768
1220
  return `Test takes ${(durationMs / 1000).toFixed(1)}s — likely performing real IO or loading a full application context. Mock external calls or use a lighter fixture.`;
@@ -772,6 +1224,143 @@ function buildSlowTestHint(durationMs) {
772
1224
  }
773
1225
  return `Test takes ${durationMs}ms — likely doing avoidable IO. Prefer isolated unit doubles and in-memory fixtures.`;
774
1226
  }
1227
+ function calculateDynamicPassRate(summary) {
1228
+ const total = summary.passed + summary.failed + summary.skipped;
1229
+ if (total === 0)
1230
+ return 0;
1231
+ return Number(((summary.passed / total) * 100).toFixed(1));
1232
+ }
1233
+ function calculateUnitScore(input) {
1234
+ var _a, _b, _c, _d, _e, _f;
1235
+ const weights = getAppliedUnitScoreWeights(input);
1236
+ return Number(((input.astQuality * weights.ast)
1237
+ + (((_a = input.lineCoverage) !== null && _a !== void 0 ? _a : 0) * ((_b = weights.line) !== null && _b !== void 0 ? _b : 0))
1238
+ + (((_c = input.branchCoverage) !== null && _c !== void 0 ? _c : 0) * ((_d = weights.branch) !== null && _d !== void 0 ? _d : 0))
1239
+ + (((_e = input.mutationScore) !== null && _e !== void 0 ? _e : 0) * ((_f = weights.mutation) !== null && _f !== void 0 ? _f : 0))).toFixed(1));
1240
+ }
1241
+ function describeAppliedWeighting(input) {
1242
+ const weights = getAppliedUnitScoreWeights({
1243
+ astQuality: 0,
1244
+ dynamicPassRate: null,
1245
+ lineCoverage: input.lineCoverage,
1246
+ branchCoverage: input.branchCoverage,
1247
+ mutationScore: input.mutationScore,
1248
+ layersApplied: input.layersApplied,
1249
+ });
1250
+ const parts = [`AST ${formatWeightPercent(weights.ast)}`];
1251
+ if (weights.line) {
1252
+ parts.push(`Line ${formatWeightPercent(weights.line)}`);
1253
+ }
1254
+ if (weights.branch) {
1255
+ parts.push(`Branch ${formatWeightPercent(weights.branch)}`);
1256
+ }
1257
+ if (weights.mutation) {
1258
+ parts.push(`Mutation ${formatWeightPercent(weights.mutation)}`);
1259
+ }
1260
+ return parts.join(' + ');
1261
+ }
1262
+ function getAppliedUnitScoreWeights(input) {
1263
+ const hasRuntime = input.layersApplied.includes('coverage') && input.lineCoverage !== null;
1264
+ const hasBranch = hasRuntime && input.branchCoverage !== null;
1265
+ const hasMutation = input.layersApplied.includes('mutation') && input.mutationScore !== null;
1266
+ if (hasRuntime && hasMutation) {
1267
+ return hasBranch
1268
+ ? SCORE_WEIGHTS.astRuntimeMutation
1269
+ : normalizeUnitScoreWeights({
1270
+ ast: SCORE_WEIGHTS.astRuntimeMutation.ast,
1271
+ line: SCORE_WEIGHTS.astRuntimeMutation.line,
1272
+ mutation: SCORE_WEIGHTS.astRuntimeMutation.mutation,
1273
+ });
1274
+ }
1275
+ if (hasRuntime) {
1276
+ return hasBranch
1277
+ ? SCORE_WEIGHTS.astRuntime
1278
+ : normalizeUnitScoreWeights({
1279
+ ast: SCORE_WEIGHTS.astRuntime.ast,
1280
+ line: SCORE_WEIGHTS.astRuntime.line,
1281
+ });
1282
+ }
1283
+ if (hasMutation) {
1284
+ return SCORE_WEIGHTS.astMutation;
1285
+ }
1286
+ return SCORE_WEIGHTS.astOnly;
1287
+ }
1288
+ function normalizeUnitScoreWeights(weights) {
1289
+ if (Object.values(weights).some((weight) => weight !== undefined && weight < 0)) {
1290
+ return { ast: 1 };
1291
+ }
1292
+ const total = Object.values(weights).reduce((sum, value) => sum + value, 0);
1293
+ if (total <= 0) {
1294
+ return { ast: 1 };
1295
+ }
1296
+ const normalized = {
1297
+ ast: weights.ast / total,
1298
+ };
1299
+ if (weights.line) {
1300
+ normalized.line = weights.line / total;
1301
+ }
1302
+ if (weights.branch) {
1303
+ normalized.branch = weights.branch / total;
1304
+ }
1305
+ if (weights.mutation) {
1306
+ normalized.mutation = weights.mutation / total;
1307
+ }
1308
+ return normalized;
1309
+ }
1310
+ function formatWeightPercent(weight) {
1311
+ const percent = weight * 100;
1312
+ const rounded = percent.toFixed(1);
1313
+ return rounded.endsWith('.0') ? `${rounded.slice(0, -2)}%` : `${rounded}%`;
1314
+ }
1315
+ function buildUnitAnalysisConsoleLines(input) {
1316
+ var _a;
1317
+ const lines = [
1318
+ '=== Unit Analysis ===',
1319
+ `Files scanned: ${input.ast.filesScanned} test files, ${input.ast.testCount} test methods`,
1320
+ '',
1321
+ `AST Quality Score: ${input.ast.qualityScore}/100`,
1322
+ ` ${scoreIcon(input.ast.dimensionScores.assertionCompleteness)} Assertion completeness: ${input.ast.dimensionScores.assertionCompleteness}/20 (${describeAssertionCompleteness(input.ast)})`,
1323
+ ` ${scoreIcon(input.ast.dimensionScores.testIsolation)} Test isolation: ${input.ast.dimensionScores.testIsolation}/20 (${input.ast.isolationPercent}% use mocks/stubs/spies)`,
1324
+ ` ${scoreIcon(input.ast.dimensionScores.namingQuality)} Naming quality: ${input.ast.dimensionScores.namingQuality}/20 (${input.ast.descriptiveNamePercent}% descriptive names)`,
1325
+ ` ${scoreIcon(input.ast.dimensionScores.disabledTests)} No disabled tests: ${input.ast.dimensionScores.disabledTests}/20 (${input.ast.disabledTests.length} disabled tests)`,
1326
+ ` ${scoreIcon(input.ast.dimensionScores.criticalSmells)} Smell-free: ${input.ast.dimensionScores.criticalSmells}/20 (${describeCriticalSmells(input.ast)})`,
1327
+ ];
1328
+ if (input.coverage) {
1329
+ lines.push('');
1330
+ lines.push(`Runtime enrichment: ✓ ${input.coverage.tool} found at ${input.coverage.reportPath}`);
1331
+ lines.push(` Line coverage: ${formatPct(input.coverage.lineCoverage)}`);
1332
+ lines.push(` Branch coverage: ${formatPct(input.coverage.branchCoverage)}`);
1333
+ if (input.coverage.methodCoverage !== null) {
1334
+ lines.push(` Method coverage: ${formatPct(input.coverage.methodCoverage)}`);
1335
+ }
1336
+ if (input.coverage.classCoverage !== null) {
1337
+ lines.push(` Class coverage: ${formatPct(input.coverage.classCoverage)}`);
1338
+ }
1339
+ }
1340
+ else {
1341
+ lines.push('');
1342
+ lines.push('Runtime enrichment: · Not found (reads existing coverage artifacts only)');
1343
+ }
1344
+ if (input.mutation) {
1345
+ lines.push('');
1346
+ lines.push(`Mutation enrichment: ✓ ${input.mutation.tool} found at ${(_a = input.mutation.reportPath) !== null && _a !== void 0 ? _a : 'mutation report'}`);
1347
+ lines.push(` Mutation score: ${input.mutation.mutationScore}%`);
1348
+ lines.push(` Survived: ${input.mutation.survivedMutants}`);
1349
+ lines.push(` Killed: ${input.mutation.killedMutants}`);
1350
+ }
1351
+ else {
1352
+ lines.push('');
1353
+ lines.push(`Mutation enrichment: · Not found (run: ${suggestMutationCommand(input.languages)})`);
1354
+ }
1355
+ if (input.dynamic) {
1356
+ lines.push('');
1357
+ lines.push(`Dynamic evidence: ${input.dynamic.reportPaths[0]} (${input.dynamic.passed} passed, ${input.dynamic.failed} failed, ${input.dynamic.skipped} skipped)`);
1358
+ }
1359
+ if (input.contract) {
1360
+ lines.push(`Contract evidence: ${input.contract.totalContracts} contracts, ${input.contract.verifiedContracts} verified`);
1361
+ }
1362
+ return lines;
1363
+ }
775
1364
  function detectTestSmells(rootDir, tests) {
776
1365
  const smells = [];
777
1366
  for (const test of tests) {
@@ -819,6 +1408,17 @@ function detectPerBlockSmells(rootDir, test) {
819
1408
  fix: 'Add assertion on the return value or verify mock interaction',
820
1409
  });
821
1410
  }
1411
+ if (isEmptyTestBody(body)) {
1412
+ smells.push({
1413
+ smellId: 'SMELL-11',
1414
+ severity: 'HIGH',
1415
+ file: test.relativePath,
1416
+ line: block.line,
1417
+ testMethod: block.name,
1418
+ description: 'Empty test body detected',
1419
+ fix: 'Add test steps and assertions or remove the placeholder test',
1420
+ });
1421
+ }
822
1422
  if (isDisabledWithoutReason(test.language, block, test.content)) {
823
1423
  smells.push({
824
1424
  smellId: 'SMELL-03',
@@ -1106,6 +1706,37 @@ function extractTestBlocks(language, content) {
1106
1706
  }
1107
1707
  return blocks;
1108
1708
  }
1709
+ if (language === 'ruby') {
1710
+ const regex = /(xit|xspecify|it|specify|test)\s*(?:\(\s*)?['"]([^'"]+)['"](?:\s*\))?\s*do\s*\n([\s\S]*?)(?=\n\s*end\b)/g;
1711
+ const blocks = [];
1712
+ let match;
1713
+ while ((match = regex.exec(content)) !== null) {
1714
+ blocks.push({
1715
+ name: match[2],
1716
+ body: match[3],
1717
+ line: lineNumberForIndex(content, match.index),
1718
+ decorators: [match[1]],
1719
+ });
1720
+ }
1721
+ return blocks;
1722
+ }
1723
+ if (language === 'go') {
1724
+ const regex = /func\s+(Test[a-zA-Z_]\w*)\s*\([^)]*\)\s*\{/g;
1725
+ const blocks = [];
1726
+ let match;
1727
+ while ((match = regex.exec(content)) !== null) {
1728
+ const start = match.index + match[0].length - 1;
1729
+ const end = findMatchingBrace(content, start);
1730
+ blocks.push({
1731
+ name: match[1],
1732
+ body: content.slice(start + 1, end),
1733
+ line: lineNumberForIndex(content, match.index),
1734
+ decorators: [],
1735
+ });
1736
+ regex.lastIndex = end + 1;
1737
+ }
1738
+ return blocks;
1739
+ }
1109
1740
  return [];
1110
1741
  }
1111
1742
  function hasVerificationCall(language, body) {
@@ -1115,6 +1746,10 @@ function hasVerificationCall(language, body) {
1115
1746
  return /expect\s*\(|toHaveBeenCalled/.test(body);
1116
1747
  if (language === 'python')
1117
1748
  return /\bassert\b|assert_called/.test(body);
1749
+ if (language === 'ruby')
1750
+ return /\bexpect\(|assert_|refute_|have_received/.test(body);
1751
+ if (language === 'go')
1752
+ return /\b(?:assert|require)\.\w+\(|t\.Fatalf?\(|t\.Errorf?\(/.test(body);
1118
1753
  return false;
1119
1754
  }
1120
1755
  function isDisabledWithoutReason(language, block, content) {
@@ -1132,6 +1767,24 @@ function isDisabledWithoutReason(language, block, content) {
1132
1767
  if (language === 'python') {
1133
1768
  return block.decorators.some((decorator) => /@pytest\.mark\.skip(?!\s*\(|.*reason\s*=)/.test(decorator));
1134
1769
  }
1770
+ if (language === 'ruby') {
1771
+ return block.decorators.some((decorator) => /^(xit|xspecify)$/.test(decorator));
1772
+ }
1773
+ return false;
1774
+ }
1775
+ function isDisabledBlock(language, block) {
1776
+ if (language === 'java' || language === 'kotlin') {
1777
+ return block.decorators.some((decorator) => decorator.startsWith('@Disabled'));
1778
+ }
1779
+ if (language === 'javascript' || language === 'typescript') {
1780
+ return block.decorators.some((decorator) => /^(xit|xtest|test\.skip|it\.skip)$/.test(decorator));
1781
+ }
1782
+ if (language === 'python') {
1783
+ return block.decorators.some((decorator) => /@pytest\.mark\.skip/.test(decorator));
1784
+ }
1785
+ if (language === 'ruby') {
1786
+ return block.decorators.some((decorator) => /^(xit|xspecify)$/.test(decorator));
1787
+ }
1135
1788
  return false;
1136
1789
  }
1137
1790
  function hasAssertionMessages(language, body) {
@@ -1224,6 +1877,7 @@ function countParameterizedExpansions(content) {
1224
1877
  var _a, _b;
1225
1878
  const matches = [
1226
1879
  ...content.matchAll(/@CsvSource\(([^)]*)\)/g),
1880
+ ...content.matchAll(/@ParameterizedTest/g),
1227
1881
  ...content.matchAll(/test\.each\(([^)]*)\)/g),
1228
1882
  ...content.matchAll(/pytest\.mark\.parametrize\(([^)]*)\)/g),
1229
1883
  ];
@@ -1241,6 +1895,10 @@ function detectMockLibrary(content) {
1241
1895
  return 'jest';
1242
1896
  if (/unittest\.mock|MagicMock|pytest-mock|mocker\./i.test(content))
1243
1897
  return 'unittest.mock';
1898
+ if (/\bdouble\(|instance_double\(|allow\(|stub_const\(/.test(content))
1899
+ return 'rspec';
1900
+ if (/gomock|testify\/mock/.test(content))
1901
+ return 'gomock';
1244
1902
  return undefined;
1245
1903
  }
1246
1904
  function classifyLayer(content, filePath) {
@@ -1251,7 +1909,7 @@ function classifyLayer(content, filePath) {
1251
1909
  return 'unit';
1252
1910
  }
1253
1911
  function countAssertions(language, content) {
1254
- var _a, _b, _c;
1912
+ var _a, _b, _c, _d, _e;
1255
1913
  if (language === 'java' || language === 'kotlin') {
1256
1914
  return ((_a = content.match(/\bassert\w*\s*\(|\bverify\s*\(/g)) !== null && _a !== void 0 ? _a : []).length;
1257
1915
  }
@@ -1261,8 +1919,151 @@ function countAssertions(language, content) {
1261
1919
  if (language === 'python') {
1262
1920
  return ((_c = content.match(/\bassert\b|assert_called/g)) !== null && _c !== void 0 ? _c : []).length;
1263
1921
  }
1922
+ if (language === 'ruby') {
1923
+ return ((_d = content.match(/\bexpect\(|\bassert_\w+|\brefute_\w+|have_received/g)) !== null && _d !== void 0 ? _d : []).length;
1924
+ }
1925
+ if (language === 'go') {
1926
+ return ((_e = content.match(/\b(?:assert|require)\.\w+\(|t\.Fatalf?\(|t\.Errorf?\(/g)) !== null && _e !== void 0 ? _e : []).length;
1927
+ }
1264
1928
  return 0;
1265
1929
  }
1930
+ function countTestClasses(language, content) {
1931
+ var _a, _b, _c, _d;
1932
+ if (language === 'java' || language === 'kotlin') {
1933
+ return ((_a = content.match(/\bclass\s+[A-Z]\w*Test\b/g)) !== null && _a !== void 0 ? _a : []).length || 1;
1934
+ }
1935
+ if (language === 'python') {
1936
+ return ((_b = content.match(/^\s*class\s+\w+\(.*TestCase.*\):/gm)) !== null && _b !== void 0 ? _b : []).length || 1;
1937
+ }
1938
+ if (language === 'ruby') {
1939
+ return ((_c = content.match(/\bRSpec\.describe\b|\bdescribe\s+['"]/g)) !== null && _c !== void 0 ? _c : []).length || 1;
1940
+ }
1941
+ if (language === 'go') {
1942
+ return 1;
1943
+ }
1944
+ return ((_d = content.match(/\bdescribe\s*\(/g)) !== null && _d !== void 0 ? _d : []).length || 1;
1945
+ }
1946
+ function countParameterizedTests(language, content) {
1947
+ var _a, _b, _c, _d;
1948
+ if (language === 'java' || language === 'kotlin')
1949
+ return ((_a = content.match(/@ParameterizedTest/g)) !== null && _a !== void 0 ? _a : []).length;
1950
+ if (language === 'javascript' || language === 'typescript')
1951
+ return ((_b = content.match(/\b(?:test|it)\.each\s*\(/g)) !== null && _b !== void 0 ? _b : []).length;
1952
+ if (language === 'python')
1953
+ return ((_c = content.match(/pytest\.mark\.parametrize/g)) !== null && _c !== void 0 ? _c : []).length;
1954
+ if (language === 'ruby')
1955
+ return ((_d = content.match(/\bshared_examples\b|\beach do \|/g)) !== null && _d !== void 0 ? _d : []).length;
1956
+ return 0;
1957
+ }
1958
+ function countMockSignals(content) {
1959
+ var _a;
1960
+ return ((_a = content.match(/@MockBean\b|@Mock\b|\bmock\(|jest\.mock\(|vi\.mock\(|patch\(|MagicMock|mocker\.|double\(|gomock|testify\/mock/g)) !== null && _a !== void 0 ? _a : []).length;
1961
+ }
1962
+ function countStubSignals(content) {
1963
+ var _a;
1964
+ return ((_a = content.match(/thenReturn\(|mockReturnValue\(|returns\(|allow\(.+\)\.to receive|stub\(/g)) !== null && _a !== void 0 ? _a : []).length;
1965
+ }
1966
+ function countSpySignals(content) {
1967
+ var _a;
1968
+ return ((_a = content.match(/@Spy\b|jest\.spyOn\(|vi\.spyOn\(|spyOn\(/g)) !== null && _a !== void 0 ? _a : []).length;
1969
+ }
1970
+ function testUsesIsolation(language, body) {
1971
+ if (language === 'java' || language === 'kotlin')
1972
+ return /@Mock\b|@MockBean\b|verify\(|mock\(|when\(.+\)\.thenReturn|spy\(/.test(body);
1973
+ if (language === 'javascript' || language === 'typescript')
1974
+ return /jest\.mock\(|vi\.mock\(|jest\.spyOn\(|vi\.spyOn\(|mockReturnValue|toHaveBeenCalled/.test(body);
1975
+ if (language === 'python')
1976
+ return /patch\(|MagicMock|mocker\.|assert_called/.test(body);
1977
+ if (language === 'ruby')
1978
+ return /double\(|instance_double\(|allow\(|have_received/.test(body);
1979
+ if (language === 'go')
1980
+ return /gomock|testify\/mock|mock\./.test(body);
1981
+ return false;
1982
+ }
1983
+ function isDescriptiveTestName(name) {
1984
+ const normalized = name
1985
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
1986
+ .replace(/[_\-]+/g, ' ')
1987
+ .trim();
1988
+ return normalized.split(/\s+/).filter(Boolean).length > 3;
1989
+ }
1990
+ function isGenericTestName(name) {
1991
+ return /^(test\d+|testMethod\d+|testMethod|shouldWork|works|test)$/i.test(name.replace(/\s+/g, ''));
1992
+ }
1993
+ function isEmptyTestBody(body) {
1994
+ const meaningful = body
1995
+ .split('\n')
1996
+ .map((line) => line.replace(/\/\/.*$/, '').trim())
1997
+ .filter(Boolean);
1998
+ return meaningful.length === 0 || meaningful.every((line) => /^(pass|todo)$/i.test(line) || /^(\/\/|#)/.test(line));
1999
+ }
2000
+ function collectLineMatches(rootDir, filePath, content, pattern) {
2001
+ const relativePath = normalizePath(path.relative(rootDir, filePath));
2002
+ return content
2003
+ .split('\n')
2004
+ .flatMap((line, index) => (pattern.test(line) ? [`${relativePath}:${index + 1}`] : []));
2005
+ }
2006
+ function hasStaticMutableStateMutation(language, content) {
2007
+ var _a, _b, _c, _d;
2008
+ if (language === 'java' || language === 'kotlin') {
2009
+ const beforeAllBlock = (_b = (_a = /@BeforeAll[\s\S]*?\{([\s\S]*?)\}/.exec(content)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
2010
+ const staticFields = [...content.matchAll(/static\s+(?!final)[^{;=]+([a-zA-Z_]\w*)/g)].map((match) => match[1]);
2011
+ if (staticFields.length === 0)
2012
+ return false;
2013
+ const mutatedFields = staticFields.filter((field) => new RegExp(`\\b${field}\\s*=`).test(beforeAllBlock));
2014
+ if (mutatedFields.length === 0)
2015
+ return false;
2016
+ const afterAllBlock = (_d = (_c = /@AfterAll[\s\S]*?\{([\s\S]*?)\}/.exec(content)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : '';
2017
+ return mutatedFields.some((field) => !new RegExp(`\\b${field}\\s*=`).test(afterAllBlock));
2018
+ }
2019
+ if (language === 'javascript' || language === 'typescript') {
2020
+ return /\blet\s+[a-zA-Z_]\w*/.test(content) && /beforeAll\s*\([\s\S]*?\b[a-zA-Z_]\w*\s*=/.test(content);
2021
+ }
2022
+ if (language === 'python') {
2023
+ return /@classmethod\s+def\s+setUpClass/.test(content) && /cls\.[a-zA-Z_]\w*\s*=/.test(content) && !/@classmethod\s+def\s+tearDownClass/.test(content);
2024
+ }
2025
+ return false;
2026
+ }
2027
+ function collectSharedMutableFields(language, content) {
2028
+ if (language === 'java' || language === 'kotlin') {
2029
+ return dedupeStrings([...content.matchAll(/(?:private|protected|public)?\s*(?!final)(?:[\w<>\[\]]+\s+)+([a-zA-Z_]\w*)\s*[;=]/g)].map((match) => match[1]));
2030
+ }
2031
+ if (language === 'javascript' || language === 'typescript') {
2032
+ return dedupeStrings([...content.matchAll(/\blet\s+([a-zA-Z_]\w*)\s*[=;]/g)].map((match) => match[1]));
2033
+ }
2034
+ if (language === 'python') {
2035
+ return dedupeStrings([...content.matchAll(/self\.([a-zA-Z_]\w*)\s*=/g)].map((match) => match[1]));
2036
+ }
2037
+ return [];
2038
+ }
2039
+ function clampScore(score) {
2040
+ return Math.max(0, Math.min(100, Number(score.toFixed(1))));
2041
+ }
2042
+ function calculateWeightedCoveredItems(totalItems, weightedScore) {
2043
+ if (totalItems <= 0) {
2044
+ return 0;
2045
+ }
2046
+ return Math.max(0, Math.min(totalItems, Math.round((totalItems * clampScore(weightedScore)) / 100)));
2047
+ }
2048
+ function countPassingQualityTests(tests) {
2049
+ let passing = 0;
2050
+ for (const test of tests) {
2051
+ for (const block of test.testBlocks) {
2052
+ const assertionCount = countAssertions(test.language, block.body);
2053
+ const hasCriticalSmell = isEmptyTestBody(block.body)
2054
+ || /Thread\.sleep\(|time\.sleep\(|setTimeout\(/.test(block.body)
2055
+ || /\b(?:password|secret|token|api[_-]?key)\b\s*[:=]\s*['"][^'"]+['"]/i.test(block.body);
2056
+ const passes = assertionCount > 0
2057
+ && !isDisabledBlock(test.language, block)
2058
+ && isDescriptiveTestName(block.name)
2059
+ && !hasCriticalSmell;
2060
+ if (passes) {
2061
+ passing += 1;
2062
+ }
2063
+ }
2064
+ }
2065
+ return passing;
2066
+ }
1266
2067
  function summarizeSmells(smells) {
1267
2068
  return {
1268
2069
  CRITICAL: smells.filter((smell) => smell.severity === 'CRITICAL').length,
@@ -1292,6 +2093,10 @@ function detectTestLanguage(filePath) {
1292
2093
  return 'typescript';
1293
2094
  if (ext === '.py')
1294
2095
  return 'python';
2096
+ if (ext === '.rb')
2097
+ return 'ruby';
2098
+ if (ext === '.go')
2099
+ return 'go';
1295
2100
  return 'other';
1296
2101
  }
1297
2102
  function findMatchingBrace(content, openBraceIndex) {
@@ -1345,6 +2150,19 @@ function coveragePercent(counter) {
1345
2150
  return null;
1346
2151
  return safePercent(counter.covered, counter.covered + counter.missed);
1347
2152
  }
2153
+ function sortCoverageFiles(fileCoverage) {
2154
+ return dedupeBy(fileCoverage, (entry) => entry.file)
2155
+ .sort((a, b) => a.lineCoverage - b.lineCoverage || a.file.localeCompare(b.file));
2156
+ }
2157
+ function buildCoverageFileFields(fileCoverage) {
2158
+ const sorted = sortCoverageFiles(fileCoverage);
2159
+ return {
2160
+ uncoveredFiles: sorted.filter((entry) => entry.lineCoverage === 0).map((entry) => entry.file),
2161
+ lowCoverageFiles: sorted
2162
+ .filter((entry) => entry.lineCoverage < 50)
2163
+ .map((entry) => ({ file: entry.file, lineCoverage: entry.lineCoverage })),
2164
+ };
2165
+ }
1348
2166
  function safePercent(covered, total) {
1349
2167
  if (!Number.isFinite(total) || total <= 0)
1350
2168
  return null;
@@ -1364,6 +2182,126 @@ function extractXmlText(xml, tag) {
1364
2182
  var _a, _b;
1365
2183
  return (_b = (_a = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`).exec(xml)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.trim();
1366
2184
  }
2185
+ function sanitizeXmlText(value) {
2186
+ return value
2187
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
2188
+ .replace(/</g, '&lt;')
2189
+ .replace(/>/g, '&gt;')
2190
+ .trim();
2191
+ }
2192
+ function rankWorstMutationFiles(fileStats) {
2193
+ return Array.from(fileStats.entries())
2194
+ .filter(([, stats]) => stats.total > 0)
2195
+ .map(([file, stats]) => ({
2196
+ file: normalizePath(file),
2197
+ survived: stats.total - stats.killed,
2198
+ killed: stats.killed,
2199
+ }))
2200
+ .sort((a, b) => b.survived - a.survived || a.file.localeCompare(b.file))
2201
+ .slice(0, 5);
2202
+ }
2203
+ function rankWeakestMutationFiles(fileStats) {
2204
+ return Array.from(fileStats.entries())
2205
+ .filter(([, stats]) => stats.total > 0)
2206
+ .sort((a, b) => {
2207
+ const aScore = (a[1].killed / a[1].total) * 100;
2208
+ const bScore = (b[1].killed / b[1].total) * 100;
2209
+ return aScore - bScore || a[0].localeCompare(b[0]);
2210
+ })
2211
+ .slice(0, 5)
2212
+ .map(([file]) => normalizePath(file));
2213
+ }
2214
+ function formatPct(value) {
2215
+ return value === null ? 'n/a' : `${value}%`;
2216
+ }
2217
+ function scoreIcon(score) {
2218
+ return score >= 20 ? '✓' : score > 0 ? '⚠' : '✗';
2219
+ }
2220
+ function describeAssertionCompleteness(ast) {
2221
+ if (ast.testsWithZeroAssertions.length === 0) {
2222
+ return 'all tests have assertions';
2223
+ }
2224
+ return `${ast.testsWithZeroAssertions.length} tests without assertions`;
2225
+ }
2226
+ function describeCriticalSmells(ast) {
2227
+ const parts = [
2228
+ ast.threadSleepUsage.length > 0 ? `${ast.threadSleepUsage.length} sleep usages` : undefined,
2229
+ ast.emptyTestBodies.length > 0 ? `${ast.emptyTestBodies.length} empty bodies` : undefined,
2230
+ ast.hardcodedCredentials.length > 0 ? `${ast.hardcodedCredentials.length} hardcoded credentials` : undefined,
2231
+ ].filter((value) => Boolean(value));
2232
+ return parts.length > 0 ? parts.join(', ') : 'no critical smells';
2233
+ }
2234
+ function appendDetectedSmells(lines, ast) {
2235
+ const smellEntries = [
2236
+ ...ast.threadSleepUsage.map((entry) => ` ⚠ Thread.sleep()/sleep usage in ${entry}`),
2237
+ ...ast.emptyTestBodies.map((entry) => ` ⚠ Empty test body: ${entry}`),
2238
+ ...ast.disabledTests.map((entry) => ` ⚠ Disabled test: ${entry}`),
2239
+ ];
2240
+ if (smellEntries.length === 0)
2241
+ return;
2242
+ lines.push('');
2243
+ lines.push(`Smells detected (${smellEntries.length}):`);
2244
+ for (const entry of smellEntries.slice(0, 10)) {
2245
+ lines.push(entry);
2246
+ }
2247
+ }
2248
+ function appendLowCoverageFiles(lines, coverage) {
2249
+ if (!coverage || coverage.lowCoverageFiles.length === 0)
2250
+ return;
2251
+ lines.push('');
2252
+ lines.push('Low coverage files (< 50%):');
2253
+ for (const entry of coverage.lowCoverageFiles.slice(0, 10)) {
2254
+ lines.push(` ⚠ ${entry.file}: ${entry.lineCoverage}% line coverage`);
2255
+ }
2256
+ }
2257
+ function suggestMutationCommand(languages) {
2258
+ const lowerLanguages = languages.map((language) => language.toLowerCase());
2259
+ if (lowerLanguages.some((language) => language === 'java' || language === 'kotlin'))
2260
+ return 'gradle pitest';
2261
+ if (lowerLanguages.some((language) => language === 'typescript' || language === 'javascript'))
2262
+ return 'npx stryker run';
2263
+ if (lowerLanguages.includes('python'))
2264
+ return 'mutmut run';
2265
+ if (lowerLanguages.includes('ruby'))
2266
+ return 'configure a mutation report under reports/mutation/';
2267
+ return 'write a supported mutation report under build/reports/pitest or reports/mutation';
2268
+ }
2269
+ function formatContractName(contract, contractPath) {
2270
+ var _a, _b;
2271
+ const consumer = typeof contract['consumer'] === 'object' && contract['consumer'] !== null
2272
+ ? String((_a = contract['consumer']['name']) !== null && _a !== void 0 ? _a : 'unknown')
2273
+ : 'unknown';
2274
+ const provider = typeof contract['provider'] === 'object' && contract['provider'] !== null
2275
+ ? String((_b = contract['provider']['name']) !== null && _b !== void 0 ? _b : 'unknown')
2276
+ : 'unknown';
2277
+ return consumer !== 'unknown' || provider !== 'unknown'
2278
+ ? `${consumer}→${provider}`
2279
+ : normalizePath(contractPath);
2280
+ }
2281
+ function extractContractVerificationStatus(contract) {
2282
+ var _a, _b, _c, _d;
2283
+ const metadata = ((_a = contract['metadata']) !== null && _a !== void 0 ? _a : {});
2284
+ const verification = ((_d = (_c = (_b = contract['verificationResult']) !== null && _b !== void 0 ? _b : contract['verification']) !== null && _c !== void 0 ? _c : metadata['verificationResult']) !== null && _d !== void 0 ? _d : metadata['verification']);
2285
+ if (typeof verification === 'boolean')
2286
+ return verification;
2287
+ if (verification && typeof verification === 'object') {
2288
+ if (typeof verification['success'] === 'boolean')
2289
+ return verification['success'];
2290
+ if (typeof verification['passed'] === 'boolean')
2291
+ return verification['passed'];
2292
+ if (typeof verification['verified'] === 'boolean')
2293
+ return verification['verified'];
2294
+ }
2295
+ if (typeof contract['verified'] === 'boolean')
2296
+ return contract['verified'];
2297
+ return null;
2298
+ }
2299
+ function extractContractFailureReason(contract) {
2300
+ var _a, _b, _c, _d, _e, _f, _g;
2301
+ const metadata = ((_a = contract['metadata']) !== null && _a !== void 0 ? _a : {});
2302
+ const verification = ((_d = (_c = (_b = contract['verificationResult']) !== null && _b !== void 0 ? _b : contract['verification']) !== null && _c !== void 0 ? _c : metadata['verificationResult']) !== null && _d !== void 0 ? _d : metadata['verification']);
2303
+ return String((_g = (_f = (_e = verification === null || verification === void 0 ? void 0 : verification['reason']) !== null && _e !== void 0 ? _e : verification === null || verification === void 0 ? void 0 : verification['message']) !== null && _f !== void 0 ? _f : contract['verificationError']) !== null && _g !== void 0 ? _g : 'verification failed');
2304
+ }
1367
2305
  function dedupeBy(items, keyFn) {
1368
2306
  const seen = new Set();
1369
2307
  const deduped = [];
@@ -1390,3 +2328,19 @@ function safeRead(filePath) {
1390
2328
  return '';
1391
2329
  }
1392
2330
  }
2331
+ function safeReadHead(filePath, maxBytes = 4096) {
2332
+ try {
2333
+ const fd = fs.openSync(filePath, 'r');
2334
+ try {
2335
+ const buffer = Buffer.alloc(maxBytes);
2336
+ const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
2337
+ return buffer.toString('utf-8', 0, bytesRead);
2338
+ }
2339
+ finally {
2340
+ fs.closeSync(fd);
2341
+ }
2342
+ }
2343
+ catch {
2344
+ return '';
2345
+ }
2346
+ }