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.
@@ -138,7 +138,7 @@ function report(copied, skipped, dryRun, label) {
138
138
 
139
139
  // src/cli/commands/report.ts
140
140
  import { mkdir as mkdir2, readFile as readFile11, writeFile } from "fs/promises";
141
- import path10 from "path";
141
+ import path14 from "path";
142
142
 
143
143
  // src/core/config.ts
144
144
  import { readFile } from "fs/promises";
@@ -146,13 +146,11 @@ import path4 from "path";
146
146
  import { parse as parseYaml } from "yaml";
147
147
  var defaultConfig = {
148
148
  paths: {
149
- specDir: ".qfai/spec",
150
- decisionsDir: ".qfai/spec/decisions",
151
- scenariosDir: ".qfai/spec/scenarios",
152
149
  contractsDir: ".qfai/contracts",
153
- uiContractsDir: ".qfai/contracts/ui",
154
- apiContractsDir: ".qfai/contracts/api",
155
- dataContractsDir: ".qfai/contracts/db",
150
+ specsDir: ".qfai/specs",
151
+ rulesDir: ".qfai/rules",
152
+ outDir: ".qfai/out",
153
+ promptsDir: ".qfai/prompts",
156
154
  srcDir: "src",
157
155
  testsDir: "tests"
158
156
  },
@@ -177,8 +175,7 @@ var defaultConfig = {
177
175
  }
178
176
  },
179
177
  output: {
180
- format: "text",
181
- jsonPath: ".qfai/out/validate.json"
178
+ validateJsonPath: ".qfai/out/validate.json"
182
179
  }
183
180
  };
