api-tests-coverage 1.0.4 → 1.0.6

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 (63) hide show
  1. package/dist/dashboard/dist/assets/_basePickBy-CeCjvJWr.js +1 -0
  2. package/dist/dashboard/dist/assets/_baseUniq-DQMeOG5y.js +1 -0
  3. package/dist/dashboard/dist/assets/arc-BDIdECDJ.js +1 -0
  4. package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-D2t8Py1B.js +36 -0
  5. package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-7B2h6RPh.js +122 -0
  6. package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-BJS73w81.js +10 -0
  7. package/dist/dashboard/dist/assets/channel-B5FEo6x8.js +1 -0
  8. package/dist/dashboard/dist/assets/chunk-4BX2VUAB-rMDV_g4i.js +1 -0
  9. package/dist/dashboard/dist/assets/chunk-55IACEB6-H2T0HHMf.js +1 -0
  10. package/dist/dashboard/dist/assets/chunk-B4BG7PRW-BIav900j.js +165 -0
  11. package/dist/dashboard/dist/assets/chunk-DI55MBZ5-CL1ZxZeg.js +220 -0
  12. package/dist/dashboard/dist/assets/chunk-FMBD7UC4-BH1h7N7Y.js +15 -0
  13. package/dist/dashboard/dist/assets/chunk-QN33PNHL-Dzk5VCZ3.js +1 -0
  14. package/dist/dashboard/dist/assets/chunk-QZHKN3VN-Z3uL3Vu6.js +1 -0
  15. package/dist/dashboard/dist/assets/chunk-TZMSLE5B-Bk8ko3-5.js +1 -0
  16. package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-M4p4DdHs.js +1 -0
  17. package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-M4p4DdHs.js +1 -0
  18. package/dist/dashboard/dist/assets/clone-DISkn1y_.js +1 -0
  19. package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-BbDNIhrI.js +1 -0
  20. package/dist/dashboard/dist/assets/dagre-6UL2VRFP-Niwmlwr0.js +4 -0
  21. package/dist/dashboard/dist/assets/diagram-PSM6KHXK-D6YSgDDJ.js +24 -0
  22. package/dist/dashboard/dist/assets/diagram-QEK2KX5R-BIV3o3EV.js +43 -0
  23. package/dist/dashboard/dist/assets/diagram-S2PKOQOG-B_V6T3Do.js +24 -0
  24. package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-Dv8XSfJj.js +60 -0
  25. package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-DHVxWNFx.js +162 -0
  26. package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-C0zKIcDh.js +267 -0
  27. package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-DbNQAtGz.js +65 -0
  28. package/dist/dashboard/dist/assets/graph-BUXAK8S4.js +1 -0
  29. package/dist/dashboard/dist/assets/index-HrRX8fCW.css +1 -0
  30. package/dist/dashboard/dist/assets/index-k7QMCdxo.js +522 -0
  31. package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-CbvfFEOs.js +2 -0
  32. package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CHncp1wO.js +139 -0
  33. package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-Ct5tvLkE.js +89 -0
  34. package/dist/dashboard/dist/assets/layout-DEw3EHVd.js +1 -0
  35. package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-CM92aa9b.js +68 -0
  36. package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-Dow8SBXC.js +30 -0
  37. package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-CHDOoeNa.js +7 -0
  38. package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-BdQwEiam.js +64 -0
  39. package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-C2tgBc3h.js +10 -0
  40. package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-BJapitlJ.js +145 -0
  41. package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-CAMqhH5J.js +1 -0
  42. package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-DMcei2iB.js +1 -0
  43. package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-CgZuzj68.js +61 -0
  44. package/dist/dashboard/dist/assets/treemap-GDKQZRPO-DISZoPlK.js +162 -0
  45. package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-WhBuJ91k.js +7 -0
  46. package/dist/dashboard/dist/index.html +2 -2
  47. package/dist/src/businessCoverage.d.ts.map +1 -1
  48. package/dist/src/businessCoverage.js +14 -1
  49. package/dist/src/index.js +429 -29
  50. package/dist/src/inference/businessRuleInference.d.ts +8 -0
  51. package/dist/src/inference/businessRuleInference.d.ts.map +1 -1
  52. package/dist/src/inference/businessRuleInference.js +52 -0
  53. package/dist/src/inference/routeInference.d.ts +50 -0
  54. package/dist/src/inference/routeInference.d.ts.map +1 -0
  55. package/dist/src/inference/routeInference.js +220 -0
  56. package/dist/src/inference/scanManifest.d.ts +35 -0
  57. package/dist/src/inference/scanManifest.d.ts.map +1 -0
  58. package/dist/src/inference/scanManifest.js +58 -0
  59. package/dist/src/integrationCoverage.d.ts.map +1 -1
  60. package/dist/src/integrationCoverage.js +13 -1
  61. package/dist/src/serveDashboard.d.ts.map +1 -1
  62. package/dist/src/serveDashboard.js +11 -0
  63. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -55,14 +55,19 @@ const index_3 = require("./intelligence/index");
55
55
  const observability_2 = require("./observability");
56
56
  const buildSummary_1 = require("./summary/buildSummary");
57
57
  const prSummary_1 = require("./summary/prSummary");
