@aisdlc/core 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -294,19 +294,26 @@ function extractPythonFunctions(content, filePath) {
294
294
  }
295
295
  return buildMetricsFromBlocks(blocks, content, filePath);
296
296
  }
297
- function processBraceLine(line, state, blocks, lineIndex, ext) {
298
- if (!state.currentFn) {
299
- const candidate = matchFunctionHeader(line, ext);
300
- if (!candidate) return;
297
+ function tryStartBraceFunction(line, state, ext) {
298
+ if (state.currentFn || state.inBlockComment || state.rawTerm) return;
299
+ const candidate = matchFunctionHeader(line, ext);
300
+ if (candidate) {
301
301
  state.currentFn = candidate;
302
302
  state.braceCount = 0;
303
303
  state.fnLines = [];
304
304
  }
305
- state.fnLines.push(line);
306
- const res = scanBracesInLine(line, state.inBlockComment, ext);
305
+ }
306
+ function updateBraceScanState(line, state, ext) {
307
+ const res = scanBracesInLine(line, state.inBlockComment, state.rawTerm, ext);
307
308
  state.inBlockComment = res.nowInBlock;
309
+ state.rawTerm = res.nowRawTerm;
308
310
  state.braceCount += res.delta;
309
- if (state.braceCount === 0 && state.fnLines.length > 1) {
311
+ }
312
+ function processBraceLine(line, state, blocks, lineIndex, ext) {
313
+ tryStartBraceFunction(line, state, ext);
314
+ if (state.currentFn) state.fnLines.push(line);
315
+ updateBraceScanState(line, state, ext);
316
+ if (state.currentFn && state.braceCount === 0 && state.fnLines.length > 1) {
310
317
  blocks.push({ name: state.currentFn, body: state.fnLines.join("\n"), startLine: lineIndex });
311
318
  state.currentFn = null;
312
319
  state.fnLines = [];
@@ -316,98 +323,153 @@ function extractBraceFunctions(content, filePath) {
316
323
  const ext = path4__namespace.extname(filePath).toLowerCase();
317
324
  const lines = content.split("\n");
318
325
  const blocks = [];
319
- const state = { currentFn: null, braceCount: 0, fnLines: [], inBlockComment: false };
326
+ const state = {
327
+ currentFn: null,
328
+ braceCount: 0,
329
+ fnLines: [],
330
+ inBlockComment: false,
331
+ rawTerm: null
332
+ };
320
333
  for (let i = 0; i < lines.length; i++) {
321
334
  processBraceLine(lines[i], state, blocks, i, ext);
322
335
  }
323
336
  return buildMetricsFromBlocks(blocks, content, filePath);
324
337
  }
338
+ var HEADER_EXCLUSIONS = /* @__PURE__ */ new Set([
339
+ "if",
340
+ "for",
341
+ "foreach",
342
+ "while",
343
+ "switch",
344
+ "catch",
345
+ "select",
346
+ "match",
347
+ "class",
348
+ "struct",
349
+ "interface",
350
+ "record",
351
+ "enum",
352
+ "namespace",
353
+ "using",
354
+ "lock",
355
+ "fixed",
356
+ "synchronized",
357
+ "try",
358
+ "else"
359
+ ]);
325
360
  function matchCStyleHeader(line) {
326
361
  const match = line.match(
327
- /(?:(?:public|private|protected|static|async|virtual|override|fn)\s+)*(?:function\s+([a-zA-Z0-9_$]+)|(?:[a-zA-Z0-9_<>[\]?]+\s+)+([a-zA-Z0-9_$]+)\s*\([^;{]*\)\s*(?:throws\s+[^{]+)?\{|([a-zA-Z0-9_$]+)\s*\([^;{]*\)\s*(?::\s*[^{]+\s*)?\{)/
362
+ /(?:(?:public|private|protected|internal|static|async|virtual|override|sealed|abstract|final|synchronized|default|fn)\s+)*(?:function\s+([a-zA-Z0-9_$]+)|(?:[a-zA-Z0-9_<>[\]?,.\s]+\s+)+([a-zA-Z0-9_$]+)\s*(?:<[^>]+>)?\s*\([^;{}]*\)\s*(?:where\s+[^{]+|throws\s+[^{]+)?\{|([a-zA-Z0-9_$]+)\s*\([^;{}]*\)\s*(?::\s*[^{]+\s*)?\{)/
328
363
  );
329
364
  if (!match) return null;
330
365
  const candidate = match[1] || match[2] || match[3];
331
- const exclusions = ["if", "for", "while", "switch", "catch", "select", "match", "class", "struct"];
332
- return candidate && !exclusions.includes(candidate) ? candidate : null;
366
+ return candidate && !HEADER_EXCLUSIONS.has(candidate) ? candidate : null;
333
367
  }
334
368
  function matchFunctionHeader(line, ext) {
335
369
  const trimmed = line.trim();
336
- if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) return null;
370
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) {
371
+ return null;
372
+ }
337
373
  if (ext === ".go") {
338
- const match = line.match(/func\s+(?:\([^)]*\)\s*)?([a-zA-Z0-9_$]+)\s*\(/);
374
+ const match = line.match(/func\s+(?:\([^)]*\)\s*)?([a-zA-Z0-9_$]+)\s*(?:\[[^\]]*\])?\s*\(/);
339
375
  return match ? match[1] : null;
340
376
  }
341
377
  if (ext === ".rs") {
342
- const match = line.match(/(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+([a-zA-Z0-9_$]+)\s*\(/);
378
+ const match = line.match(/(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+([a-zA-Z0-9_$]+)\s*(?:<[^>]*>)?\s*\(/);
343
379
  return match ? match[1] : null;
344
380
  }
345
381
  return matchCStyleHeader(line);
346
382
  }
347
- function handleStringChar(char, inString) {
348
- if (char === inString) return { inString: null, skip: false };
349
- return { inString, skip: char === "\\" };
350
- }
351
- function handleBlockState(char, next, inBlock) {
352
- if (inBlock && char === "*" && next === "/") return { inBlock: false, skip: true };
353
- return { inBlock, skip: false };
354
- }
355
- function checkCommentOrQuote(char, next, ext) {
356
- if (char === "/" && next === "/") return { startBlock: false, quote: null, isLineComment: true };
357
- if (char === "/" && next === "*") return { startBlock: true, quote: null, isLineComment: false };
358
- const isQuote = char === '"' || char === "'" || char === "`" && ext === ".go";
359
- return { startBlock: false, quote: isQuote ? char : null, isLineComment: false };
360
- }
361
- function stepSkippedState(char, next, inBlock, inString) {
362
- if (inBlock) {
363
- const res = handleBlockState(char, next, inBlock);
364
- return { inBlock: res.inBlock, inString, skip: res.skip, handled: true };
365
- }
366
- if (inString) {
367
- const res = handleStringChar(char, inString);
368
- return { inBlock, inString: res.inString, skip: res.skip, handled: true };
369
- }
370
- return { inBlock, inString, skip: false, handled: false };
371
- }
372
- function checkCodeChar(char, next, ext) {
373
- const trigger = checkCommentOrQuote(char, next, ext);
374
- if (trigger.isLineComment) return { delta: 0, startBlock: false, quote: null, stop: true };
375
- if (trigger.startBlock) return { delta: 0, startBlock: true, quote: null, stop: false };
376
- if (trigger.quote) return { delta: 0, startBlock: false, quote: trigger.quote, stop: false };
377
- const delta = char === "{" ? 1 : char === "}" ? -1 : 0;
378
- return { delta, startBlock: false, quote: null, stop: false };
379
- }
380
- function applyCodeChar(res, state) {
381
- if (res.stop) return { advance: 0, stop: true };
382
- if (res.startBlock) {
383
+ function detectRustOrGoRawString(sub, ext) {
384
+ if (ext === ".rs") {
385
+ const m = sub.match(/^r(#*)"/);
386
+ return m ? { term: `"${m[1]}`, advance: m[0].length - 1 } : null;
387
+ }
388
+ if (ext === ".go" && sub.startsWith("`")) {
389
+ return { term: "`", advance: 0 };
390
+ }
391
+ return null;
392
+ }
393
+ function detectCsJavaOrCppRawString(sub, ext) {
394
+ if (ext === ".cs" || ext === ".java") {
395
+ const m = sub.match(/^\$*("{3,})/);
396
+ if (m) return { term: m[1], advance: m[0].length - 1 };
397
+ const v = ext === ".cs" ? sub.match(/^(@\$|\$@|@)"/) : null;
398
+ return v ? { term: '"', advance: v[0].length - 1 } : null;
399
+ }
400
+ const cpp = sub.match(/^R"([a-zA-Z0-9_]*)\(/);
401
+ return cpp ? { term: `)${cpp[1]}"`, advance: cpp[0].length - 1 } : null;
402
+ }
403
+ function detectRawStringOpen(line, index, ext) {
404
+ const sub = line.slice(index);
405
+ return detectRustOrGoRawString(sub, ext) || detectCsJavaOrCppRawString(sub, ext);
406
+ }
407
+ function consumeRawOrBlock(line, index, state) {
408
+ if (state.inBlock) {
409
+ if (line[index] === "*" && line[index + 1] === "/") {
410
+ state.inBlock = false;
411
+ return { advance: 1, handled: true };
412
+ }
413
+ return { advance: 0, handled: true };
414
+ }
415
+ if (state.rawTerm) {
416
+ if (line.startsWith(state.rawTerm, index)) {
417
+ const len = state.rawTerm.length - 1;
418
+ state.rawTerm = null;
419
+ return { advance: len, handled: true };
420
+ }
421
+ return { advance: 0, handled: true };
422
+ }
423
+ return { advance: 0, handled: false };
424
+ }
425
+ function consumeNormalQuote(line, index, state) {
426
+ if (!state.inString) return { advance: 0, handled: false };
427
+ const ch = line[index];
428
+ if (ch === "\\") return { advance: 1, handled: true };
429
+ if (ch === state.inString) state.inString = null;
430
+ return { advance: 0, handled: true };
431
+ }
432
+ function applyQuoteOrBrace(ch, state) {
433
+ if (ch === '"' || ch === "'") {
434
+ state.inString = ch;
435
+ return;
436
+ }
437
+ if (ch === "{") state.delta++;
438
+ if (ch === "}") state.delta--;
439
+ }
440
+ function stepActiveCodeToken(line, j, ext, state) {
441
+ const pair = line.slice(j, j + 2);
442
+ if (pair === "//") return { advance: 0, stop: true };
443
+ if (pair === "/*") {
383
444
  state.inBlock = true;
384
445
  return { advance: 1, stop: false };
385
446
  }
386
- if (res.quote) {
387
- state.inString = res.quote;
388
- return { advance: 0, stop: false };
447
+ const rawOpen = detectRawStringOpen(line, j, ext);
448
+ if (rawOpen) {
449
+ state.rawTerm = rawOpen.term;
450
+ return { advance: rawOpen.advance, stop: false };
389
451
  }
390
- state.delta += res.delta;
452
+ applyQuoteOrBrace(line[j], state);
391
453
  return { advance: 0, stop: false };
392
454
  }
393
- function scanBracesInLine(line, wasInBlock, ext) {
394
- const state = { delta: 0, inBlock: wasInBlock, inString: null };
455
+ function scanBracesInLine(line, wasInBlock, wasRawTerm, ext) {
456
+ const state = { delta: 0, inBlock: wasInBlock, rawTerm: wasRawTerm, inString: null };
395
457
  for (let j = 0; j < line.length; j++) {
396
- const char = line[j];
397
- const next = j + 1 < line.length ? line[j + 1] : "";
398
- const skipped = stepSkippedState(char, next, state.inBlock, state.inString);
399
- if (skipped.handled) {
400
- state.inBlock = skipped.inBlock;
401
- state.inString = skipped.inString;
402
- if (skipped.skip) j++;
458
+ const rawOrBlock = consumeRawOrBlock(line, j, state);
459
+ if (rawOrBlock.handled) {
460
+ j += rawOrBlock.advance;
403
461
  continue;
404
462
  }
405
- const res = checkCodeChar(char, next, ext);
406
- const step = applyCodeChar(res, state);
407
- if (step.stop) break;
408
- j += step.advance;
463
+ const quoteStep = consumeNormalQuote(line, j, state);
464
+ if (quoteStep.handled) {
465
+ j += quoteStep.advance;
466
+ continue;
467
+ }
468
+ const codeStep = stepActiveCodeToken(line, j, ext, state);
469
+ if (codeStep.stop) break;
470
+ j += codeStep.advance;
409
471
  }
410
- return { delta: state.delta, nowInBlock: state.inBlock };
472
+ return { delta: state.delta, nowInBlock: state.inBlock, nowRawTerm: state.rawTerm };
411
473
  }
412
474
  function buildMetricsFromBlocks(blocks, content, filePath) {
413
475
  if (blocks.length === 0) {
@@ -431,11 +493,20 @@ function buildMetricsFromBlocks(blocks, content, filePath) {
431
493
  }
432
494
  function detectPolyglotSmells(body) {
433
495
  const smells = [];
434
- if (/#\s*noqa|#\s*type:\s*ignore|@SuppressWarnings/.test(body)) {
496
+ if (/#\s*noqa|#\s*type:\s*ignore|@SuppressWarnings|#pragma\s+warning\s+disable/.test(body)) {
435
497
  smells.push("Supresi\xF3n no autorizada de linter");
436
498
  }
437
499
  return smells;
438
500
  }
501
+ function stripStringsAndComments(body, ext) {
502
+ let cleaned = body;
503
+ if (ext === ".py") {
504
+ cleaned = cleaned.replace(/"""[\s\S]*?"""|'''[\s\S]*?'''/g, '""').replace(/#.*/g, "");
505
+ } else {
506
+ cleaned = cleaned.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").replace(/r(#+)"[\s\S]*?"\1/g, '""').replace(/R"([a-zA-Z0-9_]*)\([\s\S]*?\)\1"/g, '""').replace(/\$*"{3,}[\s\S]*?"{3,}/g, '""').replace(/`[\s\S]*?`/g, '""');
507
+ }
508
+ return cleaned.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""');
509
+ }
439
510
  function computeHalsteadMI(body, cyclomatic, loc, commentCount) {
440
511
  const cleanText = body.replace(/\/\*[\s\S]*?\*\/|\/\/.*|#.*/g, "");
441
512
  const tokens = cleanText.split(/[\s,;().{}[\]=+\-*/<>!&|:]+/).filter((t) => t.length > 0);
@@ -460,9 +531,11 @@ function countCommentsAndLoc(rawLines, isPy) {
460
531
  function calculatePolyglotMetrics(body, name, filePath, startLine) {
461
532
  const ext = path4__namespace.extname(filePath).toLowerCase();
462
533
  const rawLines = body.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
463
- const { loc, commentCount, cleanLines } = countCommentsAndLoc(rawLines, ext === ".py");
464
- const cyclomatic = computePolyglotCyclomatic(body, ext);
465
- const cognitive = computePolyglotCognitive(cleanLines, ext === ".py");
534
+ const { loc, commentCount } = countCommentsAndLoc(rawLines, ext === ".py");
535
+ const strippedBody = stripStringsAndComments(body, ext);
536
+ const strippedLines = strippedBody.split(/\r?\n/).filter((l) => l.trim().length > 0);
537
+ const cyclomatic = computePolyglotCyclomatic(strippedBody, ext);
538
+ const cognitive = computePolyglotCognitive(strippedLines, ext === ".py");
466
539
  const maintainability = computeHalsteadMI(body, cyclomatic, loc, commentCount);
467
540
  return {
468
541
  functionName: name,
@@ -475,40 +548,60 @@ function calculatePolyglotMetrics(body, name, filePath, startLine) {
475
548
  line: startLine ? startLine + 1 : 1
476
549
  };
477
550
  }
478
- function computePolyglotCyclomatic(body, ext) {
479
- let regex;
551
+ function getCyclomaticPattern(ext) {
480
552
  if (ext === ".py") {
481
- regex = /\b(if|elif|for|while|except)\b|\b(and|or)\b/g;
482
- } else if (ext === ".go") {
483
- regex = /\b(if|for|case|select)\b|&&|\|\|/g;
484
- } else if (ext === ".rs") {
485
- regex = /\b(if|for|while|match)\b|&&|\|\||\?/g;
486
- } else {
487
- regex = /\b(if|else\s+if|for|while|catch|case)\b|\?|&&|\|\|/g;
553
+ return /\b(if|elif|for|while|except|case)\b|\b(and|or)\b/g;
554
+ }
555
+ if (ext === ".go") {
556
+ return /\b(if|for|case|select)\b|&&|\|\|/g;
557
+ }
558
+ if (ext === ".rs") {
559
+ return /\b(if|for|while|match)\b|&&|\|\||\?/g;
560
+ }
561
+ if (ext === ".cs") {
562
+ return /\b(if|else\s+if|for|foreach|while|catch|case|when)\b|\?\?|\?\.(?=[a-zA-Z_])|&&|\|\|/g;
488
563
  }
489
- const cleanBody = body.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, "");
490
- const matches = cleanBody.match(regex);
564
+ if (ext === ".java") {
565
+ return /\b(if|else\s+if|for|while|catch|case|when)\b|&&|\|\|/g;
566
+ }
567
+ return /\b(if|else\s+if|for|while|catch|case)\b|\?|&&|\|\|/g;
568
+ }
569
+ function computePolyglotCyclomatic(strippedBody, ext) {
570
+ const regex = getCyclomaticPattern(ext);
571
+ const matches = strippedBody.match(regex);
491
572
  return 1 + (matches ? matches.length : 0);
492
573
  }
493
- function computePolyglotCognitive(lines, isPy) {
574
+ var PY_COGNITIVE_RE = /\b(if|elif|for|while|except|match)\b/;
575
+ var BRACE_COGNITIVE_RE = /\b(if|for|foreach|while|catch|match|select|switch)\b/;
576
+ function computePythonCognitive(lines) {
577
+ let cognitive = 0;
578
+ const baseIndent = lines.length > 0 ? lines[0].length - lines[0].trimStart().length : 0;
579
+ for (let i = 1; i < lines.length; i++) {
580
+ const line = lines[i];
581
+ if (PY_COGNITIVE_RE.test(line)) {
582
+ const relIndent = Math.max(0, line.length - line.trimStart().length - baseIndent - 4);
583
+ cognitive += 1 + Math.floor(relIndent / 4);
584
+ }
585
+ }
586
+ return cognitive;
587
+ }
588
+ function computeBraceCognitive(lines) {
494
589
  let cognitive = 0;
495
590
  let nesting = 0;
496
- for (const line of lines) {
497
- if (isPy) {
498
- const indent = line.length - line.trimStart().length;
499
- if (/\b(if|elif|for|while|except)\b/.test(line)) {
500
- cognitive += 1 + Math.floor(indent / 4);
501
- }
502
- } else {
503
- if (line.includes("{")) nesting++;
504
- if (line.includes("}")) nesting = Math.max(0, nesting - 1);
505
- if (/\b(if|for|while|catch|match|select)\b/.test(line)) {
506
- cognitive += 1 + nesting;
507
- }
591
+ for (let i = 1; i < lines.length; i++) {
592
+ const line = lines[i];
593
+ if (BRACE_COGNITIVE_RE.test(line)) {
594
+ cognitive += 1 + nesting;
508
595
  }
596
+ const opens = (line.match(/\{/g) || []).length;
597
+ const closes = (line.match(/\}/g) || []).length;
598
+ nesting = Math.max(0, nesting + opens - closes);
509
599
  }
510
600
  return cognitive;
511
601
  }
602
+ function computePolyglotCognitive(lines, isPy) {
603
+ return isPy ? computePythonCognitive(lines) : computeBraceCognitive(lines);
604
+ }
512
605
  function walkMdFiles(dir, fileList = [], excludeDirs = [
513
606
  "node_modules",
514
607
  ".git",
@@ -1092,9 +1185,54 @@ function collectTestCorpus(rootDir, allMdFiles) {
1092
1185
  }
1093
1186
  return { gherkinSources, combinedTests: testContents.join("\n") };
1094
1187
  }
1095
- function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, taskErrors, ucErrors) {
1188
+ function isCucumberScenarioPassed(element) {
1189
+ const steps = Array.isArray(element.steps) ? element.steps : [];
1190
+ if (steps.length === 0) return false;
1191
+ return steps.every((s) => {
1192
+ const res = s.result;
1193
+ return res && res.status === "passed";
1194
+ });
1195
+ }
1196
+ function countCucumberScenarios(raw) {
1197
+ let passed = 0;
1198
+ let failed = 0;
1199
+ for (const feat of raw) {
1200
+ const elements = Array.isArray(feat.elements) ? feat.elements : [];
1201
+ for (const el of elements) {
1202
+ if (isCucumberScenarioPassed(el)) passed++;
1203
+ else failed++;
1204
+ }
1205
+ }
1206
+ return { passed, failed };
1207
+ }
1208
+ function parseCucumberReportFile(reportPath) {
1209
+ if (!fs7__namespace.existsSync(reportPath)) return void 0;
1210
+ try {
1211
+ const raw = JSON.parse(fs7__namespace.readFileSync(reportPath, "utf-8"));
1212
+ if (!Array.isArray(raw)) return void 0;
1213
+ const { passed, failed } = countCucumberScenarios(raw);
1214
+ return {
1215
+ evaluatedFeatures: raw.length,
1216
+ passedScenarios: passed,
1217
+ failedScenarios: failed,
1218
+ passed: passed + failed > 0 && failed === 0
1219
+ };
1220
+ } catch {
1221
+ return void 0;
1222
+ }
1223
+ }
1224
+ function resolveCucumberReport(rootDir, options) {
1225
+ const candidate = options.cucumberReportPath ? path4__namespace.isAbsolute(options.cucumberReportPath) ? options.cucumberReportPath : path4__namespace.join(rootDir, options.cucumberReportPath) : path4__namespace.join(rootDir, "reports", "cucumber-report.json");
1226
+ const parsed = parseCucumberReportFile(candidate);
1227
+ if (parsed) return parsed;
1228
+ if (options.enforceCucumberExecution) {
1229
+ return { evaluatedFeatures: 0, passedScenarios: 0, failedScenarios: 1, passed: false };
1230
+ }
1231
+ return void 0;
1232
+ }
1233
+ function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, taskErrors, ucErrors, rootDir, options) {
1096
1234
  return {
1097
- success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0,
1235
+ success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0 && (!resolveCucumberReport(rootDir, options) || resolveCucumberReport(rootDir, options).passed),
1098
1236
  totalRequirements: reqAudit.length,
1099
1237
  passedRequirements: reqAudit.length - reqErrors,
1100
1238
  failedRequirements: reqErrors,
@@ -1105,6 +1243,7 @@ function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, ta
1105
1243
  tasks: taskAudit,
1106
1244
  useCaseFlows,
1107
1245
  failedUseCaseFlows: ucErrors,
1246
+ ...resolveCucumberReport(rootDir, options) ? { cucumberExecution: resolveCucumberReport(rootDir, options) } : {},
1108
1247
  reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
1109
1248
  };
1110
1249
  }
@@ -1156,7 +1295,9 @@ function verifyTestingCoverage(options = {}) {
1156
1295
  state.useCaseFlows,
1157
1296
  state.reqErrors,
1158
1297
  state.taskErrors,
1159
- state.ucErrors
1298
+ state.ucErrors,
1299
+ rootDir,
1300
+ options
1160
1301
  );
1161
1302
  }
1162
1303
 
@@ -5532,6 +5673,277 @@ function verifyTasksGovernance(options = {}) {
5532
5673
  );
5533
5674
  return assembleGovernanceResult(data, reportMarkdown);
5534
5675
  }
5676
+ var BddStepRegistry = class {
5677
+ definitions = [];
5678
+ register(pattern, handler) {
5679
+ this.definitions.push({ pattern, handler });
5680
+ return this;
5681
+ }
5682
+ given(pattern, handler) {
5683
+ return this.register(pattern, handler);
5684
+ }
5685
+ when(pattern, handler) {
5686
+ return this.register(pattern, handler);
5687
+ }
5688
+ then(pattern, handler) {
5689
+ return this.register(pattern, handler);
5690
+ }
5691
+ getDefinitions() {
5692
+ return this.definitions;
5693
+ }
5694
+ };
5695
+ var STEP_PREFIX_RE = /^(Given|When|Then|And|But|Dado|Cuando|Entonces|Y|Pero)\s+(.+)$/i;
5696
+ var SCENARIO_HEADER_RE = /^(Scenario|Escenario|Scenario Outline|Esquema del escenario):\s*(.+)$/i;
5697
+ function parseTableRowCells(line) {
5698
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
5699
+ }
5700
+ function attachStepTableRow(lastStep, cells) {
5701
+ if (!lastStep || cells.length < 2) return;
5702
+ if (!lastStep.table) lastStep.table = {};
5703
+ lastStep.table[cells[0]] = cells[1];
5704
+ }
5705
+ function attachExampleRow(scenario, headers, cells) {
5706
+ if (!scenario) return headers;
5707
+ if (headers.length === 0) return cells;
5708
+ const row = {};
5709
+ for (let i = 0; i < headers.length; i++) {
5710
+ row[headers[i]] = cells[i] ?? "";
5711
+ }
5712
+ scenario.examples.push(row);
5713
+ return headers;
5714
+ }
5715
+ function createInitialParseState() {
5716
+ return {
5717
+ featureName: "Unnamed Feature",
5718
+ featureTags: [],
5719
+ pendingTags: [],
5720
+ backgroundSteps: [],
5721
+ scenarios: [],
5722
+ currentScenario: null,
5723
+ inBackground: false,
5724
+ inExamples: false,
5725
+ exampleHeaders: []
5726
+ };
5727
+ }
5728
+ function handleStructuralHeaderLine(line, state) {
5729
+ if (/^(Feature|Caracter[ií]stica):/i.test(line)) {
5730
+ state.featureName = line.replace(/^(Feature|Caracter[ií]stica):\s*/i, "").trim();
5731
+ state.featureTags.push(...state.pendingTags);
5732
+ state.pendingTags = [];
5733
+ return true;
5734
+ }
5735
+ if (/^(Background|Antecedentes):/i.test(line)) {
5736
+ state.inBackground = true;
5737
+ state.inExamples = false;
5738
+ state.currentScenario = null;
5739
+ return true;
5740
+ }
5741
+ if (/^(Examples|Ejemplos):/i.test(line)) {
5742
+ state.inExamples = true;
5743
+ state.exampleHeaders = [];
5744
+ return true;
5745
+ }
5746
+ return false;
5747
+ }
5748
+ function startScenarioBlock(scenMatch, state) {
5749
+ state.inBackground = false;
5750
+ state.inExamples = false;
5751
+ const block = {
5752
+ name: scenMatch[2].trim(),
5753
+ tags: [...state.featureTags, ...state.pendingTags],
5754
+ isOutline: /outline|esquema/i.test(scenMatch[1]),
5755
+ steps: [],
5756
+ examples: []
5757
+ };
5758
+ state.pendingTags = [];
5759
+ state.scenarios.push(block);
5760
+ state.currentScenario = block;
5761
+ }
5762
+ function appendParsedStep(stepMatch, state) {
5763
+ const step = { keyword: stepMatch[1], text: stepMatch[2].trim() };
5764
+ if (state.inBackground) {
5765
+ state.backgroundSteps.push(step);
5766
+ } else if (state.currentScenario) {
5767
+ state.currentScenario.steps.push(step);
5768
+ }
5769
+ }
5770
+ function handleScenarioOrStepLine(line, state) {
5771
+ const scenMatch = line.match(SCENARIO_HEADER_RE);
5772
+ if (scenMatch) {
5773
+ startScenarioBlock(scenMatch, state);
5774
+ return;
5775
+ }
5776
+ const stepMatch = line.match(STEP_PREFIX_RE);
5777
+ if (stepMatch) {
5778
+ appendParsedStep(stepMatch, state);
5779
+ }
5780
+ }
5781
+ function processFeatureLine(rawLine, state) {
5782
+ const line = rawLine.trim();
5783
+ if (!line || line.startsWith("#")) return;
5784
+ if (line.startsWith("@")) {
5785
+ const tags = line.split(/\s+/).filter((t) => t.startsWith("@"));
5786
+ state.pendingTags.push(...tags);
5787
+ return;
5788
+ }
5789
+ if (line.startsWith("|")) {
5790
+ const cells = parseTableRowCells(line);
5791
+ if (state.inExamples) {
5792
+ state.exampleHeaders = attachExampleRow(state.currentScenario, state.exampleHeaders, cells);
5793
+ } else {
5794
+ const targetList = state.inBackground ? state.backgroundSteps : state.currentScenario?.steps || [];
5795
+ attachStepTableRow(targetList[targetList.length - 1], cells);
5796
+ }
5797
+ return;
5798
+ }
5799
+ if (!handleStructuralHeaderLine(line, state)) {
5800
+ handleScenarioOrStepLine(line, state);
5801
+ }
5802
+ }
5803
+ function parseFeatureDocument(content) {
5804
+ const state = createInitialParseState();
5805
+ for (const rawLine of content.split(/\r?\n/)) {
5806
+ processFeatureLine(rawLine, state);
5807
+ }
5808
+ return {
5809
+ featureName: state.featureName,
5810
+ tags: state.featureTags,
5811
+ backgroundSteps: state.backgroundSteps,
5812
+ scenarios: state.scenarios
5813
+ };
5814
+ }
5815
+ function interpolatePlaceholders(text, row) {
5816
+ let out = text;
5817
+ for (const [k, v] of Object.entries(row)) {
5818
+ out = out.replace(new RegExp(`<${k}>`, "g"), v);
5819
+ }
5820
+ return out;
5821
+ }
5822
+ function expandScenarioOutlines(scenarios) {
5823
+ const expanded = [];
5824
+ for (const sc of scenarios) {
5825
+ if (!sc.isOutline || sc.examples.length === 0) {
5826
+ expanded.push(sc);
5827
+ continue;
5828
+ }
5829
+ sc.examples.forEach((row, idx) => {
5830
+ expanded.push({
5831
+ name: `${sc.name} (Example #${idx + 1})`,
5832
+ tags: sc.tags,
5833
+ isOutline: false,
5834
+ steps: sc.steps.map((st) => ({
5835
+ keyword: st.keyword,
5836
+ text: interpolatePlaceholders(st.text, row),
5837
+ table: st.table
5838
+ })),
5839
+ examples: []
5840
+ });
5841
+ });
5842
+ }
5843
+ return expanded;
5844
+ }
5845
+ function matchStepDefinition(text, definitions) {
5846
+ for (const entry of definitions) {
5847
+ if (typeof entry.pattern === "string") {
5848
+ if (entry.pattern === text) return { entry, args: [] };
5849
+ } else {
5850
+ const m = text.match(entry.pattern);
5851
+ if (m) return { entry, args: m.slice(1) };
5852
+ }
5853
+ }
5854
+ return null;
5855
+ }
5856
+ async function executeSingleStep(step, definitions) {
5857
+ const matched = matchStepDefinition(step.text, definitions);
5858
+ if (!matched) {
5859
+ return { keyword: step.keyword, text: step.text, status: "undefined", durationMs: 0 };
5860
+ }
5861
+ const start = Date.now();
5862
+ try {
5863
+ await matched.entry.handler(matched.args, step.table);
5864
+ return { keyword: step.keyword, text: step.text, status: "passed", durationMs: Date.now() - start };
5865
+ } catch (err) {
5866
+ const msg = err instanceof Error ? err.message : String(err);
5867
+ return {
5868
+ keyword: step.keyword,
5869
+ text: step.text,
5870
+ status: "failed",
5871
+ durationMs: Date.now() - start,
5872
+ errorMessage: msg
5873
+ };
5874
+ }
5875
+ }
5876
+ async function executeScenarioSteps(allSteps, definitions) {
5877
+ const stepResults = [];
5878
+ let status = "passed";
5879
+ for (const step of allSteps) {
5880
+ if (status !== "passed") {
5881
+ stepResults.push({ keyword: step.keyword, text: step.text, status: "skipped", durationMs: 0 });
5882
+ continue;
5883
+ }
5884
+ const res = await executeSingleStep(step, definitions);
5885
+ stepResults.push(res);
5886
+ if (res.status === "failed" || res.status === "undefined") {
5887
+ status = res.status;
5888
+ }
5889
+ }
5890
+ return { stepResults, status };
5891
+ }
5892
+ async function runAllExpandedScenarios(parsed, definitions) {
5893
+ const expanded = expandScenarioOutlines(parsed.scenarios);
5894
+ const out = [];
5895
+ for (const sc of expanded) {
5896
+ const combinedSteps = [...parsed.backgroundSteps, ...sc.steps];
5897
+ const { stepResults, status } = await executeScenarioSteps(combinedSteps, definitions);
5898
+ out.push({ name: sc.name, tags: sc.tags, status, steps: stepResults });
5899
+ }
5900
+ return out;
5901
+ }
5902
+ async function executeBddFeature(featurePath, registry, options = {}) {
5903
+ const rootDir = options.rootDir || process.cwd();
5904
+ const fullPath = path4__namespace.isAbsolute(featurePath) ? featurePath : path4__namespace.join(rootDir, featurePath);
5905
+ const parsed = parseFeatureDocument(fs7__namespace.readFileSync(fullPath, "utf-8"));
5906
+ const scenarios = await runAllExpandedScenarios(parsed, registry.getDefinitions());
5907
+ const report = {
5908
+ uri: path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/"),
5909
+ featureName: parsed.featureName,
5910
+ tags: parsed.tags,
5911
+ scenarios,
5912
+ passed: scenarios.length > 0 && scenarios.every((s) => s.status === "passed")
5913
+ };
5914
+ if (options.writeReportPath) writeCucumberJsonReport([report], options.writeReportPath);
5915
+ return report;
5916
+ }
5917
+ function mapStepToCucumberJson(st) {
5918
+ return {
5919
+ keyword: `${st.keyword} `,
5920
+ name: st.text,
5921
+ result: {
5922
+ status: st.status,
5923
+ duration: st.durationMs * 1e6,
5924
+ ...st.errorMessage ? { error_message: st.errorMessage } : {}
5925
+ }
5926
+ };
5927
+ }
5928
+ function mapFeatureToCucumberJson(r) {
5929
+ return {
5930
+ uri: r.uri,
5931
+ name: r.featureName,
5932
+ tags: r.tags.map((t) => ({ name: t })),
5933
+ elements: r.scenarios.map((sc) => ({
5934
+ type: "scenario",
5935
+ name: sc.name,
5936
+ tags: sc.tags.map((t) => ({ name: t })),
5937
+ steps: sc.steps.map(mapStepToCucumberJson)
5938
+ }))
5939
+ };
5940
+ }
5941
+ function writeCucumberJsonReport(reports, outputPath) {
5942
+ const dir = path4__namespace.dirname(outputPath);
5943
+ if (!fs7__namespace.existsSync(dir)) fs7__namespace.mkdirSync(dir, { recursive: true });
5944
+ const payload = JSON.stringify(reports.map(mapFeatureToCucumberJson), null, 2);
5945
+ fs7__namespace.writeFileSync(outputPath, payload, "utf-8");
5946
+ }
5535
5947
  function isToolAvailable(toolName) {
5536
5948
  try {
5537
5949
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -6172,7 +6584,7 @@ function resolveProjectIdentity(options) {
6172
6584
  function buildBomMetadata(name, version) {
6173
6585
  return {
6174
6586
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6175
- tools: [{ vendor: "AI-SDLC", name: "@aisdlc/core", version: "2.3.0" }],
6587
+ tools: [{ vendor: "AI-SDLC", name: "@aisdlc/core", version: "2.4.0" }],
6176
6588
  component: { type: "application", name, version }
6177
6589
  };
6178
6590
  }
@@ -16348,6 +16760,7 @@ function sanitizeDlpPayload(payload, policy = createDefaultDlpPolicy()) {
16348
16760
 
16349
16761
  exports.ALL_ARCHITECTURE_TEMPLATES = ALL_ARCHITECTURE_TEMPLATES;
16350
16762
  exports.BRANCH_PATTERNS = BRANCH_PATTERNS;
16763
+ exports.BddStepRegistry = BddStepRegistry;
16351
16764
  exports.DEFAULT_DLP_RULES = DEFAULT_DLP_RULES;
16352
16765
  exports.DEFAULT_EXCLUDED_PATTERNS = DEFAULT_EXCLUDED_PATTERNS;
16353
16766
  exports.DEFAULT_EXCLUDES = DEFAULT_EXCLUDES;
@@ -16523,6 +16936,7 @@ exports.emitCiOutput = emitCiOutput;
16523
16936
  exports.evaluateCompoundLicense = evaluateCompoundLicense;
16524
16937
  exports.evaluateSingleDependency = evaluateSingleDependency;
16525
16938
  exports.evaluateVulnerabilityThresholds = evaluateVulnerabilityThresholds;
16939
+ exports.executeBddFeature = executeBddFeature;
16526
16940
  exports.extractBaselineEntriesFromResults = extractBaselineEntriesFromResults;
16527
16941
  exports.extractCitations = extractCitations;
16528
16942
  exports.extractCommitKpisFromRange = extractCommitKpisFromRange;
@@ -16588,6 +17002,7 @@ exports.normalizeSpdxLicense = normalizeSpdxLicense;
16588
17002
  exports.normalizeUri = normalizeUri;
16589
17003
  exports.parseCommitTrailers = parseCommitTrailers;
16590
17004
  exports.parseDlpPolicy = parseDlpPolicy;
17005
+ exports.parseFeatureDocument = parseFeatureDocument;
16591
17006
  exports.parseFrontmatter = parseFrontmatter;
16592
17007
  exports.parseFrontmatterId = parseFrontmatterId;
16593
17008
  exports.parseGenericDoc = parseGenericDoc;
@@ -16674,6 +17089,7 @@ exports.walkMdFiles = walkMdFiles;
16674
17089
  exports.walkProjectFiles = walkProjectFiles;
16675
17090
  exports.walkSourceFiles = walkSourceFiles;
16676
17091
  exports.walkTaskFiles = walkTaskFiles;
17092
+ exports.writeCucumberJsonReport = writeCucumberJsonReport;
16677
17093
  exports.writeCycloneDxSbom = writeCycloneDxSbom;
16678
17094
  exports.writeQualityBaseline = writeQualityBaseline;
16679
17095
  exports.writeReleaseKpiReport = writeReleaseKpiReport;