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.mjs CHANGED
@@ -4,13 +4,11 @@ import path from "path";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  var defaultConfig = {
6
6
  paths: {
7
- specDir: ".qfai/spec",
8
- decisionsDir: ".qfai/spec/decisions",
9
- scenariosDir: ".qfai/spec/scenarios",
10
7
  contractsDir: ".qfai/contracts",
11
- uiContractsDir: ".qfai/contracts/ui",
12
- apiContractsDir: ".qfai/contracts/api",
13
- dataContractsDir: ".qfai/contracts/db",
8
+ specsDir: ".qfai/specs",
9
+ rulesDir: ".qfai/rules",
10
+ outDir: ".qfai/out",
11
+ promptsDir: ".qfai/prompts",
14
12
  srcDir: "src",
15
13
  testsDir: "tests"
16
14
  },
@@ -35,8 +33,7 @@ var defaultConfig = {
35
33
  }
36
34
  },
37
35
  output: {
38
- format: "text",
39
- jsonPath: ".qfai/out/validate.json"
36
+ validateJsonPath: ".qfai/out/validate.json"
40
37
  }
41
38
  };
42
39
  function getConfigPath(root) {
@@ -85,27 +82,6 @@ function normalizePaths(raw, configPath, issues) {
85
82
  return base;
86
83
  }
87
84
  return {
88
- specDir: readString(
89
- raw.specDir,
90
- base.specDir,
91
- "paths.specDir",
92
- configPath,
93
- issues
94
- ),
95
- decisionsDir: readString(
96
- raw.decisionsDir,
97
- base.decisionsDir,
98
- "paths.decisionsDir",
99
- configPath,
100
- issues
101
- ),
102
- scenariosDir: readString(
103
- raw.scenariosDir,
104
- base.scenariosDir,
105
- "paths.scenariosDir",
106
- configPath,
107
- issues
108
- ),
109
85
  contractsDir: readString(
110
86
  raw.contractsDir,
111
87
  base.contractsDir,
@@ -113,24 +89,31 @@ function normalizePaths(raw, configPath, issues) {
113
89
  configPath,
114
90
  issues
115
91
  ),
116
- uiContractsDir: readString(
117
- raw.uiContractsDir,
118
- base.uiContractsDir,
119
- "paths.uiContractsDir",
92
+ specsDir: readString(
93
+ raw.specsDir,
94
+ base.specsDir,
95
+ "paths.specsDir",
120
96
  configPath,
121
97
  issues
122
98
  ),
123
- apiContractsDir: readString(
124
- raw.apiContractsDir,
125
- base.apiContractsDir,
126
- "paths.apiContractsDir",
99
+ rulesDir: readString(
100
+ raw.rulesDir,
101
+ base.rulesDir,
102
+ "paths.rulesDir",
127
103
  configPath,
128
104
  issues
129
105
  ),
130
- dataContractsDir: readString(
131
- raw.dataContractsDir,
132
- base.dataContractsDir,
133
- "paths.dataContractsDir",
106
+ outDir: readString(
107
+ raw.outDir,
108
+ base.outDir,
109
+ "paths.outDir",
110
+ configPath,
111
+ issues
112
+ ),
113
+ promptsDir: readString(
114
+ raw.promptsDir,
115
+ base.promptsDir,
116
+ "paths.promptsDir",
134
117
  configPath,
135
118
  issues
136
119
  ),
@@ -253,17 +236,10 @@ function normalizeOutput(raw, configPath, issues) {
253
236
  return base;
254
237
  }
255
238
  return {
256
- format: readOutputFormat(
257
- raw.format,
258
- base.format,
259
- "output.format",
260
- configPath,
261
- issues
262
- ),
263
- jsonPath: readString(
264
- raw.jsonPath,
265
- base.jsonPath,
266
- "output.jsonPath",
239
+ validateJsonPath: readString(
240
+ raw.validateJsonPath,
241
+ base.validateJsonPath,
242
+ "output.validateJsonPath",
267
243
  configPath,
268
244
  issues
269
245
  )
@@ -330,20 +306,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
330
306
  }
331
307
  return fallback;
332
308
  }
333
- function readOutputFormat(value, fallback, label, configPath, issues) {
334
- if (value === "text" || value === "json" || value === "github") {
335
- return value;
336
- }
337
- if (value !== void 0) {
338
- issues.push(
339
- configIssue(
340
- configPath,
341
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
342
- )
343
- );
344
- }
345
- return fallback;
346
- }
347
309
  function configIssue(file, message) {
348
310
  return {
349
311
  code: "QFAI_CONFIG_INVALID",
@@ -423,7 +385,8 @@ function isValidId(value, prefix) {
423
385
  }
424
386
 
425
387
  // src/core/report.ts
426
- import { readFile as readFile9 } from "fs/promises";
388
+ import { readFile as readFile10 } from "fs/promises";
389
+ import path10 from "path";
427
390
 
428
391
  // src/core/discovery.ts
429
392
  import path3 from "path";
@@ -484,10 +447,24 @@ async function exists(target) {
484
447
  }
485
448
 
486
449
  // src/core/discovery.ts
487
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
488
- async function collectSpecFiles(specRoot) {
489
- const files = await collectFiles(specRoot, { extensions: [".md"] });
490
- return files.filter((file) => isSpecFile(file));
450
+ var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
451
+ async function collectSpecPackDirs(specsRoot) {
452
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
453
+ const packs = /* @__PURE__ */ new Set();
454
+ for (const file of files) {
455
+ if (isSpecPackFile(file, "spec.md")) {
456
+ packs.add(path3.dirname(file));
457
+ }
458
+ }
459
+ return Array.from(packs).sort();
460
+ }
461
+ async function collectSpecFiles(specsRoot) {
462
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
463
+ return files.filter((file) => isSpecPackFile(file, "spec.md"));
464
+ }
465
+ async function collectScenarioFiles(specsRoot) {
466
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
467
+ return files.filter((file) => isSpecPackFile(file, "scenario.md"));
491
468
  }
492
469
  async function collectUiContractFiles(uiRoot) {
493
470
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -506,9 +483,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
506
483
  ]);
507
484
  return { ui, api, db };
508
485
  }
509
- function isSpecFile(filePath) {
510
- const name = path3.basename(filePath).toLowerCase();
511
- return SPEC_NAMED_PATTERN.test(name);
486
+ function isSpecPackFile(filePath, baseName) {
487
+ if (path3.basename(filePath).toLowerCase() !== baseName) {
488
+ return false;
489
+ }
490
+ const dirName = path3.basename(path3.dirname(filePath)).toLowerCase();
491
+ return SPEC_PACK_DIR_PATTERN.test(dirName);
512
492
  }
513
493
 
514
494
  // src/core/types.ts
@@ -519,8 +499,8 @@ import { readFile as readFile2 } from "fs/promises";
519
499
  import path4 from "path";
520
500
  import { fileURLToPath } from "url";
521
501
  async function resolveToolVersion() {
522
- if ("0.2.9".length > 0) {
523
- return "0.2.9";
502
+ if ("0.3.1".length > 0) {
503
+ return "0.3.1";
524
504
  }
525
505
  try {
526
506
  const packagePath = resolvePackageJsonPath();
@@ -540,6 +520,7 @@ function resolvePackageJsonPath() {
540
520
 
541
521
  // src/core/validators/contracts.ts
542
522
  import { readFile as readFile3 } from "fs/promises";
523
+ import path6 from "path";
543
524
 
544
525
  // src/core/contracts.ts
545
526
  import path5 from "path";
@@ -595,19 +576,10 @@ var SQL_DANGEROUS_PATTERNS = [
595
576
  ];
596
577
  async function validateContracts(root, config) {
597
578
  const issues = [];
598
- issues.push(
599
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
600
- );
601
- issues.push(
602
- ...await validateApiContracts(
603
- resolvePath(root, config, "apiContractsDir")
604
- )
605
- );
606
- issues.push(
607
- ...await validateDataContracts(
608
- resolvePath(root, config, "dataContractsDir")
609
- )
610
- );
579
+ const contractsRoot = resolvePath(root, config, "contractsDir");
580
+ issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
581
+ issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
582
+ issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
611
583
  return issues;
612
584
  }
613
585
  async function validateUiContracts(uiRoot) {
@@ -823,33 +795,125 @@ function formatError2(error) {
823
795
  return String(error);
824
796
  }
825
797
  function issue(code, message, severity, file, rule, refs) {
826
- const issue6 = {
798
+ const issue7 = {
827
799
  code,
828
800
  severity,
829
801
  message
830
802
  };
831
803
  if (file) {
832
- issue6.file = file;
804
+ issue7.file = file;
833
805
  }
834
806
  if (rule) {
835
- issue6.rule = rule;
807
+ issue7.rule = rule;
836
808
  }
837
809
  if (refs && refs.length > 0) {
838
- issue6.refs = refs;
810
+ issue7.refs = refs;
839
811
  }
840
- return issue6;
812
+ return issue7;
813
+ }
814
+
815
+ // src/core/validators/delta.ts
816
+ import { readFile as readFile4 } from "fs/promises";
817
+ import path7 from "path";
818
+ var SECTION_RE = /^##\s+変更区分/m;
819
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
820
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
821
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
822
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
823
+ async function validateDeltas(root, config) {
824
+ const specsRoot = resolvePath(root, config, "specsDir");
825
+ const packs = await collectSpecPackDirs(specsRoot);
826
+ if (packs.length === 0) {
827
+ return [];
828
+ }
829
+ const issues = [];
830
+ for (const pack of packs) {
831
+ const deltaPath = path7.join(pack, "delta.md");
832
+ let text;
833
+ try {
834
+ text = await readFile4(deltaPath, "utf-8");
835
+ } catch (error) {
836
+ if (isMissingFileError(error)) {
837
+ issues.push(
838
+ issue2(
839
+ "QFAI-DELTA-001",
840
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
841
+ "error",
842
+ deltaPath,
843
+ "delta.exists"
844
+ )
845
+ );
846
+ continue;
847
+ }
848
+ throw error;
849
+ }
850
+ const hasSection = SECTION_RE.test(text);
851
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
852
+ const hasChange = CHANGE_LINE_RE.test(text);
853
+ if (!hasSection || !hasCompatibility || !hasChange) {
854
+ issues.push(
855
+ issue2(
856
+ "QFAI-DELTA-002",
857
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
858
+ "error",
859
+ deltaPath,
860
+ "delta.section"
861
+ )
862
+ );
863
+ continue;
864
+ }
865
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
866
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
867
+ if (compatibilityChecked === changeChecked) {
868
+ issues.push(
869
+ issue2(
870
+ "QFAI-DELTA-003",
871
+ "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",
872
+ "error",
873
+ deltaPath,
874
+ "delta.classification"
875
+ )
876
+ );
877
+ }
878
+ }
879
+ return issues;
880
+ }
881
+ function isMissingFileError(error) {
882
+ if (!error || typeof error !== "object") {
883
+ return false;
884
+ }
885
+ return error.code === "ENOENT";
886
+ }
887
+ function issue2(code, message, severity, file, rule, refs) {
888
+ const issue7 = {
889
+ code,
890
+ severity,
891
+ message
892
+ };
893
+ if (file) {
894
+ issue7.file = file;
895
+ }
896
+ if (rule) {
897
+ issue7.rule = rule;
898
+ }
899
+ if (refs && refs.length > 0) {
900
+ issue7.refs = refs;
901
+ }
902
+ return issue7;
841
903
  }
842
904
 
843
905
  // src/core/validators/ids.ts
844
- import { readFile as readFile5 } from "fs/promises";
845
- import path6 from "path";
906
+ import { readFile as readFile6 } from "fs/promises";
907
+ import path9 from "path";
846
908
 
847
909
  // src/core/contractIndex.ts
848
- import { readFile as readFile4 } from "fs/promises";
910
+ import { readFile as readFile5 } from "fs/promises";
911
+ import path8 from "path";
849
912
  async function buildContractIndex(root, config) {
850
- const uiRoot = resolvePath(root, config, "uiContractsDir");
851
- const apiRoot = resolvePath(root, config, "apiContractsDir");
852
- const dataRoot = resolvePath(root, config, "dataContractsDir");
913
+ const contractsRoot = resolvePath(root, config, "contractsDir");
914
+ const uiRoot = path8.join(contractsRoot, "ui");
915
+ const apiRoot = path8.join(contractsRoot, "api");
916
+ const dataRoot = path8.join(contractsRoot, "db");
853
917
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
854
918
  collectUiContractFiles(uiRoot),
855
919
  collectApiContractFiles(apiRoot),
@@ -868,7 +932,7 @@ async function buildContractIndex(root, config) {
868
932
  }
869
933
  async function indexUiContracts(files, index) {
870
934
  for (const file of files) {
871
- const text = await readFile4(file, "utf-8");
935
+ const text = await readFile5(file, "utf-8");
872
936
  try {
873
937
  const doc = parseStructuredContract(file, text);
874
938
  extractUiContractIds(doc).forEach((id) => record(index, id, file));
@@ -880,7 +944,7 @@ async function indexUiContracts(files, index) {
880
944
  }
881
945
  async function indexApiContracts(files, index) {
882
946
  for (const file of files) {
883
- const text = await readFile4(file, "utf-8");
947
+ const text = await readFile5(file, "utf-8");
884
948
  try {
885
949
  const doc = parseStructuredContract(file, text);
886
950
  extractApiContractIds(doc).forEach((id) => record(index, id, file));
@@ -892,7 +956,7 @@ async function indexApiContracts(files, index) {
892
956
  }
893
957
  async function indexDataContracts(files, index) {
894
958
  for (const file of files) {
895
- const text = await readFile4(file, "utf-8");
959
+ const text = await readFile5(file, "utf-8");
896
960
  extractIds(text, "DATA").forEach((id) => record(index, id, file));
897
961
  }
898
962
  }
@@ -903,15 +967,191 @@ function record(index, id, file) {
903
967
  index.idToFiles.set(id, current);
904
968
  }
905
969
 
970
+ // src/core/parse/gherkin.ts
971
+ var FEATURE_RE = /^\s*Feature:\s+/;
972
+ var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
973
+ var TAG_LINE_RE = /^\s*@/;
974
+ function parseTags(line) {
975
+ return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
976
+ }
977
+ function parseGherkinFeature(text, file) {
978
+ const lines = text.split(/\r?\n/);
979
+ const scenarios = [];
980
+ let featurePresent = false;
981
+ let featureTags = [];
982
+ let pendingTags = [];
983
+ let current = null;
984
+ const flush = () => {
985
+ if (!current) return;
986
+ scenarios.push({
987
+ ...current,
988
+ body: current.body.trim()
989
+ });
990
+ current = null;
991
+ };
992
+ for (let i = 0; i < lines.length; i++) {
993
+ const line = lines[i] ?? "";
994
+ const trimmed = line.trim();
995
+ if (TAG_LINE_RE.test(trimmed)) {
996
+ pendingTags.push(...parseTags(trimmed));
997
+ continue;
998
+ }
999
+ if (FEATURE_RE.test(trimmed)) {
1000
+ featurePresent = true;
1001
+ featureTags = [...pendingTags];
1002
+ pendingTags = [];
1003
+ continue;
1004
+ }
1005
+ const match = trimmed.match(SCENARIO_RE);
1006
+ if (match) {
1007
+ const scenarioName = match[1]?.trim();
1008
+ if (!scenarioName) {
1009
+ continue;
1010
+ }
1011
+ flush();
1012
+ current = {
1013
+ name: scenarioName,
1014
+ line: i + 1,
1015
+ tags: [...featureTags, ...pendingTags],
1016
+ body: ""
1017
+ };
1018
+ pendingTags = [];
1019
+ continue;
1020
+ }
1021
+ if (current) {
1022
+ current.body += `${line}
1023
+ `;
1024
+ }
1025
+ }
1026
+ flush();
1027
+ return { file, featurePresent, scenarios };
1028
+ }
1029
+
1030
+ // src/core/parse/markdown.ts
1031
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1032
+ function parseHeadings(md) {
1033
+ const lines = md.split(/\r?\n/);
1034
+ const headings = [];
1035
+ for (let i = 0; i < lines.length; i++) {
1036
+ const line = lines[i] ?? "";
1037
+ const match = line.match(HEADING_RE);
1038
+ if (!match) continue;
1039
+ const levelToken = match[1];
1040
+ const title = match[2];
1041
+ if (!levelToken || !title) continue;
1042
+ headings.push({
1043
+ level: levelToken.length,
1044
+ title: title.trim(),
1045
+ line: i + 1
1046
+ });
1047
+ }
1048
+ return headings;
1049
+ }
1050
+ function extractH2Sections(md) {
1051
+ const lines = md.split(/\r?\n/);
1052
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1053
+ const sections = /* @__PURE__ */ new Map();
1054
+ for (let i = 0; i < headings.length; i++) {
1055
+ const current = headings[i];
1056
+ if (!current) continue;
1057
+ const next = headings[i + 1];
1058
+ const startLine = current.line + 1;
1059
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1060
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1061
+ sections.set(current.title.trim(), {
1062
+ title: current.title.trim(),
1063
+ startLine,
1064
+ endLine,
1065
+ body
1066
+ });
1067
+ }
1068
+ return sections;
1069
+ }
1070
+
1071
+ // src/core/parse/spec.ts
1072
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1073
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1074
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1075
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1076
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1077
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1078
+ function parseSpec(md, file) {
1079
+ const headings = parseHeadings(md);
1080
+ const h1 = headings.find((heading) => heading.level === 1);
1081
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1082
+ const sections = extractH2Sections(md);
1083
+ const sectionNames = new Set(Array.from(sections.keys()));
1084
+ const brSection = sections.get(BR_SECTION_TITLE);
1085
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1086
+ const startLine = brSection?.startLine ?? 1;
1087
+ const brs = [];
1088
+ const brsWithoutPriority = [];
1089
+ const brsWithInvalidPriority = [];
1090
+ for (let i = 0; i < brLines.length; i++) {
1091
+ const lineText = brLines[i] ?? "";
1092
+ const lineNumber = startLine + i;
1093
+ const validMatch = lineText.match(BR_LINE_RE);
1094
+ if (validMatch) {
1095
+ const id = validMatch[1];
1096
+ const priority = validMatch[2];
1097
+ const text = validMatch[3];
1098
+ if (!id || !priority || !text) continue;
1099
+ brs.push({
1100
+ id,
1101
+ priority,
1102
+ text: text.trim(),
1103
+ line: lineNumber
1104
+ });
1105
+ continue;
1106
+ }
1107
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1108
+ if (anyPriorityMatch) {
1109
+ const id = anyPriorityMatch[1];
1110
+ const priority = anyPriorityMatch[2];
1111
+ const text = anyPriorityMatch[3];
1112
+ if (!id || !priority || !text) continue;
1113
+ if (!VALID_PRIORITIES.has(priority)) {
1114
+ brsWithInvalidPriority.push({
1115
+ id,
1116
+ priority,
1117
+ text: text.trim(),
1118
+ line: lineNumber
1119
+ });
1120
+ }
1121
+ continue;
1122
+ }
1123
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1124
+ if (noPriorityMatch) {
1125
+ const id = noPriorityMatch[1];
1126
+ const text = noPriorityMatch[2];
1127
+ if (!id || !text) continue;
1128
+ brsWithoutPriority.push({
1129
+ id,
1130
+ text: text.trim(),
1131
+ line: lineNumber
1132
+ });
1133
+ }
1134
+ }
1135
+ const parsed = {
1136
+ file,
1137
+ sections: sectionNames,
1138
+ brs,
1139
+ brsWithoutPriority,
1140
+ brsWithInvalidPriority
1141
+ };
1142
+ if (specId) {
1143
+ parsed.specId = specId;
1144
+ }
1145
+ return parsed;
1146
+ }
1147
+
906
1148
  // src/core/validators/ids.ts
1149
+ var SC_TAG_RE = /^SC-\d{4}$/;
907
1150
  async function validateDefinedIds(root, config) {
908
1151
  const issues = [];
909
- const specRoot = resolvePath(root, config, "specDir");
910
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
911
- const specFiles = await collectSpecFiles(specRoot);
912
- const scenarioFiles = await collectFiles(scenarioRoot, {
913
- extensions: [".feature"]
914
- });
1152
+ const specsRoot = resolvePath(root, config, "specsDir");
1153
+ const specFiles = await collectSpecFiles(specsRoot);
1154
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
915
1155
  const defined = /* @__PURE__ */ new Map();
916
1156
  await collectSpecDefinitionIds(specFiles, defined);
917
1157
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -927,7 +1167,7 @@ async function validateDefinedIds(root, config) {
927
1167
  }
928
1168
  const sorted = Array.from(files).sort();
929
1169
  issues.push(
930
- issue2(
1170
+ issue3(
931
1171
  "QFAI-ID-001",
932
1172
  `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
933
1173
  "error",
@@ -940,15 +1180,25 @@ async function validateDefinedIds(root, config) {
940
1180
  }
941
1181
  async function collectSpecDefinitionIds(files, out) {
942
1182
  for (const file of files) {
943
- const text = await readFile5(file, "utf-8");
944
- extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
945
- extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1183
+ const text = await readFile6(file, "utf-8");
1184
+ const parsed = parseSpec(text, file);
1185
+ if (parsed.specId) {
1186
+ recordId(out, parsed.specId, file);
1187
+ }
1188
+ parsed.brs.forEach((br) => recordId(out, br.id, file));
946
1189
  }
947
1190
  }
948
1191
  async function collectScenarioDefinitionIds(files, out) {
949
1192
  for (const file of files) {
950
- const text = await readFile5(file, "utf-8");
951
- extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1193
+ const text = await readFile6(file, "utf-8");
1194
+ const parsed = parseGherkinFeature(text, file);
1195
+ for (const scenario of parsed.scenarios) {
1196
+ for (const tag of scenario.tags) {
1197
+ if (SC_TAG_RE.test(tag)) {
1198
+ recordId(out, tag, file);
1199
+ }
1200
+ }
1201
+ }
952
1202
  }
953
1203
  }
954
1204
  function recordId(out, id, file) {
@@ -958,58 +1208,60 @@ function recordId(out, id, file) {
958
1208
  }
959
1209
  function formatFileList(files, root) {
960
1210
  return files.map((file) => {
961
- const relative = path6.relative(root, file);
1211
+ const relative = path9.relative(root, file);
962
1212
  return relative.length > 0 ? relative : file;
963
1213
  }).join(", ");
964
1214
  }
965
- function issue2(code, message, severity, file, rule, refs) {
966
- const issue6 = {
1215
+ function issue3(code, message, severity, file, rule, refs) {
1216
+ const issue7 = {
967
1217
  code,
968
1218
  severity,
969
1219
  message
970
1220
  };
971
1221
  if (file) {
972
- issue6.file = file;
1222
+ issue7.file = file;
973
1223
  }
974
1224
  if (rule) {
975
- issue6.rule = rule;
1225
+ issue7.rule = rule;
976
1226
  }
977
1227
  if (refs && refs.length > 0) {
978
- issue6.refs = refs;
1228
+ issue7.refs = refs;
979
1229
  }
980
- return issue6;
1230
+ return issue7;
981
1231
  }
982
1232
 
983
1233
  // src/core/validators/scenario.ts
984
- import { readFile as readFile6 } from "fs/promises";
1234
+ import { readFile as readFile7 } from "fs/promises";
985
1235
  var GIVEN_PATTERN = /\bGiven\b/;
986
1236
  var WHEN_PATTERN = /\bWhen\b/;
987
1237
  var THEN_PATTERN = /\bThen\b/;
1238
+ var SC_TAG_RE2 = /^SC-\d{4}$/;
1239
+ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1240
+ var BR_TAG_RE = /^BR-\d{4}$/;
988
1241
  async function validateScenarios(root, config) {
989
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
990
- const files = await collectFiles(scenariosRoot, {
991
- extensions: [".feature"]
992
- });
1242
+ const specsRoot = resolvePath(root, config, "specsDir");
1243
+ const files = await collectScenarioFiles(specsRoot);
993
1244
  if (files.length === 0) {
994
1245
  return [
995
- issue3(
1246
+ issue4(
996
1247
  "QFAI-SC-000",
997
1248
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
998
1249
  "info",
999
- scenariosRoot,
1250
+ specsRoot,
1000
1251
  "scenario.files"
1001
1252
  )
1002
1253
  ];
1003
1254
  }
1004
1255
  const issues = [];
1005
1256
  for (const file of files) {
1006
- const text = await readFile6(file, "utf-8");
1257
+ const text = await readFile7(file, "utf-8");
1007
1258
  issues.push(...validateScenarioContent(text, file));
1008
1259
  }
1009
1260
  return issues;
1010
1261
  }
1011
1262
  function validateScenarioContent(text, file) {
1012
1263
  const issues = [];
1264
+ const parsed = parseGherkinFeature(text, file);
1013
1265
  const invalidIds = extractInvalidIds(text, [
1014
1266
  "SPEC",
1015
1267
  "BR",
@@ -1021,7 +1273,7 @@ function validateScenarioContent(text, file) {
1021
1273
  ]);
1022
1274
  if (invalidIds.length > 0) {
1023
1275
  issues.push(
1024
- issue3(
1276
+ issue4(
1025
1277
  "QFAI-ID-002",
1026
1278
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1027
1279
  "error",
@@ -1031,94 +1283,114 @@ function validateScenarioContent(text, file) {
1031
1283
  )
1032
1284
  );
1033
1285
  }
1034
- const scIds = extractIds(text, "SC");
1035
- if (scIds.length === 0) {
1036
- issues.push(
1037
- issue3(
1038
- "QFAI-SC-001",
1039
- "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1040
- "error",
1041
- file,
1042
- "scenario.id"
1043
- )
1044
- );
1045
- }
1046
- const specIds = extractIds(text, "SPEC");
1047
- if (specIds.length === 0) {
1048
- issues.push(
1049
- issue3(
1050
- "QFAI-SC-002",
1051
- "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1052
- "error",
1053
- file,
1054
- "scenario.spec"
1055
- )
1056
- );
1057
- }
1058
- const brIds = extractIds(text, "BR");
1059
- if (brIds.length === 0) {
1286
+ const missingStructure = [];
1287
+ if (!parsed.featurePresent) missingStructure.push("Feature");
1288
+ if (parsed.scenarios.length === 0) missingStructure.push("Scenario");
1289
+ if (missingStructure.length > 0) {
1060
1290
  issues.push(
1061
- issue3(
1062
- "QFAI-SC-003",
1063
- "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1291
+ issue4(
1292
+ "QFAI-SC-006",
1293
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u306B\u5FC5\u8981\u306A\u69CB\u9020\u304C\u3042\u308A\u307E\u305B\u3093: ${missingStructure.join(
1294
+ ", "
1295
+ )}`,
1064
1296
  "error",
1065
1297
  file,
1066
- "scenario.br"
1298
+ "scenario.structure"
1067
1299
  )
1068
1300
  );
1069
1301
  }
1070
- const missingSteps = [];
1071
- if (!GIVEN_PATTERN.test(text)) {
1072
- missingSteps.push("Given");
1073
- }
1074
- if (!WHEN_PATTERN.test(text)) {
1075
- missingSteps.push("When");
1076
- }
1077
- if (!THEN_PATTERN.test(text)) {
1078
- missingSteps.push("Then");
1302
+ for (const scenario of parsed.scenarios) {
1303
+ if (scenario.tags.length === 0) {
1304
+ issues.push(
1305
+ issue4(
1306
+ "QFAI-SC-007",
1307
+ `Scenario \u30BF\u30B0\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${scenario.name}`,
1308
+ "error",
1309
+ file,
1310
+ "scenario.tags"
1311
+ )
1312
+ );
1313
+ continue;
1314
+ }
1315
+ const missingTags = [];
1316
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1317
+ if (scTags.length === 0) {
1318
+ missingTags.push("SC(0\u4EF6)");
1319
+ } else if (scTags.length > 1) {
1320
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1321
+ }
1322
+ if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1323
+ missingTags.push("SPEC");
1324
+ }
1325
+ if (!scenario.tags.some((tag) => BR_TAG_RE.test(tag))) {
1326
+ missingTags.push("BR");
1327
+ }
1328
+ if (missingTags.length > 0) {
1329
+ issues.push(
1330
+ issue4(
1331
+ "QFAI-SC-008",
1332
+ `Scenario \u30BF\u30B0\u306B\u4E0D\u8DB3\u304C\u3042\u308A\u307E\u3059: ${missingTags.join(", ")} (${scenario.name})`,
1333
+ "error",
1334
+ file,
1335
+ "scenario.tagIds"
1336
+ )
1337
+ );
1338
+ }
1079
1339
  }
1080
- if (missingSteps.length > 0) {
1081
- issues.push(
1082
- issue3(
1083
- "QFAI-SC-005",
1084
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1085
- "warning",
1086
- file,
1087
- "scenario.steps"
1088
- )
1089
- );
1340
+ for (const scenario of parsed.scenarios) {
1341
+ const missingSteps = [];
1342
+ if (!GIVEN_PATTERN.test(scenario.body)) {
1343
+ missingSteps.push("Given");
1344
+ }
1345
+ if (!WHEN_PATTERN.test(scenario.body)) {
1346
+ missingSteps.push("When");
1347
+ }
1348
+ if (!THEN_PATTERN.test(scenario.body)) {
1349
+ missingSteps.push("Then");
1350
+ }
1351
+ if (missingSteps.length > 0) {
1352
+ issues.push(
1353
+ issue4(
1354
+ "QFAI-SC-005",
1355
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1356
+ "warning",
1357
+ file,
1358
+ "scenario.steps"
1359
+ )
1360
+ );
1361
+ }
1090
1362
  }
1091
1363
  return issues;
1092
1364
  }
1093
- function issue3(code, message, severity, file, rule, refs) {
1094
- const issue6 = {
1365
+ function issue4(code, message, severity, file, rule, refs) {
1366
+ const issue7 = {
1095
1367
  code,
1096
1368
  severity,
1097
1369
  message
1098
1370
  };
1099
1371
  if (file) {
1100
- issue6.file = file;
1372
+ issue7.file = file;
1101
1373
  }
1102
1374
  if (rule) {
1103
- issue6.rule = rule;
1375
+ issue7.rule = rule;
1104
1376
  }
1105
1377
  if (refs && refs.length > 0) {
1106
- issue6.refs = refs;
1378
+ issue7.refs = refs;
1107
1379
  }
1108
- return issue6;
1380
+ return issue7;
1109
1381
  }
1110
1382
 
1111
1383
  // src/core/validators/spec.ts
1112
- import { readFile as readFile7 } from "fs/promises";
1384
+ import { readFile as readFile8 } from "fs/promises";
1113
1385
  async function validateSpecs(root, config) {
1114
- const specsRoot = resolvePath(root, config, "specDir");
1386
+ const specsRoot = resolvePath(root, config, "specsDir");
1115
1387
  const files = await collectSpecFiles(specsRoot);
1116
1388
  if (files.length === 0) {
1117
- const expected = "spec-0001-<slug>.md";
1389
+ const expected = "spec-001/spec.md";
1118
1390
  return [
1119
- issue4(
1391
+ issue5(
1120
1392
  "QFAI-SPEC-000",
1121
- `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}`,
1393
+ `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}`,
1122
1394
  "info",
1123
1395
  specsRoot,
1124
1396
  "spec.files"
@@ -1127,7 +1399,7 @@ async function validateSpecs(root, config) {
1127
1399
  }
1128
1400
  const issues = [];
1129
1401
  for (const file of files) {
1130
- const text = await readFile7(file, "utf-8");
1402
+ const text = await readFile8(file, "utf-8");
1131
1403
  issues.push(
1132
1404
  ...validateSpecContent(
1133
1405
  text,
@@ -1140,6 +1412,7 @@ async function validateSpecs(root, config) {
1140
1412
  }
1141
1413
  function validateSpecContent(text, file, requiredSections) {
1142
1414
  const issues = [];
1415
+ const parsed = parseSpec(text, file);
1143
1416
  const invalidIds = extractInvalidIds(text, [
1144
1417
  "SPEC",
1145
1418
  "BR",
@@ -1151,7 +1424,7 @@ function validateSpecContent(text, file, requiredSections) {
1151
1424
  ]);
1152
1425
  if (invalidIds.length > 0) {
1153
1426
  issues.push(
1154
- issue4(
1427
+ issue5(
1155
1428
  "QFAI-ID-002",
1156
1429
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1157
1430
  "error",
@@ -1161,10 +1434,9 @@ function validateSpecContent(text, file, requiredSections) {
1161
1434
  )
1162
1435
  );
1163
1436
  }
1164
- const specIds = extractIds(text, "SPEC");
1165
- if (specIds.length === 0) {
1437
+ if (!parsed.specId) {
1166
1438
  issues.push(
1167
- issue4(
1439
+ issue5(
1168
1440
  "QFAI-SPEC-001",
1169
1441
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1170
1442
  "error",
@@ -1173,10 +1445,9 @@ function validateSpecContent(text, file, requiredSections) {
1173
1445
  )
1174
1446
  );
1175
1447
  }
1176
- const brIds = extractIds(text, "BR");
1177
- if (brIds.length === 0) {
1448
+ if (parsed.brs.length === 0) {
1178
1449
  issues.push(
1179
- issue4(
1450
+ issue5(
1180
1451
  "QFAI-SPEC-002",
1181
1452
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1182
1453
  "error",
@@ -1185,10 +1456,34 @@ function validateSpecContent(text, file, requiredSections) {
1185
1456
  )
1186
1457
  );
1187
1458
  }
1459
+ for (const br of parsed.brsWithoutPriority) {
1460
+ issues.push(
1461
+ issue5(
1462
+ "QFAI-BR-001",
1463
+ `BR \u884C\u306B Priority \u304C\u3042\u308A\u307E\u305B\u3093: ${br.id}`,
1464
+ "error",
1465
+ file,
1466
+ "spec.brPriority",
1467
+ [br.id]
1468
+ )
1469
+ );
1470
+ }
1471
+ for (const br of parsed.brsWithInvalidPriority) {
1472
+ issues.push(
1473
+ issue5(
1474
+ "QFAI-BR-002",
1475
+ `BR Priority \u304C\u4E0D\u6B63\u3067\u3059: ${br.id} (${br.priority})`,
1476
+ "error",
1477
+ file,
1478
+ "spec.brPriority",
1479
+ [br.id]
1480
+ )
1481
+ );
1482
+ }
1188
1483
  const scIds = extractIds(text, "SC");
1189
1484
  if (scIds.length > 0) {
1190
1485
  issues.push(
1191
- issue4(
1486
+ issue5(
1192
1487
  "QFAI-SPEC-003",
1193
1488
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1194
1489
  "warning",
@@ -1199,9 +1494,9 @@ function validateSpecContent(text, file, requiredSections) {
1199
1494
  );
1200
1495
  }
1201
1496
  for (const section of requiredSections) {
1202
- if (!text.includes(section)) {
1497
+ if (!parsed.sections.has(section)) {
1203
1498
  issues.push(
1204
- issue4(
1499
+ issue5(
1205
1500
  "QFAI-SPEC-004",
1206
1501
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1207
1502
  "error",
@@ -1213,40 +1508,39 @@ function validateSpecContent(text, file, requiredSections) {
1213
1508
  }
1214
1509
  return issues;
1215
1510
  }
1216
- function issue4(code, message, severity, file, rule, refs) {
1217
- const issue6 = {
1511
+ function issue5(code, message, severity, file, rule, refs) {
1512
+ const issue7 = {
1218
1513
  code,
1219
1514
  severity,
1220
1515
  message
1221
1516
  };
1222
1517
  if (file) {
1223
- issue6.file = file;
1518
+ issue7.file = file;
1224
1519
  }
1225
1520
  if (rule) {
1226
- issue6.rule = rule;
1521
+ issue7.rule = rule;
1227
1522
  }
1228
1523
  if (refs && refs.length > 0) {
1229
- issue6.refs = refs;
1524
+ issue7.refs = refs;
1230
1525
  }
1231
- return issue6;
1526
+ return issue7;
1232
1527
  }
1233
1528
 
1234
1529
  // src/core/validators/traceability.ts
1235
- import { readFile as readFile8 } from "fs/promises";
1530
+ import { readFile as readFile9 } from "fs/promises";
1531
+ var SC_TAG_RE3 = /^SC-\d{4}$/;
1532
+ var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1533
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1534
+ var UI_TAG_RE = /^UI-\d{4}$/;
1535
+ var API_TAG_RE = /^API-\d{4}$/;
1536
+ var DATA_TAG_RE = /^DATA-\d{4}$/;
1236
1537
  async function validateTraceability(root, config) {
1237
1538
  const issues = [];
1238
- const specsRoot = resolvePath(root, config, "specDir");
1239
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1240
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1539
+ const specsRoot = resolvePath(root, config, "specsDir");
1241
1540
  const srcRoot = resolvePath(root, config, "srcDir");
1242
1541
  const testsRoot = resolvePath(root, config, "testsDir");
1243
1542
  const specFiles = await collectSpecFiles(specsRoot);
1244
- const decisionFiles = await collectFiles(decisionsRoot, {
1245
- extensions: [".md"]
1246
- });
1247
- const scenarioFiles = await collectFiles(scenariosRoot, {
1248
- extensions: [".feature"]
1249
- });
1543
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1250
1544
  const upstreamIds = /* @__PURE__ */ new Set();
1251
1545
  const specIds = /* @__PURE__ */ new Set();
1252
1546
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1258,11 +1552,13 @@ async function validateTraceability(root, config) {
1258
1552
  const contractIndex = await buildContractIndex(root, config);
1259
1553
  const contractIds = contractIndex.ids;
1260
1554
  for (const file of specFiles) {
1261
- const text = await readFile8(file, "utf-8");
1555
+ const text = await readFile9(file, "utf-8");
1262
1556
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1263
- const specIdsInFile = extractIds(text, "SPEC");
1264
- specIdsInFile.forEach((id) => specIds.add(id));
1265
- const brIds = extractIds(text, "BR");
1557
+ const parsed = parseSpec(text, file);
1558
+ if (parsed.specId) {
1559
+ specIds.add(parsed.specId);
1560
+ }
1561
+ const brIds = parsed.brs.map((br) => br.id);
1266
1562
  brIds.forEach((id) => brIdsInSpecs.add(id));
1267
1563
  const referencedContractIds = /* @__PURE__ */ new Set([
1268
1564
  ...extractIds(text, "UI"),
@@ -1274,7 +1570,7 @@ async function validateTraceability(root, config) {
1274
1570
  );
1275
1571
  if (unknownContractIds.length > 0) {
1276
1572
  issues.push(
1277
- issue5(
1573
+ issue6(
1278
1574
  "QFAI-TRACE-009",
1279
1575
  `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1280
1576
  ", "
@@ -1286,37 +1582,50 @@ async function validateTraceability(root, config) {
1286
1582
  )
1287
1583
  );
1288
1584
  }
1289
- for (const specId of specIdsInFile) {
1290
- const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1585
+ if (parsed.specId) {
1586
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1291
1587
  brIds.forEach((id) => current.add(id));
1292
- specToBrIds.set(specId, current);
1588
+ specToBrIds.set(parsed.specId, current);
1293
1589
  }
1294
1590
  }
1295
- for (const file of decisionFiles) {
1296
- const text = await readFile8(file, "utf-8");
1297
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1298
- }
1299
1591
  for (const file of scenarioFiles) {
1300
- const text = await readFile8(file, "utf-8");
1592
+ const text = await readFile9(file, "utf-8");
1301
1593
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1302
- const specIdsInScenario = extractIds(text, "SPEC");
1303
- const brIds = extractIds(text, "BR");
1304
- const scIds = extractIds(text, "SC");
1305
- const scenarioIds = [
1306
- ...extractIds(text, "UI"),
1307
- ...extractIds(text, "API"),
1308
- ...extractIds(text, "DATA")
1309
- ];
1310
- brIds.forEach((id) => brIdsInScenarios.add(id));
1311
- scIds.forEach((id) => scIdsInScenarios.add(id));
1312
- scenarioIds.forEach((id) => scenarioContractIds.add(id));
1313
- if (scenarioIds.length > 0) {
1314
- scIds.forEach((id) => scWithContracts.add(id));
1594
+ const parsed = parseGherkinFeature(text, file);
1595
+ const specIdsInScenario = /* @__PURE__ */ new Set();
1596
+ const brIds = /* @__PURE__ */ new Set();
1597
+ const scIds = /* @__PURE__ */ new Set();
1598
+ const scenarioIds = /* @__PURE__ */ new Set();
1599
+ for (const scenario of parsed.scenarios) {
1600
+ for (const tag of scenario.tags) {
1601
+ if (SPEC_TAG_RE2.test(tag)) {
1602
+ specIdsInScenario.add(tag);
1603
+ }
1604
+ if (BR_TAG_RE2.test(tag)) {
1605
+ brIds.add(tag);
1606
+ }
1607
+ if (SC_TAG_RE3.test(tag)) {
1608
+ scIds.add(tag);
1609
+ }
1610
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
1611
+ scenarioIds.add(tag);
1612
+ }
1613
+ }
1315
1614
  }
1316
- const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1615
+ const specIdsList = Array.from(specIdsInScenario);
1616
+ const brIdsList = Array.from(brIds);
1617
+ const scIdsList = Array.from(scIds);
1618
+ const scenarioIdsList = Array.from(scenarioIds);
1619
+ brIdsList.forEach((id) => brIdsInScenarios.add(id));
1620
+ scIdsList.forEach((id) => scIdsInScenarios.add(id));
1621
+ scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
1622
+ if (scenarioIdsList.length > 0) {
1623
+ scIdsList.forEach((id) => scWithContracts.add(id));
1624
+ }
1625
+ const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
1317
1626
  if (unknownSpecIds.length > 0) {
1318
1627
  issues.push(
1319
- issue5(
1628
+ issue6(
1320
1629
  "QFAI-TRACE-005",
1321
1630
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1322
1631
  "error",
@@ -1326,10 +1635,10 @@ async function validateTraceability(root, config) {
1326
1635
  )
1327
1636
  );
1328
1637
  }
1329
- const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1638
+ const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
1330
1639
  if (unknownBrIds.length > 0) {
1331
1640
  issues.push(
1332
- issue5(
1641
+ issue6(
1333
1642
  "QFAI-TRACE-006",
1334
1643
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1335
1644
  "error",
@@ -1339,10 +1648,12 @@ async function validateTraceability(root, config) {
1339
1648
  )
1340
1649
  );
1341
1650
  }
1342
- const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1651
+ const unknownContractIds = scenarioIdsList.filter(
1652
+ (id) => !contractIds.has(id)
1653
+ );
1343
1654
  if (unknownContractIds.length > 0) {
1344
1655
  issues.push(
1345
- issue5(
1656
+ issue6(
1346
1657
  "QFAI-TRACE-008",
1347
1658
  `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1348
1659
  ", "
@@ -1354,23 +1665,23 @@ async function validateTraceability(root, config) {
1354
1665
  )
1355
1666
  );
1356
1667
  }
1357
- if (specIdsInScenario.length > 0) {
1668
+ if (specIdsList.length > 0) {
1358
1669
  const allowedBrIds = /* @__PURE__ */ new Set();
1359
- for (const specId of specIdsInScenario) {
1670
+ for (const specId of specIdsList) {
1360
1671
  const brIdsForSpec = specToBrIds.get(specId);
1361
1672
  if (!brIdsForSpec) {
1362
1673
  continue;
1363
1674
  }
1364
1675
  brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1365
1676
  }
1366
- const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1677
+ const invalidBrIds = brIdsList.filter((id) => !allowedBrIds.has(id));
1367
1678
  if (invalidBrIds.length > 0) {
1368
1679
  issues.push(
1369
- issue5(
1680
+ issue6(
1370
1681
  "QFAI-TRACE-007",
1371
1682
  `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1372
1683
  ", "
1373
- )} (SPEC: ${specIdsInScenario.join(", ")})`,
1684
+ )} (SPEC: ${specIdsList.join(", ")})`,
1374
1685
  "error",
1375
1686
  file,
1376
1687
  "traceability.scenarioBrUnderSpec",
@@ -1382,7 +1693,7 @@ async function validateTraceability(root, config) {
1382
1693
  }
1383
1694
  if (upstreamIds.size === 0) {
1384
1695
  return [
1385
- issue5(
1696
+ issue6(
1386
1697
  "QFAI-TRACE-000",
1387
1698
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1388
1699
  "info",
@@ -1397,7 +1708,7 @@ async function validateTraceability(root, config) {
1397
1708
  );
1398
1709
  if (orphanBrIds.length > 0) {
1399
1710
  issues.push(
1400
- issue5(
1711
+ issue6(
1401
1712
  "QFAI_TRACE_BR_ORPHAN",
1402
1713
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1403
1714
  "error",
@@ -1414,13 +1725,13 @@ async function validateTraceability(root, config) {
1414
1725
  );
1415
1726
  if (scWithoutContracts.length > 0) {
1416
1727
  issues.push(
1417
- issue5(
1728
+ issue6(
1418
1729
  "QFAI_TRACE_SC_NO_CONTRACT",
1419
1730
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1420
1731
  ", "
1421
1732
  )}`,
1422
1733
  "error",
1423
- scenariosRoot,
1734
+ specsRoot,
1424
1735
  "traceability.scMustTouchContracts",
1425
1736
  scWithoutContracts
1426
1737
  )
@@ -1434,11 +1745,11 @@ async function validateTraceability(root, config) {
1434
1745
  );
1435
1746
  if (orphanContracts.length > 0) {
1436
1747
  issues.push(
1437
- issue5(
1748
+ issue6(
1438
1749
  "QFAI_CONTRACT_ORPHAN",
1439
1750
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1440
1751
  "error",
1441
- scenariosRoot,
1752
+ specsRoot,
1442
1753
  "traceability.allowOrphanContracts",
1443
1754
  orphanContracts
1444
1755
  )
@@ -1462,7 +1773,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1462
1773
  const targetFiles = [...codeFiles, ...testFiles];
1463
1774
  if (targetFiles.length === 0) {
1464
1775
  issues.push(
1465
- issue5(
1776
+ issue6(
1466
1777
  "QFAI-TRACE-001",
1467
1778
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1468
1779
  "info",
@@ -1475,7 +1786,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1475
1786
  const pattern = buildIdPattern(Array.from(upstreamIds));
1476
1787
  let found = false;
1477
1788
  for (const file of targetFiles) {
1478
- const text = await readFile8(file, "utf-8");
1789
+ const text = await readFile9(file, "utf-8");
1479
1790
  if (pattern.test(text)) {
1480
1791
  found = true;
1481
1792
  break;
@@ -1483,7 +1794,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1483
1794
  }
1484
1795
  if (!found) {
1485
1796
  issues.push(
1486
- issue5(
1797
+ issue6(
1487
1798
  "QFAI-TRACE-002",
1488
1799
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1489
1800
  "warning",
@@ -1498,22 +1809,22 @@ function buildIdPattern(ids) {
1498
1809
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1499
1810
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1500
1811
  }
1501
- function issue5(code, message, severity, file, rule, refs) {
1502
- const issue6 = {
1812
+ function issue6(code, message, severity, file, rule, refs) {
1813
+ const issue7 = {
1503
1814
  code,
1504
1815
  severity,
1505
1816
  message
1506
1817
  };
1507
1818
  if (file) {
1508
- issue6.file = file;
1819
+ issue7.file = file;
1509
1820
  }
1510
1821
  if (rule) {
1511
- issue6.rule = rule;
1822
+ issue7.rule = rule;
1512
1823
  }
1513
1824
  if (refs && refs.length > 0) {
1514
- issue6.refs = refs;
1825
+ issue7.refs = refs;
1515
1826
  }
1516
- return issue6;
1827
+ return issue7;
1517
1828
  }
1518
1829
 
1519
1830
  // src/core/validate.ts
@@ -1523,6 +1834,7 @@ async function validateProject(root, configResult) {
1523
1834
  const issues = [
1524
1835
  ...configIssues,
1525
1836
  ...await validateSpecs(root, config),
1837
+ ...await validateDeltas(root, config),
1526
1838
  ...await validateScenarios(root, config),
1527
1839
  ...await validateContracts(root, config),
1528
1840
  ...await validateDefinedIds(root, config),
@@ -1538,8 +1850,8 @@ async function validateProject(root, configResult) {
1538
1850
  }
1539
1851
  function countIssues(issues) {
1540
1852
  return issues.reduce(
1541
- (acc, issue6) => {
1542
- acc[issue6.severity] += 1;
1853
+ (acc, issue7) => {
1854
+ acc[issue7.severity] += 1;
1543
1855
  return acc;
1544
1856
  },
1545
1857
  { info: 0, warning: 0, error: 0 }
@@ -1552,21 +1864,15 @@ async function createReportData(root, validation, configResult) {
1552
1864
  const resolved = configResult ?? await loadConfig(root);
1553
1865
  const config = resolved.config;
1554
1866
  const configPath = resolved.configPath;
1555
- const specRoot = resolvePath(root, config, "specDir");
1556
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1557
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1558
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1559
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1560
- const dbRoot = resolvePath(root, config, "dataContractsDir");
1867
+ const specsRoot = resolvePath(root, config, "specsDir");
1868
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1869
+ const apiRoot = path10.join(contractsRoot, "api");
1870
+ const uiRoot = path10.join(contractsRoot, "ui");
1871
+ const dbRoot = path10.join(contractsRoot, "db");
1561
1872
  const srcRoot = resolvePath(root, config, "srcDir");
1562
1873
  const testsRoot = resolvePath(root, config, "testsDir");
1563
- const specFiles = await collectSpecFiles(specRoot);
1564
- const scenarioFiles = await collectFiles(scenariosRoot, {
1565
- extensions: [".feature"]
1566
- });
1567
- const decisionFiles = await collectFiles(decisionsRoot, {
1568
- extensions: [".md"]
1569
- });
1874
+ const specFiles = await collectSpecFiles(specsRoot);
1875
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1570
1876
  const {
1571
1877
  api: apiFiles,
1572
1878
  ui: uiFiles,
@@ -1575,7 +1881,6 @@ async function createReportData(root, validation, configResult) {
1575
1881
  const idsByPrefix = await collectIds([
1576
1882
  ...specFiles,
1577
1883
  ...scenarioFiles,
1578
- ...decisionFiles,
1579
1884
  ...apiFiles,
1580
1885
  ...uiFiles,
1581
1886
  ...dbFiles
@@ -1600,7 +1905,6 @@ async function createReportData(root, validation, configResult) {
1600
1905
  summary: {
1601
1906
  specs: specFiles.length,
1602
1907
  scenarios: scenarioFiles.length,
1603
- decisions: decisionFiles.length,
1604
1908
  contracts: {
1605
1909
  api: apiFiles.length,
1606
1910
  ui: uiFiles.length,
@@ -1634,7 +1938,6 @@ function formatReportMarkdown(data) {
1634
1938
  lines.push("## \u6982\u8981");
1635
1939
  lines.push(`- specs: ${data.summary.specs}`);
1636
1940
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1637
- lines.push(`- decisions: ${data.summary.decisions}`);
1638
1941
  lines.push(
1639
1942
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1640
1943
  );
@@ -1710,7 +2013,7 @@ async function collectIds(files) {
1710
2013
  DATA: /* @__PURE__ */ new Set()
1711
2014
  };
1712
2015
  for (const file of files) {
1713
- const text = await readFile9(file, "utf-8");
2016
+ const text = await readFile10(file, "utf-8");
1714
2017
  for (const prefix of ID_PREFIXES2) {
1715
2018
  const ids = extractIds(text, prefix);
1716
2019
  ids.forEach((id) => result[prefix].add(id));
@@ -1728,7 +2031,7 @@ async function collectIds(files) {
1728
2031
  async function collectUpstreamIds(files) {
1729
2032
  const ids = /* @__PURE__ */ new Set();
1730
2033
  for (const file of files) {
1731
- const text = await readFile9(file, "utf-8");
2034
+ const text = await readFile10(file, "utf-8");
1732
2035
  extractAllIds(text).forEach((id) => ids.add(id));
1733
2036
  }
1734
2037
  return ids;
@@ -1749,7 +2052,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1749
2052
  }
1750
2053
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1751
2054
  for (const file of targetFiles) {
1752
- const text = await readFile9(file, "utf-8");
2055
+ const text = await readFile10(file, "utf-8");
1753
2056
  if (pattern.test(text)) {
1754
2057
  return true;
1755
2058
  }
@@ -1771,20 +2074,20 @@ function toSortedArray(values) {
1771
2074
  }
1772
2075
  function buildHotspots(issues) {
1773
2076
  const map = /* @__PURE__ */ new Map();
1774
- for (const issue6 of issues) {
1775
- if (!issue6.file) {
2077
+ for (const issue7 of issues) {
2078
+ if (!issue7.file) {
1776
2079
  continue;
1777
2080
  }
1778
- const current = map.get(issue6.file) ?? {
1779
- file: issue6.file,
2081
+ const current = map.get(issue7.file) ?? {
2082
+ file: issue7.file,
1780
2083
  total: 0,
1781
2084
  error: 0,
1782
2085
  warning: 0,
1783
2086
  info: 0
1784
2087
  };
1785
2088
  current.total += 1;
1786
- current[issue6.severity] += 1;
1787
- map.set(issue6.file, current);
2089
+ current[issue7.severity] += 1;
2090
+ map.set(issue7.file, current);
1788
2091
  }
1789
2092
  return Array.from(map.values()).sort(
1790
2093
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1806,6 +2109,7 @@ export {
1806
2109
  resolveToolVersion,
1807
2110
  validateContracts,
1808
2111
  validateDefinedIds,
2112
+ validateDeltas,
1809
2113
  validateProject,
1810
2114
  validateScenarioContent,
1811
2115
  validateScenarios,