qfai 0.3.0 → 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
@@ -44,8 +44,8 @@ __export(src_exports, {
44
44
  resolvePath: () => resolvePath,
45
45
  resolveToolVersion: () => resolveToolVersion,
46
46
  validateContracts: () => validateContracts,
47
- validateDecisions: () => validateDecisions,
48
47
  validateDefinedIds: () => validateDefinedIds,
48
+ validateDeltas: () => validateDeltas,
49
49
  validateProject: () => validateProject,
50
50
  validateScenarioContent: () => validateScenarioContent,
51
51
  validateScenarios: () => validateScenarios,
@@ -61,13 +61,11 @@ var import_node_path = __toESM(require("path"), 1);
61
61
  var import_yaml = require("yaml");
62
62
  var defaultConfig = {
63
63
  paths: {
64
- specDir: ".qfai/spec",
65
- decisionsDir: ".qfai/spec/decisions",
66
- scenariosDir: ".qfai/spec/scenarios",
67
64
  contractsDir: ".qfai/contracts",
68
- uiContractsDir: ".qfai/contracts/ui",
69
- apiContractsDir: ".qfai/contracts/api",
70
- dataContractsDir: ".qfai/contracts/db",
65
+ specsDir: ".qfai/specs",
66
+ rulesDir: ".qfai/rules",
67
+ outDir: ".qfai/out",
68
+ promptsDir: ".qfai/prompts",
71
69
  srcDir: "src",
72
70
  testsDir: "tests"
73
71
  },
@@ -92,8 +90,7 @@ var defaultConfig = {
92
90
  }
93
91
  },
94
92
  output: {
95
- format: "text",
96
- jsonPath: ".qfai/out/validate.json"
93
+ validateJsonPath: ".qfai/out/validate.json"
97
94
  }
98
95
  };
99
96
  function getConfigPath(root) {
@@ -142,27 +139,6 @@ function normalizePaths(raw, configPath, issues) {
142
139
  return base;
143
140
  }
144
141
  return {
145
- specDir: readString(
146
- raw.specDir,
147
- base.specDir,
148
- "paths.specDir",
149
- configPath,
150
- issues
151
- ),
152
- decisionsDir: readString(
153
- raw.decisionsDir,
154
- base.decisionsDir,
155
- "paths.decisionsDir",
156
- configPath,
157
- issues
158
- ),
159
- scenariosDir: readString(
160
- raw.scenariosDir,
161
- base.scenariosDir,
162
- "paths.scenariosDir",
163
- configPath,
164
- issues
165
- ),
166
142
  contractsDir: readString(
167
143
  raw.contractsDir,
168
144
  base.contractsDir,
@@ -170,24 +146,31 @@ function normalizePaths(raw, configPath, issues) {
170
146
  configPath,
171
147
  issues
172
148
  ),
173
- uiContractsDir: readString(
174
- raw.uiContractsDir,
175
- base.uiContractsDir,
176
- "paths.uiContractsDir",
149
+ specsDir: readString(
150
+ raw.specsDir,
151
+ base.specsDir,
152
+ "paths.specsDir",
153
+ configPath,
154
+ issues
155
+ ),
156
+ rulesDir: readString(
157
+ raw.rulesDir,
158
+ base.rulesDir,
159
+ "paths.rulesDir",
177
160
  configPath,
178
161
  issues
179
162
  ),
180
- apiContractsDir: readString(
181
- raw.apiContractsDir,
182
- base.apiContractsDir,
183
- "paths.apiContractsDir",
163
+ outDir: readString(
164
+ raw.outDir,
165
+ base.outDir,
166
+ "paths.outDir",
184
167
  configPath,
185
168
  issues
186
169
  ),
187
- dataContractsDir: readString(
188
- raw.dataContractsDir,
189
- base.dataContractsDir,
190
- "paths.dataContractsDir",
170
+ promptsDir: readString(
171
+ raw.promptsDir,
172
+ base.promptsDir,
173
+ "paths.promptsDir",
191
174
  configPath,
192
175
  issues
193
176
  ),
@@ -310,17 +293,10 @@ function normalizeOutput(raw, configPath, issues) {
310
293
  return base;
311
294
  }
312
295
  return {
313
- format: readOutputFormat(
314
- raw.format,
315
- base.format,
316
- "output.format",
317
- configPath,
318
- issues
319
- ),
320
- jsonPath: readString(
321
- raw.jsonPath,
322
- base.jsonPath,
323
- "output.jsonPath",
296
+ validateJsonPath: readString(
297
+ raw.validateJsonPath,
298
+ base.validateJsonPath,
299
+ "output.validateJsonPath",
324
300
  configPath,
325
301
  issues
326
302
  )
@@ -387,20 +363,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
387
363
  }
388
364
  return fallback;
389
365
  }
390
- function readOutputFormat(value, fallback, label, configPath, issues) {
391
- if (value === "text" || value === "json" || value === "github") {
392
- return value;
393
- }
394
- if (value !== void 0) {
395
- issues.push(
396
- configIssue(
397
- configPath,
398
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
399
- )
400
- );
401
- }
402
- return fallback;
403
- }
404
366
  function configIssue(file, message) {
405
367
  return {
406
368
  code: "QFAI_CONFIG_INVALID",
@@ -481,6 +443,7 @@ function isValidId(value, prefix) {
481
443
 
482
444
  // src/core/report.ts
483
445
  var import_promises11 = require("fs/promises");
446
+ var import_node_path10 = __toESM(require("path"), 1);
484
447
 
485
448
  // src/core/discovery.ts
486
449
  var import_node_path3 = __toESM(require("path"), 1);
@@ -541,10 +504,24 @@ async function exists(target) {
541
504
  }
542
505
 
543
506
  // src/core/discovery.ts
544
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
545
- async function collectSpecFiles(specRoot) {
546
- const files = await collectFiles(specRoot, { extensions: [".md"] });
547
- return files.filter((file) => isSpecFile(file));
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"));
548
525
  }
549
526
  async function collectUiContractFiles(uiRoot) {
550
527
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -563,9 +540,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
563
540
  ]);
564
541
  return { ui, api, db };
565
542
  }
566
- function isSpecFile(filePath) {
567
- const name = import_node_path3.default.basename(filePath).toLowerCase();
568
- 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);
569
549
  }
570
550
 
571
551
  // src/core/types.ts
@@ -576,8 +556,8 @@ var import_promises3 = require("fs/promises");
576
556
  var import_node_path4 = __toESM(require("path"), 1);
577
557
  var import_node_url = require("url");
578
558
  async function resolveToolVersion() {
579
- if ("0.3.0".length > 0) {
580
- return "0.3.0";
559
+ if ("0.3.1".length > 0) {
560
+ return "0.3.1";
581
561
  }
582
562
  try {
583
563
  const packagePath = resolvePackageJsonPath();
@@ -597,6 +577,7 @@ function resolvePackageJsonPath() {
597
577
 
598
578
  // src/core/validators/contracts.ts
599
579
  var import_promises4 = require("fs/promises");
580
+ var import_node_path6 = __toESM(require("path"), 1);
600
581
 
601
582
  // src/core/contracts.ts
602
583
  var import_node_path5 = __toESM(require("path"), 1);
@@ -652,19 +633,10 @@ var SQL_DANGEROUS_PATTERNS = [
652
633
  ];
653
634
  async function validateContracts(root, config) {
654
635
  const issues = [];
655
- issues.push(
656
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
657
- );
658
- issues.push(
659
- ...await validateApiContracts(
660
- resolvePath(root, config, "apiContractsDir")
661
- )
662
- );
663
- issues.push(
664
- ...await validateDataContracts(
665
- resolvePath(root, config, "dataContractsDir")
666
- )
667
- );
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")));
668
640
  return issues;
669
641
  }
670
642
  async function validateUiContracts(uiRoot) {
@@ -897,72 +869,78 @@ function issue(code, message, severity, file, rule, refs) {
897
869
  return issue7;
898
870
  }
899
871
 
900
- // src/core/validators/decisions.ts
872
+ // src/core/validators/delta.ts
901
873
  var import_promises5 = require("fs/promises");
902
-
903
- // src/core/parse/adr.ts
904
- var ADR_ID_RE = /\bADR-\d{4}\b/;
905
- function extractField(md, key) {
906
- const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
907
- return md.match(pattern)?.[1]?.trim();
908
- }
909
- function parseAdr(md, file) {
910
- const adrId = md.match(ADR_ID_RE)?.[0];
911
- const fields = {};
912
- const status = extractField(md, "Status");
913
- const context = extractField(md, "Context");
914
- const decision = extractField(md, "Decision");
915
- const consequences = extractField(md, "Consequences");
916
- const related = extractField(md, "Related");
917
- if (status) fields.status = status;
918
- if (context) fields.context = context;
919
- if (decision) fields.decision = decision;
920
- if (consequences) fields.consequences = consequences;
921
- if (related) fields.related = related;
922
- const parsed = {
923
- file,
924
- fields
925
- };
926
- if (adrId) {
927
- parsed.adrId = adrId;
928
- }
929
- return parsed;
930
- }
931
-
932
- // src/core/validators/decisions.ts
933
- var REQUIRED_FIELDS = [
934
- { key: "status", label: "Status" },
935
- { key: "context", label: "Context" },
936
- { key: "decision", label: "Decision" },
937
- { key: "consequences", label: "Consequences" }
938
- ];
939
- async function validateDecisions(root, config) {
940
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
941
- const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
942
- if (files.length === 0) {
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) {
943
884
  return [];
944
885
  }
945
886
  const issues = [];
946
- for (const file of files) {
947
- const text = await (0, import_promises5.readFile)(file, "utf-8");
948
- const parsed = parseAdr(text, file);
949
- const missing = REQUIRED_FIELDS.filter(
950
- (field) => !parsed.fields[field.key]
951
- );
952
- if (missing.length > 0) {
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) {
953
911
  issues.push(
954
912
  issue2(
955
- "QFAI-ADR-001",
956
- `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
913
+ "QFAI-DELTA-002",
914
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
957
915
  "error",
958
- file,
959
- "adr.requiredFields"
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"
960
932
  )
961
933
  );
962
934
  }
963
935
  }
964
936
  return issues;
965
937
  }
938
+ function isMissingFileError(error) {
939
+ if (!error || typeof error !== "object") {
940
+ return false;
941
+ }
942
+ return error.code === "ENOENT";
943
+ }
966
944
  function issue2(code, message, severity, file, rule, refs) {
967
945
  const issue7 = {
968
946
  code,
@@ -983,14 +961,16 @@ function issue2(code, message, severity, file, rule, refs) {
983
961
 
984
962
  // src/core/validators/ids.ts
985
963
  var import_promises7 = require("fs/promises");
986
- var import_node_path6 = __toESM(require("path"), 1);
964
+ var import_node_path9 = __toESM(require("path"), 1);
987
965
 
988
966
  // src/core/contractIndex.ts
989
967
  var import_promises6 = require("fs/promises");
968
+ var import_node_path8 = __toESM(require("path"), 1);
990
969
  async function buildContractIndex(root, config) {
991
- const uiRoot = resolvePath(root, config, "uiContractsDir");
992
- const apiRoot = resolvePath(root, config, "apiContractsDir");
993
- const dataRoot = resolvePath(root, config, "dataContractsDir");
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");
994
974
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
995
975
  collectUiContractFiles(uiRoot),
996
976
  collectApiContractFiles(apiRoot),
@@ -1046,7 +1026,7 @@ function record(index, id, file) {
1046
1026
 
1047
1027
  // src/core/parse/gherkin.ts
1048
1028
  var FEATURE_RE = /^\s*Feature:\s+/;
1049
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
1029
+ var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
1050
1030
  var TAG_LINE_RE = /^\s*@/;
1051
1031
  function parseTags(line) {
1052
1032
  return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
@@ -1055,24 +1035,52 @@ function parseGherkinFeature(text, file) {
1055
1035
  const lines = text.split(/\r?\n/);
1056
1036
  const scenarios = [];
1057
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
+ };
1058
1049
  for (let i = 0; i < lines.length; i++) {
1059
1050
  const line = lines[i] ?? "";
1060
- if (FEATURE_RE.test(line)) {
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)) {
1061
1057
  featurePresent = true;
1058
+ featureTags = [...pendingTags];
1059
+ pendingTags = [];
1060
+ continue;
1062
1061
  }
1063
- const match = line.match(SCENARIO_RE);
1064
- if (!match) continue;
1065
- const scenarioName = match[1];
1066
- if (!scenarioName) continue;
1067
- const tags = [];
1068
- for (let j = i - 1; j >= 0; j--) {
1069
- const previous = lines[j] ?? "";
1070
- if (previous.trim() === "") continue;
1071
- if (!TAG_LINE_RE.test(previous)) break;
1072
- tags.unshift(...parseTags(previous));
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
+ `;
1073
1081
  }
1074
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1075
1082
  }
1083
+ flush();
1076
1084
  return { file, featurePresent, scenarios };
1077
1085
  }
1078
1086
 
@@ -1119,9 +1127,9 @@ function extractH2Sections(md) {
1119
1127
 
1120
1128
  // src/core/parse/spec.ts
1121
1129
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1122
- var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1123
- var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1124
- var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
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.*)$/;
1125
1133
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1126
1134
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1127
1135
  function parseSpec(md, file) {
@@ -1198,12 +1206,9 @@ function parseSpec(md, file) {
1198
1206
  var SC_TAG_RE = /^SC-\d{4}$/;
1199
1207
  async function validateDefinedIds(root, config) {
1200
1208
  const issues = [];
1201
- const specRoot = resolvePath(root, config, "specDir");
1202
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
1203
- const specFiles = await collectSpecFiles(specRoot);
1204
- const scenarioFiles = await collectFiles(scenarioRoot, {
1205
- extensions: [".feature"]
1206
- });
1209
+ const specsRoot = resolvePath(root, config, "specsDir");
1210
+ const specFiles = await collectSpecFiles(specsRoot);
1211
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1207
1212
  const defined = /* @__PURE__ */ new Map();
1208
1213
  await collectSpecDefinitionIds(specFiles, defined);
1209
1214
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1260,7 +1265,7 @@ function recordId(out, id, file) {
1260
1265
  }
1261
1266
  function formatFileList(files, root) {
1262
1267
  return files.map((file) => {
1263
- const relative = import_node_path6.default.relative(root, file);
1268
+ const relative = import_node_path9.default.relative(root, file);
1264
1269
  return relative.length > 0 ? relative : file;
1265
1270
  }).join(", ");
1266
1271
  }
@@ -1291,17 +1296,15 @@ var SC_TAG_RE2 = /^SC-\d{4}$/;
1291
1296
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1292
1297
  var BR_TAG_RE = /^BR-\d{4}$/;
1293
1298
  async function validateScenarios(root, config) {
1294
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1295
- const files = await collectFiles(scenariosRoot, {
1296
- extensions: [".feature"]
1297
- });
1299
+ const specsRoot = resolvePath(root, config, "specsDir");
1300
+ const files = await collectScenarioFiles(specsRoot);
1298
1301
  if (files.length === 0) {
1299
1302
  return [
1300
1303
  issue4(
1301
1304
  "QFAI-SC-000",
1302
1305
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1303
1306
  "info",
1304
- scenariosRoot,
1307
+ specsRoot,
1305
1308
  "scenario.files"
1306
1309
  )
1307
1310
  ];
@@ -1367,8 +1370,11 @@ function validateScenarioContent(text, file) {
1367
1370
  continue;
1368
1371
  }
1369
1372
  const missingTags = [];
1370
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1371
- missingTags.push("SC");
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)`);
1372
1378
  }
1373
1379
  if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1374
1380
  missingTags.push("SPEC");
@@ -1388,26 +1394,28 @@ function validateScenarioContent(text, file) {
1388
1394
  );
1389
1395
  }
1390
1396
  }
1391
- const missingSteps = [];
1392
- if (!GIVEN_PATTERN.test(text)) {
1393
- missingSteps.push("Given");
1394
- }
1395
- if (!WHEN_PATTERN.test(text)) {
1396
- missingSteps.push("When");
1397
- }
1398
- if (!THEN_PATTERN.test(text)) {
1399
- missingSteps.push("Then");
1400
- }
1401
- if (missingSteps.length > 0) {
1402
- issues.push(
1403
- issue4(
1404
- "QFAI-SC-005",
1405
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1406
- "warning",
1407
- file,
1408
- "scenario.steps"
1409
- )
1410
- );
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
+ }
1411
1419
  }
1412
1420
  return issues;
1413
1421
  }
@@ -1432,14 +1440,14 @@ function issue4(code, message, severity, file, rule, refs) {
1432
1440
  // src/core/validators/spec.ts
1433
1441
  var import_promises9 = require("fs/promises");
1434
1442
  async function validateSpecs(root, config) {
1435
- const specsRoot = resolvePath(root, config, "specDir");
1443
+ const specsRoot = resolvePath(root, config, "specsDir");
1436
1444
  const files = await collectSpecFiles(specsRoot);
1437
1445
  if (files.length === 0) {
1438
- const expected = "spec-0001-<slug>.md";
1446
+ const expected = "spec-001/spec.md";
1439
1447
  return [
1440
1448
  issue5(
1441
1449
  "QFAI-SPEC-000",
1442
- `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
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}`,
1443
1451
  "info",
1444
1452
  specsRoot,
1445
1453
  "spec.files"
@@ -1585,18 +1593,11 @@ var API_TAG_RE = /^API-\d{4}$/;
1585
1593
  var DATA_TAG_RE = /^DATA-\d{4}$/;
1586
1594
  async function validateTraceability(root, config) {
1587
1595
  const issues = [];
1588
- const specsRoot = resolvePath(root, config, "specDir");
1589
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1590
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1596
+ const specsRoot = resolvePath(root, config, "specsDir");
1591
1597
  const srcRoot = resolvePath(root, config, "srcDir");
1592
1598
  const testsRoot = resolvePath(root, config, "testsDir");
1593
1599
  const specFiles = await collectSpecFiles(specsRoot);
1594
- const decisionFiles = await collectFiles(decisionsRoot, {
1595
- extensions: [".md"]
1596
- });
1597
- const scenarioFiles = await collectFiles(scenariosRoot, {
1598
- extensions: [".feature"]
1599
- });
1600
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1600
1601
  const upstreamIds = /* @__PURE__ */ new Set();
1601
1602
  const specIds = /* @__PURE__ */ new Set();
1602
1603
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1644,10 +1645,6 @@ async function validateTraceability(root, config) {
1644
1645
  specToBrIds.set(parsed.specId, current);
1645
1646
  }
1646
1647
  }
1647
- for (const file of decisionFiles) {
1648
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1649
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1650
- }
1651
1648
  for (const file of scenarioFiles) {
1652
1649
  const text = await (0, import_promises10.readFile)(file, "utf-8");
1653
1650
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
@@ -1791,7 +1788,7 @@ async function validateTraceability(root, config) {
1791
1788
  ", "
1792
1789
  )}`,
1793
1790
  "error",
1794
- scenariosRoot,
1791
+ specsRoot,
1795
1792
  "traceability.scMustTouchContracts",
1796
1793
  scWithoutContracts
1797
1794
  )
@@ -1809,7 +1806,7 @@ async function validateTraceability(root, config) {
1809
1806
  "QFAI_CONTRACT_ORPHAN",
1810
1807
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1811
1808
  "error",
1812
- scenariosRoot,
1809
+ specsRoot,
1813
1810
  "traceability.allowOrphanContracts",
1814
1811
  orphanContracts
1815
1812
  )
@@ -1894,8 +1891,8 @@ async function validateProject(root, configResult) {
1894
1891
  const issues = [
1895
1892
  ...configIssues,
1896
1893
  ...await validateSpecs(root, config),
1894
+ ...await validateDeltas(root, config),
1897
1895
  ...await validateScenarios(root, config),
1898
- ...await validateDecisions(root, config),
1899
1896
  ...await validateContracts(root, config),
1900
1897
  ...await validateDefinedIds(root, config),
1901
1898
  ...await validateTraceability(root, config)
@@ -1924,21 +1921,15 @@ async function createReportData(root, validation, configResult) {
1924
1921
  const resolved = configResult ?? await loadConfig(root);
1925
1922
  const config = resolved.config;
1926
1923
  const configPath = resolved.configPath;
1927
- const specRoot = resolvePath(root, config, "specDir");
1928
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1929
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1930
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1931
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1932
- const dbRoot = resolvePath(root, config, "dataContractsDir");
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");
1933
1929
  const srcRoot = resolvePath(root, config, "srcDir");
1934
1930
  const testsRoot = resolvePath(root, config, "testsDir");
1935
- const specFiles = await collectSpecFiles(specRoot);
1936
- const scenarioFiles = await collectFiles(scenariosRoot, {
1937
- extensions: [".feature"]
1938
- });
1939
- const decisionFiles = await collectFiles(decisionsRoot, {
1940
- extensions: [".md"]
1941
- });
1931
+ const specFiles = await collectSpecFiles(specsRoot);
1932
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1942
1933
  const {
1943
1934
  api: apiFiles,
1944
1935
  ui: uiFiles,
@@ -1947,7 +1938,6 @@ async function createReportData(root, validation, configResult) {
1947
1938
  const idsByPrefix = await collectIds([
1948
1939
  ...specFiles,
1949
1940
  ...scenarioFiles,
1950
- ...decisionFiles,
1951
1941
  ...apiFiles,
1952
1942
  ...uiFiles,
1953
1943
  ...dbFiles
@@ -1972,7 +1962,6 @@ async function createReportData(root, validation, configResult) {
1972
1962
  summary: {
1973
1963
  specs: specFiles.length,
1974
1964
  scenarios: scenarioFiles.length,
1975
- decisions: decisionFiles.length,
1976
1965
  contracts: {
1977
1966
  api: apiFiles.length,
1978
1967
  ui: uiFiles.length,
@@ -2006,7 +1995,6 @@ function formatReportMarkdown(data) {
2006
1995
  lines.push("## \u6982\u8981");
2007
1996
  lines.push(`- specs: ${data.summary.specs}`);
2008
1997
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2009
- lines.push(`- decisions: ${data.summary.decisions}`);
2010
1998
  lines.push(
2011
1999
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
2012
2000
  );
@@ -2178,8 +2166,8 @@ function buildHotspots(issues) {
2178
2166
  resolvePath,
2179
2167
  resolveToolVersion,
2180
2168
  validateContracts,
2181
- validateDecisions,
2182
2169
  validateDefinedIds,
2170
+ validateDeltas,
2183
2171
  validateProject,
2184
2172
  validateScenarioContent,
2185
2173
  validateScenarios,