qfai 0.2.9 → 0.3.1

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
@@ -45,6 +45,7 @@ __export(src_exports, {
45
45
  resolveToolVersion: () => resolveToolVersion,
46
46
  validateContracts: () => validateContracts,
47
47
  validateDefinedIds: () => validateDefinedIds,
48
+ validateDeltas: () => validateDeltas,
48
49
  validateProject: () => validateProject,
49
50
  validateScenarioContent: () => validateScenarioContent,
50
51
  validateScenarios: () => validateScenarios,
@@ -60,13 +61,11 @@ var import_node_path = __toESM(require("path"), 1);
60
61
  var import_yaml = require("yaml");
61
62
  var defaultConfig = {
62
63
  paths: {
63
- specDir: ".qfai/spec",
64
- decisionsDir: ".qfai/spec/decisions",
65
- scenariosDir: ".qfai/spec/scenarios",
66
64
  contractsDir: ".qfai/contracts",
67
- uiContractsDir: ".qfai/contracts/ui",
68
- apiContractsDir: ".qfai/contracts/api",
69
- dataContractsDir: ".qfai/contracts/db",
65
+ specsDir: ".qfai/specs",
66
+ rulesDir: ".qfai/rules",
67
+ outDir: ".qfai/out",
68
+ promptsDir: ".qfai/prompts",
70
69
  srcDir: "src",
71
70
  testsDir: "tests"
72
71
  },
@@ -91,8 +90,7 @@ var defaultConfig = {
91
90
  }
92
91
  },
93
92
  output: {
94
- format: "text",
95
- jsonPath: ".qfai/out/validate.json"
93
+ validateJsonPath: ".qfai/out/validate.json"
96
94
  }
97
95
  };
98
96
  function getConfigPath(root) {
@@ -141,27 +139,6 @@ function normalizePaths(raw, configPath, issues) {
141
139
  return base;
142
140
  }
143
141
  return {
144
- specDir: readString(
145
- raw.specDir,
146
- base.specDir,
147
- "paths.specDir",
148
- configPath,
149
- issues
150
- ),
151
- decisionsDir: readString(
152
- raw.decisionsDir,
153
- base.decisionsDir,
154
- "paths.decisionsDir",
155
- configPath,
156
- issues
157
- ),
158
- scenariosDir: readString(
159
- raw.scenariosDir,
160
- base.scenariosDir,
161
- "paths.scenariosDir",
162
- configPath,
163
- issues
164
- ),
165
142
  contractsDir: readString(
166
143
  raw.contractsDir,
167
144
  base.contractsDir,
@@ -169,24 +146,31 @@ function normalizePaths(raw, configPath, issues) {
169
146
  configPath,
170
147
  issues
171
148
  ),
172
- uiContractsDir: readString(
173
- raw.uiContractsDir,
174
- base.uiContractsDir,
175
- "paths.uiContractsDir",
149
+ specsDir: readString(
150
+ raw.specsDir,
151
+ base.specsDir,
152
+ "paths.specsDir",
176
153
  configPath,
177
154
  issues
178
155
  ),
179
- apiContractsDir: readString(
180
- raw.apiContractsDir,
181
- base.apiContractsDir,
182
- "paths.apiContractsDir",
156
+ rulesDir: readString(
157
+ raw.rulesDir,
158
+ base.rulesDir,
159
+ "paths.rulesDir",
183
160
  configPath,
184
161
  issues
185
162
  ),
186
- dataContractsDir: readString(
187
- raw.dataContractsDir,
188
- base.dataContractsDir,
189
- "paths.dataContractsDir",
163
+ outDir: readString(
164
+ raw.outDir,
165
+ base.outDir,
166
+ "paths.outDir",
167
+ configPath,
168
+ issues
169
+ ),
170
+ promptsDir: readString(
171
+ raw.promptsDir,
172
+ base.promptsDir,
173
+ "paths.promptsDir",
190
174
  configPath,
191
175
  issues
192
176
  ),
@@ -309,17 +293,10 @@ function normalizeOutput(raw, configPath, issues) {
309
293
  return base;
310
294
  }
311
295
  return {
312
- format: readOutputFormat(
313
- raw.format,
314
- base.format,
315
- "output.format",
316
- configPath,
317
- issues
318
- ),
319
- jsonPath: readString(
320
- raw.jsonPath,
321
- base.jsonPath,
322
- "output.jsonPath",
296
+ validateJsonPath: readString(
297
+ raw.validateJsonPath,
298
+ base.validateJsonPath,
299
+ "output.validateJsonPath",
323
300
  configPath,
324
301
  issues
325
302
  )
@@ -386,20 +363,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
386
363
  }
387
364
  return fallback;
388
365
  }
389
- function readOutputFormat(value, fallback, label, configPath, issues) {
390
- if (value === "text" || value === "json" || value === "github") {
391
- return value;
392
- }
393
- if (value !== void 0) {
394
- issues.push(
395
- configIssue(
396
- configPath,
397
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
398
- )
399
- );
400
- }
401
- return fallback;
402
- }
403
366
  function configIssue(file, message) {
404
367
  return {
405
368
  code: "QFAI_CONFIG_INVALID",
@@ -479,7 +442,8 @@ function isValidId(value, prefix) {
479
442
  }
480
443
 
481
444
  // src/core/report.ts
482
- var import_promises10 = require("fs/promises");
445
+ var import_promises11 = require("fs/promises");
446
+ var import_node_path10 = __toESM(require("path"), 1);
483
447
 
484
448
  // src/core/discovery.ts
485
449
  var import_node_path3 = __toESM(require("path"), 1);
@@ -540,10 +504,24 @@ async function exists(target) {
540
504
  }
541
505
 
542
506
  // src/core/discovery.ts
543
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
544
- async function collectSpecFiles(specRoot) {
545
- const files = await collectFiles(specRoot, { extensions: [".md"] });
546
- return files.filter((file) => isSpecFile(file));
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));
514
+ }
515
+ }
516
+ return Array.from(packs).sort();
517
+ }
518
+ async function collectSpecFiles(specsRoot) {
519
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
520
+ return files.filter((file) => isSpecPackFile(file, "spec.md"));
521
+ }
522
+ async function collectScenarioFiles(specsRoot) {
523
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
524
+ return files.filter((file) => isSpecPackFile(file, "scenario.md"));
547
525
  }
548
526
  async function collectUiContractFiles(uiRoot) {
549
527
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -562,9 +540,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
562
540
  ]);
563
541
  return { ui, api, db };
564
542
  }
565
- function isSpecFile(filePath) {
566
- const name = import_node_path3.default.basename(filePath).toLowerCase();
567
- return SPEC_NAMED_PATTERN.test(name);
543
+ function isSpecPackFile(filePath, baseName) {
544
+ if (import_node_path3.default.basename(filePath).toLowerCase() !== baseName) {
545
+ return false;
546
+ }
547
+ const dirName = import_node_path3.default.basename(import_node_path3.default.dirname(filePath)).toLowerCase();
548
+ return SPEC_PACK_DIR_PATTERN.test(dirName);
568
549
  }
569
550
 
570
551
  // src/core/types.ts
@@ -575,8 +556,8 @@ var import_promises3 = require("fs/promises");
575
556
  var import_node_path4 = __toESM(require("path"), 1);
576
557
  var import_node_url = require("url");
577
558
  async function resolveToolVersion() {
578
- if ("0.2.9".length > 0) {
579
- return "0.2.9";
559
+ if ("0.3.1".length > 0) {
560
+ return "0.3.1";
580
561
  }
581
562
  try {
582
563
  const packagePath = resolvePackageJsonPath();
@@ -596,6 +577,7 @@ function resolvePackageJsonPath() {
596
577
 
597
578
  // src/core/validators/contracts.ts
598
579
  var import_promises4 = require("fs/promises");
580
+ var import_node_path6 = __toESM(require("path"), 1);
599
581
 
600
582
  // src/core/contracts.ts
601
583
  var import_node_path5 = __toESM(require("path"), 1);
@@ -651,19 +633,10 @@ var SQL_DANGEROUS_PATTERNS = [
651
633
  ];
652
634
  async function validateContracts(root, config) {
653
635
  const issues = [];
654
- issues.push(
655
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
656
- );
657
- issues.push(
658
- ...await validateApiContracts(
659
- resolvePath(root, config, "apiContractsDir")
660
- )
661
- );
662
- issues.push(
663
- ...await validateDataContracts(
664
- resolvePath(root, config, "dataContractsDir")
665
- )
666
- );
636
+ const contractsRoot = resolvePath(root, config, "contractsDir");
637
+ issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
638
+ issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
639
+ issues.push(...await validateDataContracts(import_node_path6.default.join(contractsRoot, "db")));
667
640
  return issues;
668
641
  }
669
642
  async function validateUiContracts(uiRoot) {
@@ -879,33 +852,125 @@ function formatError2(error) {
879
852
  return String(error);
880
853
  }
881
854
  function issue(code, message, severity, file, rule, refs) {
882
- const issue6 = {
855
+ const issue7 = {
883
856
  code,
884
857
  severity,
885
858
  message
886
859
  };
887
860
  if (file) {
888
- issue6.file = file;
861
+ issue7.file = file;
889
862
  }
890
863
  if (rule) {
891
- issue6.rule = rule;
864
+ issue7.rule = rule;
892
865
  }
893
866
  if (refs && refs.length > 0) {
894
- issue6.refs = refs;
867
+ issue7.refs = refs;
895
868
  }
896
- return issue6;
869
+ return issue7;
870
+ }
871
+
872
+ // src/core/validators/delta.ts
873
+ var import_promises5 = require("fs/promises");
874
+ var import_node_path7 = __toESM(require("path"), 1);
875
+ var SECTION_RE = /^##\s+変更区分/m;
876
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
877
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
878
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
879
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
880
+ async function validateDeltas(root, config) {
881
+ const specsRoot = resolvePath(root, config, "specsDir");
882
+ const packs = await collectSpecPackDirs(specsRoot);
883
+ if (packs.length === 0) {
884
+ return [];
885
+ }
886
+ const issues = [];
887
+ for (const pack of packs) {
888
+ const deltaPath = import_node_path7.default.join(pack, "delta.md");
889
+ let text;
890
+ try {
891
+ text = await (0, import_promises5.readFile)(deltaPath, "utf-8");
892
+ } catch (error) {
893
+ if (isMissingFileError(error)) {
894
+ issues.push(
895
+ issue2(
896
+ "QFAI-DELTA-001",
897
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
898
+ "error",
899
+ deltaPath,
900
+ "delta.exists"
901
+ )
902
+ );
903
+ continue;
904
+ }
905
+ throw error;
906
+ }
907
+ const hasSection = SECTION_RE.test(text);
908
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
909
+ const hasChange = CHANGE_LINE_RE.test(text);
910
+ if (!hasSection || !hasCompatibility || !hasChange) {
911
+ issues.push(
912
+ issue2(
913
+ "QFAI-DELTA-002",
914
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
915
+ "error",
916
+ deltaPath,
917
+ "delta.section"
918
+ )
919
+ );
920
+ continue;
921
+ }
922
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
923
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
924
+ if (compatibilityChecked === changeChecked) {
925
+ issues.push(
926
+ issue2(
927
+ "QFAI-DELTA-003",
928
+ "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",
929
+ "error",
930
+ deltaPath,
931
+ "delta.classification"
932
+ )
933
+ );
934
+ }
935
+ }
936
+ return issues;
937
+ }
938
+ function isMissingFileError(error) {
939
+ if (!error || typeof error !== "object") {
940
+ return false;
941
+ }
942
+ return error.code === "ENOENT";
943
+ }
944
+ function issue2(code, message, severity, file, rule, refs) {
945
+ const issue7 = {
946
+ code,
947
+ severity,
948
+ message
949
+ };
950
+ if (file) {
951
+ issue7.file = file;
952
+ }
953
+ if (rule) {
954
+ issue7.rule = rule;
955
+ }
956
+ if (refs && refs.length > 0) {
957
+ issue7.refs = refs;
958
+ }
959
+ return issue7;
897
960
  }
898
961
 
899
962
  // src/core/validators/ids.ts
900
- var import_promises6 = require("fs/promises");
901
- var import_node_path6 = __toESM(require("path"), 1);
963
+ var import_promises7 = require("fs/promises");
964
+ var import_node_path9 = __toESM(require("path"), 1);
902
965
 
903
966
  // src/core/contractIndex.ts
904
- var import_promises5 = require("fs/promises");
967
+ var import_promises6 = require("fs/promises");
968
+ var import_node_path8 = __toESM(require("path"), 1);
905
969
  async function buildContractIndex(root, config) {
906
- const uiRoot = resolvePath(root, config, "uiContractsDir");
907
- const apiRoot = resolvePath(root, config, "apiContractsDir");
908
- const dataRoot = resolvePath(root, config, "dataContractsDir");
970
+ const contractsRoot = resolvePath(root, config, "contractsDir");
971
+ const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
972
+ const apiRoot = import_node_path8.default.join(contractsRoot, "api");
973
+ const dataRoot = import_node_path8.default.join(contractsRoot, "db");
909
974
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
910
975
  collectUiContractFiles(uiRoot),
911
976
  collectApiContractFiles(apiRoot),
@@ -924,7 +989,7 @@ async function buildContractIndex(root, config) {
924
989
  }
925
990
  async function indexUiContracts(files, index) {
926
991
  for (const file of files) {
927
- const text = await (0, import_promises5.readFile)(file, "utf-8");
992
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
928
993
  try {
929
994
  const doc = parseStructuredContract(file, text);
930
995
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -936,7 +1001,7 @@ async function indexUiContracts(files, index) {
936
1001
  }
937
1002
  async function indexApiContracts(files, index) {
938
1003
  for (const file of files) {
939
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1004
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
940
1005
  try {
941
1006
  const doc = parseStructuredContract(file, text);
942
1007
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -948,7 +1013,7 @@ async function indexApiContracts(files, index) {
948
1013
  }
949
1014
  async function indexDataContracts(files, index) {
950
1015
  for (const file of files) {
951
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1016
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
952
1017
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
953
1018
  }
954
1019
  }
@@ -959,15 +1024,191 @@ function record(index, id, file) {
959
1024
  index.idToFiles.set(id, current);
960
1025
  }
961
1026
 
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
+ // src/core/parse/markdown.ts
1088
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1089
+ function parseHeadings(md) {
1090
+ const lines = md.split(/\r?\n/);
1091
+ const headings = [];
1092
+ for (let i = 0; i < lines.length; i++) {
1093
+ const line = lines[i] ?? "";
1094
+ const match = line.match(HEADING_RE);
1095
+ if (!match) continue;
1096
+ const levelToken = match[1];
1097
+ const title = match[2];
1098
+ if (!levelToken || !title) continue;
1099
+ headings.push({
1100
+ level: levelToken.length,
1101
+ title: title.trim(),
1102
+ line: i + 1
1103
+ });
1104
+ }
1105
+ return headings;
1106
+ }
1107
+ function extractH2Sections(md) {
1108
+ const lines = md.split(/\r?\n/);
1109
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1110
+ const sections = /* @__PURE__ */ new Map();
1111
+ for (let i = 0; i < headings.length; i++) {
1112
+ const current = headings[i];
1113
+ if (!current) continue;
1114
+ const next = headings[i + 1];
1115
+ const startLine = current.line + 1;
1116
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1117
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1118
+ sections.set(current.title.trim(), {
1119
+ title: current.title.trim(),
1120
+ startLine,
1121
+ endLine,
1122
+ body
1123
+ });
1124
+ }
1125
+ return sections;
1126
+ }
1127
+
1128
+ // src/core/parse/spec.ts
1129
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1130
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1131
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1132
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1133
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1134
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1135
+ function parseSpec(md, file) {
1136
+ const headings = parseHeadings(md);
1137
+ const h1 = headings.find((heading) => heading.level === 1);
1138
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1139
+ const sections = extractH2Sections(md);
1140
+ const sectionNames = new Set(Array.from(sections.keys()));
1141
+ const brSection = sections.get(BR_SECTION_TITLE);
1142
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1143
+ const startLine = brSection?.startLine ?? 1;
1144
+ const brs = [];
1145
+ const brsWithoutPriority = [];
1146
+ const brsWithInvalidPriority = [];
1147
+ for (let i = 0; i < brLines.length; i++) {
1148
+ const lineText = brLines[i] ?? "";
1149
+ const lineNumber = startLine + i;
1150
+ const validMatch = lineText.match(BR_LINE_RE);
1151
+ if (validMatch) {
1152
+ const id = validMatch[1];
1153
+ const priority = validMatch[2];
1154
+ const text = validMatch[3];
1155
+ if (!id || !priority || !text) continue;
1156
+ brs.push({
1157
+ id,
1158
+ priority,
1159
+ text: text.trim(),
1160
+ line: lineNumber
1161
+ });
1162
+ continue;
1163
+ }
1164
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1165
+ if (anyPriorityMatch) {
1166
+ const id = anyPriorityMatch[1];
1167
+ const priority = anyPriorityMatch[2];
1168
+ const text = anyPriorityMatch[3];
1169
+ if (!id || !priority || !text) continue;
1170
+ if (!VALID_PRIORITIES.has(priority)) {
1171
+ brsWithInvalidPriority.push({
1172
+ id,
1173
+ priority,
1174
+ text: text.trim(),
1175
+ line: lineNumber
1176
+ });
1177
+ }
1178
+ continue;
1179
+ }
1180
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1181
+ if (noPriorityMatch) {
1182
+ const id = noPriorityMatch[1];
1183
+ const text = noPriorityMatch[2];
1184
+ if (!id || !text) continue;
1185
+ brsWithoutPriority.push({
1186
+ id,
1187
+ text: text.trim(),
1188
+ line: lineNumber
1189
+ });
1190
+ }
1191
+ }
1192
+ const parsed = {
1193
+ file,
1194
+ sections: sectionNames,
1195
+ brs,
1196
+ brsWithoutPriority,
1197
+ brsWithInvalidPriority
1198
+ };
1199
+ if (specId) {
1200
+ parsed.specId = specId;
1201
+ }
1202
+ return parsed;
1203
+ }
1204
+
962
1205
  // src/core/validators/ids.ts
1206
+ var SC_TAG_RE = /^SC-\d{4}$/;
963
1207
  async function validateDefinedIds(root, config) {
964
1208
  const issues = [];
965
- const specRoot = resolvePath(root, config, "specDir");
966
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
967
- const specFiles = await collectSpecFiles(specRoot);
968
- const scenarioFiles = await collectFiles(scenarioRoot, {
969
- extensions: [".feature"]
970
- });
1209
+ const specsRoot = resolvePath(root, config, "specsDir");
1210
+ const specFiles = await collectSpecFiles(specsRoot);
1211
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
971
1212
  const defined = /* @__PURE__ */ new Map();
972
1213
  await collectSpecDefinitionIds(specFiles, defined);
973
1214
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -983,7 +1224,7 @@ async function validateDefinedIds(root, config) {
983
1224
  }
984
1225
  const sorted = Array.from(files).sort();
985
1226
  issues.push(
986
- issue2(
1227
+ issue3(
987
1228
  "QFAI-ID-001",
988
1229
  `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
989
1230
  "error",
@@ -996,15 +1237,25 @@ async function validateDefinedIds(root, config) {
996
1237
  }
997
1238
  async function collectSpecDefinitionIds(files, out) {
998
1239
  for (const file of files) {
999
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1000
- extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
1001
- extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1240
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1241
+ const parsed = parseSpec(text, file);
1242
+ if (parsed.specId) {
1243
+ recordId(out, parsed.specId, file);
1244
+ }
1245
+ parsed.brs.forEach((br) => recordId(out, br.id, file));
1002
1246
  }
1003
1247
  }
1004
1248
  async function collectScenarioDefinitionIds(files, out) {
1005
1249
  for (const file of files) {
1006
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1007
- extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1250
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1251
+ const parsed = parseGherkinFeature(text, file);
1252
+ for (const scenario of parsed.scenarios) {
1253
+ for (const tag of scenario.tags) {
1254
+ if (SC_TAG_RE.test(tag)) {
1255
+ recordId(out, tag, file);
1256
+ }
1257
+ }
1258
+ }
1008
1259
  }
1009
1260
  }
1010
1261
  function recordId(out, id, file) {
@@ -1014,58 +1265,60 @@ function recordId(out, id, file) {
1014
1265
  }
1015
1266
  function formatFileList(files, root) {
1016
1267
  return files.map((file) => {
1017
- const relative = import_node_path6.default.relative(root, file);
1268
+ const relative = import_node_path9.default.relative(root, file);
1018
1269
  return relative.length > 0 ? relative : file;
1019
1270
  }).join(", ");
1020
1271
  }
1021
- function issue2(code, message, severity, file, rule, refs) {
1022
- const issue6 = {
1272
+ function issue3(code, message, severity, file, rule, refs) {
1273
+ const issue7 = {
1023
1274
  code,
1024
1275
  severity,
1025
1276
  message
1026
1277
  };
1027
1278
  if (file) {
1028
- issue6.file = file;
1279
+ issue7.file = file;
1029
1280
  }
1030
1281
  if (rule) {
1031
- issue6.rule = rule;
1282
+ issue7.rule = rule;
1032
1283
  }
1033
1284
  if (refs && refs.length > 0) {
1034
- issue6.refs = refs;
1285
+ issue7.refs = refs;
1035
1286
  }
1036
- return issue6;
1287
+ return issue7;
1037
1288
  }
1038
1289
 
1039
1290
  // src/core/validators/scenario.ts
1040
- var import_promises7 = require("fs/promises");
1291
+ var import_promises8 = require("fs/promises");
1041
1292
  var GIVEN_PATTERN = /\bGiven\b/;
1042
1293
  var WHEN_PATTERN = /\bWhen\b/;
1043
1294
  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}$/;
1044
1298
  async function validateScenarios(root, config) {
1045
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1046
- const files = await collectFiles(scenariosRoot, {
1047
- extensions: [".feature"]
1048
- });
1299
+ const specsRoot = resolvePath(root, config, "specsDir");
1300
+ const files = await collectScenarioFiles(specsRoot);
1049
1301
  if (files.length === 0) {
1050
1302
  return [
1051
- issue3(
1303
+ issue4(
1052
1304
  "QFAI-SC-000",
1053
1305
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1054
1306
  "info",
1055
- scenariosRoot,
1307
+ specsRoot,
1056
1308
  "scenario.files"
1057
1309
  )
1058
1310
  ];
1059
1311
  }
1060
1312
  const issues = [];
1061
1313
  for (const file of files) {
1062
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1314
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1063
1315
  issues.push(...validateScenarioContent(text, file));
1064
1316
  }
1065
1317
  return issues;
1066
1318
  }
1067
1319
  function validateScenarioContent(text, file) {
1068
1320
  const issues = [];
1321
+ const parsed = parseGherkinFeature(text, file);
1069
1322
  const invalidIds = extractInvalidIds(text, [
1070
1323
  "SPEC",
1071
1324
  "BR",
@@ -1077,7 +1330,7 @@ function validateScenarioContent(text, file) {
1077
1330
  ]);
1078
1331
  if (invalidIds.length > 0) {
1079
1332
  issues.push(
1080
- issue3(
1333
+ issue4(
1081
1334
  "QFAI-ID-002",
1082
1335
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1083
1336
  "error",
@@ -1087,94 +1340,114 @@ function validateScenarioContent(text, file) {
1087
1340
  )
1088
1341
  );
1089
1342
  }
1090
- const scIds = extractIds(text, "SC");
1091
- if (scIds.length === 0) {
1092
- issues.push(
1093
- issue3(
1094
- "QFAI-SC-001",
1095
- "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1096
- "error",
1097
- file,
1098
- "scenario.id"
1099
- )
1100
- );
1101
- }
1102
- const specIds = extractIds(text, "SPEC");
1103
- if (specIds.length === 0) {
1104
- issues.push(
1105
- issue3(
1106
- "QFAI-SC-002",
1107
- "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1108
- "error",
1109
- file,
1110
- "scenario.spec"
1111
- )
1112
- );
1113
- }
1114
- const brIds = extractIds(text, "BR");
1115
- if (brIds.length === 0) {
1343
+ const missingStructure = [];
1344
+ if (!parsed.featurePresent) missingStructure.push("Feature");
1345
+ if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1346
+ if (missingStructure.length > 0) {
1116
1347
  issues.push(
1117
- issue3(
1118
- "QFAI-SC-003",
1119
- "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1348
+ issue4(
1349
+ "QFAI-SC-006",
1350
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
1351
+ ", "
1352
+ )}`,
1120
1353
  "error",
1121
1354
  file,
1122
- "scenario.br"
1355
+ "scenario.structure"
1123
1356
  )
1124
1357
  );
1125
1358
  }
1126
- const missingSteps = [];
1127
- if (!GIVEN_PATTERN.test(text)) {
1128
- missingSteps.push("Given");
1129
- }
1130
- if (!WHEN_PATTERN.test(text)) {
1131
- missingSteps.push("When");
1132
- }
1133
- if (!THEN_PATTERN.test(text)) {
1134
- missingSteps.push("Then");
1359
+ for (const scenario of parsed.scenarios) {
1360
+ if (scenario.tags.length === 0) {
1361
+ issues.push(
1362
+ issue4(
1363
+ "QFAI-SC-007",
1364
+ `Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
1365
+ "error",
1366
+ file,
1367
+ "scenario.tags"
1368
+ )
1369
+ );
1370
+ continue;
1371
+ }
1372
+ const missingTags = [];
1373
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1374
+ if (scTags.length === 0) {
1375
+ missingTags.push("SC(0\u4EF6)");
1376
+ } else if (scTags.length > 1) {
1377
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1378
+ }
1379
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1380
+ missingTags.push("SPEC");
1381
+ }
1382
+ if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1383
+ missingTags.push("BR");
1384
+ }
1385
+ if (missingTags.length > 0) {
1386
+ issues.push(
1387
+ issue4(
1388
+ "QFAI-SC-008",
1389
+ `Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
1390
+ "error",
1391
+ file,
1392
+ "scenario.tagIds"
1393
+ )
1394
+ );
1395
+ }
1135
1396
  }
1136
- if (missingSteps.length > 0) {
1137
- issues.push(
1138
- issue3(
1139
- "QFAI-SC-005",
1140
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1141
- "warning",
1142
- file,
1143
- "scenario.steps"
1144
- )
1145
- );
1397
+ for (const scenario of parsed.scenarios) {
1398
+ const missingSteps = [];
1399
+ if (!GIVEN_PATTERN.test(scenario.body)) {
1400
+ missingSteps.push("Given");
1401
+ }
1402
+ if (!WHEN_PATTERN.test(scenario.body)) {
1403
+ missingSteps.push("When");
1404
+ }
1405
+ if (!THEN_PATTERN.test(scenario.body)) {
1406
+ missingSteps.push("Then");
1407
+ }
1408
+ if (missingSteps.length > 0) {
1409
+ issues.push(
1410
+ issue4(
1411
+ "QFAI-SC-005",
1412
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1413
+ "warning",
1414
+ file,
1415
+ "scenario.steps"
1416
+ )
1417
+ );
1418
+ }
1146
1419
  }
1147
1420
  return issues;
1148
1421
  }
1149
- function issue3(code, message, severity, file, rule, refs) {
1150
- const issue6 = {
1422
+ function issue4(code, message, severity, file, rule, refs) {
1423
+ const issue7 = {
1151
1424
  code,
1152
1425
  severity,
1153
1426
  message
1154
1427
  };
1155
1428
  if (file) {
1156
- issue6.file = file;
1429
+ issue7.file = file;
1157
1430
  }
1158
1431
  if (rule) {
1159
- issue6.rule = rule;
1432
+ issue7.rule = rule;
1160
1433
  }
1161
1434
  if (refs && refs.length > 0) {
1162
- issue6.refs = refs;
1435
+ issue7.refs = refs;
1163
1436
  }
1164
- return issue6;
1437
+ return issue7;
1165
1438
  }
1166
1439
 
1167
1440
  // src/core/validators/spec.ts
1168
- var import_promises8 = require("fs/promises");
1441
+ var import_promises9 = require("fs/promises");
1169
1442
  async function validateSpecs(root, config) {
1170
- const specsRoot = resolvePath(root, config, "specDir");
1443
+ const specsRoot = resolvePath(root, config, "specsDir");
1171
1444
  const files = await collectSpecFiles(specsRoot);
1172
1445
  if (files.length === 0) {
1173
- const expected = "spec-0001-<slug>.md";
1446
+ const expected = "spec-001/spec.md";
1174
1447
  return [
1175
- issue4(
1448
+ issue5(
1176
1449
  "QFAI-SPEC-000",
1177
- `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}`,
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}`,
1178
1451
  "info",
1179
1452
  specsRoot,
1180
1453
  "spec.files"
@@ -1183,7 +1456,7 @@ async function validateSpecs(root, config) {
1183
1456
  }
1184
1457
  const issues = [];
1185
1458
  for (const file of files) {
1186
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1459
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1187
1460
  issues.push(
1188
1461
  ...validateSpecContent(
1189
1462
  text,
@@ -1196,6 +1469,7 @@ async function validateSpecs(root, config) {
1196
1469
  }
1197
1470
  function validateSpecContent(text, file, requiredSections) {
1198
1471
  const issues = [];
1472
+ const parsed = parseSpec(text, file);
1199
1473
  const invalidIds = extractInvalidIds(text, [
1200
1474
  "SPEC",
1201
1475
  "BR",
@@ -1207,7 +1481,7 @@ function validateSpecContent(text, file, requiredSections) {
1207
1481
  ]);
1208
1482
  if (invalidIds.length > 0) {
1209
1483
  issues.push(
1210
- issue4(
1484
+ issue5(
1211
1485
  "QFAI-ID-002",
1212
1486
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1213
1487
  "error",
@@ -1217,10 +1491,9 @@ function validateSpecContent(text, file, requiredSections) {
1217
1491
  )
1218
1492
  );
1219
1493
  }
1220
- const specIds = extractIds(text, "SPEC");
1221
- if (specIds.length === 0) {
1494
+ if (!parsed.specId) {
1222
1495
  issues.push(
1223
- issue4(
1496
+ issue5(
1224
1497
  "QFAI-SPEC-001",
1225
1498
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1226
1499
  "error",
@@ -1229,10 +1502,9 @@ function validateSpecContent(text, file, requiredSections) {
1229
1502
  )
1230
1503
  );
1231
1504
  }
1232
- const brIds = extractIds(text, "BR");
1233
- if (brIds.length === 0) {
1505
+ if (parsed.brs.length === 0) {
1234
1506
  issues.push(
1235
- issue4(
1507
+ issue5(
1236
1508
  "QFAI-SPEC-002",
1237
1509
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1238
1510
  "error",
@@ -1241,10 +1513,34 @@ function validateSpecContent(text, file, requiredSections) {
1241
1513
  )
1242
1514
  );
1243
1515
  }
1516
+ for (const br of parsed.brsWithoutPriority) {
1517
+ issues.push(
1518
+ issue5(
1519
+ "QFAI-BR-001",
1520
+ `BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
1521
+ "error",
1522
+ file,
1523
+ "spec.brPriority",
1524
+ [br.id]
1525
+ )
1526
+ );
1527
+ }
1528
+ for (const br of parsed.brsWithInvalidPriority) {
1529
+ issues.push(
1530
+ issue5(
1531
+ "QFAI-BR-002",
1532
+ `BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
1533
+ "error",
1534
+ file,
1535
+ "spec.brPriority",
1536
+ [br.id]
1537
+ )
1538
+ );
1539
+ }
1244
1540
  const scIds = extractIds(text, "SC");
1245
1541
  if (scIds.length > 0) {
1246
1542
  issues.push(
1247
- issue4(
1543
+ issue5(
1248
1544
  "QFAI-SPEC-003",
1249
1545
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1250
1546
  "warning",
@@ -1255,9 +1551,9 @@ function validateSpecContent(text, file, requiredSections) {
1255
1551
  );
1256
1552
  }
1257
1553
  for (const section of requiredSections) {
1258
- if (!text.includes(section)) {
1554
+ if (!parsed.sections.has(section)) {
1259
1555
  issues.push(
1260
- issue4(
1556
+ issue5(
1261
1557
  "QFAI-SPEC-004",
1262
1558
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1263
1559
  "error",
@@ -1269,40 +1565,39 @@ function validateSpecContent(text, file, requiredSections) {
1269
1565
  }
1270
1566
  return issues;
1271
1567
  }
1272
- function issue4(code, message, severity, file, rule, refs) {
1273
- const issue6 = {
1568
+ function issue5(code, message, severity, file, rule, refs) {
1569
+ const issue7 = {
1274
1570
  code,
1275
1571
  severity,
1276
1572
  message
1277
1573
  };
1278
1574
  if (file) {
1279
- issue6.file = file;
1575
+ issue7.file = file;
1280
1576
  }
1281
1577
  if (rule) {
1282
- issue6.rule = rule;
1578
+ issue7.rule = rule;
1283
1579
  }
1284
1580
  if (refs && refs.length > 0) {
1285
- issue6.refs = refs;
1581
+ issue7.refs = refs;
1286
1582
  }
1287
- return issue6;
1583
+ return issue7;
1288
1584
  }
1289
1585
 
1290
1586
  // src/core/validators/traceability.ts
1291
- var import_promises9 = require("fs/promises");
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}$/;
1292
1594
  async function validateTraceability(root, config) {
1293
1595
  const issues = [];
1294
- const specsRoot = resolvePath(root, config, "specDir");
1295
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1296
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1596
+ const specsRoot = resolvePath(root, config, "specsDir");
1297
1597
  const srcRoot = resolvePath(root, config, "srcDir");
1298
1598
  const testsRoot = resolvePath(root, config, "testsDir");
1299
1599
  const specFiles = await collectSpecFiles(specsRoot);
1300
- const decisionFiles = await collectFiles(decisionsRoot, {
1301
- extensions: [".md"]
1302
- });
1303
- const scenarioFiles = await collectFiles(scenariosRoot, {
1304
- extensions: [".feature"]
1305
- });
1600
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1306
1601
  const upstreamIds = /* @__PURE__ */ new Set();
1307
1602
  const specIds = /* @__PURE__ */ new Set();
1308
1603
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1314,11 +1609,13 @@ async function validateTraceability(root, config) {
1314
1609
  const contractIndex = await buildContractIndex(root, config);
1315
1610
  const contractIds = contractIndex.ids;
1316
1611
  for (const file of specFiles) {
1317
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1612
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1318
1613
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1319
- const specIdsInFile = extractIds(text, "SPEC");
1320
- specIdsInFile.forEach((id) => specIds.add(id));
1321
- const brIds = extractIds(text, "BR");
1614
+ const parsed = parseSpec(text, file);
1615
+ if (parsed.specId) {
1616
+ specIds.add(parsed.specId);
1617
+ }
1618
+ const brIds = parsed.brs.map((br) => br.id);
1322
1619
  brIds.forEach((id) => brIdsInSpecs.add(id));
1323
1620
  const referencedContractIds = /* @__PURE__ */ new Set([
1324
1621
  ...extractIds(text, "UI"),
@@ -1330,7 +1627,7 @@ async function validateTraceability(root, config) {
1330
1627
  );
1331
1628
  if (unknownContractIds.length > 0) {
1332
1629
  issues.push(
1333
- issue5(
1630
+ issue6(
1334
1631
  "QFAI-TRACE-009",
1335
1632
  `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1336
1633
  ", "
@@ -1342,37 +1639,50 @@ async function validateTraceability(root, config) {
1342
1639
  )
1343
1640
  );
1344
1641
  }
1345
- for (const specId of specIdsInFile) {
1346
- const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1642
+ if (parsed.specId) {
1643
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1347
1644
  brIds.forEach((id) => current.add(id));
1348
- specToBrIds.set(specId, current);
1645
+ specToBrIds.set(parsed.specId, current);
1349
1646
  }
1350
1647
  }
1351
- for (const file of decisionFiles) {
1352
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1353
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1354
- }
1355
1648
  for (const file of scenarioFiles) {
1356
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1649
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1357
1650
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1358
- const specIdsInScenario = extractIds(text, "SPEC");
1359
- const brIds = extractIds(text, "BR");
1360
- const scIds = extractIds(text, "SC");
1361
- const scenarioIds = [
1362
- ...extractIds(text, "UI"),
1363
- ...extractIds(text, "API"),
1364
- ...extractIds(text, "DATA")
1365
- ];
1366
- brIds.forEach((id) => brIdsInScenarios.add(id));
1367
- scIds.forEach((id) => scIdsInScenarios.add(id));
1368
- scenarioIds.forEach((id) => scenarioContractIds.add(id));
1369
- if (scenarioIds.length > 0) {
1370
- scIds.forEach((id) => scWithContracts.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
+ }
1371
1671
  }
1372
- const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
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));
1373
1683
  if (unknownSpecIds.length > 0) {
1374
1684
  issues.push(
1375
- issue5(
1685
+ issue6(
1376
1686
  "QFAI-TRACE-005",
1377
1687
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1378
1688
  "error",
@@ -1382,10 +1692,10 @@ async function validateTraceability(root, config) {
1382
1692
  )
1383
1693
  );
1384
1694
  }
1385
- const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1695
+ const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1386
1696
  if (unknownBrIds.length > 0) {
1387
1697
  issues.push(
1388
- issue5(
1698
+ issue6(
1389
1699
  "QFAI-TRACE-006",
1390
1700
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1391
1701
  "error",
@@ -1395,10 +1705,12 @@ async function validateTraceability(root, config) {
1395
1705
  )
1396
1706
  );
1397
1707
  }
1398
- const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1708
+ const unknownContractIds = scenarioIdsList.filter(
1709
+ (id) => !contractIds.has(id)
1710
+ );
1399
1711
  if (unknownContractIds.length > 0) {
1400
1712
  issues.push(
1401
- issue5(
1713
+ issue6(
1402
1714
  "QFAI-TRACE-008",
1403
1715
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1404
1716
  ", "
@@ -1410,23 +1722,23 @@ async function validateTraceability(root, config) {
1410
1722
  )
1411
1723
  );
1412
1724
  }
1413
- if (specIdsInScenario.length > 0) {
1725
+ if (specIdsList.length > 0) {
1414
1726
  const allowedBrIds = /* @__PURE__ */ new Set();
1415
- for (const specId of specIdsInScenario) {
1727
+ for (const specId of specIdsList) {
1416
1728
  const brIdsForSpec = specToBrIds.get(specId);
1417
1729
  if (!brIdsForSpec) {
1418
1730
  continue;
1419
1731
  }
1420
1732
  brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1421
1733
  }
1422
- const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1734
+ const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1423
1735
  if (invalidBrIds.length > 0) {
1424
1736
  issues.push(
1425
- issue5(
1737
+ issue6(
1426
1738
  "QFAI-TRACE-007",
1427
1739
  `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1428
1740
  ", "
1429
- )} (SPEC: ${specIdsInScenario.join(", ")})`,
1741
+ )} (SPEC: ${specIdsList.join(", ")})`,
1430
1742
  "error",
1431
1743
  file,
1432
1744
  "traceability.scenarioBrUnderSpec",
@@ -1438,7 +1750,7 @@ async function validateTraceability(root, config) {
1438
1750
  }
1439
1751
  if (upstreamIds.size === 0) {
1440
1752
  return [
1441
- issue5(
1753
+ issue6(
1442
1754
  "QFAI-TRACE-000",
1443
1755
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1444
1756
  "info",
@@ -1453,7 +1765,7 @@ async function validateTraceability(root, config) {
1453
1765
  );
1454
1766
  if (orphanBrIds.length > 0) {
1455
1767
  issues.push(
1456
- issue5(
1768
+ issue6(
1457
1769
  "QFAI_TRACE_BR_ORPHAN",
1458
1770
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1459
1771
  "error",
@@ -1470,13 +1782,13 @@ async function validateTraceability(root, config) {
1470
1782
  );
1471
1783
  if (scWithoutContracts.length > 0) {
1472
1784
  issues.push(
1473
- issue5(
1785
+ issue6(
1474
1786
  "QFAI_TRACE_SC_NO_CONTRACT",
1475
1787
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1476
1788
  ", "
1477
1789
  )}`,
1478
1790
  "error",
1479
- scenariosRoot,
1791
+ specsRoot,
1480
1792
  "traceability.scMustTouchContracts",
1481
1793
  scWithoutContracts
1482
1794
  )
@@ -1490,11 +1802,11 @@ async function validateTraceability(root, config) {
1490
1802
  );
1491
1803
  if (orphanContracts.length > 0) {
1492
1804
  issues.push(
1493
- issue5(
1805
+ issue6(
1494
1806
  "QFAI_CONTRACT_ORPHAN",
1495
1807
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1496
1808
  "error",
1497
- scenariosRoot,
1809
+ specsRoot,
1498
1810
  "traceability.allowOrphanContracts",
1499
1811
  orphanContracts
1500
1812
  )
@@ -1518,7 +1830,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1518
1830
  const targetFiles = [...codeFiles, ...testFiles];
1519
1831
  if (targetFiles.length === 0) {
1520
1832
  issues.push(
1521
- issue5(
1833
+ issue6(
1522
1834
  "QFAI-TRACE-001",
1523
1835
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1524
1836
  "info",
@@ -1531,7 +1843,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1531
1843
  const pattern = buildIdPattern(Array.from(upstreamIds));
1532
1844
  let found = false;
1533
1845
  for (const file of targetFiles) {
1534
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1846
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1535
1847
  if (pattern.test(text)) {
1536
1848
  found = true;
1537
1849
  break;
@@ -1539,7 +1851,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1539
1851
  }
1540
1852
  if (!found) {
1541
1853
  issues.push(
1542
- issue5(
1854
+ issue6(
1543
1855
  "QFAI-TRACE-002",
1544
1856
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1545
1857
  "warning",
@@ -1554,22 +1866,22 @@ function buildIdPattern(ids) {
1554
1866
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1555
1867
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1556
1868
  }
1557
- function issue5(code, message, severity, file, rule, refs) {
1558
- const issue6 = {
1869
+ function issue6(code, message, severity, file, rule, refs) {
1870
+ const issue7 = {
1559
1871
  code,
1560
1872
  severity,
1561
1873
  message
1562
1874
  };
1563
1875
  if (file) {
1564
- issue6.file = file;
1876
+ issue7.file = file;
1565
1877
  }
1566
1878
  if (rule) {
1567
- issue6.rule = rule;
1879
+ issue7.rule = rule;
1568
1880
  }
1569
1881
  if (refs && refs.length > 0) {
1570
- issue6.refs = refs;
1882
+ issue7.refs = refs;
1571
1883
  }
1572
- return issue6;
1884
+ return issue7;
1573
1885
  }
1574
1886
 
1575
1887
  // src/core/validate.ts
@@ -1579,6 +1891,7 @@ async function validateProject(root, configResult) {
1579
1891
  const issues = [
1580
1892
  ...configIssues,
1581
1893
  ...await validateSpecs(root, config),
1894
+ ...await validateDeltas(root, config),
1582
1895
  ...await validateScenarios(root, config),
1583
1896
  ...await validateContracts(root, config),
1584
1897
  ...await validateDefinedIds(root, config),
@@ -1594,8 +1907,8 @@ async function validateProject(root, configResult) {
1594
1907
  }
1595
1908
  function countIssues(issues) {
1596
1909
  return issues.reduce(
1597
- (acc, issue6) => {
1598
- acc[issue6.severity] += 1;
1910
+ (acc, issue7) => {
1911
+ acc[issue7.severity] += 1;
1599
1912
  return acc;
1600
1913
  },
1601
1914
  { info: 0, warning: 0, error: 0 }
@@ -1608,21 +1921,15 @@ async function createReportData(root, validation, configResult) {
1608
1921
  const resolved = configResult ?? await loadConfig(root);
1609
1922
  const config = resolved.config;
1610
1923
  const configPath = resolved.configPath;
1611
- const specRoot = resolvePath(root, config, "specDir");
1612
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1613
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1614
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1615
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1616
- const dbRoot = resolvePath(root, config, "dataContractsDir");
1924
+ const specsRoot = resolvePath(root, config, "specsDir");
1925
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1926
+ const apiRoot = import_node_path10.default.join(contractsRoot, "api");
1927
+ const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
1928
+ const dbRoot = import_node_path10.default.join(contractsRoot, "db");
1617
1929
  const srcRoot = resolvePath(root, config, "srcDir");
1618
1930
  const testsRoot = resolvePath(root, config, "testsDir");
1619
- const specFiles = await collectSpecFiles(specRoot);
1620
- const scenarioFiles = await collectFiles(scenariosRoot, {
1621
- extensions: [".feature"]
1622
- });
1623
- const decisionFiles = await collectFiles(decisionsRoot, {
1624
- extensions: [".md"]
1625
- });
1931
+ const specFiles = await collectSpecFiles(specsRoot);
1932
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1626
1933
  const {
1627
1934
  api: apiFiles,
1628
1935
  ui: uiFiles,
@@ -1631,7 +1938,6 @@ async function createReportData(root, validation, configResult) {
1631
1938
  const idsByPrefix = await collectIds([
1632
1939
  ...specFiles,
1633
1940
  ...scenarioFiles,
1634
- ...decisionFiles,
1635
1941
  ...apiFiles,
1636
1942
  ...uiFiles,
1637
1943
  ...dbFiles
@@ -1656,7 +1962,6 @@ async function createReportData(root, validation, configResult) {
1656
1962
  summary: {
1657
1963
  specs: specFiles.length,
1658
1964
  scenarios: scenarioFiles.length,
1659
- decisions: decisionFiles.length,
1660
1965
  contracts: {
1661
1966
  api: apiFiles.length,
1662
1967
  ui: uiFiles.length,
@@ -1690,7 +1995,6 @@ function formatReportMarkdown(data) {
1690
1995
  lines.push("## \u6982\u8981");
1691
1996
  lines.push(`- specs: ${data.summary.specs}`);
1692
1997
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1693
- lines.push(`- decisions: ${data.summary.decisions}`);
1694
1998
  lines.push(
1695
1999
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1696
2000
  );
@@ -1766,7 +2070,7 @@ async function collectIds(files) {
1766
2070
  DATA: /* @__PURE__ */ new Set()
1767
2071
  };
1768
2072
  for (const file of files) {
1769
- const text = await (0, import_promises10.readFile)(file, "utf-8");
2073
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1770
2074
  for (const prefix of ID_PREFIXES2) {
1771
2075
  const ids = extractIds(text, prefix);
1772
2076
  ids.forEach((id) => result[prefix].add(id));
@@ -1784,7 +2088,7 @@ async function collectIds(files) {
1784
2088
  async function collectUpstreamIds(files) {
1785
2089
  const ids = /* @__PURE__ */ new Set();
1786
2090
  for (const file of files) {
1787
- const text = await (0, import_promises10.readFile)(file, "utf-8");
2091
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1788
2092
  extractAllIds(text).forEach((id) => ids.add(id));
1789
2093
  }
1790
2094
  return ids;
@@ -1805,7 +2109,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1805
2109
  }
1806
2110
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1807
2111
  for (const file of targetFiles) {
1808
- const text = await (0, import_promises10.readFile)(file, "utf-8");
2112
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1809
2113
  if (pattern.test(text)) {
1810
2114
  return true;
1811
2115
  }
@@ -1827,20 +2131,20 @@ function toSortedArray(values) {
1827
2131
  }
1828
2132
  function buildHotspots(issues) {
1829
2133
  const map = /* @__PURE__ */ new Map();
1830
- for (const issue6 of issues) {
1831
- if (!issue6.file) {
2134
+ for (const issue7 of issues) {
2135
+ if (!issue7.file) {
1832
2136
  continue;
1833
2137
  }
1834
- const current = map.get(issue6.file) ?? {
1835
- file: issue6.file,
2138
+ const current = map.get(issue7.file) ?? {
2139
+ file: issue7.file,
1836
2140
  total: 0,
1837
2141
  error: 0,
1838
2142
  warning: 0,
1839
2143
  info: 0
1840
2144
  };
1841
2145
  current.total += 1;
1842
- current[issue6.severity] += 1;
1843
- map.set(issue6.file, current);
2146
+ current[issue7.severity] += 1;
2147
+ map.set(issue7.file, current);
1844
2148
  }
1845
2149
  return Array.from(map.values()).sort(
1846
2150
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1863,6 +2167,7 @@ function buildHotspots(issues) {
1863
2167
  resolveToolVersion,
1864
2168
  validateContracts,
1865
2169
  validateDefinedIds,
2170
+ validateDeltas,
1866
2171
  validateProject,
1867
2172
  validateScenarioContent,
1868
2173
  validateScenarios,