58
+ const summaryTypes_1 = require("./summary/summaryTypes");
58
59
  const astAnalysisOrchestrator_1 = require("./ast/astAnalysisOrchestrator");
59
60
  const projectDiscovery_1 = require("./discovery/projectDiscovery");
60
61
  const businessRuleInference_1 = require("./inference/businessRuleInference");
61
62
  const integrationFlowInference_1 = require("./inference/integrationFlowInference");
63
+ const routeInference_1 = require("./inference/routeInference");
64
+ const scanManifest_1 = require("./inference/scanManifest");
62
65
  const serveDashboard_1 = require("./serveDashboard");
63
66
  // Register all language AST analyzers at startup.
64
67
  // This side-effect import ensures each language module's registerAnalyzer() call runs.
65
68
  (0, astAnalysisOrchestrator_1.registerAllAnalyzers)();
69
+ /** Keywords that indicate a test is exercising an error/failure path. */
70
+ const ERROR_TEST_KEYWORDS = ['error', 'fail', 'throw', 'exception', 'reject', 'invalid', 'blank', 'missing'];
66
71
  const program = new commander_1.Command();
67
72
  program
68
73
  .name('api-tests-coverage-analyzer')
@@ -1349,7 +1354,7 @@ program
1349
1354
  .option('--port <port>', 'Port for the dashboard server (requires --dashboard)', parseInt)
1350
1355
  .option('--open', 'Open the dashboard in your browser automatically (requires --dashboard)')
