api-tests-coverage 1.0.25 → 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 (59) hide show
  1. package/dist/dashboard/dist/assets/_basePickBy-DIphIltc.js +1 -0
  2. package/dist/dashboard/dist/assets/_baseUniq-D57u2_9m.js +1 -0
  3. package/dist/dashboard/dist/assets/arc-DQosMxPM.js +1 -0
  4. package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-CNbtIqHR.js +36 -0
  5. package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-NgdJaQvK.js +122 -0
  6. package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-ChTe70Dn.js +10 -0
  7. package/dist/dashboard/dist/assets/channel-B3Mj1BTw.js +1 -0
  8. package/dist/dashboard/dist/assets/chunk-4BX2VUAB-BS3-4dfL.js +1 -0
  9. package/dist/dashboard/dist/assets/chunk-55IACEB6-BCczdImM.js +1 -0
  10. package/dist/dashboard/dist/assets/chunk-B4BG7PRW-D6Mi4ccz.js +165 -0
  11. package/dist/dashboard/dist/assets/chunk-DI55MBZ5-B0tOisd5.js +220 -0
  12. package/dist/dashboard/dist/assets/chunk-FMBD7UC4-RSShKwSG.js +15 -0
  13. package/dist/dashboard/dist/assets/chunk-QN33PNHL-DFyjAoyD.js +1 -0
  14. package/dist/dashboard/dist/assets/chunk-QZHKN3VN-ARq4habW.js +1 -0
  15. package/dist/dashboard/dist/assets/chunk-TZMSLE5B-DrmzpdLp.js +1 -0
  16. package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-cvlgQ4cC.js +1 -0
  17. package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-cvlgQ4cC.js +1 -0
  18. package/dist/dashboard/dist/assets/clone-DRiM0_7k.js +1 -0
  19. package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-_dXvVagP.js +1 -0
  20. package/dist/dashboard/dist/assets/dagre-6UL2VRFP-BfhkcdcZ.js +4 -0
  21. package/dist/dashboard/dist/assets/diagram-PSM6KHXK-C8bgfsC2.js +24 -0
  22. package/dist/dashboard/dist/assets/diagram-QEK2KX5R-SPnyk4NX.js +43 -0
  23. package/dist/dashboard/dist/assets/diagram-S2PKOQOG-Cv8CAseP.js +24 -0
  24. package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-DHMIYnca.js +60 -0
  25. package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-B-9A_TD6.js +162 -0
  26. package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-DDmcIEO0.js +267 -0
  27. package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-D4YFQ0Qf.js +65 -0
  28. package/dist/dashboard/dist/assets/graph-DI2MOSai.js +1 -0
  29. package/dist/dashboard/dist/assets/index-BQfUzgMV.js +778 -0
  30. package/dist/dashboard/dist/assets/index-Bpho1Ov5.css +1 -0
  31. package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-BULYGXV8.js +2 -0
  32. package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CpGd67rs.js +139 -0
  33. package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-CXjvcKGc.js +89 -0
  34. package/dist/dashboard/dist/assets/layout-D5qgY_UX.js +1 -0
  35. package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-CKZrc1IF.js +68 -0
  36. package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-uIOJq-u0.js +30 -0
  37. package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-D5g_wTRC.js +7 -0
  38. package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-BLpGY-Om.js +64 -0
  39. package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-E0klRQfk.js +10 -0
  40. package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-Byy1IdkL.js +145 -0
  41. package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-x3lHxmNY.js +1 -0
  42. package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-D0gUM6SR.js +1 -0
  43. package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-D490JqJU.js +61 -0
  44. package/dist/dashboard/dist/assets/treemap-GDKQZRPO-C5Nk6dQh.js +162 -0
  45. package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-CYsIKi3H.js +7 -0
  46. package/dist/dashboard/dist/index.html +2 -2
  47. package/dist/src/analyzeHelpers.d.ts +60 -0
  48. package/dist/src/analyzeHelpers.d.ts.map +1 -0
  49. package/dist/src/analyzeHelpers.js +670 -0
  50. package/dist/src/config/defaultConfig.js +1 -1
  51. package/dist/src/index.js +553 -615
  52. package/dist/src/reporting.d.ts +10 -0
  53. package/dist/src/reporting.d.ts.map +1 -1
  54. package/dist/src/reporting.js +3 -0
  55. package/dist/src/summary/evaluateMetrics.js +3 -2
  56. package/dist/src/unitAnalysis.d.ts +132 -1
  57. package/dist/src/unitAnalysis.d.ts.map +1 -1
  58. package/dist/src/unitAnalysis.js +1139 -185
  59. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
+ const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const endpointCoverage_1 = require("./endpointCoverage");
40
41
  const parameterCoverage_1 = require("./parameterCoverage");
@@ -44,6 +45,7 @@ const errorCoverage_1 = require("./errorCoverage");
44
45
  const securityCoverage_1 = require("./securityCoverage");
45
46
  const perfResilienceCoverage_1 = require("./perfResilienceCoverage");
46
47
  const compatibilityCoverage_1 = require("./compatibilityCoverage");
48
+ const qualityGate_1 = require("./qualityGate");
47
49
  const springCloudContractParser_1 = require("./contracts/springCloudContractParser");
48
50
  const pactBrokerClient_1 = require("./contracts/pactBrokerClient");
49
51
  const schema_1 = require("./streaming/schema");
@@ -70,11 +72,10 @@ const routeInference_1 = require("./inference/routeInference");
70
72
  const scanManifest_1 = require("./inference/scanManifest");
71
73
  const serveDashboard_1 = require("./serveDashboard");
72
74
  const unitAnalysis_1 = require("./unitAnalysis");
75
+ const analyzeHelpers_1 = require("./analyzeHelpers");
73
76
  // Register all language AST analyzers at startup.
74
77
  // This side-effect import ensures each language module's registerAnalyzer() call runs.
75
78
  (0, astAnalysisOrchestrator_1.registerAllAnalyzers)();
76
- /** Keywords that indicate a test is exercising an error/failure path. */
77
- const ERROR_TEST_KEYWORDS = ['error', 'fail', 'throw', 'exception', 'reject', 'invalid', 'blank', 'missing'];
78
79
  const program = new commander_1.Command();
79
80
  program
80
81
  .name('api-tests-coverage-analyzer')
