qfai 0.3.0 → 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
@@ -44,8 +44,8 @@ __export(src_exports, {
44
44
  resolvePath: () => resolvePath,
45
45
  resolveToolVersion: () => resolveToolVersion,
46
46
  validateContracts: () => validateContracts,
47
- validateDecisions: () => validateDecisions,
48
47
  validateDefinedIds: () => validateDefinedIds,
48
+ validateDeltas: () => validateDeltas,
49
49
  validateProject: () => validateProject,
50
50
  validateScenarioContent: () => validateScenarioContent,
51
51
  validateScenarios: () => validateScenarios,
@@ -61,13 +61,11 @@ var import_node_path = __toESM(require("path"), 1);
61
61
  var import_yaml = require("yaml");
62
62
  var defaultConfig = {
63
63
  paths: {
64
- specDir: ".qfai/spec",
65
- decisionsDir: ".qfai/spec/decisions",
66
- scenariosDir: ".qfai/spec/scenarios",
67
64
  contractsDir: ".qfai/contracts",
68
- uiContractsDir: ".qfai/contracts/ui",
69
- apiContractsDir: ".qfai/contracts/api",
70
- dataContractsDir: ".qfai/contracts/db",
65
+ specsDir: ".qfai/specs",
66
+ rulesDir: ".qfai/rules",
67
+ outDir: ".qfai/out",
68
+ promptsDir: ".qfai/prompts",
71
69
  srcDir: "src",
72
70
  testsDir: "tests"
73
71
  },
@@ -92,8 +90,7 @@ var defaultConfig = {
92
90
  }
93
91
  },
94
92
  output: {
95
- format: "text",
96
- jsonPath: ".qfai/out/validate.json"
93
+ validateJsonPath: ".qfai/out/validate.json"
97
94
  }
98
95
  };
99
96
  function getConfigPath(root) {
@@ -142,27 +139,6 @@ function normalizePaths(raw, configPath, issues) {
142
139
  return base;
143
140
  }
144
141
  return {
145
- specDir: readString(
146
- raw.specDir,
147
- base.specDir,
148
- "paths.specDir",
149
- configPath,
150
- issues
151
- ),
152
- decisionsDir: readString(
153
- raw.decisionsDir,
154
- base.decisionsDir,
155
- "paths.decisionsDir",
156
- configPath,
157
- issues
158
- ),
159
- scenariosDir: readString(
160
- raw.scenariosDir,
161
- base.scenariosDir,
162
- "paths.scenariosDir",
163
- configPath,
164
- issues
165
- ),
166
142
  contractsDir: readString(
167
143
  raw.contractsDir,
168
144
  base.contractsDir,
@@ -170,24 +146,31 @@ function normalizePaths(raw, configPath, issues) {
170
146
  configPath,
171
147
  issues
172
148
  ),
173
- uiContractsDir: readString(
174
- raw.uiContractsDir,
175
- base.uiContractsDir,
176
- "paths.uiContractsDir",
149
+ specsDir: readString(
150
+ raw.specsDir,
151
+ base.specsDir,
152
+ "paths.specsDir",
153
+ configPath,
154
+ issues
155
+ ),
156
+ rulesDir: readString(
157
+ raw.rulesDir,
158
+ base.rulesDir,
159
+ "paths.rulesDir",
177
160
  configPath,
178
161
  issues
179
162
  ),
180
- apiContractsDir: readString(
181
- raw.apiContractsDir,
182
- base.apiContractsDir,
183
- "paths.apiContractsDir",
163
+ outDir: readString(
164
+ raw.outDir,
165
+ base.outDir,
166
+ "paths.outDir",
184
167
  configPath,
185
168
  issues
186
169
  ),
187
- dataContractsDir: readString(
188
- raw.dataContractsDir,
189
- base.dataContractsDir,
190
- "paths.dataContractsDir",
170
+ promptsDir: readString(
171
+ raw.promptsDir,
172
+ base.promptsDir,
173
+ "paths.promptsDir",
191
174
  configPath,
192
175
  issues
193
176
  ),
@@ -310,17 +293,10 @@ function normalizeOutput(raw, configPath, issues) {
310
293
  return base;
311
294
  }
312
295
  return {
313
- format: readOutputFormat(
314
- raw.format,
315
- base.format,
316
- "output.format",
317
- configPath,
318
- issues
319
- ),
320
- jsonPath: readString(
321
- raw.jsonPath,
322
- base.jsonPath,
323
- "output.jsonPath",
296
+ validateJsonPath: readString(
297
+ raw.validateJsonPath,
298
+ base.validateJsonPath,
299
+ "output.validateJsonPath",
324
300
  configPath,
325
301
  issues
326
302
  )
@@ -387,20 +363,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
387
363
  }
388
364
  return fallback;
389
365
  }
390
- function readOutputFormat(value, fallback, label, configPath, issues) {
391
- if (value === "text" || value === "json" || value === "github") {
392
- return value;
393
- }
394
- if (value !== void 0) {
395
- issues.push(
396
- configIssue(
397
- configPath,
398
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
399
- )
400
- );
401
- }
402
- return fallback;
403
- }
404
366
  function configIssue(file, message) {
405
367
  return {
406
368
  code: "QFAI_CONFIG_INVALID",
@@ -480,10 +442,11 @@ function isValidId(value, prefix) {
480
442
  }
481
443
 
482
444
  // src/core/report.ts
483
- var import_promises11 = require("fs/promises");
445
+ var import_promises13 = require("fs/promises");
446
+ var import_node_path10 = __toESM(require("path"), 1);
484
447
 
485
448
  // src/core/discovery.ts
486
- var import_node_path3 = __toESM(require("path"), 1);
449
+ var import_promises4 = require("fs/promises");
487
450
 
488
451
  // src/core/fs.ts
489
452
  var import_promises2 = require("fs/promises");
@@ -540,11 +503,50 @@ async function exists(target) {
540
503
  }
541
504
  }
542
505
 
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 [];
527
+ }
528
+ throw error;
529
+ }
530
+ }
531
+ function isMissingFileError(error) {
532
+ if (!error || typeof error !== "object") {
533
+ return false;
534
+ }
535
+ return error.code === "ENOENT";
536
+ }
537
+
543
538
  // src/core/discovery.ts
544
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
545
- async function collectSpecFiles(specRoot) {
546
- const files = await collectFiles(specRoot, { extensions: [".md"] });
547
- return files.filter((file) => isSpecFile(file));
539
+ async function collectSpecPackDirs(specsRoot) {
540
+ const entries = await collectSpecEntries(specsRoot);
541
+ return entries.map((entry) => entry.dir);
542
+ }
543
+ async function collectSpecFiles(specsRoot) {
544
+ const entries = await collectSpecEntries(specsRoot);
545
+ return filterExisting(entries.map((entry) => entry.specPath));
546
+ }
547
+ async function collectScenarioFiles(specsRoot) {
548
+ const entries = await collectSpecEntries(specsRoot);
549
+ return filterExisting(entries.map((entry) => entry.scenarioPath));
548
550
  }
549
551
  async function collectUiContractFiles(uiRoot) {
550
552
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -563,25 +565,38 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
563
565
  ]);
564
566
  return { ui, api, db };
565
567
  }
566
- function isSpecFile(filePath) {
567
- const name = import_node_path3.default.basename(filePath).toLowerCase();
568
- return SPEC_NAMED_PATTERN.test(name);
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 {
582
+ return false;
583
+ }
569
584
  }
570
585
 
571
586
  // src/core/types.ts
572
587
  var VALIDATION_SCHEMA_VERSION = "0.2";
573
588
 
574
589
  // src/core/version.ts
575
- var import_promises3 = require("fs/promises");
590
+ var import_promises5 = require("fs/promises");
576
591
  var import_node_path4 = __toESM(require("path"), 1);
577
592
  var import_node_url = require("url");
578
593
  async function resolveToolVersion() {
579
- if ("0.3.0".length > 0) {
580
- return "0.3.0";
594
+ if ("0.3.2".length > 0) {
595
+ return "0.3.2";
581
596
  }
582
597
  try {
583
598
  const packagePath = resolvePackageJsonPath();
584
- const raw = await (0, import_promises3.readFile)(packagePath, "utf-8");
599
+ const raw = await (0, import_promises5.readFile)(packagePath, "utf-8");
585
600
  const parsed = JSON.parse(raw);
586
601
  const version = typeof parsed.version === "string" ? parsed.version : "";
587
602
  return version.length > 0 ? version : "unknown";
@@ -596,7 +611,8 @@ function resolvePackageJsonPath() {
596
611
  }
597
612
 
598
613
  // src/core/validators/contracts.ts
599
- var import_promises4 = require("fs/promises");
614
+ var import_promises6 = require("fs/promises");
615
+ var import_node_path6 = __toESM(require("path"), 1);
600
616
 
601
617
  // src/core/contracts.ts
602
618
  var import_node_path5 = __toESM(require("path"), 1);
@@ -652,19 +668,10 @@ var SQL_DANGEROUS_PATTERNS = [
652
668
  ];
653
669
  async function validateContracts(root, config) {
654
670
  const issues = [];
655
- issues.push(
656
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
657
- );
658
- issues.push(
659
- ...await validateApiContracts(
660
- resolvePath(root, config, "apiContractsDir")
661
- )
662
- );
663
- issues.push(
664
- ...await validateDataContracts(
665
- resolvePath(root, config, "dataContractsDir")
666
- )
667
- );
671
+ const contractsRoot = resolvePath(root, config, "contractsDir");
672
+ issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
673
+ issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
674
+ issues.push(...await validateDataContracts(import_node_path6.default.join(contractsRoot, "db")));
668
675
  return issues;
669
676
  }
670
677
  async function validateUiContracts(uiRoot) {
@@ -682,7 +689,7 @@ async function validateUiContracts(uiRoot) {
682
689
  }
683
690
  const issues = [];
684
691
  for (const file of files) {
685
- const text = await (0, import_promises4.readFile)(file, "utf-8");
692
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
686
693
  const invalidIds = extractInvalidIds(text, [
687
694
  "SPEC",
688
695
  "BR",
@@ -749,7 +756,7 @@ async function validateApiContracts(apiRoot) {
749
756
  }
750
757
  const issues = [];
751
758
  for (const file of files) {
752
- const text = await (0, import_promises4.readFile)(file, "utf-8");
759
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
753
760
  const invalidIds = extractInvalidIds(text, [
754
761
  "SPEC",
755
762
  "BR",
@@ -827,7 +834,7 @@ async function validateDataContracts(dataRoot) {
827
834
  }
828
835
  const issues = [];
829
836
  for (const file of files) {
830
- const text = await (0, import_promises4.readFile)(file, "utf-8");
837
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
831
838
  const invalidIds = extractInvalidIds(text, [
832
839
  "SPEC",
833
840
  "BR",
@@ -897,72 +904,78 @@ function issue(code, message, severity, file, rule, refs) {
897
904
  return issue7;
898
905
  }
899
906
 
900
- // src/core/validators/decisions.ts
901
- var import_promises5 = require("fs/promises");
902
-
903
- // src/core/parse/adr.ts
904
- var ADR_ID_RE = /\bADR-\d{4}\b/;
905
- function extractField(md, key) {
906
- const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
907
- return md.match(pattern)?.[1]?.trim();
908
- }
909
- function parseAdr(md, file) {
910
- const adrId = md.match(ADR_ID_RE)?.[0];
911
- const fields = {};
912
- const status = extractField(md, "Status");
913
- const context = extractField(md, "Context");
914
- const decision = extractField(md, "Decision");
915
- const consequences = extractField(md, "Consequences");
916
- const related = extractField(md, "Related");
917
- if (status) fields.status = status;
918
- if (context) fields.context = context;
919
- if (decision) fields.decision = decision;
920
- if (consequences) fields.consequences = consequences;
921
- if (related) fields.related = related;
922
- const parsed = {
923
- file,
924
- fields
925
- };
926
- if (adrId) {
927
- parsed.adrId = adrId;
928
- }
929
- return parsed;
930
- }
931
-
932
- // src/core/validators/decisions.ts
933
- var REQUIRED_FIELDS = [
934
- { key: "status", label: "Status" },
935
- { key: "context", label: "Context" },
936
- { key: "decision", label: "Decision" },
937
- { key: "consequences", label: "Consequences" }
938
- ];
939
- async function validateDecisions(root, config) {
940
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
941
- const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
942
- if (files.length === 0) {
907
+ // src/core/validators/delta.ts
908
+ var import_promises7 = require("fs/promises");
909
+ var import_node_path7 = __toESM(require("path"), 1);
910
+ var SECTION_RE = /^##\s+変更区分/m;
911
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
912
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
913
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
914
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
915
+ async function validateDeltas(root, config) {
916
+ const specsRoot = resolvePath(root, config, "specsDir");
917
+ const packs = await collectSpecPackDirs(specsRoot);
918
+ if (packs.length === 0) {
943
919
  return [];
944
920
  }
945
921
  const issues = [];
946
- for (const file of files) {
947
- const text = await (0, import_promises5.readFile)(file, "utf-8");
948
- const parsed = parseAdr(text, file);
949
- const missing = REQUIRED_FIELDS.filter(
950
- (field) => !parsed.fields[field.key]
951
- );
952
- if (missing.length > 0) {
922
+ for (const pack of packs) {
923
+ const deltaPath = import_node_path7.default.join(pack, "delta.md");
924
+ let text;
925
+ try {
926
+ text = await (0, import_promises7.readFile)(deltaPath, "utf-8");
927
+ } catch (error) {
928
+ if (isMissingFileError2(error)) {
929
+ issues.push(
930
+ issue2(
931
+ "QFAI-DELTA-001",
932
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
933
+ "error",
934
+ deltaPath,
935
+ "delta.exists"
936
+ )
937
+ );
938
+ continue;
939
+ }
940
+ throw error;
941
+ }
942
+ const hasSection = SECTION_RE.test(text);
943
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
944
+ const hasChange = CHANGE_LINE_RE.test(text);
945
+ if (!hasSection || !hasCompatibility || !hasChange) {
953
946
  issues.push(
954
947
  issue2(
955
- "QFAI-ADR-001",
956
- `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
948
+ "QFAI-DELTA-002",
949
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
957
950
  "error",
958
- file,
959
- "adr.requiredFields"
951
+ deltaPath,
952
+ "delta.section"
953
+ )
954
+ );
955
+ continue;
956
+ }
957
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
958
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
959
+ if (compatibilityChecked === changeChecked) {
960
+ issues.push(
961
+ issue2(
962
+ "QFAI-DELTA-003",
963
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
964
+ "error",
965
+ deltaPath,
966
+ "delta.classification"
960
967
  )
961
968
  );
962
969
  }
963
970
  }
964
971
  return issues;
965
972
  }
973
+ function isMissingFileError2(error) {
974
+ if (!error || typeof error !== "object") {
975
+ return false;
976
+ }
977
+ return error.code === "ENOENT";
978
+ }
966
979
  function issue2(code, message, severity, file, rule, refs) {
967
980
  const issue7 = {
968
981
  code,
@@ -982,15 +995,17 @@ function issue2(code, message, severity, file, rule, refs) {
982
995
  }
983
996
 
984
997
  // src/core/validators/ids.ts
985
- var import_promises7 = require("fs/promises");
986
- var import_node_path6 = __toESM(require("path"), 1);
998
+ var import_promises9 = require("fs/promises");
999
+ var import_node_path9 = __toESM(require("path"), 1);
987
1000
 
988
1001
  // src/core/contractIndex.ts
989
- var import_promises6 = require("fs/promises");
1002
+ var import_promises8 = require("fs/promises");
1003
+ var import_node_path8 = __toESM(require("path"), 1);
990
1004
  async function buildContractIndex(root, config) {
991
- const uiRoot = resolvePath(root, config, "uiContractsDir");
992
- const apiRoot = resolvePath(root, config, "apiContractsDir");
993
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1005
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1006
+ const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
1007
+ const apiRoot = import_node_path8.default.join(contractsRoot, "api");
1008
+ const dataRoot = import_node_path8.default.join(contractsRoot, "db");
994
1009
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
995
1010
  collectUiContractFiles(uiRoot),
996
1011
  collectApiContractFiles(apiRoot),
@@ -1009,7 +1024,7 @@ async function buildContractIndex(root, config) {
1009
1024
  }
1010
1025
  async function indexUiContracts(files, index) {
1011
1026
  for (const file of files) {
1012
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1027
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1013
1028
  try {
1014
1029
  const doc = parseStructuredContract(file, text);
1015
1030
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1021,7 +1036,7 @@ async function indexUiContracts(files, index) {
1021
1036
  }
1022
1037
  async function indexApiContracts(files, index) {
1023
1038
  for (const file of files) {
1024
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1039
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1025
1040
  try {
1026
1041
  const doc = parseStructuredContract(file, text);
1027
1042
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -1033,7 +1048,7 @@ async function indexApiContracts(files, index) {
1033
1048
  }
1034
1049
  async function indexDataContracts(files, index) {
1035
1050
  for (const file of files) {
1036
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1051
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1037
1052
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
1038
1053
  }
1039
1054
  }
@@ -1044,38 +1059,6 @@ function record(index, id, file) {
1044
1059
  index.idToFiles.set(id, current);
1045
1060
  }
1046
1061
 
1047
- // src/core/parse/gherkin.ts
1048
- var FEATURE_RE = /^\s*Feature:\s+/;
1049
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
1050
- var TAG_LINE_RE = /^\s*@/;
1051
- function parseTags(line) {
1052
- return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
1053
- }
1054
- function parseGherkinFeature(text, file) {
1055
- const lines = text.split(/\r?\n/);
1056
- const scenarios = [];
1057
- let featurePresent = false;
1058
- for (let i = 0; i < lines.length; i++) {
1059
- const line = lines[i] ?? "";
1060
- if (FEATURE_RE.test(line)) {
1061
- featurePresent = true;
1062
- }
1063
- const match = line.match(SCENARIO_RE);
1064
- if (!match) continue;
1065
- const scenarioName = match[1];
1066
- if (!scenarioName) continue;
1067
- const tags = [];
1068
- for (let j = i - 1; j >= 0; j--) {
1069
- const previous = lines[j] ?? "";
1070
- if (previous.trim() === "") continue;
1071
- if (!TAG_LINE_RE.test(previous)) break;
1072
- tags.unshift(...parseTags(previous));
1073
- }
1074
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1075
- }
1076
- return { file, featurePresent, scenarios };
1077
- }
1078
-
1079
1062
  // src/core/parse/markdown.ts
1080
1063
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1081
1064
  function parseHeadings(md) {
@@ -1119,9 +1102,9 @@ function extractH2Sections(md) {
1119
1102
 
1120
1103
  // src/core/parse/spec.ts
1121
1104
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1122
- var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1123
- var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1124
- var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
1105
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1106
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1107
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1125
1108
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1126
1109
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1127
1110
  function parseSpec(md, file) {
@@ -1194,16 +1177,167 @@ function parseSpec(md, file) {
1194
1177
  return parsed;
1195
1178
  }
1196
1179
 
1197
- // 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}$/;
1198
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}$/;
1199
1336
  async function validateDefinedIds(root, config) {
1200
1337
  const issues = [];
1201
- const specRoot = resolvePath(root, config, "specDir");
1202
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
1203
- const specFiles = await collectSpecFiles(specRoot);
1204
- const scenarioFiles = await collectFiles(scenarioRoot, {
1205
- extensions: [".feature"]
1206
- });
1338
+ const specsRoot = resolvePath(root, config, "specsDir");
1339
+ const specFiles = await collectSpecFiles(specsRoot);
1340
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1207
1341
  const defined = /* @__PURE__ */ new Map();
1208
1342
  await collectSpecDefinitionIds(specFiles, defined);
1209
1343
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1232,7 +1366,7 @@ async function validateDefinedIds(root, config) {
1232
1366
  }
1233
1367
  async function collectSpecDefinitionIds(files, out) {
1234
1368
  for (const file of files) {
1235
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1369
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1236
1370
  const parsed = parseSpec(text, file);
1237
1371
  if (parsed.specId) {
1238
1372
  recordId(out, parsed.specId, file);
@@ -1242,11 +1376,14 @@ async function collectSpecDefinitionIds(files, out) {
1242
1376
  }
1243
1377
  async function collectScenarioDefinitionIds(files, out) {
1244
1378
  for (const file of files) {
1245
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1246
- const parsed = parseGherkinFeature(text, file);
1247
- 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) {
1248
1385
  for (const tag of scenario.tags) {
1249
- if (SC_TAG_RE.test(tag)) {
1386
+ if (SC_TAG_RE2.test(tag)) {
1250
1387
  recordId(out, tag, file);
1251
1388
  }
1252
1389
  }
@@ -1260,7 +1397,7 @@ function recordId(out, id, file) {
1260
1397
  }
1261
1398
  function formatFileList(files, root) {
1262
1399
  return files.map((file) => {
1263
- const relative = import_node_path6.default.relative(root, file);
1400
+ const relative = import_node_path9.default.relative(root, file);
1264
1401
  return relative.length > 0 ? relative : file;
1265
1402
  }).join(", ");
1266
1403
  }
@@ -1283,39 +1420,55 @@ function issue3(code, message, severity, file, rule, refs) {
1283
1420
  }
1284
1421
 
1285
1422
  // src/core/validators/scenario.ts
1286
- var import_promises8 = require("fs/promises");
1423
+ var import_promises10 = require("fs/promises");
1287
1424
  var GIVEN_PATTERN = /\bGiven\b/;
1288
1425
  var WHEN_PATTERN = /\bWhen\b/;
1289
1426
  var THEN_PATTERN = /\bThen\b/;
1290
- var SC_TAG_RE2 = /^SC-\d{4}$/;
1291
- var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1292
- 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}$/;
1293
1430
  async function validateScenarios(root, config) {
1294
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1295
- const files = await collectFiles(scenariosRoot, {
1296
- extensions: [".feature"]
1297
- });
1298
- if (files.length === 0) {
1431
+ const specsRoot = resolvePath(root, config, "specsDir");
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";
1299
1436
  return [
1300
1437
  issue4(
1301
1438
  "QFAI-SC-000",
1302
- "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)`,
1303
1440
  "info",
1304
- scenariosRoot,
1441
+ specsRoot,
1305
1442
  "scenario.files"
1306
1443
  )
1307
1444
  ];
1308
1445
  }
1309
1446
  const issues = [];
1310
- for (const file of files) {
1311
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1312
- 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));
1313
1467
  }
1314
1468
  return issues;
1315
1469
  }
1316
1470
  function validateScenarioContent(text, file) {
1317
1471
  const issues = [];
1318
- const parsed = parseGherkinFeature(text, file);
1319
1472
  const invalidIds = extractInvalidIds(text, [
1320
1473
  "SPEC",
1321
1474
  "BR",
@@ -1337,9 +1490,47 @@ function validateScenarioContent(text, file) {
1337
1490
  )
1338
1491
  );
1339
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
+ }
1340
1531
  const missingStructure = [];
1341
- if (!parsed.featurePresent) missingStructure.push("Feature");
1342
- if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1532
+ if (!document.featureName) missingStructure.push("Feature");
1533
+ if (document.scenarios.length === 0) missingStructure.push("Scenario");
1343
1534
  if (missingStructure.length > 0) {
1344
1535
  issues.push(
1345
1536
  issue4(
@@ -1353,7 +1544,7 @@ function validateScenarioContent(text, file) {
1353
1544
  )
1354
1545
  );
1355
1546
  }
1356
- for (const scenario of parsed.scenarios) {
1547
+ for (const scenario of document.scenarios) {
1357
1548
  if (scenario.tags.length === 0) {
1358
1549
  issues.push(
1359
1550
  issue4(
@@ -1367,13 +1558,16 @@ function validateScenarioContent(text, file) {
1367
1558
  continue;
1368
1559
  }
1369
1560
  const missingTags = [];
1370
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1371
- missingTags.push("SC");
1561
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
1562
+ if (scTags.length === 0) {
1563
+ missingTags.push("SC(0\u4EF6)");
1564
+ } else if (scTags.length > 1) {
1565
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1372
1566
  }
1373
- if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1567
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1374
1568
  missingTags.push("SPEC");
1375
1569
  }
1376
- if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1570
+ if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1377
1571
  missingTags.push("BR");
1378
1572
  }
1379
1573
  if (missingTags.length > 0) {
@@ -1388,26 +1582,29 @@ function validateScenarioContent(text, file) {
1388
1582
  );
1389
1583
  }
1390
1584
  }
1391
- const missingSteps = [];
1392
- if (!GIVEN_PATTERN.test(text)) {
1393
- missingSteps.push("Given");
1394
- }
1395
- if (!WHEN_PATTERN.test(text)) {
1396
- missingSteps.push("When");
1397
- }
1398
- if (!THEN_PATTERN.test(text)) {
1399
- missingSteps.push("Then");
1400
- }
1401
- if (missingSteps.length > 0) {
1402
- issues.push(
1403
- issue4(
1404
- "QFAI-SC-005",
1405
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1406
- "warning",
1407
- file,
1408
- "scenario.steps"
1409
- )
1410
- );
1585
+ for (const scenario of document.scenarios) {
1586
+ const missingSteps = [];
1587
+ const keywords = scenario.steps.map((step) => step.keyword.trim());
1588
+ if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
1589
+ missingSteps.push("Given");
1590
+ }
1591
+ if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
1592
+ missingSteps.push("When");
1593
+ }
1594
+ if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
1595
+ missingSteps.push("Then");
1596
+ }
1597
+ if (missingSteps.length > 0) {
1598
+ issues.push(
1599
+ issue4(
1600
+ "QFAI-SC-005",
1601
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1602
+ "warning",
1603
+ file,
1604
+ "scenario.steps"
1605
+ )
1606
+ );
1607
+ }
1411
1608
  }
1412
1609
  return issues;
1413
1610
  }
@@ -1428,18 +1625,25 @@ function issue4(code, message, severity, file, rule, refs) {
1428
1625
  }
1429
1626
  return issue7;
1430
1627
  }
1628
+ function isMissingFileError3(error) {
1629
+ if (!error || typeof error !== "object") {
1630
+ return false;
1631
+ }
1632
+ return error.code === "ENOENT";
1633
+ }
1431
1634
 
1432
1635
  // src/core/validators/spec.ts
1433
- var import_promises9 = require("fs/promises");
1636
+ var import_promises11 = require("fs/promises");
1434
1637
  async function validateSpecs(root, config) {
1435
- const specsRoot = resolvePath(root, config, "specDir");
1436
- const files = await collectSpecFiles(specsRoot);
1437
- if (files.length === 0) {
1438
- const expected = "spec-0001-<slug>.md";
1638
+ const specsRoot = resolvePath(root, config, "specsDir");
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";
1439
1643
  return [
1440
1644
  issue5(
1441
1645
  "QFAI-SPEC-000",
1442
- `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \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)`,
1443
1647
  "info",
1444
1648
  specsRoot,
1445
1649
  "spec.files"
@@ -1447,12 +1651,29 @@ async function validateSpecs(root, config) {
1447
1651
  ];
1448
1652
  }
1449
1653
  const issues = [];
1450
- for (const file of files) {
1451
- 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
+ }
1452
1673
  issues.push(
1453
1674
  ...validateSpecContent(
1454
1675
  text,
1455
- file,
1676
+ entry.specPath,
1456
1677
  config.validation.require.specSections
1457
1678
  )
1458
1679
  );
@@ -1574,29 +1795,25 @@ function issue5(code, message, severity, file, rule, refs) {
1574
1795
  }
1575
1796
  return issue7;
1576
1797
  }
1798
+ function isMissingFileError4(error) {
1799
+ if (!error || typeof error !== "object") {
1800
+ return false;
1801
+ }
1802
+ return error.code === "ENOENT";
1803
+ }
1577
1804
 
1578
1805
  // src/core/validators/traceability.ts
1579
- var import_promises10 = require("fs/promises");
1580
- var SC_TAG_RE3 = /^SC-\d{4}$/;
1581
- var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1582
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1583
- var UI_TAG_RE = /^UI-\d{4}$/;
1584
- var API_TAG_RE = /^API-\d{4}$/;
1585
- 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}$/;
1586
1810
  async function validateTraceability(root, config) {
1587
1811
  const issues = [];
1588
- const specsRoot = resolvePath(root, config, "specDir");
1589
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1590
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1812
+ const specsRoot = resolvePath(root, config, "specsDir");
1591
1813
  const srcRoot = resolvePath(root, config, "srcDir");
1592
1814
  const testsRoot = resolvePath(root, config, "testsDir");
1593
1815
  const specFiles = await collectSpecFiles(specsRoot);
1594
- const decisionFiles = await collectFiles(decisionsRoot, {
1595
- extensions: [".md"]
1596
- });
1597
- const scenarioFiles = await collectFiles(scenariosRoot, {
1598
- extensions: [".feature"]
1599
- });
1816
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1600
1817
  const upstreamIds = /* @__PURE__ */ new Set();
1601
1818
  const specIds = /* @__PURE__ */ new Set();
1602
1819
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1608,7 +1825,7 @@ async function validateTraceability(root, config) {
1608
1825
  const contractIndex = await buildContractIndex(root, config);
1609
1826
  const contractIds = contractIndex.ids;
1610
1827
  for (const file of specFiles) {
1611
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1828
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1612
1829
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1613
1830
  const parsed = parseSpec(text, file);
1614
1831
  if (parsed.specId) {
@@ -1644,111 +1861,100 @@ async function validateTraceability(root, config) {
1644
1861
  specToBrIds.set(parsed.specId, current);
1645
1862
  }
1646
1863
  }
1647
- for (const file of decisionFiles) {
1648
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1649
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1650
- }
1651
1864
  for (const file of scenarioFiles) {
1652
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1865
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1653
1866
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1654
- const parsed = parseGherkinFeature(text, file);
1655
- const specIdsInScenario = /* @__PURE__ */ new Set();
1656
- const brIds = /* @__PURE__ */ new Set();
1657
- const scIds = /* @__PURE__ */ new Set();
1658
- const scenarioIds = /* @__PURE__ */ new Set();
1659
- for (const scenario of parsed.scenarios) {
1660
- for (const tag of scenario.tags) {
1661
- if (SPEC_TAG_RE2.test(tag)) {
1662
- specIdsInScenario.add(tag);
1663
- }
1664
- if (BR_TAG_RE2.test(tag)) {
1665
- brIds.add(tag);
1666
- }
1667
- if (SC_TAG_RE3.test(tag)) {
1668
- scIds.add(tag);
1669
- }
1670
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1671
- scenarioIds.add(tag);
1672
- }
1673
- }
1674
- }
1675
- const specIdsList = Array.from(specIdsInScenario);
1676
- const brIdsList = Array.from(brIds);
1677
- const scIdsList = Array.from(scIds);
1678
- const scenarioIdsList = Array.from(scenarioIds);
1679
- brIdsList.forEach((id) => brIdsInScenarios.add(id));
1680
- scIdsList.forEach((id) => scIdsInScenarios.add(id));
1681
- scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
1682
- if (scenarioIdsList.length > 0) {
1683
- scIdsList.forEach((id) => scWithContracts.add(id));
1684
- }
1685
- const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1686
- if (unknownSpecIds.length > 0) {
1687
- issues.push(
1688
- issue6(
1689
- "QFAI-TRACE-005",
1690
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1691
- "error",
1692
- file,
1693
- "traceability.scenarioSpecExists",
1694
- unknownSpecIds
1695
- )
1696
- );
1697
- }
1698
- const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1699
- if (unknownBrIds.length > 0) {
1700
- issues.push(
1701
- issue6(
1702
- "QFAI-TRACE-006",
1703
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1704
- "error",
1705
- file,
1706
- "traceability.scenarioBrExists",
1707
- unknownBrIds
1708
- )
1709
- );
1710
- }
1711
- const unknownContractIds = scenarioIdsList.filter(
1712
- (id) => !contractIds.has(id)
1713
- );
1714
- if (unknownContractIds.length > 0) {
1715
- issues.push(
1716
- issue6(
1717
- "QFAI-TRACE-008",
1718
- `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1719
- ", "
1720
- )}`,
1721
- config.validation.traceability.unknownContractIdSeverity,
1722
- file,
1723
- "traceability.scenarioContractExists",
1724
- unknownContractIds
1725
- )
1726
- );
1867
+ const { document, errors } = parseScenarioDocument(text, file);
1868
+ if (!document || errors.length > 0) {
1869
+ continue;
1727
1870
  }
1728
- if (specIdsList.length > 0) {
1729
- const allowedBrIds = /* @__PURE__ */ new Set();
1730
- for (const specId of specIdsList) {
1731
- const brIdsForSpec = specToBrIds.get(specId);
1732
- if (!brIdsForSpec) {
1733
- continue;
1734
- }
1735
- 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
+ );
1736
1900
  }
1737
- const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1738
- if (invalidBrIds.length > 0) {
1901
+ const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
1902
+ if (unknownBrIds.length > 0) {
1739
1903
  issues.push(
1740
1904
  issue6(
1741
- "QFAI-TRACE-007",
1742
- `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(
1743
1907
  ", "
1744
- )} (SPEC: ${specIdsList.join(", ")})`,
1908
+ )} (${scenario.name})`,
1745
1909
  "error",
1746
1910
  file,
1747
- "traceability.scenarioBrUnderSpec",
1748
- invalidBrIds
1911
+ "traceability.scenarioBrExists",
1912
+ unknownBrIds
1749
1913
  )
1750
1914
  );
1751
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
1930
+ )
1931
+ );
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
+ }
1752
1958
  }
1753
1959
  }
1754
1960
  if (upstreamIds.size === 0) {
@@ -1791,7 +1997,7 @@ async function validateTraceability(root, config) {
1791
1997
  ", "
1792
1998
  )}`,
1793
1999
  "error",
1794
- scenariosRoot,
2000
+ specsRoot,
1795
2001
  "traceability.scMustTouchContracts",
1796
2002
  scWithoutContracts
1797
2003
  )
@@ -1809,7 +2015,7 @@ async function validateTraceability(root, config) {
1809
2015
  "QFAI_CONTRACT_ORPHAN",
1810
2016
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1811
2017
  "error",
1812
- scenariosRoot,
2018
+ specsRoot,
1813
2019
  "traceability.allowOrphanContracts",
1814
2020
  orphanContracts
1815
2021
  )
@@ -1846,7 +2052,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1846
2052
  const pattern = buildIdPattern(Array.from(upstreamIds));
1847
2053
  let found = false;
1848
2054
  for (const file of targetFiles) {
1849
- const text = await (0, import_promises10.readFile)(file, "utf-8");
2055
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1850
2056
  if (pattern.test(text)) {
1851
2057
  found = true;
1852
2058
  break;
@@ -1894,8 +2100,8 @@ async function validateProject(root, configResult) {
1894
2100
  const issues = [
1895
2101
  ...configIssues,
1896
2102
  ...await validateSpecs(root, config),
2103
+ ...await validateDeltas(root, config),
1897
2104
  ...await validateScenarios(root, config),
1898
- ...await validateDecisions(root, config),
1899
2105
  ...await validateContracts(root, config),
1900
2106
  ...await validateDefinedIds(root, config),
1901
2107
  ...await validateTraceability(root, config)
@@ -1924,21 +2130,15 @@ async function createReportData(root, validation, configResult) {
1924
2130
  const resolved = configResult ?? await loadConfig(root);
1925
2131
  const config = resolved.config;
1926
2132
  const configPath = resolved.configPath;
1927
- const specRoot = resolvePath(root, config, "specDir");
1928
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1929
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1930
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1931
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1932
- const dbRoot = resolvePath(root, config, "dataContractsDir");
2133
+ const specsRoot = resolvePath(root, config, "specsDir");
2134
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2135
+ const apiRoot = import_node_path10.default.join(contractsRoot, "api");
2136
+ const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
2137
+ const dbRoot = import_node_path10.default.join(contractsRoot, "db");
1933
2138
  const srcRoot = resolvePath(root, config, "srcDir");
1934
2139
  const testsRoot = resolvePath(root, config, "testsDir");
1935
- const specFiles = await collectSpecFiles(specRoot);
1936
- const scenarioFiles = await collectFiles(scenariosRoot, {
1937
- extensions: [".feature"]
1938
- });
1939
- const decisionFiles = await collectFiles(decisionsRoot, {
1940
- extensions: [".md"]
1941
- });
2140
+ const specFiles = await collectSpecFiles(specsRoot);
2141
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1942
2142
  const {
1943
2143
  api: apiFiles,
1944
2144
  ui: uiFiles,
@@ -1947,7 +2147,6 @@ async function createReportData(root, validation, configResult) {
1947
2147
  const idsByPrefix = await collectIds([
1948
2148
  ...specFiles,
1949
2149
  ...scenarioFiles,
1950
- ...decisionFiles,
1951
2150
  ...apiFiles,
1952
2151
  ...uiFiles,
1953
2152
  ...dbFiles
@@ -1972,7 +2171,6 @@ async function createReportData(root, validation, configResult) {
1972
2171
  summary: {
1973
2172
  specs: specFiles.length,
1974
2173
  scenarios: scenarioFiles.length,
1975
- decisions: decisionFiles.length,
1976
2174
  contracts: {
1977
2175
  api: apiFiles.length,
1978
2176
  ui: uiFiles.length,
@@ -2006,7 +2204,6 @@ function formatReportMarkdown(data) {
2006
2204
  lines.push("## \u6982\u8981");
2007
2205
  lines.push(`- specs: ${data.summary.specs}`);
2008
2206
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2009
- lines.push(`- decisions: ${data.summary.decisions}`);
2010
2207
  lines.push(
2011
2208
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
2012
2209
  );
@@ -2082,7 +2279,7 @@ async function collectIds(files) {
2082
2279
  DATA: /* @__PURE__ */ new Set()
2083
2280
  };
2084
2281
  for (const file of files) {
2085
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2282
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2086
2283
  for (const prefix of ID_PREFIXES2) {
2087
2284
  const ids = extractIds(text, prefix);
2088
2285
  ids.forEach((id) => result[prefix].add(id));
@@ -2100,7 +2297,7 @@ async function collectIds(files) {
2100
2297
  async function collectUpstreamIds(files) {
2101
2298
  const ids = /* @__PURE__ */ new Set();
2102
2299
  for (const file of files) {
2103
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2300
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2104
2301
  extractAllIds(text).forEach((id) => ids.add(id));
2105
2302
  }
2106
2303
  return ids;
@@ -2121,7 +2318,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2121
2318
  }
2122
2319
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2123
2320
  for (const file of targetFiles) {
2124
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2321
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2125
2322
  if (pattern.test(text)) {
2126
2323
  return true;
2127
2324
  }
@@ -2178,8 +2375,8 @@ function buildHotspots(issues) {
2178
2375
  resolvePath,
2179
2376
  resolveToolVersion,
2180
2377
  validateContracts,
2181
- validateDecisions,
2182
2378
  validateDefinedIds,
2379
+ validateDeltas,
2183
2380
  validateProject,
2184
2381
  validateScenarioContent,
2185
2382
  validateScenarios,