api-tests-coverage 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +703 -0
  2. package/config.yaml.example +227 -0
  3. package/dist/action/src/index.d.ts +2 -0
  4. package/dist/action/src/index.d.ts.map +1 -0
  5. package/dist/action/src/index.js +349 -0
  6. package/dist/action/src/prComment.d.ts +34 -0
  7. package/dist/action/src/prComment.d.ts.map +1 -0
  8. package/dist/action/src/prComment.js +146 -0
  9. package/dist/src/ast/astAnalysisOrchestrator.d.ts +36 -0
  10. package/dist/src/ast/astAnalysisOrchestrator.d.ts.map +1 -0
  11. package/dist/src/ast/astAnalysisOrchestrator.js +123 -0
  12. package/dist/src/ast/astTypes.d.ts +105 -0
  13. package/dist/src/ast/astTypes.d.ts.map +1 -0
  14. package/dist/src/ast/astTypes.js +9 -0
  15. package/dist/src/ast/languageAnalyzer.d.ts +46 -0
  16. package/dist/src/ast/languageAnalyzer.d.ts.map +1 -0
  17. package/dist/src/ast/languageAnalyzer.js +9 -0
  18. package/dist/src/ast/languageCapabilities.d.ts +24 -0
  19. package/dist/src/ast/languageCapabilities.d.ts.map +1 -0
  20. package/dist/src/ast/languageCapabilities.js +92 -0
  21. package/dist/src/ast/parseFile.d.ts +16 -0
  22. package/dist/src/ast/parseFile.d.ts.map +1 -0
  23. package/dist/src/ast/parseFile.js +65 -0
  24. package/dist/src/ast/parserRegistry.d.ts +39 -0
  25. package/dist/src/ast/parserRegistry.d.ts.map +1 -0
  26. package/dist/src/ast/parserRegistry.js +66 -0
  27. package/dist/src/buildSummary.d.ts +26 -0
  28. package/dist/src/buildSummary.d.ts.map +1 -0
  29. package/dist/src/buildSummary.js +193 -0
  30. package/dist/src/businessCoverage.d.ts +68 -0
  31. package/dist/src/businessCoverage.d.ts.map +1 -0
  32. package/dist/src/businessCoverage.js +290 -0
  33. package/dist/src/compatibilityCoverage.d.ts +83 -0
  34. package/dist/src/compatibilityCoverage.d.ts.map +1 -0
  35. package/dist/src/compatibilityCoverage.js +501 -0
  36. package/dist/src/config/defaultConfig.d.ts +9 -0
  37. package/dist/src/config/defaultConfig.d.ts.map +1 -0
  38. package/dist/src/config/defaultConfig.js +97 -0
  39. package/dist/src/config/index.d.ts +12 -0
  40. package/dist/src/config/index.d.ts.map +1 -0
  41. package/dist/src/config/index.js +37 -0
  42. package/dist/src/config/loadConfig.d.ts +29 -0
  43. package/dist/src/config/loadConfig.d.ts.map +1 -0
  44. package/dist/src/config/loadConfig.js +135 -0
  45. package/dist/src/config/mergeConfig.d.ts +15 -0
  46. package/dist/src/config/mergeConfig.d.ts.map +1 -0
  47. package/dist/src/config/mergeConfig.js +57 -0
  48. package/dist/src/config/schema.d.ts +15 -0
  49. package/dist/src/config/schema.d.ts.map +1 -0
  50. package/dist/src/config/schema.js +30 -0
  51. package/dist/src/config/types.d.ts +175 -0
  52. package/dist/src/config/types.d.ts.map +1 -0
  53. package/dist/src/config/types.js +9 -0
  54. package/dist/src/config/validateConfig.d.ts +22 -0
  55. package/dist/src/config/validateConfig.d.ts.map +1 -0
  56. package/dist/src/config/validateConfig.js +171 -0
  57. package/dist/src/config.d.ts +168 -0
  58. package/dist/src/config.d.ts.map +1 -0
  59. package/dist/src/config.js +204 -0
  60. package/dist/src/coverage/deep-analysis/callGraph.d.ts +67 -0
  61. package/dist/src/coverage/deep-analysis/callGraph.d.ts.map +1 -0
  62. package/dist/src/coverage/deep-analysis/callGraph.js +275 -0
  63. package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts +23 -0
  64. package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts.map +1 -0
  65. package/dist/src/coverage/deep-analysis/deepEndpointResolver.js +394 -0
  66. package/dist/src/coverage/deep-analysis/index.d.ts +17 -0
  67. package/dist/src/coverage/deep-analysis/index.d.ts.map +1 -0
  68. package/dist/src/coverage/deep-analysis/index.js +63 -0
  69. package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts +60 -0
  70. package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts.map +1 -0
  71. package/dist/src/coverage/deep-analysis/resolveAssertions.js +121 -0
  72. package/dist/src/coverage/deep-analysis/resolveConstants.d.ts +36 -0
  73. package/dist/src/coverage/deep-analysis/resolveConstants.d.ts.map +1 -0
  74. package/dist/src/coverage/deep-analysis/resolveConstants.js +92 -0
  75. package/dist/src/coverage/deep-analysis/resolveEnums.d.ts +55 -0
  76. package/dist/src/coverage/deep-analysis/resolveEnums.d.ts.map +1 -0
  77. package/dist/src/coverage/deep-analysis/resolveEnums.js +152 -0
  78. package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts +70 -0
  79. package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts.map +1 -0
  80. package/dist/src/coverage/deep-analysis/resolveMethodChains.js +152 -0
  81. package/dist/src/coverage/deep-analysis/resolvePaths.d.ts +80 -0
  82. package/dist/src/coverage/deep-analysis/resolvePaths.d.ts.map +1 -0
  83. package/dist/src/coverage/deep-analysis/resolvePaths.js +216 -0
  84. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts +71 -0
  85. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts.map +1 -0
  86. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.js +226 -0
  87. package/dist/src/coverage/deep-analysis/symbolTable.d.ts +58 -0
  88. package/dist/src/coverage/deep-analysis/symbolTable.d.ts.map +1 -0
  89. package/dist/src/coverage/deep-analysis/symbolTable.js +230 -0
  90. package/dist/src/coverage/deep-analysis/types.d.ts +122 -0
  91. package/dist/src/coverage/deep-analysis/types.d.ts.map +1 -0
  92. package/dist/src/coverage/deep-analysis/types.js +21 -0
  93. package/dist/src/discovery/fileClassifier.d.ts +50 -0
  94. package/dist/src/discovery/fileClassifier.d.ts.map +1 -0
  95. package/dist/src/discovery/fileClassifier.js +238 -0
  96. package/dist/src/discovery/projectDiscovery.d.ts +66 -0
  97. package/dist/src/discovery/projectDiscovery.d.ts.map +1 -0
  98. package/dist/src/discovery/projectDiscovery.js +287 -0
  99. package/dist/src/endpointCoverage.d.ts +70 -0
  100. package/dist/src/endpointCoverage.d.ts.map +1 -0
  101. package/dist/src/endpointCoverage.js +381 -0
  102. package/dist/src/errorCoverage.d.ts +93 -0
  103. package/dist/src/errorCoverage.d.ts.map +1 -0
  104. package/dist/src/errorCoverage.js +698 -0
  105. package/dist/src/index.d.ts +3 -0
  106. package/dist/src/index.d.ts.map +1 -0
  107. package/dist/src/index.js +1441 -0
  108. package/dist/src/inference/businessRuleInference.d.ts +63 -0
  109. package/dist/src/inference/businessRuleInference.d.ts.map +1 -0
  110. package/dist/src/inference/businessRuleInference.js +268 -0
  111. package/dist/src/inference/integrationFlowInference.d.ts +56 -0
  112. package/dist/src/inference/integrationFlowInference.d.ts.map +1 -0
  113. package/dist/src/inference/integrationFlowInference.js +266 -0
  114. package/dist/src/integrationCoverage.d.ts +72 -0
  115. package/dist/src/integrationCoverage.d.ts.map +1 -0
  116. package/dist/src/integrationCoverage.js +317 -0
  117. package/dist/src/intelligence/index.d.ts +20 -0
  118. package/dist/src/intelligence/index.d.ts.map +1 -0
  119. package/dist/src/intelligence/index.js +105 -0
  120. package/dist/src/intelligence/linkageEngine.d.ts +20 -0
  121. package/dist/src/intelligence/linkageEngine.d.ts.map +1 -0
  122. package/dist/src/intelligence/linkageEngine.js +522 -0
  123. package/dist/src/intelligence/markdownReporter.d.ts +12 -0
  124. package/dist/src/intelligence/markdownReporter.d.ts.map +1 -0
  125. package/dist/src/intelligence/markdownReporter.js +265 -0
  126. package/dist/src/intelligence/riskScoring.d.ts +53 -0
  127. package/dist/src/intelligence/riskScoring.d.ts.map +1 -0
  128. package/dist/src/intelligence/riskScoring.js +181 -0
  129. package/dist/src/intelligence/types.d.ts +121 -0
  130. package/dist/src/intelligence/types.d.ts.map +1 -0
  131. package/dist/src/intelligence/types.js +8 -0
  132. package/dist/src/languageDetection.d.ts +100 -0
  133. package/dist/src/languageDetection.d.ts.map +1 -0
  134. package/dist/src/languageDetection.js +349 -0
  135. package/dist/src/languages/java/index.d.ts +16 -0
  136. package/dist/src/languages/java/index.d.ts.map +1 -0
  137. package/dist/src/languages/java/index.js +103 -0
  138. package/dist/src/languages/java/parser.d.ts +7 -0
  139. package/dist/src/languages/java/parser.d.ts.map +1 -0
  140. package/dist/src/languages/java/parser.js +50 -0
  141. package/dist/src/languages/java/semanticBuilder.d.ts +21 -0
  142. package/dist/src/languages/java/semanticBuilder.d.ts.map +1 -0
  143. package/dist/src/languages/java/semanticBuilder.js +358 -0
  144. package/dist/src/languages/javascript/annotationExtractor.d.ts +20 -0
  145. package/dist/src/languages/javascript/annotationExtractor.d.ts.map +1 -0
  146. package/dist/src/languages/javascript/annotationExtractor.js +94 -0
  147. package/dist/src/languages/javascript/assertionResolver.d.ts +18 -0
  148. package/dist/src/languages/javascript/assertionResolver.d.ts.map +1 -0
  149. package/dist/src/languages/javascript/assertionResolver.js +150 -0
  150. package/dist/src/languages/javascript/callResolver.d.ts +23 -0
  151. package/dist/src/languages/javascript/callResolver.d.ts.map +1 -0
  152. package/dist/src/languages/javascript/callResolver.js +236 -0
  153. package/dist/src/languages/javascript/httpInteractionExtractor.d.ts +23 -0
  154. package/dist/src/languages/javascript/httpInteractionExtractor.d.ts.map +1 -0
  155. package/dist/src/languages/javascript/httpInteractionExtractor.js +205 -0
  156. package/dist/src/languages/javascript/index.d.ts +20 -0
  157. package/dist/src/languages/javascript/index.d.ts.map +1 -0
  158. package/dist/src/languages/javascript/index.js +136 -0
  159. package/dist/src/languages/javascript/parser.d.ts +14 -0
  160. package/dist/src/languages/javascript/parser.d.ts.map +1 -0
  161. package/dist/src/languages/javascript/parser.js +38 -0
  162. package/dist/src/languages/javascript/symbolResolver.d.ts +31 -0
  163. package/dist/src/languages/javascript/symbolResolver.d.ts.map +1 -0
  164. package/dist/src/languages/javascript/symbolResolver.js +183 -0
  165. package/dist/src/languages/kotlin/index.d.ts +16 -0
  166. package/dist/src/languages/kotlin/index.d.ts.map +1 -0
  167. package/dist/src/languages/kotlin/index.js +151 -0
  168. package/dist/src/languages/kotlin/parser.d.ts +11 -0
  169. package/dist/src/languages/kotlin/parser.d.ts.map +1 -0
  170. package/dist/src/languages/kotlin/parser.js +74 -0
  171. package/dist/src/languages/python/index.d.ts +15 -0
  172. package/dist/src/languages/python/index.d.ts.map +1 -0
  173. package/dist/src/languages/python/index.js +293 -0
  174. package/dist/src/languages/ruby/index.d.ts +15 -0
  175. package/dist/src/languages/ruby/index.d.ts.map +1 -0
  176. package/dist/src/languages/ruby/index.js +274 -0
  177. package/dist/src/languages/shared/treeSitterUtils.d.ts +43 -0
  178. package/dist/src/languages/shared/treeSitterUtils.d.ts.map +1 -0
  179. package/dist/src/languages/shared/treeSitterUtils.js +100 -0
  180. package/dist/src/languages/typescript/index.d.ts +14 -0
  181. package/dist/src/languages/typescript/index.d.ts.map +1 -0
  182. package/dist/src/languages/typescript/index.js +25 -0
  183. package/dist/src/lib/index.d.ts +228 -0
  184. package/dist/src/lib/index.d.ts.map +1 -0
  185. package/dist/src/lib/index.js +486 -0
  186. package/dist/src/mcp/client/index.d.ts +37 -0
  187. package/dist/src/mcp/client/index.d.ts.map +1 -0
  188. package/dist/src/mcp/client/index.js +235 -0
  189. package/dist/src/mcp/config.d.ts +50 -0
  190. package/dist/src/mcp/config.d.ts.map +1 -0
  191. package/dist/src/mcp/config.js +125 -0
  192. package/dist/src/mcp/events.d.ts +24 -0
  193. package/dist/src/mcp/events.d.ts.map +1 -0
  194. package/dist/src/mcp/events.js +48 -0
  195. package/dist/src/mcp/fallback/index.d.ts +50 -0
  196. package/dist/src/mcp/fallback/index.d.ts.map +1 -0
  197. package/dist/src/mcp/fallback/index.js +216 -0
  198. package/dist/src/mcp/index.d.ts +67 -0
  199. package/dist/src/mcp/index.d.ts.map +1 -0
  200. package/dist/src/mcp/index.js +212 -0
  201. package/dist/src/mcp/normalizer.d.ts +21 -0
  202. package/dist/src/mcp/normalizer.d.ts.map +1 -0
  203. package/dist/src/mcp/normalizer.js +99 -0
  204. package/dist/src/mcp/prompts/index.d.ts +86 -0
  205. package/dist/src/mcp/prompts/index.d.ts.map +1 -0
  206. package/dist/src/mcp/prompts/index.js +304 -0
  207. package/dist/src/mcp/templates/index.d.ts +35 -0
  208. package/dist/src/mcp/templates/index.d.ts.map +1 -0
  209. package/dist/src/mcp/templates/index.js +143 -0
  210. package/dist/src/mcp/testing/mock-server/index.d.ts +47 -0
  211. package/dist/src/mcp/testing/mock-server/index.d.ts.map +1 -0
  212. package/dist/src/mcp/testing/mock-server/index.js +157 -0
  213. package/dist/src/mcp/types.d.ts +127 -0
  214. package/dist/src/mcp/types.d.ts.map +1 -0
  215. package/dist/src/mcp/types.js +8 -0
  216. package/dist/src/observability.d.ts +138 -0
  217. package/dist/src/observability.d.ts.map +1 -0
  218. package/dist/src/observability.js +519 -0
  219. package/dist/src/parameterCoverage.d.ts +75 -0
  220. package/dist/src/parameterCoverage.d.ts.map +1 -0
  221. package/dist/src/parameterCoverage.js +629 -0
  222. package/dist/src/perfResilienceCoverage.d.ts +155 -0
  223. package/dist/src/perfResilienceCoverage.d.ts.map +1 -0
  224. package/dist/src/perfResilienceCoverage.js +670 -0
  225. package/dist/src/pluginLoader.d.ts +51 -0
  226. package/dist/src/pluginLoader.d.ts.map +1 -0
  227. package/dist/src/pluginLoader.js +72 -0
  228. package/dist/src/publishing.d.ts +63 -0
  229. package/dist/src/publishing.d.ts.map +1 -0
  230. package/dist/src/publishing.js +379 -0
  231. package/dist/src/qualityGate.d.ts +58 -0
  232. package/dist/src/qualityGate.d.ts.map +1 -0
  233. package/dist/src/qualityGate.js +118 -0
  234. package/dist/src/reporting.d.ts +41 -0
  235. package/dist/src/reporting.d.ts.map +1 -0
  236. package/dist/src/reporting.js +278 -0
  237. package/dist/src/screenshots.d.ts +71 -0
  238. package/dist/src/screenshots.d.ts.map +1 -0
  239. package/dist/src/screenshots.js +141 -0
  240. package/dist/src/security/gate/index.d.ts +11 -0
  241. package/dist/src/security/gate/index.d.ts.map +1 -0
  242. package/dist/src/security/gate/index.js +65 -0
  243. package/dist/src/security/index.d.ts +30 -0
  244. package/dist/src/security/index.d.ts.map +1 -0
  245. package/dist/src/security/index.js +342 -0
  246. package/dist/src/security/normalizers/semgrep.d.ts +10 -0
  247. package/dist/src/security/normalizers/semgrep.d.ts.map +1 -0
  248. package/dist/src/security/normalizers/semgrep.js +104 -0
  249. package/dist/src/security/normalizers/trivy.d.ts +10 -0
  250. package/dist/src/security/normalizers/trivy.d.ts.map +1 -0
  251. package/dist/src/security/normalizers/trivy.js +78 -0
  252. package/dist/src/security/normalizers/zap.d.ts +10 -0
  253. package/dist/src/security/normalizers/zap.d.ts.map +1 -0
  254. package/dist/src/security/normalizers/zap.js +104 -0
  255. package/dist/src/security/scanners/semgrep.d.ts +6 -0
  256. package/dist/src/security/scanners/semgrep.d.ts.map +1 -0
  257. package/dist/src/security/scanners/semgrep.js +125 -0
  258. package/dist/src/security/scanners/trivy.d.ts +6 -0
  259. package/dist/src/security/scanners/trivy.d.ts.map +1 -0
  260. package/dist/src/security/scanners/trivy.js +115 -0
  261. package/dist/src/security/scanners/zap.d.ts +6 -0
  262. package/dist/src/security/scanners/zap.d.ts.map +1 -0
  263. package/dist/src/security/scanners/zap.js +135 -0
  264. package/dist/src/security/types.d.ts +146 -0
  265. package/dist/src/security/types.d.ts.map +1 -0
  266. package/dist/src/security/types.js +6 -0
  267. package/dist/src/securityCoverage.d.ts +116 -0
  268. package/dist/src/securityCoverage.d.ts.map +1 -0
  269. package/dist/src/securityCoverage.js +725 -0
  270. package/dist/src/summary/buildSummary.d.ts +28 -0
  271. package/dist/src/summary/buildSummary.d.ts.map +1 -0
  272. package/dist/src/summary/buildSummary.js +257 -0
  273. package/dist/src/summary/evaluateMetrics.d.ts +31 -0
  274. package/dist/src/summary/evaluateMetrics.d.ts.map +1 -0
  275. package/dist/src/summary/evaluateMetrics.js +118 -0
  276. package/dist/src/summary/index.d.ts +10 -0
  277. package/dist/src/summary/index.d.ts.map +1 -0
  278. package/dist/src/summary/index.js +22 -0
  279. package/dist/src/summary/markdownRenderer.d.ts +139 -0
  280. package/dist/src/summary/markdownRenderer.d.ts.map +1 -0
  281. package/dist/src/summary/markdownRenderer.js +459 -0
  282. package/dist/src/summary/prSummary.d.ts +24 -0
  283. package/dist/src/summary/prSummary.d.ts.map +1 -0
  284. package/dist/src/summary/prSummary.js +233 -0
  285. package/dist/src/summary/summaryTypes.d.ts +35 -0
  286. package/dist/src/summary/summaryTypes.d.ts.map +1 -0
  287. package/dist/src/summary/summaryTypes.js +27 -0
  288. package/package.json +84 -0