@@ -181,7 +182,7 @@ async function finaliseObservability(allResults, thresholds, metricsPort, servic
181
182
  // Log threshold breaches
182
183
  for (const r of allResults) {
183
184
  const t = thresholds[r.type];
184
- if (t !== undefined && r.coveragePercent < t) {
185
+ if (t !== undefined && r.totalItems > 0 && r.coveragePercent < t) {
185
186
  (0, observability_1.logThresholdBreach)(logger, r.type, r.coveragePercent, t);
186
187
  }
187
188
  }
@@ -1562,102 +1563,64 @@ program
1562
1563
  .command('analyze')
1563
1564
  .description('Zero-config full analysis: discover project artifacts, infer missing rules/flows, compute coverage.')
1564
1565
  .option('--root <dir>', 'Project root to analyze (default: current working directory)')
1566
+ .option('--spec <path>', 'Path to the current OpenAPI/Swagger spec file (auto-detected when omitted)')
1567
+ .option('--spec-old <path>', 'Path to the previous OpenAPI/Swagger spec file for compatibility analysis')
1565
1568
  .option('--reports-dir <dir>', 'Directory to write reports to (default: reports/)')
1566
1569
  .option('--export-inferred-rules', 'Export inferred rules/flows as editable YAML files')
1567
- .option('--no-infer-business-rules', 'Disable business rule inference')
1568
- .option('--no-infer-integration-flows', 'Disable integration flow inference')
1570
+ .option('--no-infer-business-rules', 'Disable business rule inference for this run when config would otherwise allow it')
1571
+ .option('--no-infer-integration-flows', 'Disable integration flow inference for this run when config would otherwise allow it')
1569
1572
  .option('--dashboard', 'Start the coverage dashboard after analysis')
1570
1573
  .option('--port <port>', 'Port for the dashboard server (requires --dashboard)', parseInt)
1571
1574
  .option('--open', 'Open the dashboard in your browser automatically (requires --dashboard)')
1572
1575
  .action(async (options) => {
1573
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
1576
+ var _a, _b, _c, _d, _e, _f, _g;
1574
1577
  const { metricsPort, serviceName } = setupObservability();
1575
1578
  const logger = (0, observability_1.getLogger)();
1576
1579
  const configPath = program.opts()['config'];
1577
1580
  const analyzerCfg = (0, config_1.loadCentralConfig)(configPath);
1578
- const rootDir = (_a = options['root']) !== null && _a !== void 0 ? _a : process.cwd();
1579
- const reportsDir = (_d = (_b = options['reportsDir']) !== null && _b !== void 0 ? _b : (_c = analyzerCfg.reports) === null || _c === void 0 ? void 0 : _c.outputDir) !== null && _d !== void 0 ? _d : 'reports';
1580
- const doInferRules = options['inferBusinessRules'] !== false &&
1581
- ((_f = (_e = analyzerCfg.analysis) === null || _e === void 0 ? void 0 : _e.inferBusinessRules) !== null && _f !== void 0 ? _f : true);
1582
- const doInferFlows = options['inferIntegrationFlows'] !== false &&
1583
- ((_h = (_g = analyzerCfg.analysis) === null || _g === void 0 ? void 0 : _g.inferIntegrationFlows) !== null && _h !== void 0 ? _h : true);
1584
- const agnosticDisc = (_k = (_j = analyzerCfg.analysis) === null || _j === void 0 ? void 0 : _j.agnosticDiscovery) !== null && _k !== void 0 ? _k : true;
1585
- logger.info({ event: 'analyze_start', rootDir }, 'Starting agnostic project analysis');
1586
- // ── 1. Discover project artifacts ──────────────────────────────────────
1581
+ const startedAt = Date.now();
1582
+ const rootDir = path.resolve((_a = options['root']) !== null && _a !== void 0 ? _a : process.cwd());
1583
+ const reportsDir = path.resolve((_d = (_b = options['reportsDir']) !== null && _b !== void 0 ? _b : (_c = analyzerCfg.reports) === null || _c === void 0 ? void 0 : _c.outputDir) !== null && _d !== void 0 ? _d : 'reports');
1584
+ const doInferRules = ((_e = analyzerCfg.analysis) === null || _e === void 0 ? void 0 : _e.inferBusinessRules) !== false && options['inferBusinessRules'] !== false;
1585
+ const doInferFlows = ((_f = analyzerCfg.analysis) === null || _f === void 0 ? void 0 : _f.inferIntegrationFlows) !== false && options['inferIntegrationFlows'] !== false;
1586
+ const fsMod = fs;
1587
+ fsMod.mkdirSync(reportsDir, { recursive: true });
1588
+ logger.info({ event: 'analyze_start', rootDir }, 'Starting full analyze command');
1587
1589
  const artifacts = (0, projectDiscovery_1.discoverProject)({
1588
1590
  rootDir,
1589
1591
  generatedSourceDirs: analyzerCfg.project.generatedSourceDirs,
1590
1592
  });
1593
+ const specPath = await (0, analyzeHelpers_1.detectOpenApiSpec)(rootDir, reportsDir, options['spec']);
1594
+ if (specPath) {
1595
+ console.log(`Auto-detected OpenAPI spec at: ${specPath}`);
1596
+ }
1591
1597
  console.log('\n=== API Test Coverage Analyzer ===');
1592
1598
  console.log(`Project root: ${rootDir}`);
1593
1599
  console.log(`Languages detected: ${artifacts.languages.join(', ') || 'none'}`);
1594
1600
  console.log(`Frameworks detected: ${artifacts.frameworks.join(', ') || 'none'}`);
1595
- console.log(`API specs found: ${artifacts.specs.length}`);
1596
1601
  console.log(`Test files found: ${artifacts.testFiles.length}`);
1597
1602
  console.log(`Service files found: ${artifacts.serviceFiles.length}`);
1598
- if (!agnosticDisc && artifacts.specs.length === 0) {
1599
- console.error('\n[ERROR] No API spec found and agnosticDiscovery is disabled. Aborting.');
1600
- process.exitCode = 1;
1601
- return;
1602
- }
1603
1603
  const warnings = [];
1604
1604
  let inferredRulesResult = null;
1605
1605
  let inferredFlowsResult = null;
1606
- // ── 2. Business rule inference ─────────────────────────────────────────
1607
1606
  if (doInferRules) {
1608
1607
  inferredRulesResult = (0, businessRuleInference_1.inferBusinessRules)(artifacts.serviceFiles, warnings);
1609
- const rulesPath = (0, businessRuleInference_1.writeInferredBusinessRules)(inferredRulesResult, reportsDir);
1610
- console.log(`\nBusiness Rule Inference`);
1611
- console.log(` Rules detected in service code: ${inferredRulesResult.rules.length}`);
1612
- console.log(` Written to: ${rulesPath}`);
1613
- if (options['exportInferredRules']) {
1614
- const fsMod = require('fs');
1615
- const yaml = inferredRulesResult.rules.map((r) => [
1616
- `- id: ${r.id}`,
1617
- ` name: ${r.name}`,
1618
- ` type: ${r.type}`,
1619
- r.endpoint ? ` endpoint: "${r.endpoint}"` : null,
1620
- ` condition: "${r.condition.replace(/"/g, "'")}"`,
1621
- ` source: ${r.source_location}`,
1622
- ].filter(Boolean).join('\n')).join('\n');
1623
- fsMod.writeFileSync(path.join(rootDir, 'generated-business-rules.yaml'), yaml, 'utf-8');
1624
- console.log(' Exported: generated-business-rules.yaml');
1625
- }
1626
- }
1627
- // ── 3. Integration flow inference ──────────────────────────────────────
1608
+ (0, businessRuleInference_1.writeInferredBusinessRules)(inferredRulesResult, reportsDir);
1609
+ }
1628
1610
  if (doInferFlows) {
1629
1611
  inferredFlowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings);
1630
- const flowsPath = (0, integrationFlowInference_1.writeInferredIntegrationFlows)(inferredFlowsResult, reportsDir);
1631
- console.log(`\nIntegration Flow Inference`);
1632
- console.log(` Multi-step flows detected in tests: ${inferredFlowsResult.flows.length}`);
1633
- console.log(` Written to: ${flowsPath}`);
1634
- if (options['exportInferredRules']) {
1635
- const fs = require('fs');
1636
- const yaml = inferredFlowsResult.flows.map((f) => [
1637
- `- id: ${f.id}`,
1638
- ` name: "${f.name}"`,
1639
- ` steps:`,
1640
- ...f.steps.map((s) => ` - { method: ${s.method}, path: "${s.path}" }`),
1641
- ].join('\n')).join('\n');
1642
- fs.writeFileSync(path.join(rootDir, 'generated-integration-flows.yaml'), yaml, 'utf-8');
1643
- console.log(' Exported: generated-integration-flows.yaml');
1644
- }
1645
- }
1646
- // ── 4. Full coverage analysis → coverage-summary.json ─────────────────
1647
- const allCoverageResults = [];
1648
- const fsMod = require('fs');
1649
- // When test files are explicitly discovered, use them directly.
1650
- // Otherwise fall back to a test-file-pattern glob (avoids scanning node_modules
1651
- // or the entire project tree, which can hang on large repositories).
1652
- const testsGlob = artifacts.testFiles.length > 0
1653
- ? '{' + artifacts.testFiles.join(',') + '}'
1654
- : path.join(rootDir, '**', '*.{test,spec}.{js,ts,jsx,tsx,mjs,cjs,py,rb}');
1612
+ (0, integrationFlowInference_1.writeInferredIntegrationFlows)(inferredFlowsResult, reportsDir);
1613
+ }
1614
+ const discoveredTestsPattern = `{${artifacts.testFiles.join(',')}}`;
1615
+ const discoveredSourcePattern = `{${artifacts.serviceFiles.join(',')}}`;
1616
+ const testsGlob = artifacts.testFiles.length > 0 && discoveredTestsPattern.length <= 8000
1617
+ ? discoveredTestsPattern
1618
+ : path.join(rootDir, '**', '*.{test,spec}.{js,ts,jsx,tsx,mjs,cjs,py,rb,java,kt}');
1619
+ const sourceGlob = artifacts.serviceFiles.length > 0 && discoveredSourcePattern.length <= 8000
1620
+ ? discoveredSourcePattern
1621
+ : path.join(rootDir, '**/*.{js,jsx,ts,tsx,java,kt,py,rb}');
1655
1622
  const detectedLanguages = artifacts.languages;
1656
- // Pre-compute test entries once from the discovered test files.
1657
- // These are reused for endpoint, error, and business-rule coverage matching
1658
- // (avoids multiple glob expansions which can hang on large projects).
1659
1623
  const TEST_DECL_RE = /\b(?:test|it)\s*\(\s*(['"`])([\s\S]*?)\1/g;
1660
- // Java/Kotlin JUnit: @Test followed by a method declaration
1661
1624
  const JAVA_TEST_RE = /@Test\b[^{]*?(?:public|protected|private|default)?\s+(?:\w+\s+)?(\w+)\s*\(\s*\)/g;
1662
1625
  const testEntries = artifacts.testFiles.flatMap((tf) => {
1663
1626
  let content = '';
@@ -1671,590 +1634,552 @@ program
1671
1634
  const contentLower = content.toLowerCase();
1672
1635
  const ext = path.extname(tf).toLowerCase();
1673
1636
  const isJavaLike = ext === '.java' || ext === '.kt' || ext === '.kts';
1674
- let m;
1637
+ let javaMatch;
1675
1638
  if (isJavaLike) {
1676
- // Extract @Test-annotated method names; convert snake_case to spaces for keyword matching
1677
1639
  JAVA_TEST_RE.lastIndex = 0;
1678
- while ((m = JAVA_TEST_RE.exec(content)) !== null) {
1679
- descriptions.push(m[1].replace(/_/g, ' ').toLowerCase());
1640
+ while ((javaMatch = JAVA_TEST_RE.exec(content)) !== null) {
1641
+ descriptions.push(javaMatch[1].replace(/_/g, ' ').toLowerCase());
1680
1642
  }
1681
1643
  }
1682
1644
  else {
1645
+ let jsMatch;
1683
1646
  TEST_DECL_RE.lastIndex = 0;
1684
- while ((m = TEST_DECL_RE.exec(content)) !== null) {
1685
- descriptions.push(m[2].toLowerCase());
1647
+ while ((jsMatch = TEST_DECL_RE.exec(content)) !== null) {
1648
+ descriptions.push(jsMatch[2].toLowerCase());
1686
1649
  }
1687
1650
  }
1688
1651
  return [{ file: tf, contentLower, descriptions, isJavaLike }];
1689
1652
  });
1690
- if (artifacts.specs.length > 0) {
1691
- for (const specPath of artifacts.specs) {
1692
- // ── 4a. Endpoint coverage ───────────────────────────────────────────
1693
- try {
1694
- console.log(`\nAnalyzing endpoint coverage for: ${path.basename(specPath)}`);
1695
- const endpoints = await (0, endpointCoverage_1.parseOpenApiSpec)(specPath);
1696
- const coverageMap = await (0, endpointCoverage_1.analyzeTestCoverage)(endpoints, testsGlob, detectedLanguages);
1697
- const report = (0, endpointCoverage_1.buildCoverageReport)(coverageMap);
1698
- const result = {
1699
- type: 'endpoint',
1700
- totalItems: report.total,
1701
- coveredItems: report.covered,
1702
- coveragePercent: report.percentage,
1703
- details: report,
1704
- };
1705
- allCoverageResults.push(result);
1706
- console.log(` ${report.covered}/${report.total} endpoints covered (${report.percentage}%)`);
1707
- }
1708
- catch (err) {
1709
- warnings.push(`Endpoint coverage failed for ${path.basename(specPath)}: ` +
1710
- `${err instanceof Error ? err.message : String(err)}`);
1711
- }
1712
- // ── 4b. Parameter coverage ──────────────────────────────────────────
1713
- try {
1714
- const parameters = await (0, parameterCoverage_1.parseParameters)(specPath);
1715
- const astParamOptions = {
1716
- astConfig: (_m = (_l = analyzerCfg.analysis) === null || _l === void 0 ? void 0 : _l.ast) !== null && _m !== void 0 ? _m : {},
1717
- deepConfig: undefined,
1718
- };
1719
- const paramCoverages = await (0, parameterCoverage_1.analyzeParameterCoverage)(parameters, testsGlob, astParamOptions);
1720
- const paramReport = (0, parameterCoverage_1.buildParameterCoverageReport)(paramCoverages);
1721
- const paramResult = {
1722
- type: 'parameter',
1723
- totalItems: paramReport.totalParameters,
1724
- coveredItems: paramCoverages.filter((c) => c.ratio > 0).length,
1725
- coveragePercent: paramReport.averageCoverage,
1726
- details: paramReport,
1727
- };
1728
- allCoverageResults.push(paramResult);
1729
- console.log(` ${paramResult.coveredItems}/${paramResult.totalItems} parameters covered (${paramReport.averageCoverage}%)`);
1730
- }
1731
- catch (err) {
1732
- warnings.push(`Parameter coverage failed for ${path.basename(specPath)}: ` +
1733
- `${err instanceof Error ? err.message : String(err)}`);
1734
- }
1735
- // ── 4c. Error scenario coverage ─────────────────────────────────────
1736
- try {
1737
- const scenarios = await (0, errorCoverage_1.parseErrorScenarios)(specPath);
1738
- if (scenarios.length > 0) {
1739
- const astErrorOptions = {
1740
- astConfig: (_p = (_o = analyzerCfg.analysis) === null || _o === void 0 ? void 0 : _o.ast) !== null && _p !== void 0 ? _p : {},
1741
- deepConfig: undefined,
1742
- };
1743
- const errorCoverages = await (0, errorCoverage_1.analyzeErrorCoverage)(scenarios, testsGlob, astErrorOptions);
1744
- const errorReport = (0, errorCoverage_1.buildErrorCoverageReport)(errorCoverages);
1745
- const errorResult = {
1746
- type: 'error',
1747
- totalItems: errorReport.total,
1748
- coveredItems: errorReport.covered,
1749
- coveragePercent: errorReport.percentage,
1750
- details: errorReport,
1751
- };
1752
- allCoverageResults.push(errorResult);
1753
- console.log(` ${errorReport.covered}/${errorReport.total} error scenarios covered (${errorReport.percentage}%)`);
1754
- }
1755
- }
1756
- catch (err) {
1757
- warnings.push(`Error coverage failed for ${path.basename(specPath)}: ` +
1758
- `${err instanceof Error ? err.message : String(err)}`);
1653
+ const loadResultCandidates = [
1654
+ path.join(rootDir, 'load-results.json'),
1655
+ path.join(rootDir, 'load-results.self-analysis.json'),
1656
+ path.join(rootDir, 'reports', 'load-results.json'),
1657
+ path.join(rootDir, 'reports', 'load-results.self-analysis.json'),
1658
+ ].filter((candidate) => fsMod.existsSync(candidate));
1659
+ const resilienceKeywordPattern = /\b(circuit.?breaker|retry|timeout|fallback|bulkhead)\b/i;
1660
+ const resiliencePatternFiles = artifacts.serviceFiles.filter((filePath) => {
1661
+ try {
1662
+ return resilienceKeywordPattern.test(fsMod.readFileSync(filePath, 'utf-8'));
1663
+ }
1664
+ catch {
1665
+ return false;
1666
+ }
1667
+ });
1668
+ const performanceFilesScanned = artifacts.serviceFiles.length + loadResultCandidates.length;
1669
+ const allCoverageResults = [];
1670
+ const endpointResult = await (0, analyzeHelpers_1.runMetricSafe)('endpoint', { filesScanned: artifacts.serviceFiles.length, scanDurationMs: 0, detectionMethod: specPath ? 'spec-driven' : 'inferred' }, async () => {
1671
+ const metricStartedAt = Date.now();
1672
+ if (specPath) {
1673
+ const endpoints = await (0, endpointCoverage_1.parseOpenApiSpec)(specPath);
1674
+ const coverageMap = await (0, endpointCoverage_1.analyzeTestCoverage)(endpoints, testsGlob, detectedLanguages);
1675
+ const report = (0, endpointCoverage_1.buildCoverageReport)(coverageMap);
1676
+ (0, endpointCoverage_1.generateReports)(report, reportsDir);
1677
+ if (report.total === 0) {
1678
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('endpoint', (0, analyzeHelpers_1.requiredNaReason)('endpoint', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, report);
1759
1679
  }
1680
+ return {
1681
+ type: 'endpoint',
1682
+ totalItems: report.total,
1683
+ coveredItems: report.covered,
1684
+ coveragePercent: report.percentage,
1685
+ scanMeta: { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
1686
+ details: report,
1687
+ };
1760
1688
  }
1761
- }
1762
- else {
1763
- warnings.push('No API spec files found; attempting route inference for endpoint/error coverage.');
1764
- // ── 4a-alt. Inferred route endpoint coverage ──────────────────────────
1765
- try {
1766
- const routeResult = (0, routeInference_1.inferRoutes)(artifacts.serviceFiles, {
1767
- generatedSourceDirs: analyzerCfg.project.generatedSourceDirs,
1768
- openApiFirst: analyzerCfg.project.openApiFirst,
1769
- });
1770
- if (routeResult.routes.length > 0) {
1771
- const routesPath = (0, routeInference_1.writeInferredRoutes)(routeResult, reportsDir);
1772
- console.log(`\nRoute Inference`);
1773
- console.log(` Routes detected in service code: ${routeResult.routes.length}`);
1774
- console.log(` Written to: ${routesPath}`);
1775
- console.log(`\nAnalyzing endpoint coverage (from inferred routes)...`);
1776
- const endpointItems = routeResult.routes.map((route) => {
1777
- var _a;
1778
- const pathSegments = route.path.split('/').filter((s) => s.length > 1 && !s.startsWith(':'));
1779
- const method = route.method.toLowerCase();
1780
- // Leaf path segment is the most specific identifier (e.g., "comments", "favorite", "feed")
1781
- const leafSegment = (_a = pathSegments[pathSegments.length - 1]) !== null && _a !== void 0 ? _a : '';
1782
- const matchedTests = [];
1783
- for (const { file, contentLower, descriptions } of testEntries) {
1784
- let matched = false;
1785
- // Priority 1: handler function name appears in test file imports/calls
1786
- if (route.handlerFunction) {
1787
- const fnLower = route.handlerFunction.toLowerCase();
1788
- if (contentLower.includes(fnLower)) {
1789
- matched = true;
1790
- }
1791
- }
1792
- // Priority 2: test description mentions method + leaf path segment
1793
- if (!matched && leafSegment.length > 2) {
1794
- matched = descriptions.some((desc) => desc.includes(method) && desc.includes(leafSegment));
1795
- }
1796
- // Priority 3: test description mentions the exact path
1797
- if (!matched && route.path.length >= 1) {
1798
- matched = descriptions.some((desc) => desc.includes(route.path.toLowerCase()));
1799
- }
1800
- // Priority 4: test file directly calls the URL path (e.g., axios.get('/articles'))
1801
- if (!matched && pathSegments.length > 0) {
1802
- // Check for full path string in file (e.g., axios.get('/articles/feed'))
1803
- const quotedPathPattern = new RegExp(`['"\`]${route.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]`);
1804
- if (quotedPathPattern.test(contentLower)) {
1805
- matched = true;
1806
- }
1807
- }
1808
- if (matched) {
1809
- matchedTests.push(path.basename(file));
1810
- }
1811
- }
1812
- const covered = matchedTests.length > 0;
1813
- return {
1814
- id: `${route.method.toUpperCase()} ${route.path}`,
1815
- covered,
1816
- matchedTests,
1817
- handler_function: route.handlerFunction,
1818
- source_file: route.sourceFile,
1819
- line_number: route.lineNumber,
1820
- };
1821
- });
1822
- const coveredCount = endpointItems.filter((i) => i.covered).length;
1823
- const pct = endpointItems.length > 0 ? Math.round((coveredCount / endpointItems.length) * 100) : 0;
1824
- const endpointResult = {
1825
- type: 'endpoint',
1826
- totalItems: endpointItems.length,
1827
- coveredItems: coveredCount,
1828
- coveragePercent: pct,
1829
- details: {
1830
- total: endpointItems.length,
1831
- covered: coveredCount,
1832
- percentage: pct,
1833
- items: endpointItems,
1834
- source: 'inferred',
1835
- },
1836
- };
1837
- allCoverageResults.push(endpointResult);
1838
- console.log(` ${coveredCount}/${endpointItems.length} inferred routes have test coverage (${pct}%)`);
1839
- // ── 4c-alt. Error coverage from inferred routes + rules ──────────
1840
- if (inferredRulesResult && inferredRulesResult.rules.length > 0) {
1841
- console.log(`\nAnalyzing error coverage (from inferred business rules)...`);
1842
- const errorCandidateRules = inferredRulesResult.rules.filter((r) => r.type === 'validation' || r.type === 'business_logic');
1843
- const errorItems = errorCandidateRules.map((rule) => {
1844
- var _a;
1845
- const matchedTestDescriptions = [];
1846
- for (const { file, descriptions, isJavaLike, contentLower } of testEntries) {
1847
- // Match at TEST DESCRIPTION level, not file level
1848
- // Require: description contains an error indicator + at least one specific keyword
1849
- const specificKws = (_a = rule.specificKeywords) !== null && _a !== void 0 ? _a : [];
1850
- const matchingDescs = descriptions.filter((desc) => {
1851
- const hasErrorKeyword = ERROR_TEST_KEYWORDS.some((kw) => desc.includes(kw));
1852
- if (!hasErrorKeyword)
1853
- return false;
1854
- // If we have specific keywords, at least one must match in the description
1855
- if (specificKws.length > 0) {
1856
- return specificKws.some((kw) => desc.includes(kw.toLowerCase()));
1857
- }
1858
- // No specific keywords — use handler function name as fallback
1859
- return true;
1860
- });
1861
- if (matchingDescs.length > 0) {
1862
- matchedTestDescriptions.push(...matchingDescs.map((d) => `[${path.basename(file)}] ${d}`));
1863
- }
1864
- else if (isJavaLike) {
1865
- // Java/Kotlin: test method names rarely contain exception class names.
1866
- // Check file body for specific long keywords (≥8 chars, e.g. "authorization")
1867
- // combined with HTTP 4xx status checks or exception throws.
1868
- const specificLongKws = specificKws
1869
- .filter((k) => k.length >= 8)
1870
- .map((k) => k.toLowerCase());
1871
- if (specificLongKws.length > 0 && specificLongKws.some((kw) => contentLower.includes(kw))) {
1872
- const hasErrorInContent = /\.statuscode\s*\(\s*[45]\d{2}|throw\s+new\s+\w*exception/i.test(contentLower);
1873
- if (hasErrorInContent) {
1874
- // Prefer test methods that look like error/boundary tests
1875
- const errorDescs = descriptions.filter((d) => /\b(4\d\d|error|fail|forbidden|unauthorized|invalid|exception|not.?found)\b/.test(d));
1876
- const descsToReport = errorDescs.length > 0 ? errorDescs : descriptions.slice(0, 1);
1877
- matchedTestDescriptions.push(...descsToReport.map((d) => `[${path.basename(file)}] ${d}`));
1878
- }
1879
- }
1880
- }
1881
- }
1882
- return {
1883
- id: rule.id,
1884
- description: rule.condition,
1885
- covered: matchedTestDescriptions.length > 0,
1886
- matchedTests: matchedTestDescriptions,
1887
- source_location: rule.source_location,
1888
- code_snippet: rule.code_snippet,
1889
- };
1890
- });
1891
- const errorCovered = errorItems.filter((i) => i.covered).length;
1892
- const errorPct = errorItems.length > 0 ? Math.round((errorCovered / errorItems.length) * 100) : 0;
1893
- const errorResult = {
1894
- type: 'error',
1895
- totalItems: errorItems.length,
1896
- coveredItems: errorCovered,
1897
- coveragePercent: errorPct,
1898
- details: {
1899
- total: errorItems.length,
1900
- covered: errorCovered,
1901
- percentage: errorPct,
1902
- items: errorItems,
1903
- source: 'inferred',
1904
- },
1905
- };
1906
- allCoverageResults.push(errorResult);
1907
- console.log(` ${errorCovered}/${errorItems.length} inferred error scenarios have test coverage (${errorPct}%)`);
1908
- }
1909
- // ── 4b-alt. Parameter coverage from inferred routes + body params ──
1910
- // Extract path parameters (e.g. ':article', ':id') and body params
1911
- // (e.g. 'req.body.email') from inferred routes and business rules.
1912
- // This populates the Parameters tab even without an OpenAPI spec.
1913
- console.log(`\nAnalyzing parameter coverage (from inferred routes)...`);
1914
- try {
1915
- const paramItems = [];
1916
- // Path parameters from routes
1917
- const seenParamKeys = new Set();
1918
- for (const epItem of endpointItems) {
1919
- const route = routeResult.routes.find((r) => `${r.method.toUpperCase()} ${r.path}` === epItem.id);
1920
- if (!route)
1921
- continue;
1922
- const pathParams = route.path.split('/').filter((s) => s.startsWith(':'));
1923
- for (const param of pathParams) {
1924
- const paramName = param.slice(1); // strip ':'
1925
- const key = `${paramName}@${route.method.toUpperCase()}`;
1926
- if (seenParamKeys.has(key))
1927
- continue;
1928
- seenParamKeys.add(key);
1929
- paramItems.push({
1930
- id: `PATH :${paramName} @ ${epItem.id}`,
1931
- covered: epItem.covered, // param is covered if the route is covered
1932
- matchedTests: epItem.matchedTests,
1933
- param_type: 'path',
1934
- source_file: route.sourceFile,
1935
- line_number: route.lineNumber,
1936
- });
1937
- }
1938
- }
1939
- // Body parameters from inferred business rules (req.body.<field>)
1940
- if (inferredRulesResult) {
1941
- const BODY_PARAM_RE = /req\.body\.(\w+)/g;
1942
- const seenBodyParams = new Set();
1943
- for (const rule of inferredRulesResult.rules) {
1944
- let bm;
1945
- BODY_PARAM_RE.lastIndex = 0;
1946
- const combinedText = `${rule.condition} ${rule.code_snippet}`;
1947
- while ((bm = BODY_PARAM_RE.exec(combinedText)) !== null) {
1948
- const fieldName = bm[1];
1949
- const paramKey = `body_${fieldName}_${(_q = rule.endpoint) !== null && _q !== void 0 ? _q : ''}`;
1950
- if (seenBodyParams.has(paramKey))
1951
- continue;
1952
- seenBodyParams.add(paramKey);
1953
- const endpointLabel = (_r = rule.endpoint) !== null && _r !== void 0 ? _r : 'unknown endpoint';
1954
- // Match: any test description mentioning the field name
1955
- const fieldNameLower = fieldName.toLowerCase();
1956
- const matchedTestDescs = [];
1957
- for (const { file, descriptions } of testEntries) {
1958
- const hitting = descriptions.filter((desc) => desc.includes(fieldNameLower));
1959
- if (hitting.length > 0) {
1960
- matchedTestDescs.push(...hitting.map((d) => `[${path.basename(file)}] ${d}`));
1961
- }
1962
- }
1963
- const srcParts = (_t = (_s = rule.source_location) === null || _s === void 0 ? void 0 : _s.split(':')) !== null && _t !== void 0 ? _t : [];
1964
- const srcFile = srcParts[0];
1965
- const srcLine = srcParts[1] !== undefined ? parseInt(srcParts[1], 10) : undefined;
1966
- paramItems.push({
1967
- id: `BODY ${fieldName} @ ${endpointLabel}`,
1968
- covered: matchedTestDescs.length > 0,
1969
- matchedTests: matchedTestDescs,
1970
- param_type: 'body',
1971
- source_file: srcFile,
1972
- line_number: srcLine && srcLine > 0 ? srcLine : undefined,
1973
- });
1974
- }
1975
- }
1976
- }
1977
- if (paramItems.length > 0) {
1978
- const paramCovered = paramItems.filter((i) => i.covered).length;
1979
- const paramPct = Math.round((paramCovered / paramItems.length) * 100);
1980
- allCoverageResults.push({
1981
- type: 'parameter',
1982
- totalItems: paramItems.length,
1983
- coveredItems: paramCovered,
1984
- coveragePercent: paramPct,
1985
- details: {
1986
- total: paramItems.length,
1987
- covered: paramCovered,
1988
- percentage: paramPct,
1989
- items: paramItems,
1990
- source: 'inferred',
1991
- },
1992
- });
1993
- console.log(` ${paramCovered}/${paramItems.length} inferred parameters have test coverage (${paramPct}%)`);
1994
- }
1995
- }
1996
- catch (paramErr) {
1997
- warnings.push(`Parameter inference failed: ${paramErr instanceof Error ? paramErr.message : String(paramErr)}`);
1689
+ const routeResult = (0, routeInference_1.inferRoutes)(artifacts.serviceFiles, {
1690
+ generatedSourceDirs: analyzerCfg.project.generatedSourceDirs,
1691
+ openApiFirst: analyzerCfg.project.openApiFirst,
1692
+ });
1693
+ (0, routeInference_1.writeInferredRoutes)(routeResult, reportsDir);
1694
+ const endpointItems = routeResult.routes.map((route) => {
1695
+ var _a;
1696
+ const pathSegments = route.path.split('/').filter((segment) => segment.length > 1 && !segment.startsWith(':'));
1697
+ const leafSegment = (_a = pathSegments[pathSegments.length - 1]) !== null && _a !== void 0 ? _a : '';
1698
+ const method = route.method.toLowerCase();
1699
+ const matchedTests = [];
1700
+ for (const { file, contentLower, descriptions } of testEntries) {
1701
+ const matched = (route.handlerFunction ? contentLower.includes(route.handlerFunction.toLowerCase()) : false) ||
1702
+ (leafSegment.length > 2 && descriptions.some((desc) => desc.includes(method) && desc.includes(leafSegment))) ||
1703
+ descriptions.some((desc) => desc.includes(route.path.toLowerCase()));
1704
+ if (matched) {
1705
+ matchedTests.push(path.basename(file));
1998
1706
  }
1999
1707
  }
2000
- else {
2001
- warnings.push('No routes detected in service files; endpoint coverage skipped.');
2002
- }
1708
+ return {
1709
+ id: `${route.method.toUpperCase()} ${route.path}`,
1710
+ covered: matchedTests.length > 0,
1711
+ matchedTests,
1712
+ source_file: route.sourceFile,
1713
+ line_number: route.lineNumber,
1714
+ };
1715
+ });
1716
+ if (endpointItems.length === 0) {
1717
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('endpoint', (0, analyzeHelpers_1.requiredNaReason)('endpoint', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' }, { items: [] });
1718
+ }
1719
+ const covered = endpointItems.filter((item) => item.covered).length;
1720
+ const percentage = Math.round((covered / endpointItems.length) * 100);
1721
+ return {
1722
+ type: 'endpoint',
1723
+ totalItems: endpointItems.length,
1724
+ coveredItems: covered,
1725
+ coveragePercent: percentage,
1726
+ scanMeta: { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' },
1727
+ details: {
1728
+ total: endpointItems.length,
1729
+ covered,
1730
+ percentage,
1731
+ items: endpointItems,
1732
+ source: 'inferred',
1733
+ },
1734
+ };
1735
+ });
1736
+ allCoverageResults.push(endpointResult);
1737
+ const parameterResult = await (0, analyzeHelpers_1.runMetricSafe)('parameter', { filesScanned: artifacts.serviceFiles.length, scanDurationMs: 0, detectionMethod: specPath ? 'spec-driven' : 'inferred' }, async () => {
1738
+ var _a, _b;
1739
+ const metricStartedAt = Date.now();
1740
+ if (!specPath) {
1741
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('parameter', (0, analyzeHelpers_1.requiredNaReason)('parameter', { routesScanned: endpointResult.totalItems }), { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' });
2003
1742
  }
2004
- catch (err) {
2005
- warnings.push(`Route/error inference failed: ${err instanceof Error ? err.message : String(err)}`);
1743
+ const parameters = await (0, parameterCoverage_1.parseParameters)(specPath);
1744
+ const astParamOptions = {
1745
+ astConfig: (_b = (_a = analyzerCfg.analysis) === null || _a === void 0 ? void 0 : _a.ast) !== null && _b !== void 0 ? _b : {},
1746
+ deepConfig: undefined,
1747
+ };
1748
+ const coverages = await (0, parameterCoverage_1.analyzeParameterCoverage)(parameters, testsGlob, astParamOptions);
1749
+ const report = (0, parameterCoverage_1.buildParameterCoverageReport)(coverages);
1750
+ (0, parameterCoverage_1.generateParameterReports)(report, reportsDir);
1751
+ if (report.totalParameters === 0) {
1752
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('parameter', (0, analyzeHelpers_1.requiredNaReason)('parameter', { routesScanned: endpointResult.totalItems }), { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, report);
2006
1753
  }
2007
- }
2008
- // ── 4d. Business rules coverage ─────────────────────────────────────────
1754
+ return {
1755
+ type: 'parameter',
1756
+ totalItems: report.totalParameters,
1757
+ coveredItems: coverages.filter((coverage) => coverage.ratio > 0).length,
1758
+ coveragePercent: report.averageCoverage,
1759
+ scanMeta: { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
1760
+ details: report,
1761
+ };
1762
+ });
1763
+ allCoverageResults.push(parameterResult);
2009
1764
  const businessRulesYaml = path.join(rootDir, 'business-rules.yaml');
2010
- if (fsMod.existsSync(businessRulesYaml)) {
2011
- // Explicit YAML takes precedence
2012
- try {
2013
- console.log(`\nAnalyzing business rules coverage...`);
1765
+ const businessResult = await (0, analyzeHelpers_1.runMetricSafe)('business', { filesScanned: artifacts.serviceFiles.length, scanDurationMs: 0, detectionMethod: fsMod.existsSync(businessRulesYaml) ? 'spec-driven' : 'inferred' }, async () => {
1766
+ var _a;
1767
+ const metricStartedAt = Date.now();
1768
+ if (fsMod.existsSync(businessRulesYaml)) {
2014
1769
  const rules = (0, businessCoverage_1.parseBusinessRules)(businessRulesYaml);
2015
- const bizCoverages = await (0, businessCoverage_1.analyzeBusinessCoverage)(rules, testsGlob);
2016
- const bizReport = (0, businessCoverage_1.buildBusinessCoverageReport)(bizCoverages);
2017
- const bizResult = {
1770
+ const coverages = await (0, businessCoverage_1.analyzeBusinessCoverage)(rules, testsGlob);
1771
+ const report = (0, businessCoverage_1.buildBusinessCoverageReport)(coverages);
1772
+ (0, businessCoverage_1.generateBusinessReports)(report, reportsDir);
1773
+ if (report.total === 0) {
1774
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('business', (0, analyzeHelpers_1.requiredNaReason)('business', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, report);
1775
+ }
1776
+ return {
2018
1777
  type: 'business',
2019
- totalItems: bizReport.total,
2020
- coveredItems: bizReport.covered,
2021
- coveragePercent: bizReport.percentage,
2022
- details: bizReport,
1778
+ totalItems: report.total,
1779
+ coveredItems: report.covered,
1780
+ coveragePercent: report.percentage,
1781
+ scanMeta: { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
1782
+ details: report,
2023
1783
  };
2024
- allCoverageResults.push(bizResult);
2025
- console.log(` ${bizReport.covered}/${bizReport.total} business rules covered (${bizReport.percentage}%)`);
2026
1784
  }
2027
- catch (err) {
2028
- warnings.push(`Business rules coverage failed: ${err instanceof Error ? err.message : String(err)}`);
1785
+ const inferredRules = (_a = inferredRulesResult === null || inferredRulesResult === void 0 ? void 0 : inferredRulesResult.rules) !== null && _a !== void 0 ? _a : [];
1786
+ if (inferredRules.length === 0) {
1787
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('business', (0, analyzeHelpers_1.requiredNaReason)('business', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' }, { items: [] });
2029
1788
  }
2030
- }
2031
- else if (inferredRulesResult && inferredRulesResult.rules.length > 0) {
2032
- // Auto-inferred: use specificKeywords from rule for accurate test matching.
2033
- // Reuse the testEntries already computed for endpoint coverage avoids
2034
- // a second glob expansion (which can be slow/hang on large projects).
2035
- try {
2036
- console.log(`\nAnalyzing business rules coverage (from inferred rules)...`);
2037
- const syntheticRules = inferredRulesResult.rules.map((r) => {
2038
- const kwSet = new Set();
2039
- // Use specificKeywords extracted from the condition (most accurate)
2040
- if (r.specificKeywords && r.specificKeywords.length > 0) {
2041
- r.specificKeywords.forEach((kw) => { if (!businessRuleInference_1.KEYWORD_STOP_WORDS.has(kw))
2042
- kwSet.add(kw); });
2043
- }
2044
- else {
2045
- // Fallback: words from rule name only (filter stop words)
2046
- r.name.toLowerCase().split(/[-_\s]+/).forEach((w) => {
2047
- if (w.length > 2 && !businessRuleInference_1.KEYWORD_STOP_WORDS.has(w))
2048
- kwSet.add(w);
2049
- });
2050
- }
2051
- // Non-trivial path segments from endpoint
2052
- if (r.endpoint) {
2053
- r.endpoint.toLowerCase().split(/[/.\s:]+/)
2054
- .forEach((w) => { if (w.length > 2 && !/^(api|v\d)$/.test(w) && !businessRuleInference_1.KEYWORD_STOP_WORDS.has(w))
2055
- kwSet.add(w); });
2056
- }
2057
- return {
2058
- id: r.id,
2059
- description: r.name,
2060
- endpoints: r.endpoint ? [r.endpoint] : [],
2061
- keywords: [...kwSet],
2062
- scenarios: [],
2063
- };
1789
+ const rules = inferredRules.map((rule) => {
1790
+ var _a;
1791
+ const kwSet = new Set();
1792
+ ((_a = rule.specificKeywords) !== null && _a !== void 0 ? _a : []).forEach((keyword) => {
1793
+ if (!businessRuleInference_1.KEYWORD_STOP_WORDS.has(keyword))
1794
+ kwSet.add(keyword.toLowerCase());
2064
1795
  });
2065
- // Match rules against the testEntries already built for endpoint coverage.
2066
- // This avoids a second glob expansion and is safe when there are no test files.
2067
- const bizCoverages = syntheticRules.map((rule) => {
2068
- const kwsLower = rule.keywords.map((k) => k.toLowerCase());
2069
- const matchedDescs = [];
2070
- const matchedFileSet = new Set();
2071
- for (const { file, descriptions, isJavaLike, contentLower } of testEntries) {
2072
- const hitting = descriptions.filter((desc) => kwsLower.length > 0 && kwsLower.some((kw) => desc.includes(kw)));
2073
- if (hitting.length > 0) {
2074
- matchedDescs.push(...hitting);
2075
- matchedFileSet.add(file);
2076
- }
2077
- else if (isJavaLike) {
2078
- // Java/Kotlin: test method names may not contain exception class names.
2079
- // Fall back to checking the file body for specific long keywords (≥8 chars).
2080
- const specificLongKws = kwsLower.filter((k) => k.length >= 8);
2081
- if (specificLongKws.length > 0 && specificLongKws.some((kw) => contentLower.includes(kw))) {
2082
- // Prefer tests that look like error/boundary tests; otherwise include all
2083
- const relevantDescs = descriptions.filter((d) => /\b(4\d\d|error|fail|forbidden|unauthorized|invalid|exception|not.?found)\b/.test(d));
2084
- const descsToAdd = relevantDescs.length > 0 ? relevantDescs : descriptions;
2085
- matchedDescs.push(...descsToAdd);
2086
- matchedFileSet.add(file);
2087
- }
2088
- }
1796
+ if (kwSet.size === 0) {
1797
+ rule.name.toLowerCase().split(/[-_\s]+/).forEach((word) => {
1798
+ if (word.length > 2 && !businessRuleInference_1.KEYWORD_STOP_WORDS.has(word))
1799
+ kwSet.add(word);
1800
+ });
1801
+ }
1802
+ return {
1803
+ id: rule.id,
1804
+ description: rule.name,
1805
+ endpoints: rule.endpoint ? [rule.endpoint] : [],
1806
+ keywords: [...kwSet],
1807
+ scenarios: [],
1808
+ };
1809
+ });
1810
+ const coverages = rules.map((rule) => {
1811
+ const keywords = rule.keywords.map((keyword) => keyword.toLowerCase());
1812
+ const matchedDescriptions = [];
1813
+ const matchedFiles = new Set();
1814
+ for (const { file, descriptions } of testEntries) {
1815
+ const hits = descriptions.filter((description) => keywords.length > 0 && keywords.some((keyword) => description.includes(keyword)));
1816
+ if (hits.length > 0) {
1817
+ hits.forEach((hit) => matchedDescriptions.push(hit));
1818
+ matchedFiles.add(file);
2089
1819
  }
2090
- return {
2091
- rule,
2092
- covered: matchedDescs.length > 0,
2093
- testFiles: [...matchedFileSet],
2094
- matchedTests: matchedDescs,
2095
- scenarios: [],
2096
- };
2097
- });
2098
- const bizReport = (0, businessCoverage_1.buildBusinessCoverageReport)(bizCoverages);
2099
- const bizResult = {
2100
- type: 'business',
2101
- totalItems: bizReport.total,
2102
- coveredItems: bizReport.covered,
2103
- coveragePercent: bizReport.percentage,
2104
- details: {
2105
- ...bizReport,
2106
- inferred_details: inferredRulesResult.rules.reduce((acc, r) => {
2107
- acc[r.id] = {
2108
- source_location: r.source_location,
2109
- condition: r.condition,
2110
- code_snippet: r.code_snippet,
2111
- type: r.type,
2112
- specificKeywords: r.specificKeywords,
1820
+ }
1821
+ return {
1822
+ rule,
1823
+ covered: matchedDescriptions.length > 0,
1824
+ testFiles: [...matchedFiles],
1825
+ matchedTests: matchedDescriptions,
1826
+ scenarios: [],
1827
+ };
1828
+ });
1829
+ const report = (0, businessCoverage_1.buildBusinessCoverageReport)(coverages);
1830
+ (0, businessCoverage_1.generateBusinessReports)(report, reportsDir);
1831
+ return {
1832
+ type: 'business',
1833
+ totalItems: report.total,
1834
+ coveredItems: report.covered,
1835
+ coveragePercent: report.percentage,
1836
+ scanMeta: { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' },
1837
+ details: report.total === 0
1838
+ ? { items: [] }
1839
+ : {
1840
+ ...report,
1841
+ inferred_details: inferredRules.reduce((acc, rule) => {
1842
+ acc[rule.id] = {
1843
+ source_location: rule.source_location,
1844
+ condition: rule.condition,
1845
+ code_snippet: rule.code_snippet,
1846
+ type: rule.type,
1847
+ specificKeywords: rule.specificKeywords,
2113
1848
  };
2114
1849
  return acc;
2115
1850
  }, {}),
2116
1851
  },
2117
- };
2118
- allCoverageResults.push(bizResult);
2119
- console.log(` ${bizReport.covered}/${bizReport.total} inferred business rules have test coverage (${bizReport.percentage}%)`);
2120
- }
2121
- catch (err) {
2122
- warnings.push(`Business rules coverage failed: ${err instanceof Error ? err.message : String(err)}`);
2123
- }
2124
- }
2125
- // ── 4e. Integration flows coverage ──────────────────────────────────────
1852
+ reason: report.total === 0 ? (0, analyzeHelpers_1.requiredNaReason)('business', { filesScanned: artifacts.serviceFiles.length }) : undefined,
1853
+ status: report.total === 0 ? 'N/A' : undefined,
1854
+ };
1855
+ });
1856
+ allCoverageResults.push(businessResult);
2126
1857
  const integrationFlowsYaml = path.join(rootDir, 'integration-flows.yaml');
2127
- if (fsMod.existsSync(integrationFlowsYaml)) {
2128
- // Explicit YAML takes precedence
2129
- try {
2130
- console.log(`\nAnalyzing integration flows coverage...`);
1858
+ const integrationResult = await (0, analyzeHelpers_1.runMetricSafe)('integration', { filesScanned: artifacts.testFiles.length, scanDurationMs: 0, detectionMethod: fsMod.existsSync(integrationFlowsYaml) ? 'spec-driven' : 'inferred' }, async () => {
1859
+ var _a;
1860
+ const metricStartedAt = Date.now();
1861
+ if (fsMod.existsSync(integrationFlowsYaml)) {
2131
1862
  const flows = (0, integrationCoverage_1.parseIntegrationFlows)(integrationFlowsYaml);
2132
- const flowCoverages = await (0, integrationCoverage_1.analyzeIntegrationCoverage)(flows, testsGlob);
2133
- const flowReport = (0, integrationCoverage_1.buildIntegrationCoverageReport)(flowCoverages);
2134
- const flowResult = {
1863
+ const coverages = await (0, integrationCoverage_1.analyzeIntegrationCoverage)(flows, testsGlob);
1864
+ const report = (0, integrationCoverage_1.buildIntegrationCoverageReport)(coverages);
1865
+ (0, integrationCoverage_1.generateIntegrationReports)(report, reportsDir);
1866
+ if (report.total === 0) {
1867
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('integration', (0, analyzeHelpers_1.requiredNaReason)('integration', { filesScanned: artifacts.testFiles.length }), { filesScanned: artifacts.testFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, report);
1868
+ }
1869
+ return {
2135
1870
  type: 'integration',
2136
- totalItems: flowReport.total,
2137
- coveredItems: flowReport.complete,
2138
- coveragePercent: flowReport.percentage,
2139
- details: flowReport,
1871
+ totalItems: report.total,
1872
+ coveredItems: report.complete,
1873
+ coveragePercent: report.percentage,
1874
+ scanMeta: { filesScanned: artifacts.testFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
1875
+ details: report,
2140
1876
  };
2141
- allCoverageResults.push(flowResult);
2142
- console.log(` ${flowReport.complete}/${flowReport.total} integration flows covered (${flowReport.percentage}%)`);
2143
1877
  }
2144
- catch (err) {
2145
- warnings.push(`Integration flows coverage failed: ${err instanceof Error ? err.message : String(err)}`);
1878
+ const inferredFlows = (_a = inferredFlowsResult === null || inferredFlowsResult === void 0 ? void 0 : inferredFlowsResult.flows) !== null && _a !== void 0 ? _a : [];
1879
+ if (inferredFlows.length === 0) {
1880
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('integration', (0, analyzeHelpers_1.requiredNaReason)('integration', { filesScanned: artifacts.testFiles.length }), { filesScanned: artifacts.testFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' }, { items: [] });
2146
1881
  }
2147
- }
2148
- else if (inferredFlowsResult && inferredFlowsResult.flows.length > 0) {
2149
- // Auto-inferred: flows are extracted FROM tests, so by definition they're all covered
2150
- console.log(`\nIntegration flows coverage (from inferred flows)...`);
2151
- const syntheticItems = inferredFlowsResult.flows.map((f) => ({
2152
- id: f.id,
2153
- name: f.name,
2154
- total: f.steps.length,
2155
- covered: f.steps.length,
1882
+ const items = inferredFlows.map((flow) => ({
1883
+ id: flow.id,
1884
+ name: flow.name,
1885
+ total: flow.steps.length,
1886
+ covered: flow.steps.length,
2156
1887
  complete: true,
2157
1888
  percentage: 100,
2158
1889
  uncoveredSteps: [],
2159
1890
  }));
2160
- const syntheticReport = {
2161
- total: syntheticItems.length,
2162
- complete: syntheticItems.length,
2163
- percentage: 100,
2164
- items: syntheticItems,
1891
+ const report = {
1892
+ total: items.length,
1893
+ complete: items.length,
1894
+ percentage: items.length > 0 ? 100 : 0,
1895
+ items,
2165
1896
  };
2166
- const flowResult = {
1897
+ return {
2167
1898
  type: 'integration',
2168
- totalItems: syntheticReport.total,
2169
- coveredItems: syntheticReport.complete,
2170
- coveragePercent: syntheticReport.percentage,
2171
- details: syntheticReport,
1899
+ totalItems: report.total,
1900
+ coveredItems: report.complete,
1901
+ coveragePercent: report.percentage,
1902
+ scanMeta: { filesScanned: artifacts.testFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' },
1903
+ details: report,
2172
1904
  };
2173
- allCoverageResults.push(flowResult);
2174
- console.log(` ${syntheticReport.complete}/${syntheticReport.total} multi-step flows detected and covered (100%)`);
2175
- }
2176
- if (allCoverageResults.length > 0) {
2177
- const observabilityInfo = (0, observability_1.buildObservabilityInfo)(metricsPort);
2178
- (0, reporting_1.generateMultiFormatReports)(allCoverageResults, ['json'], reportsDir, {}, observabilityInfo);
2179
- // Append discoveryInfo to coverage-summary.json for the dashboard
2180
- const summaryPath = path.join(reportsDir, 'coverage-summary.json');
2181
- try {
2182
- const summaryJson = JSON.parse(fsMod.readFileSync(summaryPath, 'utf-8'));
2183
- summaryJson.discoveryInfo = {
2184
- projectRoot: rootDir,
2185
- analyzedAt: new Date().toISOString(),
2186
- languages: artifacts.languages,
2187
- frameworks: artifacts.frameworks,
2188
- serviceFilesCount: artifacts.serviceFiles.length,
2189
- testFilesCount: artifacts.testFiles.length,
2190
- specFilesCount: artifacts.specs.length,
2191
- analysisMode: artifacts.specs.length > 0 ? 'explicit-spec' : 'inferred',
1905
+ });
1906
+ allCoverageResults.push(integrationResult);
1907
+ const errorResult = await (0, analyzeHelpers_1.runMetricSafe)('error', { filesScanned: endpointResult.totalItems, scanDurationMs: 0, detectionMethod: specPath ? 'spec-driven' : 'inferred' }, async () => {
1908
+ var _a, _b;
1909
+ const metricStartedAt = Date.now();
1910
+ if (!specPath) {
1911
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('error', (0, analyzeHelpers_1.requiredNaReason)('error'), { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'inferred' });
1912
+ }
1913
+ const scenarios = await (0, errorCoverage_1.parseErrorScenarios)(specPath);
1914
+ if (scenarios.length === 0) {
1915
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('error', (0, analyzeHelpers_1.requiredNaReason)('error'), { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, { items: [] });
1916
+ }
1917
+ const astErrorOptions = {
1918
+ astConfig: (_b = (_a = analyzerCfg.analysis) === null || _a === void 0 ? void 0 : _a.ast) !== null && _b !== void 0 ? _b : {},
1919
+ deepConfig: undefined,
1920
+ };
1921
+ const coverages = await (0, errorCoverage_1.analyzeErrorCoverage)(scenarios, testsGlob, astErrorOptions);
1922
+ const report = (0, errorCoverage_1.buildErrorCoverageReport)(coverages);
1923
+ (0, errorCoverage_1.generateErrorReports)(report, reportsDir);
1924
+ return {
1925
+ type: 'error',
1926
+ totalItems: report.total,
1927
+ coveredItems: report.covered,
1928
+ coveragePercent: report.percentage,
1929
+ scanMeta: { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
1930
+ details: report,
1931
+ };
1932
+ });
1933
+ allCoverageResults.push(errorResult);
1934
+ const securityResult = await (0, analyzeHelpers_1.runMetricSafe)('security', { filesScanned: endpointResult.totalItems, scanDurationMs: 0, detectionMethod: specPath ? 'spec-driven' : 'ast' }, async () => {
1935
+ var _a, _b, _c;
1936
+ const metricStartedAt = Date.now();
1937
+ if (!specPath) {
1938
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('security', (0, analyzeHelpers_1.requiredNaReason)('security'), { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'ast' });
1939
+ }
1940
+ const controls = await (0, securityCoverage_1.parseSecurityControls)(specPath);
1941
+ if (controls.length === 0) {
1942
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('security', (0, analyzeHelpers_1.requiredNaReason)('security', { specDetected: true }), { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, { items: [] });
1943
+ }
1944
+ const astSecurityOptions = {
1945
+ astConfig: (_b = (_a = analyzerCfg.analysis) === null || _a === void 0 ? void 0 : _a.ast) !== null && _b !== void 0 ? _b : {},
1946
+ deepConfig: (_c = analyzerCfg.scans.coverage) === null || _c === void 0 ? void 0 : _c.deepAnalysis,
1947
+ };
1948
+ const coverages = await (0, securityCoverage_1.analyzeSecurityCoverage)(controls, testsGlob, undefined, astSecurityOptions);
1949
+ const report = (0, securityCoverage_1.buildSecurityCoverageReport)(coverages, 0);
1950
+ (0, securityCoverage_1.generateSecurityReports)(report, reportsDir);
1951
+ return {
1952
+ type: 'security',
1953
+ totalItems: report.total,
1954
+ coveredItems: report.covered,
1955
+ coveragePercent: report.percentage,
1956
+ scanMeta: { filesScanned: endpointResult.totalItems, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
1957
+ details: report,
1958
+ };
1959
+ });
1960
+ allCoverageResults.push(securityResult);
1961
+ const performanceResult = await (0, analyzeHelpers_1.runMetricSafe)('performance', { filesScanned: performanceFilesScanned, scanDurationMs: 0, detectionMethod: 'cascaded' }, async () => {
1962
+ const metricStartedAt = Date.now();
1963
+ if (!specPath || loadResultCandidates.length === 0) {
1964
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('performance', (0, analyzeHelpers_1.requiredNaReason)('performance', { filesScanned: performanceFilesScanned }), { filesScanned: performanceFilesScanned, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' });
1965
+ }
1966
+ const endpoints = await (0, perfResilienceCoverage_1.parseEndpointsFromSpec)(specPath);
1967
+ const thresholds = { responseMs: 500, errorRate: 0.05 };
1968
+ const metricsMap = (0, perfResilienceCoverage_1.parseLoadTestResults)(loadResultCandidates);
1969
+ const perfCoverages = (0, perfResilienceCoverage_1.analyzePerformanceCoverage)(endpoints, metricsMap, thresholds);
1970
+ const scenarios = (0, perfResilienceCoverage_1.buildResilienceScenarios)(endpoints);
1971
+ const resilienceCoverages = await (0, perfResilienceCoverage_1.analyzeResilienceCoverage)(scenarios, testsGlob);
1972
+ const report = (0, perfResilienceCoverage_1.buildPerfResilienceReport)(perfCoverages, resilienceCoverages);
1973
+ (0, perfResilienceCoverage_1.generatePerfResilienceReports)(report, reportsDir);
1974
+ if (report.totalEndpoints === 0) {
1975
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('performance', (0, analyzeHelpers_1.requiredNaReason)('performance', { filesScanned: performanceFilesScanned }), { filesScanned: performanceFilesScanned, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' }, report);
1976
+ }
1977
+ return {
1978
+ type: 'performance',
1979
+ totalItems: report.totalEndpoints,
1980
+ coveredItems: report.endpointsWithLoadData,
1981
+ coveragePercent: report.performanceCoveragePercent,
1982
+ scanMeta: { filesScanned: performanceFilesScanned, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' },
1983
+ details: report,
1984
+ };
1985
+ });
1986
+ allCoverageResults.push(performanceResult);
1987
+ const resilienceResult = await (0, analyzeHelpers_1.runMetricSafe)('resilience', { filesScanned: artifacts.serviceFiles.length, scanDurationMs: 0, detectionMethod: 'cascaded' }, async () => {
1988
+ const metricStartedAt = Date.now();
1989
+ if (!specPath || resiliencePatternFiles.length === 0) {
1990
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('resilience', (0, analyzeHelpers_1.requiredNaReason)('resilience', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' });
1991
+ }
1992
+ const endpoints = await (0, perfResilienceCoverage_1.parseEndpointsFromSpec)(specPath);
1993
+ const metricsMap = loadResultCandidates.length > 0 ? (0, perfResilienceCoverage_1.parseLoadTestResults)(loadResultCandidates) : new Map();
1994
+ const perfCoverages = (0, perfResilienceCoverage_1.analyzePerformanceCoverage)(endpoints, metricsMap, { responseMs: 500, errorRate: 0.05 });
1995
+ const scenarios = (0, perfResilienceCoverage_1.buildResilienceScenarios)(endpoints);
1996
+ const resilienceCoverages = await (0, perfResilienceCoverage_1.analyzeResilienceCoverage)(scenarios, testsGlob);
1997
+ const report = (0, perfResilienceCoverage_1.buildPerfResilienceReport)(perfCoverages, resilienceCoverages);
1998
+ (0, perfResilienceCoverage_1.generatePerfResilienceReports)(report, reportsDir);
1999
+ if (report.totalResilienceScenarios === 0) {
2000
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('resilience', (0, analyzeHelpers_1.requiredNaReason)('resilience', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' }, report);
2001
+ }
2002
+ return {
2003
+ type: 'resilience',
2004
+ totalItems: report.totalResilienceScenarios,
2005
+ coveredItems: report.coveredResilienceScenarios,
2006
+ coveragePercent: report.resilienceCoveragePercent,
2007
+ scanMeta: { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' },
2008
+ details: report,
2009
+ };
2010
+ });
2011
+ allCoverageResults.push(resilienceResult);
2012
+ const unitResult = await (0, analyzeHelpers_1.runMetricSafe)('unit', { filesScanned: artifacts.testFiles.length, scanDurationMs: 0, detectionMethod: 'cascaded' }, async () => {
2013
+ const metricStartedAt = Date.now();
2014
+ const execution = await (0, unitAnalysis_1.analyzeUnitTests)({
2015
+ rootDir,
2016
+ testFiles: artifacts.testFiles,
2017
+ serviceFiles: artifacts.serviceFiles,
2018
+ languages: artifacts.languages,
2019
+ config: analyzerCfg.unitAnalysis,
2020
+ });
2021
+ if (execution.result.totalItems === 0) {
2022
+ return {
2023
+ ...execution.result,
2024
+ status: 'N/A',
2025
+ reason: (0, analyzeHelpers_1.requiredNaReason)('unit', { filesScanned: artifacts.testFiles.length }),
2026
+ scanMeta: { filesScanned: artifacts.testFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' },
2192
2027
  };
2193
- fsMod.writeFileSync(summaryPath, JSON.stringify(summaryJson, null, 2), 'utf-8');
2194
2028
  }
2195
- catch {
2196
- // Non-fatal — discovery info is also in scan-manifest.json
2029
+ return {
2030
+ ...execution.result,
2031
+ scanMeta: { filesScanned: artifacts.testFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'cascaded' },
2032
+ };
2033
+ });
2034
+ allCoverageResults.push(unitResult);
2035
+ const eventResult = await (0, analyzeHelpers_1.runMetricSafe)('event', { filesScanned: artifacts.serviceFiles.length, scanDurationMs: 0, detectionMethod: 'ast' }, async () => {
2036
+ const metricStartedAt = Date.now();
2037
+ const { analyzeEventCoverage, writeEventReport } = await Promise.resolve().then(() => __importStar(require('./streaming/eventCoverage')));
2038
+ const result = analyzeEventCoverage(sourceGlob, testsGlob);
2039
+ if (result.totalItems === 0) {
2040
+ const emptyReport = {
2041
+ generatedAt: new Date().toISOString(),
2042
+ ...(0, analyzeHelpers_1.buildNaCoverageResult)('event', (0, analyzeHelpers_1.requiredNaReason)('event', { filesScanned: artifacts.serviceFiles.length }), { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'ast' }, result.details),
2043
+ topics: [],
2044
+ };
2045
+ fsMod.writeFileSync(path.join(reportsDir, 'event-coverage.json'), JSON.stringify(emptyReport, null, 2), 'utf-8');
2046
+ return emptyReport;
2197
2047
  }
2198
- console.log(`\nReports written to: ${reportsDir}`);
2199
- // ── 4f-intel. Run coverage intelligence automatically ─────────────────
2200
- try {
2201
- const coverageResultsForIntel = allCoverageResults.map((r) => ({
2202
- type: r.type,
2203
- totalItems: r.totalItems,
2204
- coveredItems: r.coveredItems,
2205
- coveragePercent: r.coveragePercent,
2206
- details: normalizeDetailsForIntelligence(r.type, r.details),
2207
- }));
2208
- const intelReport = (0, index_3.runIntelligenceEngine)({
2209
- coverageResults: coverageResultsForIntel,
2210
- languages: artifacts.languages,
2211
- frameworks: artifacts.frameworks,
2212
- projectName: path.basename(rootDir),
2213
- outDir: reportsDir,
2214
- });
2215
- console.log(`\nCoverage Intelligence: ${intelReport.summary.totalFindings} findings, ` +
2216
- `${intelReport.summary.totalRecommendations} recommendations ` +
2217
- `(${intelReport.summary.criticalUncoveredItems} critical uncovered)`);
2218
- if (intelReport.summary.recommendationsByPriority.P0 > 0) {
2219
- console.log(`⚠️ P0 Recommendations: ${intelReport.summary.recommendationsByPriority.P0} — immediate action required`);
2220
- }
2048
+ writeEventReport(result, reportsDir);
2049
+ return {
2050
+ type: 'event',
2051
+ totalItems: result.totalItems,
2052
+ coveredItems: result.coveredItems,
2053
+ coveragePercent: result.coveragePercent,
2054
+ scanMeta: { filesScanned: artifacts.serviceFiles.length, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'ast' },
2055
+ details: result.details,
2056
+ };
2057
+ });
2058
+ allCoverageResults.push(eventResult);
2059
+ const compatibilityResult = await (0, analyzeHelpers_1.runMetricSafe)('compatibility', { filesScanned: specPath ? 2 : 0, scanDurationMs: 0, detectionMethod: 'spec-driven' }, async () => {
2060
+ const metricStartedAt = Date.now();
2061
+ const oldSpecPath = options['specOld'] ? path.resolve(options['specOld']) : undefined;
2062
+ if (!specPath || !oldSpecPath || !fsMod.existsSync(oldSpecPath)) {
2063
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('compatibility', (0, analyzeHelpers_1.requiredNaReason)('compatibility'), { filesScanned: specPath ? 1 : 0, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' });
2221
2064
  }
2222
- catch (intelErr) {
2223
- warnings.push(`Coverage intelligence failed: ${intelErr instanceof Error ? intelErr.message : String(intelErr)}`);
2065
+ const oldApi = await (0, compatibilityCoverage_1.loadSpec)(oldSpecPath);
2066
+ const newApi = await (0, compatibilityCoverage_1.loadSpec)(specPath);
2067
+ const changes = (0, compatibilityCoverage_1.compareSpecs)(oldApi, newApi);
2068
+ const verificationResults = (0, compatibilityCoverage_1.verifyContracts)([], newApi);
2069
+ const report = (0, compatibilityCoverage_1.buildCompatibilityReport)(oldApi, newApi, changes, verificationResults, oldSpecPath, specPath);
2070
+ (0, compatibilityCoverage_1.generateCompatibilityReports)(report, reportsDir);
2071
+ const uniqueAffectedEndpoints = new Set(report.breakingChanges.filter((change) => change.changeType !== 'added').map((change) => `${change.method}:${change.path}`)).size;
2072
+ const coveredItems = report.totalOldEndpoints - uniqueAffectedEndpoints;
2073
+ if (report.totalOldEndpoints === 0) {
2074
+ return (0, analyzeHelpers_1.buildNaCoverageResult)('compatibility', (0, analyzeHelpers_1.requiredNaReason)('compatibility'), { filesScanned: 2, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' }, report);
2075
+ }
2076
+ return {
2077
+ type: 'compatibility',
2078
+ totalItems: report.totalOldEndpoints,
2079
+ coveredItems,
2080
+ coveragePercent: report.compatibilityPercent,
2081
+ scanMeta: { filesScanned: 2, scanDurationMs: Date.now() - metricStartedAt, detectionMethod: 'spec-driven' },
2082
+ details: report,
2083
+ };
2084
+ });
2085
+ allCoverageResults.push(compatibilityResult);
2086
+ const orderedResults = (0, analyzeHelpers_1.sortResultsInKnownOrder)(allCoverageResults);
2087
+ const effectiveThresholds = Object.fromEntries(summaryTypes_1.KNOWN_METRIC_TYPES
2088
+ .map((type) => [type, (0, qualityGate_1.effectiveThresholdForCategory)(analyzerCfg.thresholds, type)])
2089
+ .filter((entry) => entry[1] !== undefined));
2090
+ const qualityGateResult = (0, qualityGate_1.evaluateQualityGate)(orderedResults, { thresholds: effectiveThresholds, qualityGate: analyzerCfg.qualityGate });
2091
+ const enrichedResults = (0, analyzeHelpers_1.enrichResultsWithStatuses)(orderedResults, effectiveThresholds, qualityGateResult);
2092
+ const observabilityInfo = (0, observability_1.buildObservabilityInfo)(metricsPort);
2093
+ (0, reporting_1.generateMultiFormatReports)(enrichedResults, ['json'], reportsDir, Object.fromEntries(Object.entries(effectiveThresholds).filter((entry) => entry[1] !== undefined)), observabilityInfo);
2094
+ for (const result of enrichedResults) {
2095
+ if (result.type !== 'event') {
2096
+ (0, analyzeHelpers_1.writeMetricReport)(result, reportsDir);
2224
2097
  }
2225
2098
  }
2226
- // ── 4f. Write scan manifest ────────────────────────────────────────────
2099
+ const coverageResultsForIntel = enrichedResults.map((result) => ({
2100
+ type: result.type,
2101
+ totalItems: result.totalItems,
2102
+ coveredItems: result.coveredItems,
2103
+ coveragePercent: result.coveragePercent,
2104
+ details: normalizeDetailsForIntelligence(result.type, result.details),
2105
+ }));
2106
+ const intelligenceReport = (0, index_3.runIntelligenceEngine)({
2107
+ coverageResults: coverageResultsForIntel,
2108
+ languages: artifacts.languages,
2109
+ frameworks: artifacts.frameworks,
2110
+ projectName: path.basename(rootDir),
2111
+ outDir: reportsDir,
2112
+ });
2113
+ (0, observability_2.recordIntelligenceMetrics)({
2114
+ projectName: path.basename(rootDir),
2115
+ totalFindings: intelligenceReport.summary.totalFindings,
2116
+ totalRecommendations: intelligenceReport.summary.totalRecommendations,
2117
+ recommendationsByPriority: intelligenceReport.summary.recommendationsByPriority,
2118
+ maxRiskScore: intelligenceReport.summary.maxRiskScore,
2119
+ avgRiskScore: intelligenceReport.summary.avgRiskScore,
2120
+ criticalUncoveredItems: intelligenceReport.summary.criticalUncoveredItems,
2121
+ unprotectedSecurityFindings: intelligenceReport.summary.unprotectedSecurityFindings,
2122
+ languages: artifacts.languages,
2123
+ frameworks: artifacts.frameworks,
2124
+ }, path.basename(rootDir));
2125
+ const summaryInput = {
2126
+ results: enrichedResults,
2127
+ qualityGate: qualityGateResult,
2128
+ thresholds: effectiveThresholds,
2129
+ projectName: path.basename(rootDir),
2130
+ intelligenceSummary: intelligenceReport.summary,
2131
+ };
2132
+ await (0, buildSummary_1.generateBuildSummary)(summaryInput, reportsDir);
2133
+ await (0, prSummary_1.generatePrSummary)(summaryInput, reportsDir);
2134
+ (0, analyzeHelpers_1.writeArchitectureArtifacts)({
2135
+ rootDir,
2136
+ reportsDir,
2137
+ languages: artifacts.languages,
2138
+ frameworks: artifacts.frameworks,
2139
+ serviceFiles: artifacts.serviceFiles.length,
2140
+ testFiles: artifacts.testFiles.length,
2141
+ specPath,
2142
+ results: enrichedResults,
2143
+ });
2144
+ const summaryPath = path.join(reportsDir, 'coverage-summary.json');
2145
+ try {
2146
+ const summaryJson = JSON.parse(fsMod.readFileSync(summaryPath, 'utf-8'));
2147
+ summaryJson.discoveryInfo = {
2148
+ projectRoot: rootDir,
2149
+ analyzedAt: new Date().toISOString(),
2150
+ languages: artifacts.languages,
2151
+ frameworks: artifacts.frameworks,
2152
+ serviceFilesCount: artifacts.serviceFiles.length,
2153
+ testFilesCount: artifacts.testFiles.length,
2154
+ specFilesCount: specPath ? 1 : 0,
2155
+ analysisMode: specPath ? 'explicit-spec' : 'inferred',
2156
+ };
2157
+ summaryJson.qualityGate = qualityGateResult;
2158
+ fsMod.writeFileSync(summaryPath, JSON.stringify(summaryJson, null, 2), 'utf-8');
2159
+ }
2160
+ catch (err) {
2161
+ warnings.push(`Coverage summary update failed: ${err instanceof Error ? err.message : String(err)}`);
2162
+ }
2227
2163
  try {
2228
- const scanTypes = allCoverageResults.map((r) => ({
2229
- type: r.type,
2230
- source: artifacts.specs.length > 0 ? 'explicit' : 'inferred',
2231
- itemsFound: r.totalItems,
2232
- itemsCovered: r.coveredItems,
2233
- coveragePercent: r.coveragePercent,
2164
+ const scanTypes = enrichedResults.map((result) => ({
2165
+ type: result.type,
2166
+ source: result.totalItems === 0
2167
+ ? 'skipped'
2168
+ : specPath
2169
+ ? 'explicit'
2170
+ : 'inferred',
2171
+ reason: result.reason,
2172
+ itemsFound: result.totalItems,
2173
+ itemsCovered: result.coveredItems,
2174
+ coveragePercent: result.coveragePercent,
2234
2175
  }));
2235
- // Add skipped types (exclude 'business' and 'integration' since these are always attempted
2236
- // via rule/flow inference regardless of whether a spec file is present; their absence from
2237
- // allCoverageResults means no rules or flows were discovered, which is informative on its own.)
2238
- const coveredTypes = new Set(allCoverageResults.map((r) => r.type));
2239
- for (const skippedType of summaryTypes_1.KNOWN_METRIC_TYPES.filter((t) => t !== 'business' && t !== 'integration')) {
2240
- if (!coveredTypes.has(skippedType)) {
2241
- scanTypes.push({
2242
- type: skippedType,
2243
- source: 'skipped',
2244
- reason: artifacts.specs.length === 0 ? 'No API spec and no routes detected' : 'No data available',
2245
- itemsFound: 0,
2246
- itemsCovered: 0,
2247
- coveragePercent: 0,
2248
- });
2249
- }
2250
- }
2251
2176
  const manifestPath = (0, scanManifest_1.writeScanManifest)({
2252
2177
  projectRoot: rootDir,
2253
2178
  analyzedAt: new Date().toISOString(),
2254
2179
  discoveredFiles: {
2255
2180
  serviceFiles: artifacts.serviceFiles,
2256
2181
  testFiles: artifacts.testFiles,
2257
- specFiles: artifacts.specs,
2182
+ specFiles: specPath ? [specPath] : [],
2258
2183
  },
2259
2184
  languages: artifacts.languages,
2260
2185
  frameworks: artifacts.frameworks,
@@ -2265,27 +2190,37 @@ program
2265
2190
  catch (err) {
2266
2191
  warnings.push(`Scan manifest write failed: ${err instanceof Error ? err.message : String(err)}`);
2267
2192
  }
2268
- // ── 5. Emit warnings ───────────────────────────────────────────────────
2269
- for (const w of warnings) {
2270
- console.warn(`[WARN] ${w}`);
2271
- }
2272
- // ── 6. Configuration override log ──────────────────────────────────────
2273
- if (options['inferBusinessRules'] === false) {
2274
- console.log('\nConfiguration override detected: business rule inference disabled via CLI');
2193
+ for (const warning of warnings) {
2194
+ console.warn(`[WARN] ${warning}`);
2275
2195
  }
2276
- if (options['inferIntegrationFlows'] === false) {
2277
- console.log('\nConfiguration override detected: integration flow inference disabled via CLI');
2196
+ console.log('');
2197
+ console.log((0, analyzeHelpers_1.renderAnalyzeSummaryTable)({
2198
+ rootDir,
2199
+ serviceFiles: artifacts.serviceFiles.length,
2200
+ testFiles: artifacts.testFiles.length,
2201
+ durationMs: Date.now() - startedAt,
2202
+ results: enrichedResults,
2203
+ thresholds: effectiveThresholds,
2204
+ qualityGate: qualityGateResult,
2205
+ }));
2206
+ console.log(`\nReports written to: ${reportsDir}`);
2207
+ logger.info({ event: 'analyze_complete', warnings: warnings.length, metrics: enrichedResults.length }, 'Full analyze command complete');
2208
+ await finaliseObservability(enrichedResults, Object.fromEntries(Object.entries(effectiveThresholds).filter((entry) => entry[1] !== undefined)), metricsPort, serviceName);
2209
+ const thresholdFailures = (0, reporting_1.checkThresholds)(enrichedResults, Object.fromEntries(Object.entries(effectiveThresholds).filter((entry) => entry[1] !== undefined)));
2210
+ if (thresholdFailures.length > 0) {
2211
+ for (const failure of thresholdFailures) {
2212
+ console.error(`THRESHOLD FAILURE: ${failure}`);
2213
+ }
2214
+ process.exitCode = 1;
2278
2215
  }
2279
- logger.info({ event: 'analyze_complete', warnings: warnings.length }, 'Agnostic project analysis complete');
2280
- await finaliseObservability(allCoverageResults, {}, metricsPort, serviceName);
2281
- // ── 7. Optionally launch the dashboard ─────────────────────────────────
2282
2216
  if (options['dashboard']) {
2217
+ const port = (_g = options['port']) !== null && _g !== void 0 ? _g : 4000;
2218
+ console.log(`Dashboard: http://localhost:${port}`);
2283
2219
  (0, serveDashboard_1.serveDashboard)({
2284
- reportsDir: reportsDir,
2285
- port: (_u = options['port']) !== null && _u !== void 0 ? _u : 4000,
2220
+ reportsDir,
2221
+ port,
2286
2222
  open: Boolean(options['open']),
2287
2223
  });
2288
- // Keep the process alive — the HTTP server holds the event loop open
2289
2224
  }
2290
2225
  });
2291
2226
  // ─── unit-analysis command ────────────────────────────────────────────────────
@@ -2327,7 +2262,10 @@ program
2327
2262
  if (execution.failures.length > 0) {
2328
2263
  process.exitCode = 1;
2329
2264
  }
2330
- console.log(`Unit analysis complete: ${execution.result.coveredItems}/${execution.result.totalItems} unit items covered (${execution.result.coveragePercent}%)`);
2265
+ for (const line of execution.consoleLines) {
2266
+ console.log(line);
2267
+ }
2268
+ console.log(`Unit analysis complete: ${execution.result.coveredItems}/${execution.result.totalItems} unit items analysed`);
2331
2269
  console.log(`Reports written to: ${reportsDir}`);
2332
2270
  await finaliseObservability([execution.result], {}, metricsPort, serviceName);
2333
2271
  });