1351
1356
  .action(async (options) => {
1352
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1357
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1353
1358
  const { metricsPort, serviceName } = setupObservability();
1354
1359
  const logger = (0, observability_1.getLogger)();
1355
1360
  const configPath = program.opts()['config'];
@@ -1377,19 +1382,18 @@ program
1377
1382
  return;
1378
1383
  }
1379
1384
  const warnings = [];
1385
+ let inferredRulesResult = null;
1386
+ let inferredFlowsResult = null;
1380
1387
  // ── 2. Business rule inference ─────────────────────────────────────────
1381
1388
  if (doInferRules) {
1382
- console.log('\nNo business-rules.yaml file detected.');
1383
- console.log('Business rules will be inferred automatically.');
1384
- const rulesResult = (0, businessRuleInference_1.inferBusinessRules)(artifacts.serviceFiles, warnings);
1385
- const rulesPath = (0, businessRuleInference_1.writeInferredBusinessRules)(rulesResult, reportsDir);
1386
- console.log(`\nBusiness Rule Coverage`);
1387
- console.log(` Source: inferred`);
1388
- console.log(` Rules detected: ${rulesResult.rules.length}`);
1389
+ inferredRulesResult = (0, businessRuleInference_1.inferBusinessRules)(artifacts.serviceFiles, warnings);
1390
+ const rulesPath = (0, businessRuleInference_1.writeInferredBusinessRules)(inferredRulesResult, reportsDir);
1391
+ console.log(`\nBusiness Rule Inference`);
1392
+ console.log(` Rules detected in service code: ${inferredRulesResult.rules.length}`);
1389
1393
  console.log(` Written to: ${rulesPath}`);
1390
1394
  if (options['exportInferredRules']) {
1391
1395
  const fsMod = require('fs');
1392
- const yaml = rulesResult.rules.map((r) => [
1396
+ const yaml = inferredRulesResult.rules.map((r) => [
1393
1397
  `- id: ${r.id}`,
1394
1398
  ` name: ${r.name}`,
1395
1399
  ` type: ${r.type}`,
@@ -1403,17 +1407,14 @@ program
1403
1407
  }
1404
1408
  // ── 3. Integration flow inference ──────────────────────────────────────
1405
1409
  if (doInferFlows) {
1406
- console.log('\nNo integration-flows.yaml file detected.');
1407
- console.log('Integration flows will be inferred automatically.');
1408
- const flowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings);
1409
- const flowsPath = (0, integrationFlowInference_1.writeInferredIntegrationFlows)(flowsResult, reportsDir);
1410
- console.log(`\nIntegration Flow Coverage`);
1411
- console.log(` Source: inferred`);
1412
- console.log(` Flows detected: ${flowsResult.flows.length}`);
1410
+ inferredFlowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings);
1411
+ const flowsPath = (0, integrationFlowInference_1.writeInferredIntegrationFlows)(inferredFlowsResult, reportsDir);
1412
+ console.log(`\nIntegration Flow Inference`);
1413
+ console.log(` Multi-step flows detected in tests: ${inferredFlowsResult.flows.length}`);
1413
1414
  console.log(` Written to: ${flowsPath}`);
1414
1415
  if (options['exportInferredRules']) {
1415
1416
  const fs = require('fs');
1416
- const yaml = flowsResult.flows.map((f) => [
1417
+ const yaml = inferredFlowsResult.flows.map((f) => [
1417
1418
  `- id: ${f.id}`,
1418
1419
  ` name: "${f.name}"`,
1419
1420
  ` steps:`,
@@ -1423,14 +1424,16 @@ program
1423
1424
  console.log(' Exported: generated-integration-flows.yaml');
1424
1425
  }
1425
1426
  }
1426
- // ── 4. Endpoint coverage analysis → coverage-summary.json ─────────────
1427
+ // ── 4. Full coverage analysis → coverage-summary.json ─────────────────
1427
1428
  const allCoverageResults = [];
1429
+ const fsMod = require('fs');
1430
+ const testsGlob = artifacts.testFiles.length > 0
1431
+ ? '{' + artifacts.testFiles.join(',') + '}'
1432
+ : path.join(rootDir, '**', '*');
1433
+ const detectedLanguages = artifacts.languages;
1428
1434
  if (artifacts.specs.length > 0) {
1429
- const testsGlob = artifacts.testFiles.length > 0
1430
- ? '{' + artifacts.testFiles.join(',') + '}'
1431
- : path.join(rootDir, '**', '*');
1432
- const detectedLanguages = artifacts.languages;
1433
1435
  for (const specPath of artifacts.specs) {
1436
+ // ── 4a. Endpoint coverage ───────────────────────────────────────────
1434
1437
  try {
1435
1438
  console.log(`\nAnalyzing endpoint coverage for: ${path.basename(specPath)}`);
1436
1439
  const endpoints = await (0, endpointCoverage_1.parseOpenApiSpec)(specPath);
@@ -1450,15 +1453,412 @@ program
1450
1453
  warnings.push(`Endpoint coverage failed for ${path.basename(specPath)}: ` +
1451
1454
  `${err instanceof Error ? err.message : String(err)}`);
1452
1455
  }
1453
- }
1454
- if (allCoverageResults.length > 0) {
1455
- const observabilityInfo = (0, observability_1.buildObservabilityInfo)(metricsPort);
1456
- (0, reporting_1.generateMultiFormatReports)(allCoverageResults, ['json'], reportsDir, {}, observabilityInfo);
1457
- console.log(`\nReports written to: ${reportsDir}`);
1456
+ // ── 4b. Parameter coverage ──────────────────────────────────────────
1457
+ try {
1458
+ const parameters = await (0, parameterCoverage_1.parseParameters)(specPath);
1459
+ const astParamOptions = {
1460
+ astConfig: (_m = (_l = analyzerCfg.analysis) === null || _l === void 0 ? void 0 : _l.ast) !== null && _m !== void 0 ? _m : {},
1461
+ deepConfig: undefined,
1462
+ };
1463
+ const paramCoverages = await (0, parameterCoverage_1.analyzeParameterCoverage)(parameters, testsGlob, astParamOptions);
1464
+ const paramReport = (0, parameterCoverage_1.buildParameterCoverageReport)(paramCoverages);
1465
+ const paramResult = {
1466
+ type: 'parameter',
1467
+ totalItems: paramReport.totalParameters,
1468
+ coveredItems: paramCoverages.filter((c) => c.ratio > 0).length,
1469
+ coveragePercent: paramReport.averageCoverage,
1470
+ details: paramReport,
1471
+ };
1472
+ allCoverageResults.push(paramResult);
1473
+ console.log(` ${paramResult.coveredItems}/${paramResult.totalItems} parameters covered (${paramReport.averageCoverage}%)`);
1474
+ }
1475
+ catch (err) {
1476
+ warnings.push(`Parameter coverage failed for ${path.basename(specPath)}: ` +
1477
+ `${err instanceof Error ? err.message : String(err)}`);
1478
+ }
1479
+ // ── 4c. Error scenario coverage ─────────────────────────────────────
1480
+ try {
1481
+ const scenarios = await (0, errorCoverage_1.parseErrorScenarios)(specPath);
1482
+ if (scenarios.length > 0) {
1483
+ const astErrorOptions = {
1484
+ astConfig: (_p = (_o = analyzerCfg.analysis) === null || _o === void 0 ? void 0 : _o.ast) !== null && _p !== void 0 ? _p : {},
1485
+ deepConfig: undefined,
1486
+ };
1487
+ const errorCoverages = await (0, errorCoverage_1.analyzeErrorCoverage)(scenarios, testsGlob, astErrorOptions);
1488
+ const errorReport = (0, errorCoverage_1.buildErrorCoverageReport)(errorCoverages);
1489
+ const errorResult = {
1490
+ type: 'error',
1491
+ totalItems: errorReport.total,
1492
+ coveredItems: errorReport.covered,
1493
+ coveragePercent: errorReport.percentage,
1494
+ details: errorReport,
1495
+ };
1496
+ allCoverageResults.push(errorResult);
1497
+ console.log(` ${errorReport.covered}/${errorReport.total} error scenarios covered (${errorReport.percentage}%)`);
1498
+ }
1499
+ }
1500
+ catch (err) {
1501
+ warnings.push(`Error coverage failed for ${path.basename(specPath)}: ` +
1502
+ `${err instanceof Error ? err.message : String(err)}`);
1503
+ }
1458
1504
  }
1459
1505
  }
1460
1506
  else {
1461
- warnings.push('No API spec files found; endpoint coverage analysis skipped.');
1507
+ warnings.push('No API spec files found; attempting route inference for endpoint/error coverage.');
1508
+ // ── 4a-alt. Inferred route endpoint coverage ──────────────────────────
1509
+ try {
1510
+ const routeResult = (0, routeInference_1.inferRoutes)(artifacts.serviceFiles);
1511
+ if (routeResult.routes.length > 0) {
1512
+ const routesPath = (0, routeInference_1.writeInferredRoutes)(routeResult, reportsDir);
1513
+ console.log(`\nRoute Inference`);
1514
+ console.log(` Routes detected in service code: ${routeResult.routes.length}`);
1515
+ console.log(` Written to: ${routesPath}`);
1516
+ console.log(`\nAnalyzing endpoint coverage (from inferred routes)...`);
1517
+ // Read test file contents for matching
1518
+ const testContents = artifacts.testFiles.map((tf) => {
1519
+ try {
1520
+ return { file: tf, content: fsMod.readFileSync(tf, 'utf-8') };
1521
+ }
1522
+ catch {
1523
+ return { file: tf, content: '' };
1524
+ }
1525
+ });
1526
+ // Extract test descriptions for fine-grained matching
1527
+ const TEST_DECL_PATTERN = /\b(?:test|it)\s*\(\s*(['"`])([\s\S]*?)\1/g;
1528
+ const testEntries = testContents.map(({ file, content }) => {
1529
+ const descriptions = [];
1530
+ const contentLower = content.toLowerCase();
1531
+ let m;
1532
+ TEST_DECL_PATTERN.lastIndex = 0;
1533
+ while ((m = TEST_DECL_PATTERN.exec(content)) !== null) {
1534
+ descriptions.push(m[2].toLowerCase());
1535
+ }
1536
+ return { file, contentLower, descriptions };
1537
+ });
1538
+ const endpointItems = routeResult.routes.map((route) => {
1539
+ var _a;
1540
+ const pathSegments = route.path.split('/').filter((s) => s.length > 1 && !s.startsWith(':'));
1541
+ const method = route.method.toLowerCase();
1542
+ // Leaf path segment is the most specific identifier (e.g., "comments", "favorite", "feed")
1543
+ const leafSegment = (_a = pathSegments[pathSegments.length - 1]) !== null && _a !== void 0 ? _a : '';
1544
+ const matchedTests = [];
1545
+ for (const { file, contentLower, descriptions } of testEntries) {
1546
+ let matched = false;
1547
+ // Priority 1: handler function name appears in test file imports/calls
1548
+ if (route.handlerFunction) {
1549
+ const fnLower = route.handlerFunction.toLowerCase();
1550
+ if (contentLower.includes(fnLower)) {
1551
+ matched = true;
1552
+ }
1553
+ }
1554
+ // Priority 2: test description mentions method + leaf path segment
1555
+ if (!matched && leafSegment.length > 2) {
1556
+ matched = descriptions.some((desc) => desc.includes(method) && desc.includes(leafSegment));
1557
+ }
1558
+ // Priority 3: test description mentions the exact path
1559
+ if (!matched && route.path.length >= 1) {
1560
+ matched = descriptions.some((desc) => desc.includes(route.path.toLowerCase()));
1561
+ }
1562
+ // Priority 4: test file directly calls the URL path (e.g., axios.get('/articles'))
1563
+ if (!matched && pathSegments.length > 0) {
1564
+ // Check for full path string in file (e.g., axios.get('/articles/feed'))
1565
+ const quotedPathPattern = new RegExp(`['"\`]${route.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]`);
1566
+ if (quotedPathPattern.test(contentLower)) {
1567
+ matched = true;
1568
+ }
1569
+ }
1570
+ if (matched) {
1571
+ matchedTests.push(path.basename(file));
1572
+ }
1573
+ }
1574
+ const covered = matchedTests.length > 0;
1575
+ return {
1576
+ id: `${route.method.toUpperCase()} ${route.path}`,
1577
+ covered,
1578
+ matchedTests,
1579
+ handler_function: route.handlerFunction,
1580
+ source_file: route.sourceFile,
1581
+ line_number: route.lineNumber,
1582
+ };
1583
+ });
1584
+ const coveredCount = endpointItems.filter((i) => i.covered).length;
1585
+ const pct = endpointItems.length > 0 ? Math.round((coveredCount / endpointItems.length) * 100) : 0;
1586
+ const endpointResult = {
1587
+ type: 'endpoint',
1588
+ totalItems: endpointItems.length,
1589
+ coveredItems: coveredCount,
1590
+ coveragePercent: pct,
1591
+ details: {
1592
+ total: endpointItems.length,
1593
+ covered: coveredCount,
1594
+ percentage: pct,
1595
+ items: endpointItems,
1596
+ source: 'inferred',
1597
+ },
1598
+ };
1599
+ allCoverageResults.push(endpointResult);
1600
+ console.log(` ${coveredCount}/${endpointItems.length} inferred routes have test coverage (${pct}%)`);
1601
+ // ── 4c-alt. Error coverage from inferred routes + rules ──────────
1602
+ if (inferredRulesResult && inferredRulesResult.rules.length > 0) {
1603
+ console.log(`\nAnalyzing error coverage (from inferred business rules)...`);
1604
+ const errorCandidateRules = inferredRulesResult.rules.filter((r) => r.type === 'validation' || r.type === 'business_logic');
1605
+ const errorItems = errorCandidateRules.map((rule) => {
1606
+ var _a;
1607
+ const matchedTestDescriptions = [];
1608
+ for (const { file, descriptions } of testEntries) {
1609
+ // Match at TEST DESCRIPTION level, not file level
1610
+ // Require: description contains an error indicator + at least one specific keyword
1611
+ const specificKws = (_a = rule.specificKeywords) !== null && _a !== void 0 ? _a : [];
1612
+ const matchingDescs = descriptions.filter((desc) => {
1613
+ const hasErrorKeyword = ERROR_TEST_KEYWORDS.some((kw) => desc.includes(kw));
1614
+ if (!hasErrorKeyword)
1615
+ return false;
1616
+ // If we have specific keywords, at least one must match in the description
1617
+ if (specificKws.length > 0) {
1618
+ return specificKws.some((kw) => desc.includes(kw.toLowerCase()));
1619
+ }
1620
+ // No specific keywords — use handler function name as fallback
1621
+ return true;
1622
+ });
1623
+ if (matchingDescs.length > 0) {
1624
+ matchedTestDescriptions.push(...matchingDescs.map((d) => `[${path.basename(file)}] ${d}`));
1625
+ }
1626
+ }
1627
+ return {
1628
+ id: rule.id,
1629
+ description: rule.condition,
1630
+ covered: matchedTestDescriptions.length > 0,
1631
+ matchedTests: matchedTestDescriptions,
1632
+ source_location: rule.source_location,
1633
+ code_snippet: rule.code_snippet,
1634
+ };
1635
+ });
1636
+ const errorCovered = errorItems.filter((i) => i.covered).length;
1637
+ const errorPct = errorItems.length > 0 ? Math.round((errorCovered / errorItems.length) * 100) : 0;
1638
+ const errorResult = {
1639
+ type: 'error',
1640
+ totalItems: errorItems.length,
1641
+ coveredItems: errorCovered,
1642
+ coveragePercent: errorPct,
1643
+ details: {
1644
+ total: errorItems.length,
1645
+ covered: errorCovered,
1646
+ percentage: errorPct,
1647
+ items: errorItems,
1648
+ source: 'inferred',
1649
+ },
1650
+ };
1651
+ allCoverageResults.push(errorResult);
1652
+ console.log(` ${errorCovered}/${errorItems.length} inferred error scenarios have test coverage (${errorPct}%)`);
1653
+ }
1654
+ }
1655
+ else {
1656
+ warnings.push('No routes detected in service files; endpoint coverage skipped.');
1657
+ }
1658
+ }
1659
+ catch (err) {
1660
+ warnings.push(`Route/error inference failed: ${err instanceof Error ? err.message : String(err)}`);
1661
+ }
1662
+ }
1663
+ // ── 4d. Business rules coverage ─────────────────────────────────────────
1664
+ const businessRulesYaml = path.join(rootDir, 'business-rules.yaml');
1665
+ if (fsMod.existsSync(businessRulesYaml)) {
1666
+ // Explicit YAML takes precedence
1667
+ try {
1668
+ console.log(`\nAnalyzing business rules coverage...`);
1669
+ const rules = (0, businessCoverage_1.parseBusinessRules)(businessRulesYaml);
1670
+ const bizCoverages = await (0, businessCoverage_1.analyzeBusinessCoverage)(rules, testsGlob);
1671
+ const bizReport = (0, businessCoverage_1.buildBusinessCoverageReport)(bizCoverages);
1672
+ const bizResult = {
1673
+ type: 'business',
1674
+ totalItems: bizReport.total,
1675
+ coveredItems: bizReport.covered,
1676
+ coveragePercent: bizReport.percentage,
1677
+ details: bizReport,
1678
+ };
1679
+ allCoverageResults.push(bizResult);
1680
+ console.log(` ${bizReport.covered}/${bizReport.total} business rules covered (${bizReport.percentage}%)`);
1681
+ }
1682
+ catch (err) {
1683
+ warnings.push(`Business rules coverage failed: ${err instanceof Error ? err.message : String(err)}`);
1684
+ }
1685
+ }
1686
+ else if (inferredRulesResult && inferredRulesResult.rules.length > 0) {
1687
+ // Auto-inferred: use specificKeywords from rule for accurate test matching
1688
+ try {
1689
+ console.log(`\nAnalyzing business rules coverage (from inferred rules)...`);
1690
+ const syntheticRules = inferredRulesResult.rules.map((r) => {
1691
+ const kwSet = new Set();
1692
+ // Use specificKeywords extracted from the condition (most accurate)
1693
+ if (r.specificKeywords && r.specificKeywords.length > 0) {
1694
+ r.specificKeywords.forEach((kw) => { if (!businessRuleInference_1.KEYWORD_STOP_WORDS.has(kw))
1695
+ kwSet.add(kw); });
1696
+ }
1697
+ else {
1698
+ // Fallback: words from rule name only (filter stop words)
1699
+ r.name.toLowerCase().split(/[-_\s]+/).forEach((w) => {
1700
+ if (w.length > 2 && !businessRuleInference_1.KEYWORD_STOP_WORDS.has(w))
1701
+ kwSet.add(w);
1702
+ });
1703
+ }
1704
+ // Non-trivial path segments from endpoint
1705
+ if (r.endpoint) {
1706
+ r.endpoint.toLowerCase().split(/[/.\s:]+/)
1707
+ .forEach((w) => { if (w.length > 2 && !/^(api|v\d)$/.test(w) && !businessRuleInference_1.KEYWORD_STOP_WORDS.has(w))
1708
+ kwSet.add(w); });
1709
+ }
1710
+ return {
1711
+ id: r.id,
1712
+ description: r.name,
1713
+ endpoints: r.endpoint ? [r.endpoint] : [],
1714
+ keywords: [...kwSet],
1715
+ scenarios: [],
1716
+ };
1717
+ });
1718
+ const bizCoverages = await (0, businessCoverage_1.analyzeBusinessCoverage)(syntheticRules, testsGlob);
1719
+ const bizReport = (0, businessCoverage_1.buildBusinessCoverageReport)(bizCoverages);
1720
+ const bizResult = {
1721
+ type: 'business',
1722
+ totalItems: bizReport.total,
1723
+ coveredItems: bizReport.covered,
1724
+ coveragePercent: bizReport.percentage,
1725
+ details: {
1726
+ ...bizReport,
1727
+ inferred_details: inferredRulesResult.rules.reduce((acc, r) => {
1728
+ acc[r.id] = {
1729
+ source_location: r.source_location,
1730
+ condition: r.condition,
1731
+ code_snippet: r.code_snippet,
1732
+ type: r.type,
1733
+ specificKeywords: r.specificKeywords,
1734
+ };
1735
+ return acc;
1736
+ }, {}),
1737
+ },
1738
+ };
1739
+ allCoverageResults.push(bizResult);
1740
+ console.log(` ${bizReport.covered}/${bizReport.total} inferred business rules have test coverage (${bizReport.percentage}%)`);
1741
+ }
1742
+ catch (err) {
1743
+ warnings.push(`Business rules coverage failed: ${err instanceof Error ? err.message : String(err)}`);
1744
+ }
1745
+ }
1746
+ // ── 4e. Integration flows coverage ──────────────────────────────────────
1747
+ const integrationFlowsYaml = path.join(rootDir, 'integration-flows.yaml');
1748
+ if (fsMod.existsSync(integrationFlowsYaml)) {
1749
+ // Explicit YAML takes precedence
1750
+ try {
1751
+ console.log(`\nAnalyzing integration flows coverage...`);
1752
+ const flows = (0, integrationCoverage_1.parseIntegrationFlows)(integrationFlowsYaml);
1753
+ const flowCoverages = await (0, integrationCoverage_1.analyzeIntegrationCoverage)(flows, testsGlob);
1754
+ const flowReport = (0, integrationCoverage_1.buildIntegrationCoverageReport)(flowCoverages);
1755
+ const flowResult = {
1756
+ type: 'integration',
1757
+ totalItems: flowReport.total,
1758
+ coveredItems: flowReport.complete,
1759
+ coveragePercent: flowReport.percentage,
1760
+ details: flowReport,
1761
+ };
1762
+ allCoverageResults.push(flowResult);
1763
+ console.log(` ${flowReport.complete}/${flowReport.total} integration flows covered (${flowReport.percentage}%)`);
1764
+ }
1765
+ catch (err) {
1766
+ warnings.push(`Integration flows coverage failed: ${err instanceof Error ? err.message : String(err)}`);
1767
+ }
1768
+ }
1769
+ else if (inferredFlowsResult && inferredFlowsResult.flows.length > 0) {
1770
+ // Auto-inferred: flows are extracted FROM tests, so by definition they're all covered
1771
+ console.log(`\nIntegration flows coverage (from inferred flows)...`);
1772
+ const syntheticItems = inferredFlowsResult.flows.map((f) => ({
1773
+ id: f.id,
1774
+ name: f.name,
1775
+ total: f.steps.length,
1776
+ covered: f.steps.length,
1777
+ complete: true,
1778
+ percentage: 100,
1779
+ uncoveredSteps: [],
1780
+ }));
1781
+ const syntheticReport = {
1782
+ total: syntheticItems.length,
1783
+ complete: syntheticItems.length,
1784
+ percentage: 100,
1785
+ items: syntheticItems,
1786
+ };
1787
+ const flowResult = {
1788
+ type: 'integration',
1789
+ totalItems: syntheticReport.total,
1790
+ coveredItems: syntheticReport.complete,
1791
+ coveragePercent: syntheticReport.percentage,
1792
+ details: syntheticReport,
1793
+ };
1794
+ allCoverageResults.push(flowResult);
1795
+ console.log(` ${syntheticReport.complete}/${syntheticReport.total} multi-step flows detected and covered (100%)`);
1796
+ }
1797
+ if (allCoverageResults.length > 0) {
1798
+ const observabilityInfo = (0, observability_1.buildObservabilityInfo)(metricsPort);
1799
+ (0, reporting_1.generateMultiFormatReports)(allCoverageResults, ['json'], reportsDir, {}, observabilityInfo);
1800
+ // Append discoveryInfo to coverage-summary.json for the dashboard
1801
+ const summaryPath = path.join(reportsDir, 'coverage-summary.json');
1802
+ try {
1803
+ const summaryJson = JSON.parse(fsMod.readFileSync(summaryPath, 'utf-8'));
1804
+ summaryJson.discoveryInfo = {
1805
+ projectRoot: rootDir,
1806
+ analyzedAt: new Date().toISOString(),
1807
+ languages: artifacts.languages,
1808
+ frameworks: artifacts.frameworks,
1809
+ serviceFilesCount: artifacts.serviceFiles.length,
1810
+ testFilesCount: artifacts.testFiles.length,
1811
+ specFilesCount: artifacts.specs.length,
1812
+ analysisMode: artifacts.specs.length > 0 ? 'explicit-spec' : 'inferred',
1813
+ };
1814
+ fsMod.writeFileSync(summaryPath, JSON.stringify(summaryJson, null, 2), 'utf-8');
1815
+ }
1816
+ catch {
1817
+ // Non-fatal — discovery info is also in scan-manifest.json
1818
+ }
1819
+ console.log(`\nReports written to: ${reportsDir}`);
1820
+ }
1821
+ // ── 4f. Write scan manifest ────────────────────────────────────────────
1822
+ try {
1823
+ const scanTypes = allCoverageResults.map((r) => ({
1824
+ type: r.type,
1825
+ source: artifacts.specs.length > 0 ? 'explicit' : 'inferred',
1826
+ itemsFound: r.totalItems,
1827
+ itemsCovered: r.coveredItems,
1828
+ coveragePercent: r.coveragePercent,
1829
+ }));
1830
+ // Add skipped types (exclude 'business' and 'integration' since these are always attempted
1831
+ // via rule/flow inference regardless of whether a spec file is present; their absence from
1832
+ // allCoverageResults means no rules or flows were discovered, which is informative on its own.)
1833
+ const coveredTypes = new Set(allCoverageResults.map((r) => r.type));
1834
+ for (const skippedType of summaryTypes_1.KNOWN_METRIC_TYPES.filter((t) => t !== 'business' && t !== 'integration')) {
1835
+ if (!coveredTypes.has(skippedType)) {
1836
+ scanTypes.push({
1837
+ type: skippedType,
1838
+ source: 'skipped',
1839
+ reason: artifacts.specs.length === 0 ? 'No API spec and no routes detected' : 'No data available',
1840
+ itemsFound: 0,
1841
+ itemsCovered: 0,
1842
+ coveragePercent: 0,
1843
+ });
1844
+ }
1845
+ }
1846
+ const manifestPath = (0, scanManifest_1.writeScanManifest)({
1847
+ projectRoot: rootDir,
1848
+ analyzedAt: new Date().toISOString(),
1849
+ discoveredFiles: {
1850
+ serviceFiles: artifacts.serviceFiles,
1851
+ testFiles: artifacts.testFiles,
1852
+ specFiles: artifacts.specs,
1853
+ },
1854
+ languages: artifacts.languages,
1855
+ frameworks: artifacts.frameworks,
1856
+ scanTypes,
1857
+ }, reportsDir);
1858
+ console.log(`Scan manifest written to: ${manifestPath}`);
1859
+ }
1860
+ catch (err) {
1861
+ warnings.push(`Scan manifest write failed: ${err instanceof Error ? err.message : String(err)}`);
1462
1862
  }
1463
1863
  // ── 5. Emit warnings ───────────────────────────────────────────────────
1464
1864
  for (const w of warnings) {
@@ -1477,7 +1877,7 @@ program
1477
1877
  if (options['dashboard']) {
1478
1878
  (0, serveDashboard_1.serveDashboard)({
1479
1879
  reportsDir: reportsDir,
1480
- port: (_l = options['port']) !== null && _l !== void 0 ? _l : 4000,
1880
+ port: (_q = options['port']) !== null && _q !== void 0 ? _q : 4000,
1481
1881
  open: Boolean(options['open']),
1482
1882
  });
1483
1883
  // Keep the process alive — the HTTP server holds the event loop open
@@ -34,6 +34,8 @@ export interface InferredBusinessRule {
34
34
  source_location: string;
35
35
  /** Raw matched code snippet */
36
36
  code_snippet: string;
37
+ /** Most specific identifiable terms from the condition, for accurate test matching */
38
+ specificKeywords: string[];
37
39
  }
38
40
  export interface BusinessRuleInferenceResult {
39
41
  rules: InferredBusinessRule[];
@@ -60,4 +62,10 @@ export declare function inferBusinessRules(serviceFiles: string[], warnings?: st
60
62
  * Returns the path of the written file.
61
63
  */
62
64
  export declare function writeInferredBusinessRules(result: BusinessRuleInferenceResult, reportsDir: string): string;
65
+ export declare const KEYWORD_STOP_WORDS: Set<string>;
66
+ /**
67
+ * Extract the most specific identifiable terms from a rule condition string.
68
+ * Prioritises quoted field names and message fragments over generic identifiers.
69
+ */
70
+ export declare function extractSpecificKeywords(condition: string): string[];
63
71
  //# sourceMappingURL=businessRuleInference.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"businessRuleInference.d.ts","sourceRoot":"","sources":["../../../src/inference/businessRuleInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AACjD,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,YAAY,CAAC;AAEjB,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA2ID;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA2C3E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EAAE,EACtB,QAAQ,GAAE,MAAM,EAAO,GACtB,2BAA2B,CAuB7B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,2BAA2B,EACnC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR"}
1
+ {"version":3,"file":"businessRuleInference.d.ts","sourceRoot":"","sources":["../../../src/inference/businessRuleInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AACjD,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,YAAY,CAAC;AAEjB,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA2ID;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA4C3E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EAAE,EACtB,QAAQ,GAAE,MAAM,EAAO,GACtB,2BAA2B,CAuB7B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,2BAA2B,EACnC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR;AAID,eAAO,MAAM,kBAAkB,aAM7B,CAAC;AAEH;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwCnE"}
@@ -50,9 +50,11 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  };
51
51
  })();
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.KEYWORD_STOP_WORDS = void 0;
53
54
  exports.inferRulesFromFile = inferRulesFromFile;
54
55
  exports.inferBusinessRules = inferBusinessRules;
55
56
  exports.writeInferredBusinessRules = writeInferredBusinessRules;
57
+ exports.extractSpecificKeywords = extractSpecificKeywords;
56
58
  const fs = __importStar(require("fs"));
57
59
  const path = __importStar(require("path"));
58
60
  const INFERENCE_PATTERNS = [
@@ -208,6 +210,7 @@ function inferRulesFromFile(filePath) {
208
210
  expected_behavior: ip.behaviorTemplate(match),
209
211
  source_location: sourceLocation,
210
212
  code_snippet: line.trim().slice(0, 200),
213
+ specificKeywords: extractSpecificKeywords(condition),
211
214
  });
212
215
  }
213
216
  }
@@ -258,6 +261,55 @@ function writeInferredBusinessRules(result, reportsDir) {
258
261
  return outputPath;
259
262
  }
260
263
  // ─── Helpers ─────────────────────────────────────────────────────────────────
264
+ exports.KEYWORD_STOP_WORDS = new Set([
265
+ 'http', 'exception', 'error', 'errors', 'throw', 'throws', 'raise', 'raises',
266
+ 'new', 'return', 'returns', 'status', 'response', 'request', 'abort',
267
+ 'the', 'and', 'for', 'with', 'that', 'this', 'from', 'not', 'has',
268
+ 'can', 'cant', 'be', 'been', 'must', 'will', 'was', 'are', 'have',
269
+ 'its', 'too', 'also', 'just', 'only', 'than', 'then', 'when',
270
+ ]);
271
+ /**
272
+ * Extract the most specific identifiable terms from a rule condition string.
273
+ * Prioritises quoted field names and message fragments over generic identifiers.
274
+ */
275
+ function extractSpecificKeywords(condition) {
276
+ var _a, _b, _c;
277
+ const kwSet = new Set();
278
+ // 1. Extract contents of quoted strings (field names, messages)
279
+ // Minimum length of 2 prevents single-character tokens like punctuation or abbreviations
280
+ // from polluting the keyword set (e.g. single-char matches from `{ e: [...] }`).
281
+ const quotedMatches = (_a = condition.match(/['"]([^'"]{2,})['"]/g)) !== null && _a !== void 0 ? _a : [];
282
+ for (const q of quotedMatches) {
283
+ const inner = q.slice(1, -1);
284
+ // Split on common separators and add each non-trivial token
285
+ inner.toLowerCase().split(/[\s\-_.,!?:;/\\]+/).forEach((tok) => {
286
+ if (tok.length >= 2 && !exports.KEYWORD_STOP_WORDS.has(tok) && /[a-z]/.test(tok)) {
287
+ kwSet.add(tok);
288
+ }
289
+ });
290
+ }
291
+ // 2. Extract object key identifiers from patterns like { fieldName: [...] }
292
+ const objKeyMatches = (_b = condition.match(/\{\s*(?:'([^']+)'|"([^"]+)"|(\w+))\s*:/g)) !== null && _b !== void 0 ? _b : [];
293
+ for (const m of objKeyMatches) {
294
+ const inner = m.replace(/^\{\s*/, '').replace(/\s*:$/, '').replace(/['"]/g, '').toLowerCase();
295
+ if (inner.length >= 2 && !exports.KEYWORD_STOP_WORDS.has(inner)) {
296
+ kwSet.add(inner);
297
+ }
298
+ }
299
+ // 3. Camel-case parts of exception / class names (e.g. "HttpException" → "http")
300
+ // but only non-stop identifiers of reasonable length
301
+ const identifiers = (_c = condition.match(/\b[A-Za-z][A-Za-z0-9]{2,}\b/g)) !== null && _c !== void 0 ? _c : [];
302
+ for (const id of identifiers) {
303
+ // Split camelCase into parts
304
+ const parts = id.replace(/([A-Z])/g, ' $1').toLowerCase().trim().split(/\s+/);
305
+ for (const part of parts) {
306
+ if (part.length >= 3 && !exports.KEYWORD_STOP_WORDS.has(part)) {
307
+ kwSet.add(part);
308
+ }
309
+ }
310
+ }
311
+ return [...kwSet];
312
+ }
261
313
  function toSnakeCase(str) {
262
314
  return str
263
315
  .replace(/([A-Z])/g, '_$1')