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.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",
96
+ configPath,
97
+ issues
98
+ ),
99
+ rulesDir: readString(
100
+ raw.rulesDir,
101
+ base.rulesDir,
102
+ "paths.rulesDir",
120
103
  configPath,
121
104
  issues
122
105
  ),
123
- apiContractsDir: readString(
124
- raw.apiContractsDir,
125
- base.apiContractsDir,
126
- "paths.apiContractsDir",
106
+ outDir: readString(
107
+ raw.outDir,
108
+ base.outDir,
109
+ "paths.outDir",
127
110
  configPath,
128
111
  issues
129
112
  ),
130
- dataContractsDir: readString(
131
- raw.dataContractsDir,
132
- base.dataContractsDir,
133
- "paths.dataContractsDir",
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",
@@ -424,6 +386,7 @@ function isValidId(value, prefix) {
424
386
 
425
387
  // src/core/report.ts
426
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.3.0".length > 0) {
523
- return "0.3.0";
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) {
@@ -840,72 +812,78 @@ function issue(code, message, severity, file, rule, refs) {
840
812
  return issue7;
841
813
  }
842
814
 
843
- // src/core/validators/decisions.ts
815
+ // src/core/validators/delta.ts
844
816
  import { readFile as readFile4 } from "fs/promises";
845
-
846
- // src/core/parse/adr.ts
847
- var ADR_ID_RE = /\bADR-\d{4}\b/;
848
- function extractField(md, key) {
849
- const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
850
- return md.match(pattern)?.[1]?.trim();
851
- }
852
- function parseAdr(md, file) {
853
- const adrId = md.match(ADR_ID_RE)?.[0];
854
- const fields = {};
855
- const status = extractField(md, "Status");
856
- const context = extractField(md, "Context");
857
- const decision = extractField(md, "Decision");
858
- const consequences = extractField(md, "Consequences");
859
- const related = extractField(md, "Related");
860
- if (status) fields.status = status;
861
- if (context) fields.context = context;
862
- if (decision) fields.decision = decision;
863
- if (consequences) fields.consequences = consequences;
864
- if (related) fields.related = related;
865
- const parsed = {
866
- file,
867
- fields
868
- };
869
- if (adrId) {
870
- parsed.adrId = adrId;
871
- }
872
- return parsed;
873
- }
874
-
875
- // src/core/validators/decisions.ts
876
- var REQUIRED_FIELDS = [
877
- { key: "status", label: "Status" },
878
- { key: "context", label: "Context" },
879
- { key: "decision", label: "Decision" },
880
- { key: "consequences", label: "Consequences" }
881
- ];
882
- async function validateDecisions(root, config) {
883
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
884
- const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
885
- if (files.length === 0) {
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) {
886
827
  return [];
887
828
  }
888
829
  const issues = [];
889
- for (const file of files) {
890
- const text = await readFile4(file, "utf-8");
891
- const parsed = parseAdr(text, file);
892
- const missing = REQUIRED_FIELDS.filter(
893
- (field) => !parsed.fields[field.key]
894
- );
895
- if (missing.length > 0) {
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) {
896
854
  issues.push(
897
855
  issue2(
898
- "QFAI-ADR-001",
899
- `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
856
+ "QFAI-DELTA-002",
857
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
900
858
  "error",
901
- file,
902
- "adr.requiredFields"
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"
903
875
  )
904
876
  );
905
877
  }
906
878
  }
907
879
  return issues;
908
880
  }
881
+ function isMissingFileError(error) {
882
+ if (!error || typeof error !== "object") {
883
+ return false;
884
+ }
885
+ return error.code === "ENOENT";
886
+ }
909
887
  function issue2(code, message, severity, file, rule, refs) {
910
888
  const issue7 = {
911
889
  code,
@@ -926,14 +904,16 @@ function issue2(code, message, severity, file, rule, refs) {
926
904
 
927
905
  // src/core/validators/ids.ts
928
906
  import { readFile as readFile6 } from "fs/promises";
929
- import path6 from "path";
907
+ import path9 from "path";
930
908
 
931
909
  // src/core/contractIndex.ts
932
910
  import { readFile as readFile5 } from "fs/promises";
911
+ import path8 from "path";
933
912
  async function buildContractIndex(root, config) {
934
- const uiRoot = resolvePath(root, config, "uiContractsDir");
935
- const apiRoot = resolvePath(root, config, "apiContractsDir");
936
- 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");
937
917
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
938
918
  collectUiContractFiles(uiRoot),
939
919
  collectApiContractFiles(apiRoot),
@@ -989,7 +969,7 @@ function record(index, id, file) {
989
969
 
990
970
  // src/core/parse/gherkin.ts
991
971
  var FEATURE_RE = /^\s*Feature:\s+/;
992
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
972
+ var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
993
973
  var TAG_LINE_RE = /^\s*@/;
994
974
  function parseTags(line) {
995
975
  return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
@@ -998,24 +978,52 @@ function parseGherkinFeature(text, file) {
998
978
  const lines = text.split(/\r?\n/);
999
979
  const scenarios = [];
1000
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
+ };
1001
992
  for (let i = 0; i < lines.length; i++) {
1002
993
  const line = lines[i] ?? "";
1003
- if (FEATURE_RE.test(line)) {
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)) {
1004
1000
  featurePresent = true;
1001
+ featureTags = [...pendingTags];
1002
+ pendingTags = [];
1003
+ continue;
1005
1004
  }
1006
- const match = line.match(SCENARIO_RE);
1007
- if (!match) continue;
1008
- const scenarioName = match[1];
1009
- if (!scenarioName) continue;
1010
- const tags = [];
1011
- for (let j = i - 1; j >= 0; j--) {
1012
- const previous = lines[j] ?? "";
1013
- if (previous.trim() === "") continue;
1014
- if (!TAG_LINE_RE.test(previous)) break;
1015
- tags.unshift(...parseTags(previous));
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
+ `;
1016
1024
  }
1017
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1018
1025
  }
1026
+ flush();
1019
1027
  return { file, featurePresent, scenarios };
1020
1028
  }
1021
1029
 
@@ -1062,9 +1070,9 @@ function extractH2Sections(md) {
1062
1070
 
1063
1071
  // src/core/parse/spec.ts
1064
1072
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1065
- var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1066
- var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1067
- var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
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.*)$/;
1068
1076
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1069
1077
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1070
1078
  function parseSpec(md, file) {
@@ -1141,12 +1149,9 @@ function parseSpec(md, file) {
1141
1149
  var SC_TAG_RE = /^SC-\d{4}$/;
1142
1150
  async function validateDefinedIds(root, config) {
1143
1151
  const issues = [];
1144
- const specRoot = resolvePath(root, config, "specDir");
1145
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
1146
- const specFiles = await collectSpecFiles(specRoot);
1147
- const scenarioFiles = await collectFiles(scenarioRoot, {
1148
- extensions: [".feature"]
1149
- });
1152
+ const specsRoot = resolvePath(root, config, "specsDir");
1153
+ const specFiles = await collectSpecFiles(specsRoot);
1154
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1150
1155
  const defined = /* @__PURE__ */ new Map();
1151
1156
  await collectSpecDefinitionIds(specFiles, defined);
1152
1157
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1203,7 +1208,7 @@ function recordId(out, id, file) {
1203
1208
  }
1204
1209
  function formatFileList(files, root) {
1205
1210
  return files.map((file) => {
1206
- const relative = path6.relative(root, file);
1211
+ const relative = path9.relative(root, file);
1207
1212
  return relative.length > 0 ? relative : file;
1208
1213
  }).join(", ");
1209
1214
  }
@@ -1234,17 +1239,15 @@ var SC_TAG_RE2 = /^SC-\d{4}$/;
1234
1239
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1235
1240
  var BR_TAG_RE = /^BR-\d{4}$/;
1236
1241
  async function validateScenarios(root, config) {
1237
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1238
- const files = await collectFiles(scenariosRoot, {
1239
- extensions: [".feature"]
1240
- });
1242
+ const specsRoot = resolvePath(root, config, "specsDir");
1243
+ const files = await collectScenarioFiles(specsRoot);
1241
1244
  if (files.length === 0) {
1242
1245
  return [
1243
1246
  issue4(
1244
1247
  "QFAI-SC-000",
1245
1248
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1246
1249
  "info",
1247
- scenariosRoot,
1250
+ specsRoot,
1248
1251
  "scenario.files"
1249
1252
  )
1250
1253
  ];
@@ -1310,8 +1313,11 @@ function validateScenarioContent(text, file) {
1310
1313
  continue;
1311
1314
  }
1312
1315
  const missingTags = [];
1313
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1314
- missingTags.push("SC");
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)`);
1315
1321
  }
1316
1322
  if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1317
1323
  missingTags.push("SPEC");
@@ -1331,26 +1337,28 @@ function validateScenarioContent(text, file) {
1331
1337
  );
1332
1338
  }
1333
1339
  }
1334
- const missingSteps = [];
1335
- if (!GIVEN_PATTERN.test(text)) {
1336
- missingSteps.push("Given");
1337
- }
1338
- if (!WHEN_PATTERN.test(text)) {
1339
- missingSteps.push("When");
1340
- }
1341
- if (!THEN_PATTERN.test(text)) {
1342
- missingSteps.push("Then");
1343
- }
1344
- if (missingSteps.length > 0) {
1345
- issues.push(
1346
- issue4(
1347
- "QFAI-SC-005",
1348
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1349
- "warning",
1350
- file,
1351
- "scenario.steps"
1352
- )
1353
- );
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
+ }
1354
1362
  }
1355
1363
  return issues;
1356
1364
  }
@@ -1375,14 +1383,14 @@ function issue4(code, message, severity, file, rule, refs) {
1375
1383
  // src/core/validators/spec.ts
1376
1384
  import { readFile as readFile8 } from "fs/promises";
1377
1385
  async function validateSpecs(root, config) {
1378
- const specsRoot = resolvePath(root, config, "specDir");
1386
+ const specsRoot = resolvePath(root, config, "specsDir");
1379
1387
  const files = await collectSpecFiles(specsRoot);
1380
1388
  if (files.length === 0) {
1381
- const expected = "spec-0001-<slug>.md";
1389
+ const expected = "spec-001/spec.md";
1382
1390
  return [
1383
1391
  issue5(
1384
1392
  "QFAI-SPEC-000",
1385
- `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}`,
1386
1394
  "info",
1387
1395
  specsRoot,
1388
1396
  "spec.files"
@@ -1528,18 +1536,11 @@ var API_TAG_RE = /^API-\d{4}$/;
1528
1536
  var DATA_TAG_RE = /^DATA-\d{4}$/;
1529
1537
  async function validateTraceability(root, config) {
1530
1538
  const issues = [];
1531
- const specsRoot = resolvePath(root, config, "specDir");
1532
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1533
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1539
+ const specsRoot = resolvePath(root, config, "specsDir");
1534
1540
  const srcRoot = resolvePath(root, config, "srcDir");
1535
1541
  const testsRoot = resolvePath(root, config, "testsDir");
1536
1542
  const specFiles = await collectSpecFiles(specsRoot);
1537
- const decisionFiles = await collectFiles(decisionsRoot, {
1538
- extensions: [".md"]
1539
- });
1540
- const scenarioFiles = await collectFiles(scenariosRoot, {
1541
- extensions: [".feature"]
1542
- });
1543
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1543
1544
  const upstreamIds = /* @__PURE__ */ new Set();
1544
1545
  const specIds = /* @__PURE__ */ new Set();
1545
1546
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1587,10 +1588,6 @@ async function validateTraceability(root, config) {
1587
1588
  specToBrIds.set(parsed.specId, current);
1588
1589
  }
1589
1590
  }
1590
- for (const file of decisionFiles) {
1591
- const text = await readFile9(file, "utf-8");
1592
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1593
- }
1594
1591
  for (const file of scenarioFiles) {
1595
1592
  const text = await readFile9(file, "utf-8");
1596
1593
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
@@ -1734,7 +1731,7 @@ async function validateTraceability(root, config) {
1734
1731
  ", "
1735
1732
  )}`,
1736
1733
  "error",
1737
- scenariosRoot,
1734
+ specsRoot,
1738
1735
  "traceability.scMustTouchContracts",
1739
1736
  scWithoutContracts
1740
1737
  )
@@ -1752,7 +1749,7 @@ async function validateTraceability(root, config) {
1752
1749
  "QFAI_CONTRACT_ORPHAN",
1753
1750
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1754
1751
  "error",
1755
- scenariosRoot,
1752
+ specsRoot,
1756
1753
  "traceability.allowOrphanContracts",
1757
1754
  orphanContracts
1758
1755
  )
@@ -1837,8 +1834,8 @@ async function validateProject(root, configResult) {
1837
1834
  const issues = [
1838
1835
  ...configIssues,
1839
1836
  ...await validateSpecs(root, config),
1837
+ ...await validateDeltas(root, config),
1840
1838
  ...await validateScenarios(root, config),
1841
- ...await validateDecisions(root, config),
1842
1839
  ...await validateContracts(root, config),
1843
1840
  ...await validateDefinedIds(root, config),
1844
1841
  ...await validateTraceability(root, config)
@@ -1867,21 +1864,15 @@ async function createReportData(root, validation, configResult) {
1867
1864
  const resolved = configResult ?? await loadConfig(root);
1868
1865
  const config = resolved.config;
1869
1866
  const configPath = resolved.configPath;
1870
- const specRoot = resolvePath(root, config, "specDir");
1871
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1872
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1873
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1874
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1875
- 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");
1876
1872
  const srcRoot = resolvePath(root, config, "srcDir");
1877
1873
  const testsRoot = resolvePath(root, config, "testsDir");
1878
- const specFiles = await collectSpecFiles(specRoot);
1879
- const scenarioFiles = await collectFiles(scenariosRoot, {
1880
- extensions: [".feature"]
1881
- });
1882
- const decisionFiles = await collectFiles(decisionsRoot, {
1883
- extensions: [".md"]
1884
- });
1874
+ const specFiles = await collectSpecFiles(specsRoot);
1875
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1885
1876
  const {
1886
1877
  api: apiFiles,
1887
1878
  ui: uiFiles,
@@ -1890,7 +1881,6 @@ async function createReportData(root, validation, configResult) {
1890
1881
  const idsByPrefix = await collectIds([
1891
1882
  ...specFiles,
1892
1883
  ...scenarioFiles,
1893
- ...decisionFiles,
1894
1884
  ...apiFiles,
1895
1885
  ...uiFiles,
1896
1886
  ...dbFiles
@@ -1915,7 +1905,6 @@ async function createReportData(root, validation, configResult) {
1915
1905
  summary: {
1916
1906
  specs: specFiles.length,
1917
1907
  scenarios: scenarioFiles.length,
1918
- decisions: decisionFiles.length,
1919
1908
  contracts: {
1920
1909
  api: apiFiles.length,
1921
1910
  ui: uiFiles.length,
@@ -1949,7 +1938,6 @@ function formatReportMarkdown(data) {
1949
1938
  lines.push("## \u6982\u8981");
1950
1939
  lines.push(`- specs: ${data.summary.specs}`);
1951
1940
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1952
- lines.push(`- decisions: ${data.summary.decisions}`);
1953
1941
  lines.push(
1954
1942
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1955
1943
  );
@@ -2120,8 +2108,8 @@ export {
2120
2108
  resolvePath,
2121
2109
  resolveToolVersion,
2122
2110
  validateContracts,
2123
- validateDecisions,
2124
2111
  validateDefinedIds,
2112
+ validateDeltas,
2125
2113
  validateProject,
2126
2114
  validateScenarioContent,
2127
2115
  validateScenarios,