184
181
  function getConfigPath(root) {
@@ -227,27 +224,6 @@ function normalizePaths(raw, configPath, issues) {
227
224
  return base;
228
225
  }
229
226
  return {
230
- specDir: readString(
231
- raw.specDir,
232
- base.specDir,
233
- "paths.specDir",
234
- configPath,
235
- issues
236
- ),
237
- decisionsDir: readString(
238
- raw.decisionsDir,
239
- base.decisionsDir,
240
- "paths.decisionsDir",
241
- configPath,
242
- issues
243
- ),
244
- scenariosDir: readString(
245
- raw.scenariosDir,
246
- base.scenariosDir,
247
- "paths.scenariosDir",
248
- configPath,
249
- issues
250
- ),
251
227
  contractsDir: readString(
252
228
  raw.contractsDir,
253
229
  base.contractsDir,
@@ -255,24 +231,31 @@ function normalizePaths(raw, configPath, issues) {
255
231
  configPath,
256
232
  issues
257
233
  ),
258
- uiContractsDir: readString(
259
- raw.uiContractsDir,
260
- base.uiContractsDir,
261
- "paths.uiContractsDir",
234
+ specsDir: readString(
235
+ raw.specsDir,
236
+ base.specsDir,
237
+ "paths.specsDir",
238
+ configPath,
239
+ issues
240
+ ),
241
+ rulesDir: readString(
242
+ raw.rulesDir,
243
+ base.rulesDir,
244
+ "paths.rulesDir",
262
245
  configPath,
263
246
  issues
264
247
  ),
265
- apiContractsDir: readString(
266
- raw.apiContractsDir,
267
- base.apiContractsDir,
268
- "paths.apiContractsDir",
248
+ outDir: readString(
249
+ raw.outDir,
250
+ base.outDir,
251
+ "paths.outDir",
269
252
  configPath,
270
253
  issues
271
254
  ),
272
- dataContractsDir: readString(
273
- raw.dataContractsDir,
274
- base.dataContractsDir,
275
- "paths.dataContractsDir",
255
+ promptsDir: readString(
256
+ raw.promptsDir,
257
+ base.promptsDir,
258
+ "paths.promptsDir",
276
259
  configPath,
277
260
  issues
278
261
  ),
@@ -395,17 +378,10 @@ function normalizeOutput(raw, configPath, issues) {
395
378
  return base;
396
379
  }
397
380
  return {
398
- format: readOutputFormat(
399
- raw.format,
400
- base.format,
401
- "output.format",
402
- configPath,
403
- issues
404
- ),
405
- jsonPath: readString(
406
- raw.jsonPath,
407
- base.jsonPath,
408
- "output.jsonPath",
381
+ validateJsonPath: readString(
382
+ raw.validateJsonPath,
383
+ base.validateJsonPath,
384
+ "output.validateJsonPath",
409
385
  configPath,
410
386
  issues
411
387
  )
@@ -472,20 +448,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
472
448
  }
473
449
  return fallback;
474
450
  }
475
- function readOutputFormat(value, fallback, label, configPath, issues) {
476
- if (value === "text" || value === "json" || value === "github") {
477
- return value;
478
- }
479
- if (value !== void 0) {
480
- issues.push(
481
- configIssue(
482
- configPath,
483
- `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
484
- )
485
- );
486
- }
487
- return fallback;
488
- }
489
451
  function configIssue(file, message) {
490
452
  return {
491
453
  code: "QFAI_CONFIG_INVALID",
@@ -513,6 +475,7 @@ function isRecord(value) {
513
475
 
514
476
  // src/core/report.ts
515
477
  import { readFile as readFile10 } from "fs/promises";
478
+ import path13 from "path";
516
479
 
517
480
  // src/core/discovery.ts
518
481
  import path6 from "path";
@@ -573,10 +536,24 @@ async function exists2(target) {
573
536
  }
574
537
 
575
538
  // src/core/discovery.ts
576
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
577
- async function collectSpecFiles(specRoot) {
578
- const files = await collectFiles(specRoot, { extensions: [".md"] });
579
- return files.filter((file) => isSpecFile(file));
539
+ var SPEC_PACK_DIR_PATTERN = /^spec-\d{3}$/;
540
+ async function collectSpecPackDirs(specsRoot) {
541
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
542
+ const packs = /* @__PURE__ */ new Set();
543
+ for (const file of files) {
544
+ if (isSpecPackFile(file, "spec.md")) {
545
+ packs.add(path6.dirname(file));
546
+ }
547
+ }
548
+ return Array.from(packs).sort();
549
+ }
550
+ async function collectSpecFiles(specsRoot) {
551
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
552
+ return files.filter((file) => isSpecPackFile(file, "spec.md"));
553
+ }
554
+ async function collectScenarioFiles(specsRoot) {
555
+ const files = await collectFiles(specsRoot, { extensions: [".md"] });
556
+ return files.filter((file) => isSpecPackFile(file, "scenario.md"));
580
557
  }
581
558
  async function collectUiContractFiles(uiRoot) {
582
559
  return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
@@ -595,9 +572,12 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
595
572
  ]);
596
573
  return { ui, api, db };
597
574
  }
598
- function isSpecFile(filePath) {
599
- const name = path6.basename(filePath).toLowerCase();
600
- return SPEC_NAMED_PATTERN.test(name);
575
+ function isSpecPackFile(filePath, baseName) {
576
+ if (path6.basename(filePath).toLowerCase() !== baseName) {
577
+ return false;
578
+ }
579
+ const dirName = path6.basename(path6.dirname(filePath)).toLowerCase();
580
+ return SPEC_PACK_DIR_PATTERN.test(dirName);
601
581
  }
602
582
 
603
583
  // src/core/ids.ts
@@ -661,8 +641,8 @@ import { readFile as readFile2 } from "fs/promises";
661
641
  import path7 from "path";
662
642
  import { fileURLToPath as fileURLToPath2 } from "url";
663
643
  async function resolveToolVersion() {
664
- if ("0.3.0".length > 0) {
665
- return "0.3.0";
644
+ if ("0.3.1".length > 0) {
645
+ return "0.3.1";
666
646
  }
667
647
  try {
668
648
  const packagePath = resolvePackageJsonPath();
@@ -682,6 +662,7 @@ function resolvePackageJsonPath() {
682
662
 
683
663
  // src/core/validators/contracts.ts
684
664
  import { readFile as readFile3 } from "fs/promises";
665
+ import path9 from "path";
685
666
 
686
667
  // src/core/contracts.ts
687
668
  import path8 from "path";
@@ -737,19 +718,10 @@ var SQL_DANGEROUS_PATTERNS = [
737
718
  ];
738
719
  async function validateContracts(root, config) {
739
720
  const issues = [];
740
- issues.push(
741
- ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
742
- );
743
- issues.push(
744
- ...await validateApiContracts(
745
- resolvePath(root, config, "apiContractsDir")
746
- )
747
- );
748
- issues.push(
749
- ...await validateDataContracts(
750
- resolvePath(root, config, "dataContractsDir")
751
- )
752
- );
721
+ const contractsRoot = resolvePath(root, config, "contractsDir");
722
+ issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
723
+ issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
724
+ issues.push(...await validateDataContracts(path9.join(contractsRoot, "db")));
753
725
  return issues;
754
726
  }
755
727
  async function validateUiContracts(uiRoot) {
@@ -982,72 +954,78 @@ function issue(code, message, severity, file, rule, refs) {
982
954
  return issue7;
983
955
  }
984
956
 
985
- // src/core/validators/decisions.ts
957
+ // src/core/validators/delta.ts
986
958
  import { readFile as readFile4 } from "fs/promises";
987
-
988
- // src/core/parse/adr.ts
989
- var ADR_ID_RE = /\bADR-\d{4}\b/;
990
- function extractField(md, key) {
991
- const pattern = new RegExp(`^\\s*-\\s*${key}:\\s*(.+)\\s*$`, "m");
992
- return md.match(pattern)?.[1]?.trim();
993
- }
994
- function parseAdr(md, file) {
995
- const adrId = md.match(ADR_ID_RE)?.[0];
996
- const fields = {};
997
- const status = extractField(md, "Status");
998
- const context = extractField(md, "Context");
999
- const decision = extractField(md, "Decision");
1000
- const consequences = extractField(md, "Consequences");
1001
- const related = extractField(md, "Related");
1002
- if (status) fields.status = status;
1003
- if (context) fields.context = context;
1004
- if (decision) fields.decision = decision;
1005
- if (consequences) fields.consequences = consequences;
1006
- if (related) fields.related = related;
1007
- const parsed = {
1008
- file,
1009
- fields
1010
- };
1011
- if (adrId) {
1012
- parsed.adrId = adrId;
1013
- }
1014
- return parsed;
1015
- }
1016
-
1017
- // src/core/validators/decisions.ts
1018
- var REQUIRED_FIELDS = [
1019
- { key: "status", label: "Status" },
1020
- { key: "context", label: "Context" },
1021
- { key: "decision", label: "Decision" },
1022
- { key: "consequences", label: "Consequences" }
1023
- ];
1024
- async function validateDecisions(root, config) {
1025
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1026
- const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
1027
- if (files.length === 0) {
959
+ import path10 from "path";
960
+ var SECTION_RE = /^##\s+変更区分/m;
961
+ var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
962
+ var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
963
+ var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
964
+ var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
965
+ async function validateDeltas(root, config) {
966
+ const specsRoot = resolvePath(root, config, "specsDir");
967
+ const packs = await collectSpecPackDirs(specsRoot);
968
+ if (packs.length === 0) {
1028
969
  return [];
1029
970
  }
1030
971
  const issues = [];
1031
- for (const file of files) {
1032
- const text = await readFile4(file, "utf-8");
1033
- const parsed = parseAdr(text, file);
1034
- const missing = REQUIRED_FIELDS.filter(
1035
- (field) => !parsed.fields[field.key]
1036
- );
1037
- if (missing.length > 0) {
972
+ for (const pack of packs) {
973
+ const deltaPath = path10.join(pack, "delta.md");
974
+ let text;
975
+ try {
976
+ text = await readFile4(deltaPath, "utf-8");
977
+ } catch (error2) {
978
+ if (isMissingFileError(error2)) {
979
+ issues.push(
980
+ issue2(
981
+ "QFAI-DELTA-001",
982
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
983
+ "error",
984
+ deltaPath,
985
+ "delta.exists"
986
+ )
987
+ );
988
+ continue;
989
+ }
990
+ throw error2;
991
+ }
992
+ const hasSection = SECTION_RE.test(text);
993
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
994
+ const hasChange = CHANGE_LINE_RE.test(text);
995
+ if (!hasSection || !hasCompatibility || !hasChange) {
1038
996
  issues.push(
1039
997
  issue2(
1040
- "QFAI-ADR-001",
1041
- `ADR \u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missing.map((field) => field.label).join(", ")}`,
998
+ "QFAI-DELTA-002",
999
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1042
1000
  "error",
1043
- file,
1044
- "adr.requiredFields"
1001
+ deltaPath,
1002
+ "delta.section"
1003
+ )
1004
+ );
1005
+ continue;
1006
+ }
1007
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1008
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1009
+ if (compatibilityChecked === changeChecked) {
1010
+ issues.push(
1011
+ issue2(
1012
+ "QFAI-DELTA-003",
1013
+ "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",
1014
+ "error",
1015
+ deltaPath,
1016
+ "delta.classification"
1045
1017
  )
1046
1018
  );
1047
1019
  }
1048
1020
  }
1049
1021
  return issues;
1050
1022
  }
1023
+ function isMissingFileError(error2) {
1024
+ if (!error2 || typeof error2 !== "object") {
1025
+ return false;
1026
+ }
1027
+ return error2.code === "ENOENT";
1028
+ }
1051
1029
  function issue2(code, message, severity, file, rule, refs) {
1052
1030
  const issue7 = {
1053
1031
  code,
@@ -1068,14 +1046,16 @@ function issue2(code, message, severity, file, rule, refs) {
1068
1046
 
1069
1047
  // src/core/validators/ids.ts
1070
1048
  import { readFile as readFile6 } from "fs/promises";
1071
- import path9 from "path";
1049
+ import path12 from "path";
1072
1050
 
1073
1051
  // src/core/contractIndex.ts
1074
1052
  import { readFile as readFile5 } from "fs/promises";
1053
+ import path11 from "path";
1075
1054
  async function buildContractIndex(root, config) {
1076
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1077
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1078
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1055
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1056
+ const uiRoot = path11.join(contractsRoot, "ui");
1057
+ const apiRoot = path11.join(contractsRoot, "api");
1058
+ const dataRoot = path11.join(contractsRoot, "db");
1079
1059
  const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1080
1060
  collectUiContractFiles(uiRoot),
1081
1061
  collectApiContractFiles(apiRoot),
@@ -1131,7 +1111,7 @@ function record(index, id, file) {
1131
1111
 
1132
1112
  // src/core/parse/gherkin.ts
1133
1113
  var FEATURE_RE = /^\s*Feature:\s+/;
1134
- var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
1114
+ var SCENARIO_RE = /^\s*Scenario(?: Outline)?:\s*(.+)\s*$/;
1135
1115
  var TAG_LINE_RE = /^\s*@/;
1136
1116
  function parseTags(line) {
1137
1117
  return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
@@ -1140,24 +1120,52 @@ function parseGherkinFeature(text, file) {
1140
1120
  const lines = text.split(/\r?\n/);
1141
1121
  const scenarios = [];
1142
1122
  let featurePresent = false;
1123
+ let featureTags = [];
1124
+ let pendingTags = [];
1125
+ let current = null;
1126
+ const flush = () => {
1127
+ if (!current) return;
1128
+ scenarios.push({
1129
+ ...current,
1130
+ body: current.body.trim()
1131
+ });
1132
+ current = null;
1133
+ };
1143
1134
  for (let i = 0; i < lines.length; i++) {
1144
1135
  const line = lines[i] ?? "";
1145
- if (FEATURE_RE.test(line)) {
1136
+ const trimmed = line.trim();
1137
+ if (TAG_LINE_RE.test(trimmed)) {
1138
+ pendingTags.push(...parseTags(trimmed));
1139
+ continue;
1140
+ }
1141
+ if (FEATURE_RE.test(trimmed)) {
1146
1142
  featurePresent = true;
1143
+ featureTags = [...pendingTags];
1144
+ pendingTags = [];
1145
+ continue;
1147
1146
  }
1148
- const match = line.match(SCENARIO_RE);
1149
- if (!match) continue;
1150
- const scenarioName = match[1];
1151
- if (!scenarioName) continue;
1152
- const tags = [];
1153
- for (let j = i - 1; j >= 0; j--) {
1154
- const previous = lines[j] ?? "";
1155
- if (previous.trim() === "") continue;
1156
- if (!TAG_LINE_RE.test(previous)) break;
1157
- tags.unshift(...parseTags(previous));
1147
+ const match = trimmed.match(SCENARIO_RE);
1148
+ if (match) {
1149
+ const scenarioName = match[1]?.trim();
1150
+ if (!scenarioName) {
1151
+ continue;
1152
+ }
1153
+ flush();
1154
+ current = {
1155
+ name: scenarioName,
1156
+ line: i + 1,
1157
+ tags: [...featureTags, ...pendingTags],
1158
+ body: ""
1159
+ };
1160
+ pendingTags = [];
1161
+ continue;
1162
+ }
1163
+ if (current) {
1164
+ current.body += `${line}
1165
+ `;
1158
1166
  }
1159
- scenarios.push({ name: scenarioName, line: i + 1, tags });
1160
1167
  }
1168
+ flush();
1161
1169
  return { file, featurePresent, scenarios };
1162
1170
  }
1163
1171
 
@@ -1204,9 +1212,9 @@ function extractH2Sections(md) {
1204
1212
 
1205
1213
  // src/core/parse/spec.ts
1206
1214
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1207
- var BR_LINE_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[0-3])\)\s*(.+)$/;
1208
- var BR_LINE_ANY_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s*\((P[^)]+)\)\s*(.+)$/;
1209
- var BR_LINE_NO_PRIORITY_RE = /^\s*-\s*\[?(BR-\d{4})\]?\s+(?!\()(.*\S.*)$/;
1215
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1216
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1217
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1210
1218
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1211
1219
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1212
1220
  function parseSpec(md, file) {
@@ -1283,12 +1291,9 @@ function parseSpec(md, file) {
1283
1291
  var SC_TAG_RE = /^SC-\d{4}$/;
1284
1292
  async function validateDefinedIds(root, config) {
1285
1293
  const issues = [];
1286
- const specRoot = resolvePath(root, config, "specDir");
1287
- const scenarioRoot = resolvePath(root, config, "scenariosDir");
1288
- const specFiles = await collectSpecFiles(specRoot);
1289
- const scenarioFiles = await collectFiles(scenarioRoot, {
1290
- extensions: [".feature"]
1291
- });
1294
+ const specsRoot = resolvePath(root, config, "specsDir");
1295
+ const specFiles = await collectSpecFiles(specsRoot);
1296
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1292
1297
  const defined = /* @__PURE__ */ new Map();
1293
1298
  await collectSpecDefinitionIds(specFiles, defined);
1294
1299
  await collectScenarioDefinitionIds(scenarioFiles, defined);
@@ -1345,7 +1350,7 @@ function recordId(out, id, file) {
1345
1350
  }
1346
1351
  function formatFileList(files, root) {
1347
1352
  return files.map((file) => {
1348
- const relative = path9.relative(root, file);
1353
+ const relative = path12.relative(root, file);
1349
1354
  return relative.length > 0 ? relative : file;
1350
1355
  }).join(", ");
1351
1356
  }
@@ -1376,17 +1381,15 @@ var SC_TAG_RE2 = /^SC-\d{4}$/;
1376
1381
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
1377
1382
  var BR_TAG_RE = /^BR-\d{4}$/;
1378
1383
  async function validateScenarios(root, config) {
1379
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1380
- const files = await collectFiles(scenariosRoot, {
1381
- extensions: [".feature"]
1382
- });
1384
+ const specsRoot = resolvePath(root, config, "specsDir");
1385
+ const files = await collectScenarioFiles(specsRoot);
1383
1386
  if (files.length === 0) {
1384
1387
  return [
1385
1388
  issue4(
1386
1389
  "QFAI-SC-000",
1387
1390
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1388
1391
  "info",
1389
- scenariosRoot,
1392
+ specsRoot,
1390
1393
  "scenario.files"
1391
1394
  )
1392
1395
  ];
@@ -1452,8 +1455,11 @@ function validateScenarioContent(text, file) {
1452
1455
  continue;
1453
1456
  }
1454
1457
  const missingTags = [];
1455
- if (!scenario.tags.some((tag) => SC_TAG_RE2.test(tag))) {
1456
- missingTags.push("SC");
1458
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
1459
+ if (scTags.length === 0) {
1460
+ missingTags.push("SC(0\u4EF6)");
1461
+ } else if (scTags.length > 1) {
1462
+ missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1457
1463
  }
1458
1464
  if (!scenario.tags.some((tag) => SPEC_TAG_RE.test(tag))) {
1459
1465
  missingTags.push("SPEC");
@@ -1473,26 +1479,28 @@ function validateScenarioContent(text, file) {
1473
1479
  );
1474
1480
  }
1475
1481
  }
1476
- const missingSteps = [];
1477
- if (!GIVEN_PATTERN.test(text)) {
1478
- missingSteps.push("Given");
1479
- }
1480
- if (!WHEN_PATTERN.test(text)) {
1481
- missingSteps.push("When");
1482
- }
1483
- if (!THEN_PATTERN.test(text)) {
1484
- missingSteps.push("Then");
1485
- }
1486
- if (missingSteps.length > 0) {
1487
- issues.push(
1488
- issue4(
1489
- "QFAI-SC-005",
1490
- `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1491
- "warning",
1492
- file,
1493
- "scenario.steps"
1494
- )
1495
- );
1482
+ for (const scenario of parsed.scenarios) {
1483
+ const missingSteps = [];
1484
+ if (!GIVEN_PATTERN.test(scenario.body)) {
1485
+ missingSteps.push("Given");
1486
+ }
1487
+ if (!WHEN_PATTERN.test(scenario.body)) {
1488
+ missingSteps.push("When");
1489
+ }
1490
+ if (!THEN_PATTERN.test(scenario.body)) {
1491
+ missingSteps.push("Then");
1492
+ }
1493
+ if (missingSteps.length > 0) {
1494
+ issues.push(
1495
+ issue4(
1496
+ "QFAI-SC-005",
1497
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
1498
+ "warning",
1499
+ file,
1500
+ "scenario.steps"
1501
+ )
1502
+ );
1503
+ }
1496
1504
  }
1497
1505
  return issues;
1498
1506
  }
@@ -1517,14 +1525,14 @@ function issue4(code, message, severity, file, rule, refs) {
1517
1525
  // src/core/validators/spec.ts
1518
1526
  import { readFile as readFile8 } from "fs/promises";
1519
1527
  async function validateSpecs(root, config) {
1520
- const specsRoot = resolvePath(root, config, "specDir");
1528
+ const specsRoot = resolvePath(root, config, "specsDir");
1521
1529
  const files = await collectSpecFiles(specsRoot);
1522
1530
  if (files.length === 0) {
1523
- const expected = "spec-0001-<slug>.md";
1531
+ const expected = "spec-001/spec.md";
1524
1532
  return [
1525
1533
  issue5(
1526
1534
  "QFAI-SPEC-000",
1527
- `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}`,
1535
+ `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}`,
1528
1536
  "info",
1529
1537
  specsRoot,
1530
1538
  "spec.files"
@@ -1670,18 +1678,11 @@ var API_TAG_RE = /^API-\d{4}$/;
1670
1678
  var DATA_TAG_RE = /^DATA-\d{4}$/;
1671
1679
  async function validateTraceability(root, config) {
1672
1680
  const issues = [];
1673
- const specsRoot = resolvePath(root, config, "specDir");
1674
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
1675
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
1681
+ const specsRoot = resolvePath(root, config, "specsDir");
1676
1682
  const srcRoot = resolvePath(root, config, "srcDir");
1677
1683
  const testsRoot = resolvePath(root, config, "testsDir");
1678
1684
  const specFiles = await collectSpecFiles(specsRoot);
1679
- const decisionFiles = await collectFiles(decisionsRoot, {
1680
- extensions: [".md"]
1681
- });
1682
- const scenarioFiles = await collectFiles(scenariosRoot, {
1683
- extensions: [".feature"]
1684
- });
1685
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1685
1686
  const upstreamIds = /* @__PURE__ */ new Set();
1686
1687
  const specIds = /* @__PURE__ */ new Set();
1687
1688
  const brIdsInSpecs = /* @__PURE__ */ new Set();
@@ -1729,10 +1730,6 @@ async function validateTraceability(root, config) {
1729
1730
  specToBrIds.set(parsed.specId, current);
1730
1731
  }
1731
1732
  }
1732
- for (const file of decisionFiles) {
1733
- const text = await readFile9(file, "utf-8");
1734
- extractAllIds(text).forEach((id) => upstreamIds.add(id));
1735
- }
1736
1733
  for (const file of scenarioFiles) {
1737
1734
  const text = await readFile9(file, "utf-8");
1738
1735
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
@@ -1876,7 +1873,7 @@ async function validateTraceability(root, config) {
1876
1873
  ", "
1877
1874
  )}`,
1878
1875
  "error",
1879
- scenariosRoot,
1876
+ specsRoot,
1880
1877
  "traceability.scMustTouchContracts",
1881
1878
  scWithoutContracts
1882
1879
  )
@@ -1894,7 +1891,7 @@ async function validateTraceability(root, config) {
1894
1891
  "QFAI_CONTRACT_ORPHAN",
1895
1892
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1896
1893
  "error",
1897
- scenariosRoot,
1894
+ specsRoot,
1898
1895
  "traceability.allowOrphanContracts",
1899
1896
  orphanContracts
1900
1897
  )
@@ -1979,8 +1976,8 @@ async function validateProject(root, configResult) {
1979
1976
  const issues = [
1980
1977
  ...configIssues,
1981
1978
  ...await validateSpecs(root, config),
1979
+ ...await validateDeltas(root, config),
1982
1980
  ...await validateScenarios(root, config),
1983
- ...await validateDecisions(root, config),
1984
1981
  ...await validateContracts(root, config),
1985
1982
  ...await validateDefinedIds(root, config),
1986
1983
  ...await validateTraceability(root, config)
@@ -2009,21 +2006,15 @@ async function createReportData(root, validation, configResult) {
2009
2006
  const resolved = configResult ?? await loadConfig(root);
2010
2007
  const config = resolved.config;
2011
2008
  const configPath = resolved.configPath;
2012
- const specRoot = resolvePath(root, config, "specDir");
2013
- const decisionsRoot = resolvePath(root, config, "decisionsDir");
2014
- const scenariosRoot = resolvePath(root, config, "scenariosDir");
2015
- const apiRoot = resolvePath(root, config, "apiContractsDir");
2016
- const uiRoot = resolvePath(root, config, "uiContractsDir");
2017
- const dbRoot = resolvePath(root, config, "dataContractsDir");
2009
+ const specsRoot = resolvePath(root, config, "specsDir");
2010
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2011
+ const apiRoot = path13.join(contractsRoot, "api");
2012
+ const uiRoot = path13.join(contractsRoot, "ui");
2013
+ const dbRoot = path13.join(contractsRoot, "db");
2018
2014
  const srcRoot = resolvePath(root, config, "srcDir");
2019
2015
  const testsRoot = resolvePath(root, config, "testsDir");
2020
- const specFiles = await collectSpecFiles(specRoot);
2021
- const scenarioFiles = await collectFiles(scenariosRoot, {
2022
- extensions: [".feature"]
2023
- });
2024
- const decisionFiles = await collectFiles(decisionsRoot, {
2025
- extensions: [".md"]
2026
- });
2016
+ const specFiles = await collectSpecFiles(specsRoot);
2017
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2027
2018
  const {
2028
2019
  api: apiFiles,
2029
2020
  ui: uiFiles,
@@ -2032,7 +2023,6 @@ async function createReportData(root, validation, configResult) {
2032
2023
  const idsByPrefix = await collectIds([
2033
2024
  ...specFiles,
2034
2025
  ...scenarioFiles,
2035
- ...decisionFiles,
2036
2026
  ...apiFiles,
2037
2027
  ...uiFiles,
2038
2028
  ...dbFiles
@@ -2057,7 +2047,6 @@ async function createReportData(root, validation, configResult) {
2057
2047
  summary: {
2058
2048
  specs: specFiles.length,
2059
2049
  scenarios: scenarioFiles.length,
2060
- decisions: decisionFiles.length,
2061
2050
  contracts: {
2062
2051
  api: apiFiles.length,
2063
2052
  ui: uiFiles.length,
@@ -2091,7 +2080,6 @@ function formatReportMarkdown(data) {
2091
2080
  lines.push("## \u6982\u8981");
2092
2081
  lines.push(`- specs: ${data.summary.specs}`);
2093
2082
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2094
- lines.push(`- decisions: ${data.summary.decisions}`);
2095
2083
  lines.push(
2096
2084
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
2097
2085
  );
@@ -2250,21 +2238,22 @@ function buildHotspots(issues) {
2250
2238
 
2251
2239
  // src/cli/commands/report.ts
2252
2240
  async function runReport(options) {
2253
- const root = path10.resolve(options.root);
2241
+ const root = path14.resolve(options.root);
2254
2242
  const configResult = await loadConfig(root);
2255
- const input = options.jsonPath ?? configResult.config.output.jsonPath;
2256
- const inputPath = path10.isAbsolute(input) ? input : path10.resolve(root, input);
2243
+ const input = configResult.config.output.validateJsonPath;
2244
+ const inputPath = path14.isAbsolute(input) ? input : path14.resolve(root, input);
2257
2245
  let validation;
2258
2246
  try {
2259
2247
  validation = await readValidationResult(inputPath);
2260
2248
  } catch (err) {
2261
- if (isMissingFileError(err)) {
2249
+ if (isMissingFileError2(err)) {
2262
2250
  error(
2263
2251
  [
2264
2252
  `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
2265
2253
  "",
2266
- "\u307E\u305A validate.json \u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
2267
- ` qfai validate --json-path ${input}`,
2254
+ "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
2255
+ " qfai validate",
2256
+ "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
2268
2257
  "",
2269
2258
  "GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
2270
2259
  ].join("\n")
@@ -2276,10 +2265,11 @@ async function runReport(options) {
2276
2265
  }
2277
2266
  const data = await createReportData(root, validation, configResult);
2278
2267
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
2279
- const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
2268
+ const outRoot = resolvePath(root, configResult.config, "outDir");
2269
+ const defaultOut = options.format === "json" ? path14.join(outRoot, "report.json") : path14.join(outRoot, "report.md");
2280
2270
  const out = options.outPath ?? defaultOut;
2281
- const outPath = path10.isAbsolute(out) ? out : path10.resolve(root, out);
2282
- await mkdir2(path10.dirname(outPath), { recursive: true });
2271
+ const outPath = path14.isAbsolute(out) ? out : path14.resolve(root, out);
2272
+ await mkdir2(path14.dirname(outPath), { recursive: true });
2283
2273
  await writeFile(outPath, `${output}
2284
2274
  `, "utf-8");
2285
2275
  info(
@@ -2320,7 +2310,7 @@ function isValidationResult(value) {
2320
2310
  }
2321
2311
  return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
2322
2312
  }
2323
- function isMissingFileError(error2) {
2313
+ function isMissingFileError2(error2) {
2324
2314
  if (!error2 || typeof error2 !== "object") {
2325
2315
  return false;
2326
2316
  }
@@ -2330,7 +2320,7 @@ function isMissingFileError(error2) {
2330
2320
 
2331
2321
  // src/cli/commands/validate.ts
2332
2322
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
2333
- import path11 from "path";
2323
+ import path15 from "path";
2334
2324
 
2335
2325
  // src/cli/lib/failOn.ts
2336
2326
  function shouldFail(result, failOn) {
@@ -2345,24 +2335,17 @@ function shouldFail(result, failOn) {
2345
2335
 
2346
2336
  // src/cli/commands/validate.ts
2347
2337
  async function runValidate(options) {
2348
- const root = path11.resolve(options.root);
2338
+ const root = path15.resolve(options.root);
2349
2339
  const configResult = await loadConfig(root);
2350
2340
  const result = await validateProject(root, configResult);
2351
- const format = options.format ?? configResult.config.output.format;
2352
- const explicitJsonPath = options.jsonPath;
2341
+ const format = options.format ?? "text";
2353
2342
  if (format === "text") {
2354
2343
  emitText(result);
2355
2344
  }
2356
2345
  if (format === "github") {
2357
2346
  result.issues.forEach(emitGitHub);
2358
2347
  }
2359
- const shouldWriteJson = format === "json" || explicitJsonPath !== void 0;
2360
- if (shouldWriteJson) {
2361
- const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
2362
- if (jsonPath) {
2363
- await emitJson(result, root, jsonPath);
2364
- }
2365
- }
2348
+ await emitJson(result, root, configResult.config.output.validateJsonPath);
2366
2349
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
2367
2350
  return shouldFail(result, failOn) ? 1 : 0;
2368
2351
  }
@@ -2401,8 +2384,8 @@ function emitGitHub(issue7) {
2401
2384
  );
2402
2385
  }
2403
2386
  async function emitJson(result, root, jsonPath) {
2404
- const abs = path11.isAbsolute(jsonPath) ? jsonPath : path11.resolve(root, jsonPath);
2405
- await mkdir3(path11.dirname(abs), { recursive: true });
2387
+ const abs = path15.isAbsolute(jsonPath) ? jsonPath : path15.resolve(root, jsonPath);
2388
+ await mkdir3(path15.dirname(abs), { recursive: true });
2406
2389
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
2407
2390
  `, "utf-8");
2408
2391
  }
@@ -2463,15 +2446,6 @@ function parseArgs(argv, cwd) {
2463
2446
  i += 1;
2464
2447
  break;
2465
2448
  }
2466
- case "--json-path":
2467
- {
2468
- const next = args[i + 1];
2469
- if (next) {
2470
- options.jsonPath = next;
2471
- }
2472
- }
2473
- i += 1;
2474
- break;
2475
2449
  case "--out":
2476
2450
  {
2477
2451
  const next = args[i + 1];
@@ -2502,7 +2476,7 @@ function applyFormatOption(command, value, options) {
2502
2476
  return;
2503
2477
  }
2504
2478
  if (command === "validate") {
2505
- if (value === "text" || value === "json" || value === "github") {
2479
+ if (value === "text" || value === "github") {
2506
2480
  options.validateFormat = value;
2507
2481
  }
2508
2482
  return;
@@ -2510,7 +2484,7 @@ function applyFormatOption(command, value, options) {
2510
2484
  if (value === "md" || value === "json") {
2511
2485
  options.reportFormat = value;
2512
2486
  }
2513
- if (value === "text" || value === "json" || value === "github") {
2487
+ if (value === "text" || value === "github") {
2514
2488
  options.validateFormat = value;
2515
2489
  }
2516
2490
  }
@@ -2536,15 +2510,13 @@ async function run(argv, cwd) {
2536
2510
  root: options.root,
2537
2511
  strict: options.strict,
2538
2512
  format: options.validateFormat,
2539
- ...options.failOn !== void 0 ? { failOn: options.failOn } : {},
2540
- ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
2513
+ ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
2541
2514
  });
2542
2515
  return;
2543
2516
  case "report":
2544
2517
  await runReport({
2545
2518
  root: options.root,
2546
2519
  format: options.reportFormat,
2547
- ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
2548
2520
  ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
2549
2521
  });
2550
2522
  return;
@@ -2566,14 +2538,13 @@ Options:
2566
2538
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
2567
2539
  --dir <path> init \u306E\u51FA\u529B\u5148
2568
2540
  --force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
2569
- --yes init: \u975E\u5BFE\u8A71\u3067\u30C7\u30D5\u30A9\u30EB\u30C8\u3092\u63A1\u7528\uFF08\u73FE\u5728\u306F\u975E\u5BFE\u8A71\u304C\u65E2\u5B9A\u3001\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u3082\u81EA\u52D5Yes\uFF09
2541
+ --yes init: \u4E88\u7D04\u30D5\u30E9\u30B0\uFF08\u73FE\u72B6\u306F\u975E\u5BFE\u8A71\u306E\u305F\u3081\u6319\u52D5\u5DEE\u306A\u3057\u3002\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u306B\u81EA\u52D5Yes\uFF09
2570
2542
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
2571
- --format <text|json|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
2543
+ --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
2572
2544
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
2573
- --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
2545
+ --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
2574
2546
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
2575
- --json-path <path> validate: JSON \u51FA\u529B\u5148 / report: validate JSON \u5165\u529B
2576
- --out <path> report: \u51FA\u529B\u5148
2547
+ --out <path> report: \u51FA\u529B\u5148
2577
2548
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
2578
2549
  `;
2579
2550
  }