qfai 0.3.1 → 0.3.2

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
@@ -442,11 +442,11 @@ function isValidId(value, prefix) {
442
442
  }
443
443
 
444
444
  // src/core/report.ts
445
- var import_promises11 = require("fs/promises");
445
+ var import_promises13 = require("fs/promises");
446
446
  var import_node_path10 = __toESM(require("path"), 1);
447
447
 
448
448
  // src/core/discovery.ts
449
- var import_node_path3 = __toESM(require("path"), 1);
449
+ var import_promises4 = require("fs/promises");
450
450
 
451
451
  // src/core/fs.ts
452
452
  var import_promises2 = require("fs/promises");
@@ -503,25 +503,50 @@ async function exists(target) {
503
503
  }
504
504
  }
505
505
 
506
- // src/core/discovery.ts
507
- var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
508
- async function collectSpecPackDirs(specsRoot) {
509
- const files = await collectFiles(specsRoot, { extensions: [".md"] });
510
- const packs = /* @__PURE__ */ new Set();
511
- for (const file of files) {
512
- if (isSpecPackFile(file, "spec.md")) {
513
- packs.add(import_node_path3.default.dirname(file));
506
+ // src/core/specLayout.ts
507
+ var import_promises3 = require("fs/promises");
508
+ var import_node_path3 = __toESM(require("path"), 1);
509
+ var SPEC_DIR_RE = /^spec-\d{4}$/;
510
+ async function collectSpecEntries(specsRoot) {
511
+ const dirs = await listSpecDirs(specsRoot);
512
+ const entries = dirs.map((dir) => ({
513
+ dir,
514
+ specPath: import_node_path3.default.join(dir, "spec.md"),
515
+ deltaPath: import_node_path3.default.join(dir, "delta.md"),
516
+ scenarioPath: import_node_path3.default.join(dir, "scenario.md")
517
+ }));
518
+ return entries.sort((a, b) => a.dir.localeCompare(b.dir));
519
+ }
520
+ async function listSpecDirs(specsRoot) {
521
+ try {
522
+ const items = await (0, import_promises3.readdir)(specsRoot, { withFileTypes: true });
523
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path3.default.join(specsRoot, name));
524
+ } catch (error) {
525
+ if (isMissingFileError(error)) {
526
+ return [];
514
527
  }
528
+ throw error;
529
+ }
530
+ }
531
+ function isMissingFileError(error) {
532
+ if (!error || typeof error !== "object") {
533
+ return false;
515
534
  }
516
- return Array.from(packs).sort();
535
+ return error.code === "ENOENT";
536
+ }
537
+
538
+ // src/core/discovery.ts
539
+ async function collectSpecPackDirs(specsRoot) {
540
+ const entries = await collectSpecEntries(specsRoot);
541
+ return entries.map((entry) => entry.dir);
517
542
  }
518
543
  async function collectSpecFiles(specsRoot) {
519
- const files = await collectFiles(specsRoot, { extensions: [".md"] });
520
- return files.filter((file) => isSpecPackFile(file, "spec.md"));
544
+ const entries = await collectSpecEntries(specsRoot);
545
+ return filterExisting(entries.map((entry) => entry.specPath));
521
546
  }
522
547
  async function collectScenarioFiles(specsRoot) {
523
- const files = await collectFiles(specsRoot, { extensions: [".md"] });
524
- return files.filter((file) => isSpecPackFile(file, "scenario.md"));
548
+ const entries = await collectSpecEntries(specsRoot);
549
+ return filterExisting(entries.map((entry) => entry.scenarioPath));
525
550
  }
526
551
  async function collectUiContractFiles(uiRoot) {
527
552
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -540,28 +565,38 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
540
565
  ]);
541
566
  return { ui, api, db };
542
567
  }