@@ -0,0 +1,629 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.parseParameters = parseParameters;
40
+ exports.analyzeParameterCoverage = analyzeParameterCoverage;
41
+ exports.buildParameterCoverageReport = buildParameterCoverageReport;
42
+ exports.generateParameterReports = generateParameterReports;
43
+ const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const fast_glob_1 = __importDefault(require("fast-glob"));
47
+ const astAnalysisOrchestrator_1 = require("./ast/astAnalysisOrchestrator");
48
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
49
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
50
+ function escapeRegex(s) {
51
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
+ }
53
+ function extractSchemaConstraints(schema) {
54
+ if (!schema)
55
+ return {};
56
+ return {
57
+ type: schema.type,
58
+ format: schema.format,
59
+ minimum: schema.minimum,
60
+ maximum: schema.maximum,
61
+ minLength: schema.minLength,
62
+ maxLength: schema.maxLength,
63
+ pattern: schema.pattern,
64
+ enum: schema.enum,
65
+ };
66
+ }
67
+ // ─── Parameter parsing ───────────────────────────────────────────────────────
68
+ /**
69
+ * Parse an OpenAPI 3 spec file and return a flat list of all parameters,
70
+ * including request-body properties treated as body parameters.
71
+ */
72
+ async function parseParameters(specPath) {
73
+ var _a, _b, _c, _d;
74
+ const api = (await swagger_parser_1.default.dereference(specPath));
75
+ const params = [];
76
+ if (!api.paths)
77
+ return params;
78
+ for (const [apiPath, pathItem] of Object.entries(api.paths)) {
79
+ if (!pathItem)
80
+ continue;
81
+ // Path-level parameters (shared across all operations in this path)
82
+ const pathLevelParams = ((_a = pathItem.parameters) !== null && _a !== void 0 ? _a : []);
83
+ for (const method of HTTP_METHODS) {
84
+ const operation = pathItem[method];
85
+ if (!operation)
86
+ continue;
87
+ const endpoint = `${method.toUpperCase()} ${apiPath}`;
88
+ // Merge path-level and operation-level parameters; operation-level wins
89
+ const mergedParams = new Map();
90
+ for (const p of pathLevelParams) {
91
+ mergedParams.set(`${p.in}:${p.name}`, p);
92
+ }
93
+ const operationParams = ((_b = operation.parameters) !== null && _b !== void 0 ? _b : []);
94
+ for (const p of operationParams) {
95
+ mergedParams.set(`${p.in}:${p.name}`, p);
96
+ }
97
+ for (const p of mergedParams.values()) {
98
+ params.push({
99
+ endpoint,
100
+ method: method.toUpperCase(),
101
+ path: apiPath,
102
+ name: p.name,
103
+ location: p.in,
104
+ required: p.required === true || p.in === 'path',
105
+ schema: extractSchemaConstraints(p.schema),
106
+ });
107
+ }
108
+ // Request body – treat each top-level JSON schema property as a parameter
109
+ if (operation.requestBody) {
110
+ const rb = operation.requestBody;
111
+ const jsonContent = (_c = rb.content) === null || _c === void 0 ? void 0 : _c['application/json'];
112
+ if (jsonContent === null || jsonContent === void 0 ? void 0 : jsonContent.schema) {
113
+ const schema = jsonContent.schema;
114
+ if (schema.type === 'object' && schema.properties) {
115
+ const required = (_d = schema.required) !== null && _d !== void 0 ? _d : [];
116
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
117
+ params.push({
118
+ endpoint,
119
+ method: method.toUpperCase(),
120
+ path: apiPath,
121
+ name: propName,
122
+ location: 'body',
123
+ required: required.includes(propName),
124
+ schema: extractSchemaConstraints(propSchema),
125
+ });
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ return params;
133
+ }
134
+ // ─── AST augmentation ─────────────────────────────────────────────────────────
135
+ /**
136
+ * Infer coverage category flags from a `parameterScenarios` string array (as
137
+ * populated by language analyzers) or from the interaction's semantic context.
138
+ */
139
+ function scenariosToFlags(scenarios) {
140
+ const s = scenarios.map((x) => x.toLowerCase());
141
+ return {
142
+ validValue: s.includes('valid') ||
143
+ s.includes('happy-path') ||
144
+ s.includes('success') ||
145
+ s.includes('positive'),
146
+ boundaryValue: s.includes('boundary') ||
147
+ s.includes('min') ||
148
+ s.includes('max') ||
149
+ s.includes('edge') ||
150
+ s.includes('zero') ||
151
+ s.includes('empty') ||
152
+ s.includes('oversized'),
153
+ missing: s.includes('missing') ||
154
+ s.includes('missing-required') ||
155
+ s.includes('absent') ||
156
+ s.includes('omitted'),
157
+ invalidValue: s.includes('invalid') ||
158
+ s.includes('invalid-value') ||
159
+ s.includes('bad-value') ||
160
+ s.includes('wrong-type') ||
161
+ s.includes('malformed') ||
162
+ s.includes('null') ||
163
+ s.includes('invalid-enum'),
164
+ };
165
+ }
166
+ /**
167
+ * Detect the language of a file from its extension for AST analysis.
168
+ */
169
+ function detectLanguage(filePath) {
170
+ const ext = path.extname(filePath).toLowerCase();
171
+ switch (ext) {
172
+ case '.ts':
173
+ case '.tsx':
174
+ return 'typescript';
175
+ case '.js':
176
+ case '.jsx':
177
+ return 'javascript';
178
+ case '.java':
179
+ return 'java';
180
+ case '.kt':
181
+ case '.kts':
182
+ return 'kotlin';
183
+ case '.py':
184
+ return 'python';
185
+ case '.rb':
186
+ return 'ruby';
187
+ case '.feature':
188
+ return 'cucumber';
189
+ default:
190
+ return 'auto';
191
+ }
192
+ }
193
+ /**
194
+ * Normalize an endpoint path (possibly with {param} placeholders) so that it
195
+ * can be compared against an OpenAPI path template.
196
+ * e.g. '/users/123' → '/users/{id}' won't match — but '/users/{id}' → '/users/{id}' will.
197
+ * We strip the query string and trailing slash for a loose match.
198
+ */
199
+ function normalizePath(p) {
200
+ return p.split('?')[0].replace(/\/$/, '').toLowerCase();
201
+ }
202
+ /**
203
+ * Return true when the interaction's resolved path loosely matches the
204
+ * OpenAPI path template (e.g. /users/{id} ≈ /users/).
205
+ */
206
+ function pathMatches(interaction, apiPath) {
207
+ var _a;
208
+ const iPath = normalizePath((_a = interaction.normalizedPath) !== null && _a !== void 0 ? _a : interaction.path);
209
+ const aPath = normalizePath(apiPath);
210
+ // Exact match
211
+ if (iPath === aPath)
212
+ return true;
213
+ // OpenAPI template prefix match: /users/{id} starts with /users
214
+ const templateBase = aPath.split('{')[0].replace(/\/$/, '');
215
+ if (templateBase && iPath.startsWith(templateBase))
216
+ return true;
217
+ return false;
218
+ }
219
+ /**
220
+ * Run AST analysis over all files matching `testGlob` and build a map of
221
+ * endpoint path → interactions for quick lookup during parameter coverage.
222
+ */
223
+ function buildAstInteractionMap(testFiles, astOptions) {
224
+ var _a;
225
+ (0, astAnalysisOrchestrator_1.registerAllAnalyzers)();
226
+ const context = (0, astAnalysisOrchestrator_1.buildAnalysisContext)(astOptions.astConfig, astOptions.deepConfig);
227
+ const map = new Map();
228
+ for (const filePath of testFiles) {
229
+ let content;
230
+ try {
231
+ content = fs.readFileSync(filePath, 'utf-8');
232
+ }
233
+ catch {
234
+ continue;
235
+ }
236
+ const lang = detectLanguage(filePath);
237
+ const interactions = (0, astAnalysisOrchestrator_1.analyzeFile)(content, filePath, lang, context);
238
+ for (const interaction of interactions) {
239
+ const key = normalizePath((_a = interaction.normalizedPath) !== null && _a !== void 0 ? _a : interaction.path);
240
+ if (!map.has(key))
241
+ map.set(key, []);
242
+ map.get(key).push(interaction);
243
+ }
244
+ }
245
+ return map;
246
+ }
247
+ /**
248
+ * Given all AST interactions that match a parameter's endpoint path, compute
249
+ * AST-derived coverage flags and metadata.
250
+ */
251
+ function deriveAstCoverage(matchingInteractions) {
252
+ let validValue = false;
253
+ let boundaryValue = false;
254
+ let missing = false;
255
+ let invalidValue = false;
256
+ let bestInteraction;
257
+ for (const interaction of matchingInteractions) {
258
+ // Use parameterScenarios if populated
259
+ if (interaction.parameterScenarios && interaction.parameterScenarios.length > 0) {
260
+ const flags = scenariosToFlags(interaction.parameterScenarios);
261
+ validValue = validValue || flags.validValue;
262
+ boundaryValue = boundaryValue || flags.boundaryValue;
263
+ missing = missing || flags.missing;
264
+ invalidValue = invalidValue || flags.invalidValue;
265
+ }
266
+ // Pick the highest-confidence interaction for metadata
267
+ if (!bestInteraction ||
268
+ (interaction.confidence === 'high' && bestInteraction.confidence !== 'high') ||
269
+ (interaction.confidence === 'medium' && bestInteraction.confidence === 'low')) {
270
+ bestInteraction = interaction;
271
+ }
272
+ }
273
+ const astMetadata = bestInteraction
274
+ ? {
275
+ sourceLanguage: bestInteraction.sourceLanguage,
276
+ resolutionType: bestInteraction.resolutionType,
277
+ confidence: bestInteraction.confidence,
278
+ }
279
+ : undefined;
280
+ return { validValue, boundaryValue, missing, invalidValue, astMetadata };
281
+ }
282
+ /**
283
+ * Split a test file into individual test blocks (test / it declarations).
284
+ * Each block includes its description and the code up to the next test.
285
+ */
286
+ function extractTestSegments(fileContents) {
287
+ const segments = [];
288
+ // Match `test('...', ` or `it('...', ` with any quote style
289
+ const declPattern = /\b(?:test|it)\s*\(\s*(['"`])([\s\S]*?)\1/g;
290
+ const positions = [];
291
+ let m;
292
+ while ((m = declPattern.exec(fileContents)) !== null) {
293
+ positions.push({ start: m.index, desc: m[2] });
294
+ }
295
+ for (let i = 0; i < positions.length; i++) {
296
+ const start = positions[i].start;
297
+ const end = i + 1 < positions.length ? positions[i + 1].start : fileContents.length;
298
+ segments.push({ description: positions[i].desc, content: fileContents.slice(start, end) });
299
+ }
300
+ return segments;
301
+ }
302
+ /**
303
+ * Determine which coverage categories a single test segment addresses
304
+ * for the given parameter.
305
+ */
306
+ function classifySegment(segment, param) {
307
+ const descLower = segment.description.toLowerCase();
308
+ const contentLower = segment.content.toLowerCase();
309
+ const paramNameLower = param.name.toLowerCase();
310
+ // A segment is relevant if it mentions the base path (before any '{') OR the parameter name
311
+ const pathBase = param.path.split('{')[0].toLowerCase();
312
+ const mentionsPath = contentLower.includes(pathBase);
313
+ const mentionsParam = contentLower.includes(paramNameLower);
314
+ if (!mentionsPath && !mentionsParam) {
315
+ return { validValue: false, boundaryValue: false, missing: false, invalidValue: false };
316
+ }
317
+ // ── Invalid ──────────────────────────────────────────────────────────────
318
+ let invalidValue = false;
319
+ if (descLower.includes('invalid') || descLower.includes('wrong type') || descLower.includes('bad type')) {
320
+ invalidValue = true;
321
+ }
322
+ // null/undefined assigned to the param in object literal
323
+ const nullPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*(null|undefined)`, 'i');
324
+ if (nullPattern.test(segment.content)) {
325
+ invalidValue = true;
326
+ }
327
+ // Wrong type: string value for an integer param
328
+ if (param.schema.type === 'integer' || param.schema.type === 'number') {
329
+ const wrongTypePattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*['"][^'"]+['"]`, 'i');
330
+ if (wrongTypePattern.test(segment.content)) {
331
+ invalidValue = true;
332
+ }
333
+ }
334
+ // ── Missing ───────────────────────────────────────────────────────────────
335
+ let missing = false;
336
+ if (descLower.includes('missing') || descLower.includes('without') || descLower.includes('omit')) {
337
+ missing = true;
338
+ }
339
+ // Required body param absent from an object literal in the test
340
+ if (param.required && param.location === 'body') {
341
+ const bodyLiteralPattern = /\{[^{}]*\}/g;
342
+ let bm;
343
+ while ((bm = bodyLiteralPattern.exec(segment.content)) !== null) {
344
+ const obj = bm[0];
345
+ // Only consider non-empty object literals that have at least one key
346
+ if (/['"]?\w+['"]?\s*:/.test(obj) && !obj.toLowerCase().includes(paramNameLower)) {
347
+ missing = true;
348
+ }
349
+ }
350
+ }
351
+ // ── Boundary ──────────────────────────────────────────────────────────────
352
+ let boundaryValue = false;
353
+ if (descLower.includes('boundary') ||
354
+ descLower.includes('edge') ||
355
+ descLower.includes('min ') ||
356
+ descLower.includes('max ') ||
357
+ descLower.includes('minimum') ||
358
+ descLower.includes('maximum') ||
359
+ descLower.includes('zero') ||
360
+ descLower.includes('empty') ||
361
+ descLower.includes('limit')) {
362
+ boundaryValue = true;
363
+ }
364
+ if (param.schema.type === 'integer' || param.schema.type === 'number') {
365
+ // 0 or 1 are common boundary values; also check schema min/max explicitly
366
+ const zeroOrOnePattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*(?:0|1)\\b`, 'i');
367
+ if (zeroOrOnePattern.test(segment.content)) {
368
+ boundaryValue = true;
369
+ }
370
+ if (param.schema.minimum !== undefined) {
371
+ const minValPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*${param.schema.minimum}\\b`, 'i');
372
+ if (minValPattern.test(segment.content)) {
373
+ boundaryValue = true;
374
+ }
375
+ }
376
+ if (param.schema.maximum !== undefined) {
377
+ const maxValPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*${param.schema.maximum}\\b`, 'i');
378
+ if (maxValPattern.test(segment.content)) {
379
+ boundaryValue = true;
380
+ }
381
+ }
382
+ // Boundary integer in URL (e.g. /users/1 or /users/0)
383
+ if (param.location === 'path') {
384
+ const urlBoundaryPattern = new RegExp(`${escapeRegex(pathBase)}(?:0|1)(?:[/?#]|$)`, 'i');
385
+ if (urlBoundaryPattern.test(segment.content)) {
386
+ boundaryValue = true;
387
+ }
388
+ }
389
+ // Boundary query param value (e.g. limit=1, limit=0)
390
+ if (param.location === 'query') {
391
+ const queryBoundaryPattern = new RegExp(`[?&]${escapeRegex(param.name)}=(?:0|1)(?:[&# ]|$)`, 'i');
392
+ if (queryBoundaryPattern.test(segment.content)) {
393
+ boundaryValue = true;
394
+ }
395
+ }
396
+ }
397
+ else if (param.schema.type === 'string') {
398
+ // Empty string in object literal
399
+ const emptyPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*['"]\\s*['"]`, 'i');
400
+ if (emptyPattern.test(segment.content)) {
401
+ boundaryValue = true;
402
+ }
403
+ // Single-character string value (covers minLength: 1 boundary)
404
+ const singleCharPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*['"][^'"]{1}['"]`, 'i');
405
+ if (singleCharPattern.test(segment.content)) {
406
+ boundaryValue = true;
407
+ }
408
+ // Query / header single char (e.g. ?q=a or X-Api-Key: k)
409
+ if (param.location === 'query') {
410
+ const querySingleCharPattern = new RegExp(`[?&]${escapeRegex(param.name)}=[^&# ]{1}(?:[&# ]|$)`, 'i');
411
+ if (querySingleCharPattern.test(segment.content)) {
412
+ boundaryValue = true;
413
+ }
414
+ // Empty query value
415
+ const queryEmptyPattern = new RegExp(`[?&]${escapeRegex(param.name)}=(?:[&# ]|$)`, 'i');
416
+ if (queryEmptyPattern.test(segment.content)) {
417
+ boundaryValue = true;
418
+ }
419
+ }
420
+ if (param.location === 'header') {
421
+ // Header with single-char value
422
+ const headerSingleCharPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*['"][^'"]{1}['"]`, 'i');
423
+ if (headerSingleCharPattern.test(segment.content)) {
424
+ boundaryValue = true;
425
+ }
426
+ // Header with empty value
427
+ const headerEmptyPattern = new RegExp(`['"]?${escapeRegex(param.name)}['"]?\\s*:\\s*['"]\\s*['"]`, 'i');
428
+ if (headerEmptyPattern.test(segment.content)) {
429
+ boundaryValue = true;
430
+ }
431
+ }
432
+ }
433
+ // ── Valid ─────────────────────────────────────────────────────────────────
434
+ let validValue = false;
435
+ if (descLower.includes('valid') && !descLower.includes('invalid')) {
436
+ validValue = true;
437
+ }
438
+ if (descLower.includes('success') || descLower.includes('creates') || /returns? 2\d{2}/.test(descLower)) {
439
+ validValue = true;
440
+ }
441
+ // If the segment mentions the param and is not categorized as boundary/invalid/missing, treat as valid
442
+ if (!validValue && !boundaryValue && !invalidValue && !missing && mentionsParam && mentionsPath) {
443
+ validValue = true;
444
+ }
445
+ return { validValue, boundaryValue, missing, invalidValue };
446
+ }
447
+ /**
448
+ * For each parameter, scan test files and determine which coverage
449
+ * categories are satisfied. Optionally augments text-scan results with
450
+ * AST-derived semantic signals when `astOptions` is provided.
451
+ */
452
+ async function analyzeParameterCoverage(params, testGlob, astOptions) {
453
+ const testFiles = await (0, fast_glob_1.default)(testGlob, { onlyFiles: true });
454
+ // Pre-read all test files and extract test segments
455
+ const allSegments = [];
456
+ for (const filePath of testFiles) {
457
+ const contents = fs.readFileSync(filePath, 'utf-8');
458
+ allSegments.push(...extractTestSegments(contents));
459
+ }
460
+ // Build AST interaction map if AST options provided
461
+ const astMap = astOptions
462
+ ? buildAstInteractionMap(testFiles, astOptions)
463
+ : null;
464
+ return params.map((param) => {
465
+ // ── Text-scan pass ──────────────────────────────────────────────────────
466
+ let validValue = false;
467
+ let boundaryValue = false;
468
+ let missing = false;
469
+ let invalidValue = false;
470
+ for (const segment of allSegments) {
471
+ const result = classifySegment(segment, param);
472
+ validValue = validValue || result.validValue;
473
+ boundaryValue = boundaryValue || result.boundaryValue;
474
+ missing = missing || result.missing;
475
+ invalidValue = invalidValue || result.invalidValue;
476
+ }
477
+ // ── AST augmentation pass ───────────────────────────────────────────────
478
+ let astMetadata;
479
+ if (astMap !== null) {
480
+ // Collect all interactions that match this parameter's endpoint path
481
+ const matchingInteractions = [];
482
+ for (const [, interactions] of astMap) {
483
+ for (const interaction of interactions) {
484
+ if (pathMatches(interaction, param.path) &&
485
+ interaction.method.toUpperCase() === param.method.toUpperCase()) {
486
+ matchingInteractions.push(interaction);
487
+ }
488
+ }
489
+ }
490
+ if (matchingInteractions.length > 0) {
491
+ const astResult = deriveAstCoverage(matchingInteractions);
492
+ // Merge: OR semantics — AST supplements but never removes text-scan coverage
493
+ validValue = validValue || astResult.validValue;
494
+ boundaryValue = boundaryValue || astResult.boundaryValue;
495
+ missing = missing || astResult.missing;
496
+ invalidValue = invalidValue || astResult.invalidValue;
497
+ astMetadata = astResult.astMetadata;
498
+ }
499
+ }
500
+ const categoriesCovered = [validValue, boundaryValue, missing, invalidValue].filter(Boolean).length;
501
+ const ratio = categoriesCovered / 4;
502
+ return { parameter: param, validValue, boundaryValue, missing, invalidValue, ratio, astMetadata };
503
+ });
504
+ }
505
+ // ─── Report building ──────────────────────────────────────────────────────────
506
+ /**
507
+ * Aggregate individual parameter coverages into a summary report.
508
+ */
509
+ function buildParameterCoverageReport(coverages) {
510
+ const total = coverages.length;
511
+ if (total === 0) {
512
+ return {
513
+ totalParameters: 0,
514
+ averageCoverage: 0,
515
+ fullyCoveredPercentage: 0,
516
+ uncoveredParameters: [],
517
+ parameters: [],
518
+ };
519
+ }
520
+ const totalRatio = coverages.reduce((sum, c) => sum + c.ratio, 0);
521
+ const averageCoverage = Math.round((totalRatio / total) * 10000) / 100;
522
+ const fullyCovered = coverages.filter((c) => c.ratio === 1).length;
523
+ const fullyCoveredPercentage = Math.round((fullyCovered / total) * 10000) / 100;
524
+ const uncoveredParameters = coverages
525
+ .filter((c) => c.ratio === 0)
526
+ .map((c) => c.parameter);
527
+ return { totalParameters: total, averageCoverage, fullyCoveredPercentage, uncoveredParameters, parameters: coverages };
528
+ }
529
+ // ─── Report generation ────────────────────────────────────────────────────────
530
+ /**
531
+ * Write JSON and HTML parameter-coverage reports to the given directory.
532
+ */
533
+ function generateParameterReports(report, reportsDir) {
534
+ if (!fs.existsSync(reportsDir)) {
535
+ fs.mkdirSync(reportsDir, { recursive: true });
536
+ }
537
+ // ── JSON ─────────────────────────────────────────────────────────────────
538
+ const jsonPath = path.join(reportsDir, 'parameter-coverage.json');
539
+ const jsonReport = {
540
+ totalParameters: report.totalParameters,
541
+ averageCoverage: report.averageCoverage,
542
+ fullyCoveredPercentage: report.fullyCoveredPercentage,
543
+ uncoveredParameters: report.uncoveredParameters.map(({ endpoint, name, location, required }) => ({
544
+ endpoint,
545
+ name,
546
+ location,
547
+ required,
548
+ })),
549
+ parameters: report.parameters.map(({ parameter: p, validValue, boundaryValue, missing, invalidValue, ratio }) => ({
550
+ endpoint: p.endpoint,
551
+ name: p.name,
552
+ location: p.location,
553
+ required: p.required,
554
+ validValue,
555
+ boundaryValue,
556
+ missing,
557
+ invalidValue,
558
+ coveragePercent: Math.round(ratio * 100),
559
+ })),
560
+ };
561
+ fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), 'utf-8');
562
+ // ── HTML ─────────────────────────────────────────────────────────────────
563
+ const htmlPath = path.join(reportsDir, 'parameter-coverage.html');
564
+ const rows = report.parameters
565
+ .map(({ parameter: p, validValue, boundaryValue, missing, invalidValue, ratio }) => {
566
+ const pct = Math.round(ratio * 100);
567
+ const rowClass = ratio === 1 ? 'full' : ratio === 0 ? 'none' : 'partial';
568
+ const cell = (covered) => covered ? '<td class="yes">✅</td>' : '<td class="no">❌</td>';
569
+ return ` <tr class="${rowClass}">
570
+ <td>${p.endpoint}</td>
571
+ <td>${p.name}</td>
572
+ <td>${p.location}</td>
573
+ <td>${p.required ? 'Yes' : 'No'}</td>
574
+ ${cell(validValue)}
575
+ ${cell(boundaryValue)}
576
+ ${cell(missing)}
577
+ ${cell(invalidValue)}
578
+ <td>${pct}%</td>
579
+ </tr>`;
580
+ })
581
+ .join('\n');
582
+ const html = `<!DOCTYPE html>
583
+ <html lang="en">
584
+ <head>
585
+ <meta charset="UTF-8">
586
+ <title>Parameter Coverage Report</title>
587
+ <style>
588
+ body { font-family: sans-serif; padding: 2rem; }
589
+ h1 { margin-bottom: 0.5rem; }
590
+ .summary { margin-bottom: 1.5rem; font-size: 1.1rem; }
591
+ table { border-collapse: collapse; width: 100%; }
592
+ th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
593
+ th { background: #f0f0f0; }
594
+ tr.full { background: #e6ffe6; }
595
+ tr.partial { background: #fffde6; }
596
+ tr.none { background: #ffe6e6; }
597
+ td.yes { color: #2a7a2a; font-weight: bold; }
598
+ td.no { color: #aa2222; font-weight: bold; }
599
+ </style>
600
+ </head>
601
+ <body>
602
+ <h1>Parameter Coverage Report</h1>
603
+ <div class="summary">
604
+ <strong>${report.totalParameters}</strong> parameters analyzed &mdash;
605
+ average coverage <strong>${report.averageCoverage}%</strong> &mdash;
606
+ fully covered <strong>${report.fullyCoveredPercentage}%</strong>
607
+ </div>
608
+ <table>
609
+ <thead>
610
+ <tr>
611
+ <th>Endpoint</th>
612
+ <th>Parameter</th>
613
+ <th>Location</th>
614
+ <th>Required</th>
615
+ <th>Valid</th>
616
+ <th>Boundary</th>
617
+ <th>Missing</th>
618
+ <th>Invalid</th>
619
+ <th>Coverage %</th>
620
+ </tr>
621
+ </thead>
622
+ <tbody>
623
+ ${rows}
624
+ </tbody>
625
+ </table>
626
+ </body>
627
+ </html>`;
628
+ fs.writeFileSync(htmlPath, html, 'utf-8');
629
+ }