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.
- package/dist/dashboard/dist/assets/_basePickBy-CeCjvJWr.js +1 -0
- package/dist/dashboard/dist/assets/_baseUniq-DQMeOG5y.js +1 -0
- package/dist/dashboard/dist/assets/arc-BDIdECDJ.js +1 -0
- package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-D2t8Py1B.js +36 -0
- package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-7B2h6RPh.js +122 -0
- package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-BJS73w81.js +10 -0
- package/dist/dashboard/dist/assets/channel-B5FEo6x8.js +1 -0
- package/dist/dashboard/dist/assets/chunk-4BX2VUAB-rMDV_g4i.js +1 -0
- package/dist/dashboard/dist/assets/chunk-55IACEB6-H2T0HHMf.js +1 -0
- package/dist/dashboard/dist/assets/chunk-B4BG7PRW-BIav900j.js +165 -0
- package/dist/dashboard/dist/assets/chunk-DI55MBZ5-CL1ZxZeg.js +220 -0
- package/dist/dashboard/dist/assets/chunk-FMBD7UC4-BH1h7N7Y.js +15 -0
- package/dist/dashboard/dist/assets/chunk-QN33PNHL-Dzk5VCZ3.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QZHKN3VN-Z3uL3Vu6.js +1 -0
- package/dist/dashboard/dist/assets/chunk-TZMSLE5B-Bk8ko3-5.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-M4p4DdHs.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-M4p4DdHs.js +1 -0
- package/dist/dashboard/dist/assets/clone-DISkn1y_.js +1 -0
- package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-BbDNIhrI.js +1 -0
- package/dist/dashboard/dist/assets/dagre-6UL2VRFP-Niwmlwr0.js +4 -0
- package/dist/dashboard/dist/assets/diagram-PSM6KHXK-D6YSgDDJ.js +24 -0
- package/dist/dashboard/dist/assets/diagram-QEK2KX5R-BIV3o3EV.js +43 -0
- package/dist/dashboard/dist/assets/diagram-S2PKOQOG-B_V6T3Do.js +24 -0
- package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-Dv8XSfJj.js +60 -0
- package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-DHVxWNFx.js +162 -0
- package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-C0zKIcDh.js +267 -0
- package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-DbNQAtGz.js +65 -0
- package/dist/dashboard/dist/assets/graph-BUXAK8S4.js +1 -0
- package/dist/dashboard/dist/assets/index-HrRX8fCW.css +1 -0
- package/dist/dashboard/dist/assets/index-k7QMCdxo.js +522 -0
- package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-CbvfFEOs.js +2 -0
- package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CHncp1wO.js +139 -0
- package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-Ct5tvLkE.js +89 -0
- package/dist/dashboard/dist/assets/layout-DEw3EHVd.js +1 -0
- package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-CM92aa9b.js +68 -0
- package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-Dow8SBXC.js +30 -0
- package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-CHDOoeNa.js +7 -0
- package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-BdQwEiam.js +64 -0
- package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-C2tgBc3h.js +10 -0
- package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-BJapitlJ.js +145 -0
- package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-CAMqhH5J.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-DMcei2iB.js +1 -0
- package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-CgZuzj68.js +61 -0
- package/dist/dashboard/dist/assets/treemap-GDKQZRPO-DISZoPlK.js +162 -0
- package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-WhBuJ91k.js +7 -0
- package/dist/dashboard/dist/index.html +2 -2
- package/dist/src/businessCoverage.d.ts.map +1 -1
- package/dist/src/businessCoverage.js +14 -1
- package/dist/src/index.js +429 -29
- package/dist/src/inference/businessRuleInference.d.ts +8 -0
- package/dist/src/inference/businessRuleInference.d.ts.map +1 -1
- package/dist/src/inference/businessRuleInference.js +52 -0
- package/dist/src/inference/routeInference.d.ts +50 -0
- package/dist/src/inference/routeInference.d.ts.map +1 -0
- package/dist/src/inference/routeInference.js +220 -0
- package/dist/src/inference/scanManifest.d.ts +35 -0
- package/dist/src/inference/scanManifest.d.ts.map +1 -0
- package/dist/src/inference/scanManifest.js +58 -0
- package/dist/src/integrationCoverage.d.ts.map +1 -1
- package/dist/src/integrationCoverage.js +13 -1
- package/dist/src/serveDashboard.d.ts.map +1 -1
- package/dist/src/serveDashboard.js +11 -0
- 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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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 =
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
|
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: (
|
|
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;
|
|
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')
|