543
- function isSpecPackFile(filePath, baseName) {
544
- if (import_node_path3.default.basename(filePath).toLowerCase() !== baseName) {
568
+ async function filterExisting(files) {
569
+ const existing = [];
570
+ for (const file of files) {
571
+ if (await exists2(file)) {
572
+ existing.push(file);
573
+ }
574
+ }
575
+ return existing;
576
+ }
577
+ async function exists2(target) {
578
+ try {
579
+ await (0, import_promises4.access)(target);
580
+ return true;
581
+ } catch {
545
582
  return false;
546
583
  }
547
- const dirName = import_node_path3.default.basename(import_node_path3.default.dirname(filePath)).toLowerCase();
548
- return SPEC_PACK_DIR_PATTERN.test(dirName);
549
584
  }
550
585
 
551
586
  // src/core/types.ts
552
587
  var VALIDATION_SCHEMA_VERSION = "0.2";
553
588
 
554
589
  // src/core/version.ts
555
- var import_promises3 = require("fs/promises");
590
+ var import_promises5 = require("fs/promises");
556
591
  var import_node_path4 = __toESM(require("path"), 1);
557
592
  var import_node_url = require("url");
558
593
  async function resolveToolVersion() {
559
- if ("0.3.1".length > 0) {
560
- return "0.3.1";
594
+ if ("0.3.2".length > 0) {
595
+ return "0.3.2";
561
596
  }
562
597
  try {
563
598
  const packagePath = resolvePackageJsonPath();
564
- const raw = await (0, import_promises3.readFile)(packagePath, "utf-8");
599
+ const raw = await (0, import_promises5.readFile)(packagePath, "utf-8");
565
600
  const parsed = JSON.parse(raw);
566
601
  const version = typeof parsed.version === "string" ? parsed.version : "";
567
602
  return version.length > 0 ? version : "unknown";
@@ -576,7 +611,7 @@ function resolvePackageJsonPath() {
576
611
  }
577
612
 
578
613
  // src/core/validators/contracts.ts
579
- var import_promises4 = require("fs/promises");
614
+ var import_promises6 = require("fs/promises");
580
615
  var import_node_path6 = __toESM(require("path"), 1);
581
616
 
582
617
  // src/core/contracts.ts
@@ -654,7 +689,7 @@ async function validateUiContracts(uiRoot) {
654
689
  }
655
690
  const issues = [];
656
691
  for (const file of files) {
657
- const text = await (0, import_promises4.readFile)(file, "utf-8");
692
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
658
693
  const invalidIds = extractInvalidIds(text, [
659
694
  "SPEC",
660
695
  "BR",
@@ -721,7 +756,7 @@ async function validateApiContracts(apiRoot) {
721
756
  }
722
757
  const issues = [];
723
758
  for (const file of files) {
724
- const text = await (0, import_promises4.readFile)(file, "utf-8");
759
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
725
760
  const invalidIds = extractInvalidIds(text, [
726
761
  "SPEC",
727
762
  "BR",
@@ -799,7 +834,7 @@ async function validateDataContracts(dataRoot) {
799
834
  }
800
835
  const issues = [];
801
836
  for (const file of files) {
802
- const text = await (0, import_promises4.readFile)(file, "utf-8");
837
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
803
838
  const invalidIds = extractInvalidIds(text, [
804
839
  "SPEC",
805
840
  "BR",
@@ -870,7 +905,7 @@ function issue(code, message, severity, file, rule, refs) {
870
905
  }
871
906
 
872
907
  // src/core/validators/delta.ts
873
- var import_promises5 = require("fs/promises");
908
+ var import_promises7 = require("fs/promises");
874
909
  var import_node_path7 = __toESM(require("path"), 1);
875
910
  var SECTION_RE = /^##\s+変更区分/m;
876
911
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
@@ -888,9 +923,9 @@ async function validateDeltas(root, config) {
888
923
  const deltaPath = import_node_path7.default.join(pack, "delta.md");
889
924
  let text;
890
925
  try {
891
- text = await (0, import_promises5.readFile)(deltaPath, "utf-8");
926
+ text = await (0, import_promises7.readFile)(deltaPath, "utf-8");
892
927
  } catch (error) {
893
- if (isMissingFileError(error)) {
928
+ if (isMissingFileError2(error)) {
894
929
  issues.push(
895
930
  issue2(
896
931
  "QFAI-DELTA-001",
@@ -935,7 +970,7 @@ async function validateDeltas(root, config) {
935
970
  }
936
971
  return issues;
937
972
  }
938
- function isMissingFileError(error) {
973
+ function isMissingFileError2(error) {
939
974
  if (!error || typeof error !== "object") {
940
975
  return false;
941
976
  }
@@ -960,11 +995,11 @@ function issue2(code, message, severity, file, rule, refs) {
960
995
  }
961
996
 
962
997
  // src/core/validators/ids.ts
963
- var import_promises7 = require("fs/promises");
998
+ var import_promises9 = require("fs/promises");
964
999
  var import_node_path9 = __toESM(require("path"), 1);
965
1000
 
966
1001
  // src/core/contractIndex.ts
967
- var import_promises6 = require("fs/promises");
1002
+ var import_promises8 = require("fs/promises");
968
1003
  var import_node_path8 = __toESM(require("path"), 1);
969
1004
  async function buildContractIndex(root, config) {
970
1005
  const contractsRoot = resolvePath(root, config, "contractsDir");
@@ -989,7 +1024,7 @@ async function buildContractIndex(root, config) {
989
1024
  }
990
1025
  async function indexUiContracts(files, index) {
991
1026
  for (const file of files) {
992
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1027
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
993
1028
  try {
994
1029
  const doc = parseStructuredContract(file, text);
995
1030
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1001,7 +1036,7 @@ async function indexUiContracts(files, index) {
1001
1036
  }
1002
1037
  async function indexApiContracts(files, index) {
1003
1038
  for (const file of files) {
1004
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1039
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1005
1040
  try {
1006
1041
  const doc = parseStructuredContract(file, text);
1007
1042
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1013,7 +1048,7 @@ async function indexApiContracts(files, index) {
1013
1048
  }
1014
1049
  async function indexDataContracts(files, index) {
1015
1050
  for (const file of files) {
1016
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1051
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1017
1052
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1018
1053
  }
1019
1054
  }
@@ -1024,66 +1059,6 @@ function record(index, id, file) {
1024
1059
  index.idToFiles.set(id, current);
1025
1060
  }
1026
1061
 
1027
- // src/core/parse/gherkin.ts
1028
- var FEATURE_RE = /^\s*Feature:\s+/;
1029
- var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
1030
- var TAG_LINE_RE = /^\s*@/;
1031
- function parseTags(line) {
1032
- return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
1033
- }
1034
- function parseGherkinFeature(text, file) {
1035
- const lines = text.split(/\r?\n/);
1036
- const scenarios = [];
1037
- let featurePresent = false;
1038
- let featureTags = [];
1039
- let pendingTags = [];
1040
- let current = null;
1041
- const flush = () => {
1042
- if (!current) return;
1043
- scenarios.push({
1044
- ...current,
1045
- body: current.body.trim()
1046
- });
1047
- current = null;
1048
- };
1049
- for (let i = 0; i < lines.length; i++) {
1050
- const line = lines[i] ?? "";
1051
- const trimmed = line.trim();
1052
- if (TAG_LINE_RE.test(trimmed)) {
1053
- pendingTags.push(...parseTags(trimmed));
1054
- continue;
1055
- }
1056
- if (FEATURE_RE.test(trimmed)) {
1057
- featurePresent = true;
1058
- featureTags = [...pendingTags];
1059
- pendingTags = [];
1060
- continue;
1061
- }
1062
- const match = trimmed.match(SCENARIO_RE);
1063
- if (match) {
1064
- const scenarioName = match[1]?.trim();
1065
- if (!scenarioName) {
1066
- continue;
1067
- }
1068
- flush();
1069
- current = {
1070
- name: scenarioName,
1071
- line: i + 1,
1072
- tags: [...featureTags, ...pendingTags],
1073
- body: ""
1074
- };
1075
- pendingTags = [];
1076
- continue;
1077
- }
1078
- if (current) {
1079
- current.body += `${line}
1080
- `;
1081
- }
1082
- }
1083
- flush();
1084
- return { file, featurePresent, scenarios };
1085
- }
1086
-
1087
1062
  // src/core/parse/markdown.ts
1088
1063
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1089
1064
  function parseHeadings(md) {
@@ -1202,8 +1177,162 @@ function parseSpec(md, file) {
1202
1177
  return parsed;
1203
1178
  }
1204
1179
 
1205
- // src/core/validators/ids.ts
1180
+ // src/core/gherkin/parse.ts
1181
+ var import_gherkin = require("@cucumber/gherkin");
1182
+ var import_node_crypto = require("crypto");
1183
+ function parseGherkin(source, uri) {
1184
+ const errors = [];
1185
+ const uuidFn = () => (0, import_node_crypto.randomUUID)();
1186
+ const builder = new import_gherkin.AstBuilder(uuidFn);
1187
+ const matcher = new import_gherkin.GherkinClassicTokenMatcher();
1188
+ const parser = new import_gherkin.Parser(builder, matcher);
1189
+ try {
1190
+ const gherkinDocument = parser.parse(source);
1191
+ gherkinDocument.uri = uri;
1192
+ return { gherkinDocument, errors };
1193
+ } catch (error) {
1194
+ errors.push(formatError3(error));
1195
+ return { gherkinDocument: null, errors };
1196
+ }
1197
+ }
1198
+ function formatError3(error) {
1199
+ if (error instanceof Error) {
1200
+ return error.message;
1201
+ }
1202
+ return String(error);
1203
+ }
1204
+
1205
+ // src/core/scenarioModel.ts
1206
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1206
1207
  var SC_TAG_RE = /^SC-\d{4}$/;
1208
+ var BR_TAG_RE = /^BR-\d{4}$/;
1209
+ var UI_TAG_RE = /^UI-\d{4}$/;
1210
+ var API_TAG_RE = /^API-\d{4}$/;
1211
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
1212
+ function parseScenarioDocument(text, uri) {
1213
+ const { gherkinDocument, errors } = parseGherkin(text, uri);
1214
+ if (!gherkinDocument) {
1215
+ return { document: null, errors };
1216
+ }
1217
+ const feature = gherkinDocument.feature;
1218
+ if (!feature) {
1219
+ return {
1220
+ document: { uri, featureTags: [], scenarios: [] },
1221
+ errors
1222
+ };
1223
+ }
1224
+ const featureTags = collectTagNames(feature.tags);
1225
+ const scenarios = collectScenarioNodes(feature, featureTags);
1226
+ return {
1227
+ document: {
1228
+ uri,
1229
+ featureName: feature.name,
1230
+ featureTags,
1231
+ scenarios
1232
+ },
1233
+ errors
1234
+ };
1235
+ }
1236
+ function buildScenarioAtoms(document) {
1237
+ return document.scenarios.map((scenario) => {
1238
+ const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1239
+ const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1240
+ const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1241
+ const contractIds = /* @__PURE__ */ new Set();
1242
+ scenario.tags.forEach((tag) => {
1243
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1244
+ contractIds.add(tag);
1245
+ }
1246
+ });
1247
+ for (const step of scenario.steps) {
1248
+ for (const text of collectStepTexts(step)) {
1249
+ extractIds(text, "UI").forEach((id) => contractIds.add(id));
1250
+ extractIds(text, "API").forEach((id) => contractIds.add(id));
1251
+ extractIds(text, "DATA").forEach((id) => contractIds.add(id));
1252
+ }
1253
+ }
1254
+ const atom = {
1255
+ uri: document.uri,
1256
+ featureName: document.featureName ?? "",
1257
+ scenarioName: scenario.name,
1258
+ kind: scenario.kind,
1259
+ brIds,
1260
+ contractIds: Array.from(contractIds).sort()
1261
+ };
1262
+ if (scenario.line !== void 0) {
1263
+ atom.line = scenario.line;
1264
+ }
1265
+ if (specIds.length === 1) {
1266
+ const specId = specIds[0];
1267
+ if (specId) {
1268
+ atom.specId = specId;
1269
+ }
1270
+ }
1271
+ if (scIds.length === 1) {
1272
+ const scId = scIds[0];
1273
+ if (scId) {
1274
+ atom.scId = scId;
1275
+ }
1276
+ }
1277
+ return atom;
1278
+ });
1279
+ }
1280
+ function collectScenarioNodes(feature, featureTags) {
1281
+ const scenarios = [];
1282
+ for (const child of feature.children) {
1283
+ if (child.scenario) {
1284
+ scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
1285
+ }
1286
+ if (child.rule) {
1287
+ const ruleTags = collectTagNames(child.rule.tags);
1288
+ for (const ruleChild of child.rule.children) {
1289
+ if (ruleChild.scenario) {
1290
+ scenarios.push(
1291
+ buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
1292
+ );
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+ return scenarios;
1298
+ }
1299
+ function buildScenarioNode(scenario, featureTags, ruleTags) {
1300
+ const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
1301
+ const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
1302
+ return {
1303
+ name: scenario.name,
1304
+ kind,
1305
+ line: scenario.location?.line,
1306
+ tags,
1307
+ steps: scenario.steps
1308
+ };
1309
+ }
1310
+ function collectTagNames(tags) {
1311
+ return tags.map((tag) => tag.name.replace(/^@/, ""));
1312
+ }
1313
+ function collectStepTexts(step) {
1314
+ const texts = [];
1315
+ if (step.text) {
1316
+ texts.push(step.text);
1317
+ }
1318
+ if (step.docString?.content) {
1319
+ texts.push(step.docString.content);
1320
+ }
1321
+ if (step.dataTable?.rows) {
1322
+ for (const row of step.dataTable.rows) {
1323
+ for (const cell of row.cells) {
1324
+ texts.push(cell.value);
1325
+ }
1326
+ }
1327
+ }
1328
+ return texts;
1329
+ }
1330
+ function unique2(values) {
1331
+ return Array.from(new Set(values));
1332
+ }
1333
+
1334
+ // src/core/validators/ids.ts
1335
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
1207
1336
  async function validateDefinedIds(root, config) {
1208
1337
  const issues = [];
1209
1338
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1237,7 +1366,7 @@ async function validateDefinedIds(root, config) {
1237
1366
  }
1238
1367
  async function collectSpecDefinitionIds(files, out) {
1239
1368
  for (const file of files) {
1240
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1369
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1241
1370
  const parsed = parseSpec(text, file);
1242
1371
  if (parsed.specId) {
1243
1372
  recordId(out, parsed.specId, file);
@@ -1247,11 +1376,14 @@ async function collectSpecDefinitionIds(files, out) {
1247
1376
  }
1248
1377
  async function collectScenarioDefinitionIds(files, out) {
1249
1378
  for (const file of files) {
1250
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1251
- const parsed = parseGherkinFeature(text, file);
1252
- for (const scenario of parsed.scenarios) {
1379
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1380
+ const { document, errors } = parseScenarioDocument(text, file);
1381
+ if (!document || errors.length > 0) {
1382
+ continue;
1383
+ }
1384
+ for (const scenario of document.scenarios) {
1253
1385
  for (const tag of scenario.tags) {
1254
- if (SC_TAG_RE.test(tag)) {
1386
+ if (SC_TAG_RE2.test(tag)) {
1255
1387
  recordId(out, tag, file);
1256
1388
  }
1257
1389
  }
@@ -1288,21 +1420,23 @@ function issue3(code, message, severity, file, rule, refs) {
1288
1420
  }
1289
1421
 
1290
1422
  // src/core/validators/scenario.ts
1291
- var import_promises8 = require("fs/promises");
1423
+ var import_promises10 = require("fs/promises");
1292
1424
  var GIVEN_PATTERN = /\bGiven\b/;
1293
1425
  var WHEN_PATTERN = /\bWhen\b/;
1294
1426
  var THEN_PATTERN = /\bThen\b/;
1295
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1296
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1297
- var BR_TAG_RE = /^BR-\d{4}$/;
1427
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1428
+ var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1429
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1298
1430
  async function validateScenarios(root, config) {
1299
1431
  const specsRoot = resolvePath(root, config, "specsDir");
1300
- const files = await collectScenarioFiles(specsRoot);
1301
- if (files.length === 0) {
1432
+ const entries = await collectSpecEntries(specsRoot);
1433
+ if (entries.length === 0) {
1434
+ const expected = "spec-0001/scenario.md";
1435
+ const legacy = "spec-001/scenario.md";
1302
1436
  return [
1303
1437
  issue4(
1304
1438
  "QFAI-SC-000",
1305
- "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1439
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
1306
1440
  "info",
1307
1441
  specsRoot,
1308
1442
  "scenario.files"
@@ -1310,15 +1444,31 @@ async function validateScenarios(root, config) {
1310
1444
  ];
1311
1445
  }
1312
1446
  const issues = [];
1313
- for (const file of files) {
1314
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1315
- issues.push(...validateScenarioContent(text, file));
1447
+ for (const entry of entries) {
1448
+ let text;
1449
+ try {
1450
+ text = await (0, import_promises10.readFile)(entry.scenarioPath, "utf-8");
1451
+ } catch (error) {
1452
+ if (isMissingFileError3(error)) {
1453
+ issues.push(
1454
+ issue4(
1455
+ "QFAI-SC-001",
1456
+ "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1457
+ "error",
1458
+ entry.scenarioPath,
1459
+ "scenario.exists"
1460
+ )
1461
+ );
1462
+ continue;
1463
+ }
1464
+ throw error;
1465
+ }
1466
+ issues.push(...validateScenarioContent(text, entry.scenarioPath));
1316
1467
  }
1317
1468
  return issues;
1318
1469
  }
1319
1470
  function validateScenarioContent(text, file) {
1320
1471
  const issues = [];
1321
- const parsed = parseGherkinFeature(text, file);
1322
1472
  const invalidIds = extractInvalidIds(text, [
1323
1473
  "SPEC",
1324
1474
  "BR",
@@ -1340,9 +1490,47 @@ function validateScenarioContent(text, file) {
1340
1490
  )
1341
1491
  );
1342
1492
  }
1493
+ const { document, errors } = parseScenarioDocument(text, file);
1494
+ if (!document || errors.length > 0) {
1495
+ issues.push(
1496
+ issue4(
1497
+ "QFAI-SC-010",
1498
+ `Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
1499
+ "error",
1500
+ file,
1501
+ "scenario.parse"
1502
+ )
1503
+ );
1504
+ return issues;
1505
+ }
1506
+ const featureSpecTags = document.featureTags.filter(
1507
+ (tag) => SPEC_TAG_RE2.test(tag)
1508
+ );
1509
+ if (featureSpecTags.length === 0) {
1510
+ issues.push(
1511
+ issue4(
1512
+ "QFAI-SC-009",
1513
+ "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1514
+ "error",
1515
+ file,
1516
+ "scenario.featureSpec"
1517
+ )
1518
+ );
1519
+ } else if (featureSpecTags.length > 1) {
1520
+ issues.push(
1521
+ issue4(
1522
+ "QFAI-SC-009",
1523
+ `Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
1524
+ "error",
1525
+ file,
1526
+ "scenario.featureSpec",
1527
+ featureSpecTags
1528
+ )
1529
+ );
1530
+ }
1343
1531
  const missingStructure = [];
1344
- if (!parsed.featurePresent) missingStructure.push("Feature");
1345
- if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1532
+ if (!document.featureName) missingStructure.push("Feature");
1533
+ if (document.scenarios.length === 0) missingStructure.push("Scenario");
1346
1534
  if (missingStructure.length > 0) {
1347
1535
  issues.push(
1348
1536
  issue4(
@@ -1356,7 +1544,7 @@ function validateScenarioContent(text, file) {
1356
1544
  )
1357
1545
  );
1358
1546
  }
1359
- for (const scenario of parsed.scenarios) {
1547
+ for (const scenario of document.scenarios) {
1360
1548
  if (scenario.tags.length === 0) {
1361
1549
  issues.push(
1362
1550
  issue4(
@@ -1370,16 +1558,16 @@ function validateScenarioContent(text, file) {
1370
1558
  continue;
1371
1559
  }
1372
1560
  const missingTags = [];
1373
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1561
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1374
1562
  if (scTags.length === 0) {
1375
1563
  missingTags.push("SC(0\u4EF6)");
1376
1564
  } else if (scTags.length > 1) {
1377
1565
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1378
1566
  }
1379
- if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1567
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1380
1568
  missingTags.push("SPEC");
1381
1569
  }
1382
- if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1570
+ if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1383
1571
  missingTags.push("BR");
1384
1572
  }
1385
1573
  if (missingTags.length > 0) {
@@ -1394,15 +1582,16 @@ function validateScenarioContent(text, file) {
1394
1582
  );
1395
1583
  }
1396
1584
  }
1397
- for (const scenario of parsed.scenarios) {
1585
+ for (const scenario of document.scenarios) {
1398
1586
  const missingSteps = [];
1399
- if (!GIVEN_PATTERN.test(scenario.body)) {
1587
+ const keywords = scenario.steps.map((step) => step.keyword.trim());
1588
+ if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
1400
1589
  missingSteps.push("Given");
1401
1590
  }
1402
- if (!WHEN_PATTERN.test(scenario.body)) {
1591
+ if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
1403
1592
  missingSteps.push("When");
1404
1593
  }
1405
- if (!THEN_PATTERN.test(scenario.body)) {
1594
+ if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
1406
1595
  missingSteps.push("Then");
1407
1596
  }
1408
1597
  if (missingSteps.length > 0) {
@@ -1436,18 +1625,25 @@ function issue4(code, message, severity, file, rule, refs) {
1436
1625
  }
1437
1626
  return issue7;
1438
1627
  }
1628
+ function isMissingFileError3(error) {
1629
+ if (!error || typeof error !== "object") {
1630
+ return false;
1631
+ }
1632
+ return error.code === "ENOENT";
1633
+ }
1439
1634
 
1440
1635
  // src/core/validators/spec.ts
1441
- var import_promises9 = require("fs/promises");
1636
+ var import_promises11 = require("fs/promises");
1442
1637
  async function validateSpecs(root, config) {
1443
1638
  const specsRoot = resolvePath(root, config, "specsDir");
1444
- const files = await collectSpecFiles(specsRoot);
1445
- if (files.length === 0) {
1446
- const expected = "spec-001/spec.md";
1639
+ const entries = await collectSpecEntries(specsRoot);
1640
+ if (entries.length === 0) {
1641
+ const expected = "spec-0001/spec.md";
1642
+ const legacy = "spec-001/spec.md";
1447
1643
  return [
1448
1644
  issue5(
1449
1645
  "QFAI-SPEC-000",
1450
- `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
1646
+ `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
1451
1647
  "info",
1452
1648
  specsRoot,
1453
1649
  "spec.files"
@@ -1455,12 +1651,29 @@ async function validateSpecs(root, config) {
1455
1651
  ];
1456
1652
  }
1457
1653
  const issues = [];
1458
- for (const file of files) {
1459
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1654
+ for (const entry of entries) {
1655
+ let text;
1656
+ try {
1657
+ text = await (0, import_promises11.readFile)(entry.specPath, "utf-8");
1658
+ } catch (error) {
1659
+ if (isMissingFileError4(error)) {
1660
+ issues.push(
1661
+ issue5(
1662
+ "QFAI-SPEC-005",
1663
+ "spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1664
+ "error",
1665
+ entry.specPath,
1666
+ "spec.exists"
1667
+ )
1668
+ );
1669
+ continue;
1670
+ }
1671
+ throw error;
1672
+ }
1460
1673
  issues.push(
1461
1674
  ...validateSpecContent(
1462
1675
  text,
1463
- file,
1676
+ entry.specPath,
1464
1677
  config.validation.require.specSections
1465
1678
  )
1466
1679
  );
@@ -1582,15 +1795,18 @@ function issue5(code, message, severity, file, rule, refs) {
1582
1795
  }
1583
1796
  return issue7;
1584
1797
  }
1798
+ function isMissingFileError4(error) {
1799
+ if (!error || typeof error !== "object") {
1800
+ return false;
1801
+ }
1802
+ return error.code === "ENOENT";
1803
+ }
1585
1804
 
1586
1805
  // src/core/validators/traceability.ts
1587
- var import_promises10 = require("fs/promises");
1588
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1589
- var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1590
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1591
- var UI_TAG_RE = /^UI-\d{4}$/;
1592
- var API_TAG_RE = /^API-\d{4}$/;
1593
- var DATA_TAG_RE = /^DATA-\d{4}$/;
1806
+ var import_promises12 = require("fs/promises");
1807
+ var SC_TAG_RE4 = /^SC-\d{4}$/;
1808
+ var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1809
+ var BR_TAG_RE3 = /^BR-\d{4}$/;
1594
1810
  async function validateTraceability(root, config) {
1595
1811
  const issues = [];
1596
1812
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1609,7 +1825,7 @@ async function validateTraceability(root, config) {
1609
1825
  const contractIndex = await buildContractIndex(root, config);
1610
1826
  const contractIds = contractIndex.ids;
1611
1827
  for (const file of specFiles) {
1612
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1828
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1613
1829
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1614
1830
  const parsed = parseSpec(text, file);
1615
1831
  if (parsed.specId) {
@@ -1646,106 +1862,99 @@ async function validateTraceability(root, config) {
1646
1862
  }
1647
1863
  }
1648
1864
  for (const file of scenarioFiles) {
1649
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1865
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1650
1866
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1651
- const parsed = parseGherkinFeature(text, file);
1652
- const specIdsInScenario = /* @__PURE__ */ new Set();
1653
- const brIds = /* @__PURE__ */ new Set();
1654
- const scIds = /* @__PURE__ */ new Set();
1655
- const scenarioIds = /* @__PURE__ */ new Set();
1656
- for (const scenario of parsed.scenarios) {
1657
- for (const tag of scenario.tags) {
1658
- if (SPEC_TAG_RE2.test(tag)) {
1659
- specIdsInScenario.add(tag);
1660
- }
1661
- if (BR_TAG_RE2.test(tag)) {
1662
- brIds.add(tag);
1663
- }
1664
- if (SC_TAG_RE3.test(tag)) {
1665
- scIds.add(tag);
1666
- }
1667
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1668
- scenarioIds.add(tag);
1669
- }
1670
- }
1671
- }
1672
- const specIdsList = Array.from(specIdsInScenario);
1673
- const brIdsList = Array.from(brIds);
1674
- const scIdsList = Array.from(scIds);
1675
- const scenarioIdsList = Array.from(scenarioIds);
1676
- brIdsList.forEach((id) => brIdsInScenarios.add(id));
1677
- scIdsList.forEach((id) => scIdsInScenarios.add(id));
1678
- scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
1679
- if (scenarioIdsList.length > 0) {
1680
- scIdsList.forEach((id) => scWithContracts.add(id));
1681
- }
1682
- const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1683
- if (unknownSpecIds.length > 0) {
1684
- issues.push(
1685
- issue6(
1686
- "QFAI-TRACE-005",
1687
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1688
- "error",
1689
- file,
1690
- "traceability.scenarioSpecExists",
1691
- unknownSpecIds
1692
- )
1693
- );
1694
- }
1695
- const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1696
- if (unknownBrIds.length > 0) {
1697
- issues.push(
1698
- issue6(
1699
- "QFAI-TRACE-006",
1700
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1701
- "error",
1702
- file,
1703
- "traceability.scenarioBrExists",
1704
- unknownBrIds
1705
- )
1706
- );
1707
- }
1708
- const unknownContractIds = scenarioIdsList.filter(
1709
- (id) => !contractIds.has(id)
1710
- );
1711
- if (unknownContractIds.length > 0) {
1712
- issues.push(
1713
- issue6(
1714
- "QFAI-TRACE-008",
1715
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1716
- ", "
1717
- )}`,
1718
- config.validation.traceability.unknownContractIdSeverity,
1719
- file,
1720
- "traceability.scenarioContractExists",
1721
- unknownContractIds
1722
- )
1723
- );
1867
+ const { document, errors } = parseScenarioDocument(text, file);
1868
+ if (!document || errors.length > 0) {
1869
+ continue;
1724
1870
  }
1725
- if (specIdsList.length > 0) {
1726
- const allowedBrIds = /* @__PURE__ */ new Set();
1727
- for (const specId of specIdsList) {
1728
- const brIdsForSpec = specToBrIds.get(specId);
1729
- if (!brIdsForSpec) {
1730
- continue;
1731
- }
1732
- brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1871
+ const atoms = buildScenarioAtoms(document);
1872
+ for (const [index, scenario] of document.scenarios.entries()) {
1873
+ const atom = atoms[index];
1874
+ if (!atom) {
1875
+ continue;
1876
+ }
1877
+ const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1878
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1879
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
1880
+ brTags.forEach((id) => brIdsInScenarios.add(id));
1881
+ scTags.forEach((id) => scIdsInScenarios.add(id));
1882
+ atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1883
+ if (atom.contractIds.length > 0) {
1884
+ scTags.forEach((id) => scWithContracts.add(id));
1885
+ }
1886
+ const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
1887
+ if (unknownSpecIds.length > 0) {
1888
+ issues.push(
1889
+ issue6(
1890
+ "QFAI-TRACE-005",
1891
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
1892
+ ", "
1893
+ )} (${scenario.name})`,
1894
+ "error",
1895
+ file,
1896
+ "traceability.scenarioSpecExists",
1897
+ unknownSpecIds
1898
+ )
1899
+ );
1733
1900
  }
1734
- const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1735
- if (invalidBrIds.length > 0) {
1901
+ const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
1902
+ if (unknownBrIds.length > 0) {
1736
1903
  issues.push(
1737
1904
  issue6(
1738
- "QFAI-TRACE-007",
1739
- `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1905
+ "QFAI-TRACE-006",
1906
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
1740
1907
  ", "
1741
- )} (SPEC: ${specIdsList.join(", ")})`,
1908
+ )} (${scenario.name})`,
1742
1909
  "error",
1743
1910
  file,
1744
- "traceability.scenarioBrUnderSpec",
1745
- invalidBrIds
1911
+ "traceability.scenarioBrExists",
1912
+ unknownBrIds
1913
+ )
1914
+ );
1915
+ }
1916
+ const unknownContractIds = atom.contractIds.filter(
1917
+ (id) => !contractIds.has(id)
1918
+ );
1919
+ if (unknownContractIds.length > 0) {
1920
+ issues.push(
1921
+ issue6(
1922
+ "QFAI-TRACE-008",
1923
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1924
+ ", "
1925
+ )} (${scenario.name})`,
1926
+ config.validation.traceability.unknownContractIdSeverity,
1927
+ file,
1928
+ "traceability.scenarioContractExists",
1929
+ unknownContractIds
1746
1930
  )
1747
1931
  );
1748
1932
  }
1933
+ if (specTags.length > 0 && brTags.length > 0) {
1934
+ const allowedBrIds = /* @__PURE__ */ new Set();
1935
+ for (const specId of specTags) {
1936
+ const brIdsForSpec = specToBrIds.get(specId);
1937
+ if (!brIdsForSpec) {
1938
+ continue;
1939
+ }
1940
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1941
+ }
1942
+ const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
1943
+ if (invalidBrIds.length > 0) {
1944
+ issues.push(
1945
+ issue6(
1946
+ "QFAI-TRACE-007",
1947
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1948
+ ", "
1949
+ )} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
1950
+ "error",
1951
+ file,
1952
+ "traceability.scenarioBrUnderSpec",
1953
+ invalidBrIds
1954
+ )
1955
+ );
1956
+ }
1957
+ }
1749
1958
  }
1750
1959
  }
1751
1960
  if (upstreamIds.size === 0) {
@@ -1843,7 +2052,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1843
2052
  const pattern = buildIdPattern(Array.from(upstreamIds));
1844
2053
  let found = false;
1845
2054
  for (const file of targetFiles) {
1846
- const text = await (0, import_promises10.readFile)(file, "utf-8");
2055
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1847
2056
  if (pattern.test(text)) {
1848
2057
  found = true;
1849
2058
  break;
@@ -2070,7 +2279,7 @@ async function collectIds(files) {
2070
2279
  DATA: /* @__PURE__ */ new Set()
2071
2280
  };
2072
2281
  for (const file of files) {
2073
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2282
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2074
2283
  for (const prefix of ID_PREFIXES2) {
2075
2284
  const ids = extractIds(text, prefix);
2076
2285
  ids.forEach((id) => result[prefix].add(id));
@@ -2088,7 +2297,7 @@ async function collectIds(files) {
2088
2297
  async function collectUpstreamIds(files) {
2089
2298
  const ids = /* @__PURE__ */ new Set();
2090
2299
  for (const file of files) {
2091
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2300
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2092
2301
  extractAllIds(text).forEach((id) => ids.add(id));
2093
2302
  }
2094
2303
  return ids;
@@ -2109,7 +2318,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2109
2318
  }
2110
2319
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2111
2320
  for (const file of targetFiles) {
2112
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2321
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2113
2322
  if (pattern.test(text)) {
2114
2323
  return true;
2115
2324
  }